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