LabelMenu.tsx

  1import React, { useEffect, useRef, useState } from 'react';
  2
  3import { IconButton } from '@material-ui/core';
  4import Menu from '@material-ui/core/Menu';
  5import MenuItem from '@material-ui/core/MenuItem';
  6import TextField from '@material-ui/core/TextField';
  7import { makeStyles, withStyles } from '@material-ui/core/styles';
  8import { darken } from '@material-ui/core/styles/colorManipulator';
  9import CheckIcon from '@material-ui/icons/Check';
 10import SettingsIcon from '@material-ui/icons/Settings';
 11
 12import { Color } from '../../../gqlTypes';
 13import {
 14  ListLabelsDocument,
 15  useListLabelsQuery,
 16} from '../../list/ListLabels.generated';
 17import { BugFragment } from '../Bug.generated';
 18import { GetBugDocument } from '../BugQuery.generated';
 19
 20import { useSetLabelMutation } from './SetLabel.generated';
 21
 22type DropdownTuple = [string, string, Color];
 23
 24type FilterDropdownProps = {
 25  children: React.ReactNode;
 26  dropdown: DropdownTuple[];
 27  hasFilter?: boolean;
 28  itemActive: (key: string) => boolean;
 29  onClose: () => void;
 30  toggleLabel: (key: string, active: boolean) => void;
 31  onNewItem: (name: string) => void;
 32} & React.ButtonHTMLAttributes<HTMLButtonElement>;
 33
 34const CustomTextField = withStyles((theme) => ({
 35  root: {
 36    margin: '0 8px 12px 8px',
 37    '& label.Mui-focused': {
 38      margin: '0 2px',
 39      color: theme.palette.text.secondary,
 40    },
 41    '& .MuiInput-underline::before': {
 42      borderBottomColor: theme.palette.divider,
 43    },
 44    '& .MuiInput-underline::after': {
 45      borderBottomColor: theme.palette.divider,
 46    },
 47  },
 48}))(TextField);
 49
 50const ITEM_HEIGHT = 48;
 51
 52const useStyles = makeStyles((theme) => ({
 53  gearBtn: {
 54    ...theme.typography.body2,
 55    color: theme.palette.text.secondary,
 56    padding: theme.spacing(0, 1),
 57    fontWeight: 400,
 58    textDecoration: 'none',
 59    display: 'flex',
 60    background: 'none',
 61    border: 'none',
 62  },
 63  menu: {
 64    '& .MuiMenu-paper': {
 65      //somehow using "width" won't override the default width...
 66      minWidth: '35ch',
 67    },
 68  },
 69  labelcolor: {
 70    minWidth: '0.5rem',
 71    display: 'flex',
 72    borderRadius: '0.25rem',
 73    marginRight: '5px',
 74    marginLeft: '3px',
 75  },
 76  labelsheader: {
 77    display: 'flex',
 78    flexDirection: 'row',
 79  },
 80  menuRow: {
 81    display: 'flex',
 82    alignItems: 'initial',
 83  },
 84}));
 85
 86const _rgb = (color: Color) =>
 87  'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
 88
 89// Create a style object from the label RGB colors
 90const createStyle = (color: Color) => ({
 91  backgroundColor: _rgb(color),
 92  borderBottomColor: darken(_rgb(color), 0.2),
 93});
 94
 95function FilterDropdown({
 96  children,
 97  dropdown,
 98  hasFilter,
 99  itemActive,
100  onClose,
101  toggleLabel,
102  onNewItem,
103}: FilterDropdownProps) {
104  const [open, setOpen] = useState(false);
105  const [filter, setFilter] = useState<string>('');
106  const buttonRef = useRef<HTMLButtonElement>(null);
107  const searchRef = useRef<HTMLButtonElement>(null);
108  const classes = useStyles({ active: false });
109
110  useEffect(() => {
111    searchRef && searchRef.current && searchRef.current.focus();
112  }, [filter]);
113
114  return (
115    <>
116      <div className={classes.labelsheader}>
117        Labels
118        <IconButton
119          ref={buttonRef}
120          onClick={() => setOpen(!open)}
121          className={classes.gearBtn}
122        >
123          <SettingsIcon fontSize={'small'} />
124        </IconButton>
125      </div>
126
127      <Menu
128        className={classes.menu}
129        getContentAnchorEl={null}
130        ref={searchRef}
131        anchorOrigin={{
132          vertical: 'bottom',
133          horizontal: 'left',
134        }}
135        transformOrigin={{
136          vertical: 'top',
137          horizontal: 'left',
138        }}
139        open={open}
140        onClose={() => {
141          setOpen(false);
142          onClose();
143        }}
144        onExited={() => setFilter('')}
145        anchorEl={buttonRef.current}
146        PaperProps={{
147          style: {
148            maxHeight: ITEM_HEIGHT * 4.5,
149            width: '25ch',
150          },
151        }}
152      >
153        {hasFilter && (
154          <CustomTextField
155            onChange={(e) => {
156              const { value } = e.target;
157              setFilter(value);
158            }}
159            onKeyDown={(e) => e.stopPropagation()}
160            value={filter}
161            label={`Filter ${children}`}
162          />
163        )}
164        {filter !== '' &&
165          dropdown.filter((d) => d[1].toLowerCase() === filter.toLowerCase())
166            .length <= 0 && (
167            <MenuItem
168              style={{ whiteSpace: 'normal', wordBreak: 'break-all' }}
169              onClick={() => {
170                onNewItem(filter);
171                setFilter('');
172                setOpen(false);
173              }}
174            >
175              Create new label '{filter}'
176            </MenuItem>
177          )}
178        {dropdown
179          .sort(function (x, y) {
180            // true values first
181            return itemActive(x[1]) === itemActive(y[1]) ? 0 : x ? -1 : 1;
182          })
183          .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase()))
184          .map(([key, value, color]) => (
185            <MenuItem
186              style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}
187              onClick={() => {
188                toggleLabel(key, itemActive(key));
189              }}
190              key={key}
191              selected={itemActive(key)}
192            >
193              <div className={classes.menuRow}>
194                {itemActive(key) && <CheckIcon />}
195                <div
196                  className={classes.labelcolor}
197                  style={createStyle(color)}
198                />
199                {value}
200              </div>
201            </MenuItem>
202          ))}
203      </Menu>
204    </>
205  );
206}
207
208type Props = {
209  bug: BugFragment;
210};
211function LabelMenu({ bug }: Props) {
212  const { data: labelsData } = useListLabelsQuery();
213  const [bugLabelNames, setBugLabelNames] = useState(
214    bug.labels.map((l) => l.name)
215  );
216  const [selectedLabels, setSelectedLabels] = useState(
217    bug.labels.map((l) => l.name)
218  );
219
220  const [setLabelMutation] = useSetLabelMutation();
221
222  function toggleLabel(key: string, active: boolean) {
223    const labels: string[] = active
224      ? selectedLabels.filter((label) => label !== key)
225      : selectedLabels.concat([key]);
226    setSelectedLabels(labels);
227    console.log('toggle (selected)');
228    console.log(labels);
229  }
230
231  function diff(oldState: string[], newState: string[]) {
232    const added = newState.filter((x) => !oldState.includes(x));
233    const removed = oldState.filter((x) => !newState.includes(x));
234    return {
235      added: added,
236      removed: removed,
237    };
238  }
239
240  const changeBugLabels = (selectedLabels: string[]) => {
241    console.log('CBL');
242    console.log('selected labels');
243    console.log(selectedLabels);
244    console.log('buglabels');
245    console.log(bugLabelNames);
246    const labels = diff(bugLabelNames, selectedLabels);
247    console.log(labels);
248    if (labels.added.length > 0 || labels.removed.length > 0) {
249      setLabelMutation({
250        variables: {
251          input: {
252            prefix: bug.id,
253            added: labels.added,
254            Removed: labels.removed,
255          },
256        },
257        refetchQueries: [
258          // TODO: update the cache instead of refetching
259          {
260            query: GetBugDocument,
261            variables: { id: bug.id },
262          },
263          {
264            query: ListLabelsDocument,
265          },
266        ],
267        awaitRefetchQueries: true,
268      })
269        .then((res) => {
270          console.log(res);
271          setSelectedLabels(selectedLabels);
272          setBugLabelNames(selectedLabels);
273        })
274        .catch((e) => console.log(e));
275    }
276  };
277
278  function isActive(key: string) {
279    return selectedLabels.includes(key);
280  }
281
282  //TODO label wont removed, if a filter hides it!
283  function createNewLabel(name: string) {
284    console.log('CREATE NEW LABEL: ' + name);
285    changeBugLabels(selectedLabels.concat([name]));
286  }
287
288  let labels: any = [];
289  if (
290    labelsData?.repository &&
291    labelsData.repository.validLabels &&
292    labelsData.repository.validLabels.nodes
293  ) {
294    labels = labelsData.repository.validLabels.nodes.map((node) => [
295      node.name,
296      node.name,
297      node.color,
298    ]);
299  }
300
301  return (
302    <FilterDropdown
303      onClose={() => changeBugLabels(selectedLabels)}
304      itemActive={isActive}
305      toggleLabel={toggleLabel}
306      dropdown={labels}
307      onNewItem={createNewLabel}
308      hasFilter
309    >
310      Labels
311    </FilterDropdown>
312  );
313}
314
315export default LabelMenu;