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;