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 {dropdown
172 .sort(function (x, y) {
173 // true values first
174 return itemActive(x[1]) === itemActive(y[1]) ? 0 : x ? -1 : 1;
175 })
176 .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase()))
177 .map(([key, value, color]) => (
178 <MenuItem
179 style={{ whiteSpace: 'normal', wordBreak: 'break-all' }}
180 onClick={() => {
181 toggleLabel(key, itemActive(key));
182 }}
183 key={key}
184 className={itemActive(key) ? classes.itemActive : undefined}
185 >
186 <div className={classes.menuRow}>
187 {itemActive(key) ? <CheckIcon fontSize={'small'} /> : null}
188 <div
189 className={classes.labelcolor}
190 style={createStyle(color)}
191 />
192 {value}
193 </div>
194 </MenuItem>
195 ))}
196 {filter !== '' &&
197 dropdown.filter((d) => d[1].toLowerCase() === filter.toLowerCase())
198 .length <= 0 && (
199 <MenuItem
200 style={{ whiteSpace: 'normal', wordBreak: 'break-all' }}
201 onClick={() => {
202 onNewItem(filter);
203 setFilter('');
204 setOpen(false);
205 }}
206 >
207 Create new label '{filter}'
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 useEffect(() => {});
230 function toggleLabel(key: string, active: boolean) {
231 const labels: string[] = active
232 ? selectedLabels.filter((label) => label !== key)
233 : selectedLabels.concat([key]);
234 setSelectedLabels(labels);
235 console.log('toggle (selected)');
236 console.log(labels);
237 }
238
239 function diff(oldState: string[], newState: string[]) {
240 console.log('oldState / Buglabels');
241 console.log(oldState);
242 console.log('newState / Selected');
243 console.log(newState);
244 const added = newState.filter((x) => !oldState.includes(x));
245 const removed = oldState.filter((x) => !newState.includes(x));
246 return {
247 added: added,
248 removed: removed,
249 };
250 }
251
252 const changeBugLabels = (
253 bugLabels = bug.labels.map((l) => l.name),
254 selectedLabel = selectedLabels
255 ) => {
256 const labels = diff(bugLabels, selectedLabel);
257 console.log('changeBugLabels');
258 console.log(labels);
259 console.log('bugLabelNames');
260 console.log(bugLabelNames);
261 if (labels.added.length > 0 || labels.removed.length > 0) {
262 setLabelMutation({
263 variables: {
264 input: {
265 prefix: bug.id,
266 added: labels.added,
267 Removed: labels.removed,
268 },
269 },
270 refetchQueries: [
271 // TODO: update the cache instead of refetching
272 {
273 query: GetBugDocument,
274 variables: { id: bug.id },
275 },
276 {
277 query: ListLabelsDocument,
278 },
279 ],
280 awaitRefetchQueries: true,
281 })
282 .then((res) => {
283 console.log(res);
284 setBugLabelNames(selectedLabels);
285 })
286 .catch((e) => console.log(e));
287 }
288 };
289
290 function isActive(key: string) {
291 return selectedLabels.includes(key);
292 }
293
294 function createNewLabel(name: string) {
295 console.log('CREATE NEW LABEL');
296 setLabelMutation({
297 variables: {
298 input: {
299 prefix: bug.id,
300 added: [name],
301 },
302 },
303 refetchQueries: [
304 // TODO: update the cache instead of refetching
305 {
306 query: GetBugDocument,
307 variables: { id: bug.id },
308 },
309 {
310 query: ListLabelsDocument,
311 },
312 ],
313 awaitRefetchQueries: true,
314 })
315 .then((res) => {
316 console.log(res);
317
318 const tmp = selectedLabels.concat([name]);
319 console.log(tmp);
320 console.log('tmp');
321 setSelectedLabels(tmp);
322 setBugLabelNames(bugLabelNames.concat([name]));
323
324 changeBugLabels(bugLabelNames.concat([name]), tmp);
325 })
326 .catch((e) => console.log('createnewLabelError' + e));
327 }
328
329 let labels: any = [];
330 if (
331 labelsData?.repository &&
332 labelsData.repository.validLabels &&
333 labelsData.repository.validLabels.nodes
334 ) {
335 labels = labelsData.repository.validLabels.nodes.map((node) => [
336 node.name,
337 node.name,
338 node.color,
339 ]);
340 }
341
342 return (
343 <FilterDropdown
344 onClose={changeBugLabels}
345 itemActive={isActive}
346 toggleLabel={toggleLabel}
347 dropdown={labels}
348 onNewItem={createNewLabel}
349 hasFilter
350 >
351 Labels
352 </FilterDropdown>
353 );
354}
355
356export default LabelMenu;