Skip to content

Commit 9212658

Browse files
authored
3108 filter projects by guidelines version (#3335)
* create new popup for version sorting/filtering * import versionpopup to projecttablecolumnheader * add sorting by version to projectspage * fix sorting order for version * fix search on popup * fix filtering * remove comments
1 parent 7827eb8 commit 9212658

3 files changed

Lines changed: 386 additions & 7 deletions

File tree

client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.jsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import NumberPopup from "./NumberPopup";
1616
import BooleanPopup from "./BooleanPopup";
1717
import VisibilityPopup from "./VisibilityPopup";
1818
import StatusPopup from "./StatusPopup";
19+
import VersionPopup from "./VersionPopup";
1920
import { useTheme } from "react-jss";
2021

2122
const useStyles = createUseStyles(theme => ({
@@ -107,7 +108,8 @@ const ProjectTableColumnHeader = ({
107108
setSort,
108109
setCheckedProjectIds,
109110
setSelectAllChecked,
110-
droOptions
111+
droOptions,
112+
calculations
111113
}) => {
112114
const theme = useTheme();
113115
const classes = useStyles(theme);
@@ -133,7 +135,8 @@ const ProjectTableColumnHeader = ({
133135
header.popupType === "text" ||
134136
header.popupType === "string" ||
135137
header.popupType === "number" ||
136-
header.popupType === "stringList"
138+
header.popupType === "stringList" ||
139+
header.popupType === "version"
137140
) {
138141
const listPropertyName = propertyName + "List";
139142
const listValue = criteria[listPropertyName];
@@ -356,6 +359,21 @@ const ProjectTableColumnHeader = ({
356359
setCheckedProjectIds={setCheckedProjectIds}
357360
setSelectAllChecked={setSelectAllChecked}
358361
/>
362+
) : header.popupType === "version" ? (
363+
<VersionPopup
364+
close={() => handlePopoverToggle(false)}
365+
header={header}
366+
criteria={criteria}
367+
setCriteria={setCriteria}
368+
order={order}
369+
orderBy={orderBy}
370+
setSort={setSort}
371+
setCheckedProjectIds={setCheckedProjectIds}
372+
setSelectAllChecked={setSelectAllChecked}
373+
projects={projects}
374+
filter={filter}
375+
calculations={calculations}
376+
/>
359377
) : null}
360378
</div>
361379
}
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import React, { useState, useContext } from "react";
2+
import PropTypes from "prop-types";
3+
import Button from "../../Button/Button";
4+
import RadioButton from "../../UI/RadioButton";
5+
import "react-datepicker/dist/react-datepicker.css";
6+
import { MdClose } from "react-icons/md";
7+
import { MdOutlineSearch } from "react-icons/md";
8+
import { createUseStyles } from "react-jss";
9+
import ToggleCheckbox from "components/UI/ToggleCheckbox";
10+
import { selectAllCheckboxes } from "helpers/util";
11+
12+
const useStyles = createUseStyles(theme => ({
13+
container: {
14+
display: "flex",
15+
flexDirection: "column",
16+
maxWidth: "25rem",
17+
color: theme.colors.secondary.darkNavy
18+
},
19+
searchBarWrapper: {
20+
width: "100%",
21+
position: "relative",
22+
alignSelf: "center",
23+
marginBottom: "0.5rem"
24+
},
25+
searchBar: {
26+
maxWidth: "100%",
27+
width: "100%",
28+
padding: "12px 12px 12px 12px",
29+
boxSizing: "border-box"
30+
// marginRight: "0.5rem"
31+
},
32+
searchIcon: {
33+
position: "absolute",
34+
right: "16px",
35+
top: "14px"
36+
},
37+
listItem: {
38+
display: "flex",
39+
flexDirection: "row",
40+
alignItems: "center",
41+
height: "2rem",
42+
gap: "0.2em",
43+
"&:hover": {
44+
backgroundColor: "lightblue"
45+
},
46+
"& span": {
47+
maxWidth: "25ch",
48+
whiteSpace: "nowrap",
49+
overflow: "hidden",
50+
textOverflow: "ellipsis"
51+
}
52+
},
53+
toggleButton: {
54+
marginRight: "0",
55+
marginTop: "4px",
56+
marginBottom: "4px",
57+
backgroundColor: "transparent",
58+
border: "0",
59+
cursor: "pointer",
60+
textDecoration: "underline",
61+
display: "flex",
62+
fontWeight: "normal",
63+
color: theme.colors.secondary.darkNavy
64+
}
65+
}));
66+
67+
const VersionPopup = ({
68+
projects,
69+
filter,
70+
close,
71+
header,
72+
criteria,
73+
setCriteria,
74+
order,
75+
orderBy,
76+
setSort,
77+
setCheckedProjectIds,
78+
setSelectAllChecked,
79+
calculations
80+
}) => {
81+
const property = header.id;
82+
83+
const classes = useStyles();
84+
85+
const [newOrder, setNewOrder] = useState(
86+
header.id !== orderBy ? null : order
87+
);
88+
const [selectedListItems, setSelectedListItems] = useState(
89+
(criteria[header.id + "List"] || []).map(s => ({
90+
value: s,
91+
label: s
92+
}))
93+
);
94+
const [searchString, setSearchString] = useState("");
95+
96+
const initiallyChecked = o => criteria[header.id + "List"].includes(o);
97+
98+
const listCriteria = { ...criteria, [header.id + "List"]: [] };
99+
const filteredProjects = projects.filter(p => filter(p, listCriteria));
100+
101+
const getValue = p => {
102+
if (property === "calculationId") {
103+
return calculations?.[p.calculationId]?.version ?? "Beta";
104+
}
105+
return p[property];
106+
};
107+
108+
let filteredOptions;
109+
110+
const compareVersions = (a, b) => {
111+
const aChecked = initiallyChecked(a);
112+
const bChecked = initiallyChecked(b);
113+
114+
// Keep checked items at the top
115+
if (aChecked !== bChecked) {
116+
return bChecked - aChecked;
117+
}
118+
119+
// Beta is newest → always FIRST
120+
if (a === "Beta" && b === "Beta") return 0;
121+
if (a === "Beta") return -1;
122+
if (b === "Beta") return 1;
123+
124+
const aParts = String(a).split(".").map(Number);
125+
const bParts = String(b).split(".").map(Number);
126+
127+
const maxLength = Math.max(aParts.length, bParts.length);
128+
129+
for (let i = 0; i < maxLength; i++) {
130+
const aPart = aParts[i] ?? 0;
131+
const bPart = bParts[i] ?? 0;
132+
133+
if (aPart !== bPart) {
134+
return bPart - aPart;
135+
}
136+
}
137+
138+
return 0;
139+
};
140+
141+
filteredOptions = [...new Set(filteredProjects.map(getValue))]
142+
.filter(value => value !== null && value !== "")
143+
.filter(value => value.toLowerCase().includes(searchString.toLowerCase()))
144+
.sort(compareVersions);
145+
146+
const onChangeSearchString = e => {
147+
setSearchString(e.target.value);
148+
};
149+
150+
const handleCheckboxChange = e => {
151+
const optionValue = e.target.name;
152+
if (!e.target.checked) {
153+
const newSelectedListItems = selectedListItems.filter(
154+
selectedOption => selectedOption.value !== optionValue
155+
);
156+
setSelectedListItems(newSelectedListItems);
157+
} else {
158+
const newSelectedListItems = [
159+
...selectedListItems,
160+
{ value: optionValue, label: optionValue }
161+
];
162+
setSelectedListItems(newSelectedListItems);
163+
}
164+
};
165+
166+
const isChecked = optionValue => {
167+
const checked = selectedListItems.find(
168+
option => option.value === optionValue
169+
);
170+
return !!checked;
171+
};
172+
173+
const applyChanges = () => {
174+
let selectedValues = selectedListItems.map(sli => sli.value);
175+
176+
setCriteria({
177+
...criteria,
178+
[header.id + "List"]: selectedValues
179+
});
180+
181+
if (newOrder) {
182+
setSort(header.id, newOrder);
183+
}
184+
if (setCheckedProjectIds) setCheckedProjectIds([]);
185+
if (setSelectAllChecked) setSelectAllChecked(false);
186+
close();
187+
};
188+
189+
const setDefault = () => {
190+
setNewOrder(null);
191+
setSelectedListItems([]);
192+
if (setCheckedProjectIds) setCheckedProjectIds([]);
193+
if (setSelectAllChecked) setSelectAllChecked(false);
194+
};
195+
196+
return (
197+
<div className={classes.container}>
198+
<div style={{ display: "flex", justifyContent: "flex-end" }}>
199+
<MdClose
200+
style={{
201+
backgroundColor: "transparent",
202+
color: "black",
203+
position: "absolute",
204+
top: "0.5rem",
205+
right: "0.5rem"
206+
}}
207+
alt={`Close popup`}
208+
onClick={close}
209+
/>
210+
</div>
211+
<div style={{ display: "flex", flexDirection: "column" }}>
212+
<RadioButton
213+
label="Sort Newest to Oldest"
214+
value="asc"
215+
checked={newOrder === "asc"}
216+
onChange={() => setNewOrder("asc")}
217+
/>
218+
<RadioButton
219+
label="Sort Oldest to Newest"
220+
value="desc"
221+
checked={newOrder === "desc"}
222+
onChange={() => setNewOrder("desc")}
223+
/>
224+
<hr style={{ width: "100%" }} />
225+
</div>
226+
227+
<div
228+
style={{
229+
display: "flex",
230+
justifyContent: "space-between",
231+
alignItems: "baseline"
232+
}}
233+
>
234+
<div style={{ display: "flex" }}>
235+
<button
236+
className={classes.toggleButton}
237+
onClick={() =>
238+
selectAllCheckboxes(filteredOptions, setSelectedListItems)
239+
}
240+
>
241+
Select all {filteredOptions.length}
242+
</button>
243+
<div style={{ display: "flex", alignItems: "center" }}>-</div>
244+
<button
245+
className={classes.toggleButton}
246+
onClick={() => setSelectedListItems([])}
247+
>
248+
Clear
249+
</button>
250+
</div>
251+
<div>{`${selectedListItems.length} selected`}</div>
252+
</div>
253+
<div className={classes.searchBarWrapper}>
254+
<input
255+
type="text"
256+
value={searchString}
257+
onChange={onChangeSearchString}
258+
className={classes.searchBar}
259+
/>
260+
<MdOutlineSearch className={classes.searchIcon} alt="Search Icon" />
261+
</div>
262+
263+
<div style={{ overflow: "auto", maxHeight: "12rem" }}>
264+
{filteredOptions.map(o => {
265+
const checked = isChecked(o);
266+
267+
return (
268+
<div key={o} className={classes.listItem}>
269+
<ToggleCheckbox
270+
checked={checked}
271+
onChange={() =>
272+
handleCheckboxChange({
273+
target: {
274+
name: o,
275+
checked: !isChecked(o)
276+
}
277+
})
278+
}
279+
label={o}
280+
/>
281+
<span>{o}</span>
282+
</div>
283+
);
284+
})}
285+
</div>
286+
287+
<hr style={{ width: "100%" }} />
288+
<div style={{ display: "flex", justifyContent: "center" }}>
289+
<Button onClick={setDefault} variant="outlined">
290+
Reset
291+
</Button>
292+
<Button
293+
onClick={applyChanges}
294+
variant="contained"
295+
color={"colorPrimary"}
296+
>
297+
Apply
298+
</Button>
299+
</div>
300+
</div>
301+
);
302+
};
303+
304+
VersionPopup.propTypes = {
305+
projects: PropTypes.any,
306+
filter: PropTypes.func,
307+
close: PropTypes.func,
308+
header: PropTypes.any,
309+
criteria: PropTypes.any,
310+
setCriteria: PropTypes.func,
311+
order: PropTypes.string,
312+
orderBy: PropTypes.string,
313+
setSort: PropTypes.func,
314+
setCheckedProjectIds: PropTypes.func,
315+
setSelectAllChecked: PropTypes.func,
316+
droOptions: PropTypes.array
317+
};
318+
319+
export default VersionPopup;

0 commit comments

Comments
 (0)