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: '1rem',
71 minHeight: '1rem',
72 display: 'flex',
73 backgroundColor: 'blue',
74 borderRadius: '0.25rem',
75 marginRight: '5px',
76 marginLeft: '3px',
77 },
78 labelsheader: {
79 display: 'flex',
80 flexDirection: 'row',
81 },
82 menuRow: {
83 display: 'flex',
84 alignItems: 'center',
85 flexWrap: 'wrap',
86 },
87}));
88
89const _rgb = (color: Color) =>
90 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
91
92// Create a style object from the label RGB colors
93const createStyle = (color: Color) => ({
94 backgroundColor: _rgb(color),
95 borderBottomColor: darken(_rgb(color), 0.2),
96});
97
98function FilterDropdown({
99 children,
100 dropdown,
101 hasFilter,
102 itemActive,
103 onClose,
104 toggleLabel,
105 onNewItem,
106}: FilterDropdownProps) {
107 const [open, setOpen] = useState(false);
108 const [filter, setFilter] = useState<string>('');
109 const buttonRef = useRef<HTMLButtonElement>(null);
110 const searchRef = useRef<HTMLButtonElement>(null);
111 const classes = useStyles({ active: false });
112
113 useEffect(() => {
114 searchRef && searchRef.current && searchRef.current.focus();
115 }, [filter]);
116
117 return (
118 <>
119 <div className={classes.labelsheader}>
120 Labels
121 <IconButton
122 ref={buttonRef}
123 onClick={() => setOpen(!open)}
124 className={classes.gearBtn}
125 >
126 <SettingsIcon fontSize={'small'} />
127 </IconButton>
128 </div>
129
130 <Menu
131 className={classes.menu}
132 getContentAnchorEl={null}
133 ref={searchRef}
134 anchorOrigin={{
135 vertical: 'bottom',
136 horizontal: 'left',
137 }}
138 transformOrigin={{
139 vertical: 'top',
140 horizontal: 'left',
141 }}
142 open={open}
143 onClose={() => {
144 setOpen(false);
145 onClose();
146 }}
147 onExited={() => setFilter('')}
148 anchorEl={buttonRef.current}
149 PaperProps={{
150 style: {
151 maxHeight: ITEM_HEIGHT * 4.5,
152 width: '25ch',
153 },
154 }}
155 >
156 {hasFilter && (
157 <CustomTextField
158 onChange={(e) => {
159 const { value } = e.target;
160 setFilter(value);
161 }}
162 onKeyDown={(e) => e.stopPropagation()}
163 value={filter}
164 label={`Filter ${children}`}
165 />
166 )}
167 {filter !== '' &&
168 dropdown.filter((d) => d[1].toLowerCase() === filter.toLowerCase())
169 .length <= 0 && (
170 <MenuItem
171 style={{ whiteSpace: 'normal', wordBreak: 'break-all' }}
172 onClick={() => {
173 onNewItem(filter);
174 setFilter('');
175 setOpen(false);
176 }}
177 >
178 Create new label '{filter}'
179 </MenuItem>
180 )}
181 {dropdown
182 .sort(function (x, y) {
183 // true values first
184 return itemActive(x[1]) === itemActive(y[1]) ? 0 : x ? -1 : 1;
185 })
186 .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase()))
187 .map(([key, value, color]) => (
188 <MenuItem
189 style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}
190 onClick={() => {
191 toggleLabel(key, itemActive(key));
192 }}
193 key={key}
194 selected={itemActive(key)}
195 >
196 <div className={classes.menuRow}>
197 {itemActive(key) && <CheckIcon fontSize={'small'} />}
198 <div
199 className={classes.labelcolor}
200 style={createStyle(color)}
201 />
202 {value}
203 </div>
204 </MenuItem>
205 ))}
206 </Menu>
207 </>
208 );
209}
210
211type Props = {
212 bug: BugFragment;
213};
214function LabelMenu({ bug }: Props) {
215 const { data: labelsData } = useListLabelsQuery();
216 const [bugLabelNames, setBugLabelNames] = useState(
217 bug.labels.map((l) => l.name)
218 );
219 const [selectedLabels, setSelectedLabels] = useState(
220 bug.labels.map((l) => l.name)
221 );
222
223 const [setLabelMutation] = useSetLabelMutation();
224
225 function toggleLabel(key: string, active: boolean) {
226 const labels: string[] = active
227 ? selectedLabels.filter((label) => label !== key)
228 : selectedLabels.concat([key]);
229 setSelectedLabels(labels);
230 console.log('toggle (selected)');
231 console.log(labels);
232 }
233
234 function diff(oldState: string[], newState: string[]) {
235 const added = newState.filter((x) => !oldState.includes(x));
236 const removed = oldState.filter((x) => !newState.includes(x));
237 return {
238 added: added,
239 removed: removed,
240 };
241 }
242
243 const changeBugLabels = (selectedLabels: string[]) => {
244 console.log('CBL');
245 console.log('selected labels');
246 console.log(selectedLabels);
247 console.log('buglabels');
248 console.log(bugLabelNames);
249 const labels = diff(bugLabelNames, selectedLabels);
250 console.log(labels);
251 if (labels.added.length > 0 || labels.removed.length > 0) {
252 setLabelMutation({
253 variables: {
254 input: {
255 prefix: bug.id,
256 added: labels.added,
257 Removed: labels.removed,
258 },
259 },
260 refetchQueries: [
261 // TODO: update the cache instead of refetching
262 {
263 query: GetBugDocument,
264 variables: { id: bug.id },
265 },
266 {
267 query: ListLabelsDocument,
268 },
269 ],
270 awaitRefetchQueries: true,
271 })
272 .then((res) => {
273 console.log(res);
274 setSelectedLabels(selectedLabels);
275 setBugLabelNames(selectedLabels);
276 })
277 .catch((e) => console.log(e));
278 }
279 };
280
281 function isActive(key: string) {
282 return selectedLabels.includes(key);
283 }
284
285 //TODO label wont removed, if a filter hides it!
286 function createNewLabel(name: string) {
287 console.log('CREATE NEW LABEL: ' + name);
288 changeBugLabels(selectedLabels.concat([name]));
289 }
290
291 let labels: any = [];
292 if (
293 labelsData?.repository &&
294 labelsData.repository.validLabels &&
295 labelsData.repository.validLabels.nodes
296 ) {
297 labels = labelsData.repository.validLabels.nodes.map((node) => [
298 node.name,
299 node.name,
300 node.color,
301 ]);
302 }
303
304 return (
305 <FilterDropdown
306 onClose={() => changeBugLabels(selectedLabels)}
307 itemActive={isActive}
308 toggleLabel={toggleLabel}
309 dropdown={labels}
310 onNewItem={createNewLabel}
311 hasFilter
312 >
313 Labels
314 </FilterDropdown>
315 );
316}
317
318export default LabelMenu;