From f750415d40d56ae889a588320069fd7bfa3ddc49 Mon Sep 17 00:00:00 2001 From: eburdekin Date: Tue, 23 Jun 2026 12:53:30 -0700 Subject: [PATCH 1/7] create new popup for version sorting/filtering --- .../ColumnHeaderPopups/VersionPopup.jsx | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx diff --git a/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx b/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx new file mode 100644 index 000000000..833651aa8 --- /dev/null +++ b/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx @@ -0,0 +1,325 @@ +import React, { useState, useContext } from "react"; +import PropTypes from "prop-types"; +import Button from "../../Button/Button"; +import RadioButton from "../../UI/RadioButton"; +import "react-datepicker/dist/react-datepicker.css"; +import { MdClose } from "react-icons/md"; +import { MdOutlineSearch } from "react-icons/md"; +import { createUseStyles } from "react-jss"; +import ToggleCheckbox from "components/UI/ToggleCheckbox"; +import { selectAllCheckboxes } from "helpers/util"; + +const useStyles = createUseStyles(theme => ({ + container: { + display: "flex", + flexDirection: "column", + maxWidth: "25rem", + color: theme.colors.secondary.darkNavy + }, + searchBarWrapper: { + width: "100%", + position: "relative", + alignSelf: "center", + marginBottom: "0.5rem" + }, + searchBar: { + maxWidth: "100%", + width: "100%", + padding: "12px 12px 12px 12px", + boxSizing: "border-box" + // marginRight: "0.5rem" + }, + searchIcon: { + position: "absolute", + right: "16px", + top: "14px" + }, + listItem: { + display: "flex", + flexDirection: "row", + alignItems: "center", + height: "2rem", + gap: "0.2em", + "&:hover": { + backgroundColor: "lightblue" + }, + "& span": { + maxWidth: "25ch", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis" + } + }, + toggleButton: { + marginRight: "0", + marginTop: "4px", + marginBottom: "4px", + backgroundColor: "transparent", + border: "0", + cursor: "pointer", + textDecoration: "underline", + display: "flex", + fontWeight: "normal", + color: theme.colors.secondary.darkNavy + } +})); + +const VersionPopup = ({ + projects, + filter, + close, + header, + criteria, + setCriteria, + order, + orderBy, + setSort, + setCheckedProjectIds, + setSelectAllChecked, + calculations +}) => { + const property = header.accessor || header.id; + + const classes = useStyles(); + + const [newOrder, setNewOrder] = useState( + header.id !== orderBy ? null : order + ); + const [selectedListItems, setSelectedListItems] = useState( + (criteria[header.id + "List"] || []).map(s => ({ + value: s, + label: s + })) + ); + const [searchString, setSearchString] = useState(""); + + const initiallyChecked = o => criteria[header.id + "List"].includes(o); + + // To build the drop-down list, we want to apply all the criteria that + // are currently selected EXCEPT the criteria we are currently editing. + const listCriteria = { ...criteria, [header.id + "List"]: [] }; + const filteredProjects = projects.filter(p => filter(p, listCriteria)); + + const getValue = p => { + if (property === "calculationId") { + return calculations[p.calculationId]?.version || "Beta"; + } + + return p[property]; + }; + + let filteredOptions; + + const compareVersions = (a, b) => { + const aChecked = initiallyChecked(a); + const bChecked = initiallyChecked(b); + + // Keep checked items at the top + if (aChecked !== bChecked) { + return bChecked - aChecked; + } + + // Beta is newest → always FIRST + if (a === "Beta" && b === "Beta") return 0; + if (a === "Beta") return -1; + if (b === "Beta") return 1; + + const aParts = String(a).split(".").map(Number); + const bParts = String(b).split(".").map(Number); + + const maxLength = Math.max(aParts.length, bParts.length); + + for (let i = 0; i < maxLength; i++) { + const aPart = aParts[i] ?? 0; + const bPart = bParts[i] ?? 0; + + if (aPart !== bPart) { + // 🔥 REVERSED ORDER (THIS IS THE KEY FIX) + return bPart - aPart; + } + } + + return 0; + }; + + filteredOptions = [...new Set(filteredProjects.map(getValue))] + .filter(value => value !== null && value !== "") + .sort(compareVersions); + + const onChangeSearchString = e => { + setSearchString(e.target.value); + }; + + const handleCheckboxChange = e => { + const optionValue = e.target.name; + if (!e.target.checked) { + const newSelectedListItems = selectedListItems.filter( + selectedOption => selectedOption.value !== optionValue + ); + setSelectedListItems(newSelectedListItems); + } else { + const newSelectedListItems = [ + ...selectedListItems, + { value: optionValue, label: optionValue } + ]; + setSelectedListItems(newSelectedListItems); + } + }; + + const isChecked = optionValue => { + const checked = selectedListItems.find( + option => option.value === optionValue + ); + return !!checked; + }; + + const applyChanges = () => { + let selectedValues = selectedListItems.map(sli => sli.value); + + setCriteria({ + ...criteria, + [header.id + "List"]: selectedValues + }); + + if (newOrder) { + setSort(header.id, newOrder); + } + if (setCheckedProjectIds) setCheckedProjectIds([]); + if (setSelectAllChecked) setSelectAllChecked(false); + close(); + }; + + const setDefault = () => { + setNewOrder(null); + setSelectedListItems([]); + if (setCheckedProjectIds) setCheckedProjectIds([]); + if (setSelectAllChecked) setSelectAllChecked(false); + }; + + return ( +
+
+ +
+
+ setNewOrder("asc")} + /> + setNewOrder("desc")} + /> +
+
+ +
+
+ +
-
+ +
+
{`${selectedListItems.length} selected`}
+
+
+ + +
+ +
+ {/*
{JSON.stringify(selectedListItems, null, 2)}
*/} + {/*
{JSON.stringify(options, null, 2)}
*/} + + {filteredOptions.map(o => { + const checked = isChecked(o); + + return ( +
+ + handleCheckboxChange({ + target: { + name: o, + checked: !isChecked(o) + } + }) + } + label={o} + /> + {o} +
+ ); + })} +
+ +
+
+ + +
+
+ ); +}; + +VersionPopup.propTypes = { + projects: PropTypes.any, + filter: PropTypes.func, + close: PropTypes.func, + header: PropTypes.any, + criteria: PropTypes.any, + setCriteria: PropTypes.func, + order: PropTypes.string, + orderBy: PropTypes.string, + setSort: PropTypes.func, + setCheckedProjectIds: PropTypes.func, + setSelectAllChecked: PropTypes.func, + droOptions: PropTypes.array +}; + +export default VersionPopup; From 49238e106ecdf0a82bb3cab9c96e6bb7b46c933d Mon Sep 17 00:00:00 2001 From: eburdekin Date: Tue, 23 Jun 2026 12:58:51 -0700 Subject: [PATCH 2/7] import versionpopup to projecttablecolumnheader --- .../ProjectTableColumnHeader.jsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.jsx b/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.jsx index 7649ca5a5..1e69f3539 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.jsx +++ b/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.jsx @@ -16,6 +16,7 @@ import NumberPopup from "./NumberPopup"; import BooleanPopup from "./BooleanPopup"; import VisibilityPopup from "./VisibilityPopup"; import StatusPopup from "./StatusPopup"; +import VersionPopup from "./VersionPopup"; import { useTheme } from "react-jss"; const useStyles = createUseStyles(theme => ({ @@ -107,7 +108,8 @@ const ProjectTableColumnHeader = ({ setSort, setCheckedProjectIds, setSelectAllChecked, - droOptions + droOptions, + calculations }) => { const theme = useTheme(); const classes = useStyles(theme); @@ -133,7 +135,8 @@ const ProjectTableColumnHeader = ({ header.popupType === "text" || header.popupType === "string" || header.popupType === "number" || - header.popupType === "stringList" + header.popupType === "stringList" || + header.popupType === "version" ) { const listPropertyName = propertyName + "List"; const listValue = criteria[listPropertyName]; @@ -356,6 +359,21 @@ const ProjectTableColumnHeader = ({ setCheckedProjectIds={setCheckedProjectIds} setSelectAllChecked={setSelectAllChecked} /> + ) : header.popupType === "version" ? ( + handlePopoverToggle(false)} + header={header} + criteria={criteria} + setCriteria={setCriteria} + order={order} + orderBy={orderBy} + setSort={setSort} + setCheckedProjectIds={setCheckedProjectIds} + setSelectAllChecked={setSelectAllChecked} + projects={projects} + filter={filter} + calculations={calculations} + /> ) : null} } From c004eb5e6c3570a1ad8511211ce3f2804376263f Mon Sep 17 00:00:00 2001 From: eburdekin Date: Tue, 23 Jun 2026 13:00:20 -0700 Subject: [PATCH 3/7] add sorting by version to projectspage --- .../src/components/Projects/ProjectsPage.jsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/client/src/components/Projects/ProjectsPage.jsx b/client/src/components/Projects/ProjectsPage.jsx index a3d257cb3..db0bc2b20 100644 --- a/client/src/components/Projects/ProjectsPage.jsx +++ b/client/src/components/Projects/ProjectsPage.jsx @@ -4,6 +4,7 @@ import { formatId } from "../../helpers/util"; import { useNavigate } from "react-router-dom"; import { createUseStyles, useTheme } from "react-jss"; import UserContext from "../../contexts/UserContext"; +import CalculationsContext from "../../contexts/CalculationsContext"; import { MdOutlineSearch } from "react-icons/md"; import Pagination from "../UI/Pagination"; import ContentContainerNoSidebar from "../Layout/ContentContainerNoSidebar"; @@ -302,6 +303,7 @@ const ProjectsPage = ({ contentContainerRef }) => { const isAdmin = userContext.account?.isAdmin || false; const loginId = userContext.account?.id || null; const isSubmittingSnapshot = useRef(false); + const calculations = useContext(CalculationsContext); useEffect(() => { fetchDroOptions(setDroOptions); @@ -650,6 +652,33 @@ const ProjectsPage = ({ contentContainerRef }) => { } else if (orderBy === "id") { projectA = a.id !== undefined && a.id !== null ? a.id : null; projectB = b.id !== undefined && b.id !== null ? b.id : null; + } else if (orderBy === "calculationId") { + const aVal = a.calculationId ?? null; + const bVal = b.calculationId ?? null; + + const isBetaA = aVal === "Beta"; + const isBetaB = bVal === "Beta"; + + // Beta always last (ASC) + if (isBetaA && isBetaB) return 0; + if (isBetaA) return 1; + if (isBetaB) return -1; + + const aParts = String(aVal).split(".").map(Number); + const bParts = String(bVal).split(".").map(Number); + + const len = Math.max(aParts.length, bParts.length); + + for (let i = 0; i < len; i++) { + const aNum = aParts[i] ?? 0; + const bNum = bParts[i] ?? 0; + + if (aNum !== bNum) { + return aNum - bNum; + } + } + + return 0; } else { projectA = a[orderBy] ? a[orderBy].toLowerCase() : ""; projectB = b[orderBy] ? b[orderBy].toLowerCase() : ""; @@ -1032,7 +1061,7 @@ const ProjectsPage = ({ contentContainerRef }) => { { id: "calculationId", label: "Guidelines Version", - popupType: "text", + popupType: "version", accessor: "calculationId", colWidth: "10rem" } @@ -1215,6 +1244,7 @@ const ProjectsPage = ({ contentContainerRef }) => { setCheckedProjectIds={setCheckedProjectIds} setSelectAllChecked={setSelectAllChecked} droOptions={droOptions} + calculations={calculations} /> ); From 37d3941aa34c893dd0c22a08196e10b24b2bff12 Mon Sep 17 00:00:00 2001 From: eburdekin Date: Tue, 23 Jun 2026 13:29:34 -0700 Subject: [PATCH 4/7] fix sorting order for version --- .../ColumnHeaderPopups/VersionPopup.jsx | 10 ++--- .../src/components/Projects/ProjectsPage.jsx | 40 ++++++++++--------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx b/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx index 833651aa8..8d9a638c6 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx +++ b/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx @@ -78,7 +78,7 @@ const VersionPopup = ({ setSelectAllChecked, calculations }) => { - const property = header.accessor || header.id; + const property = header.id; const classes = useStyles(); @@ -100,11 +100,12 @@ const VersionPopup = ({ const listCriteria = { ...criteria, [header.id + "List"]: [] }; const filteredProjects = projects.filter(p => filter(p, listCriteria)); + const isCalculationColumn = property === "calculationId"; + const getValue = p => { if (property === "calculationId") { - return calculations[p.calculationId]?.version || "Beta"; + return calculations?.[p.calculationId]?.version ?? "Beta"; } - return p[property]; }; @@ -264,9 +265,6 @@ const VersionPopup = ({
- {/*
{JSON.stringify(selectedListItems, null, 2)}
*/} - {/*
{JSON.stringify(options, null, 2)}
*/} - {filteredOptions.map(o => { const checked = isChecked(o); diff --git a/client/src/components/Projects/ProjectsPage.jsx b/client/src/components/Projects/ProjectsPage.jsx index db0bc2b20..6d6de92dd 100644 --- a/client/src/components/Projects/ProjectsPage.jsx +++ b/client/src/components/Projects/ProjectsPage.jsx @@ -60,7 +60,8 @@ const DEFAULT_FILTER_CRITERIA = { droList: [], adminNotesList: [], startDateModifiedAdmin: null, - endDateModifiedAdmin: null + endDateModifiedAdmin: null, + calculationIdList: [] }; const useStyles = createUseStyles(theme => ({ @@ -612,6 +613,9 @@ const ProjectsPage = ({ contentContainerRef }) => { setSelectAllChecked(!selectAllChecked); }; + const getCalculationVersion = (p, calculations) => + calculations?.[p.calculationId]?.version ?? "Beta"; + const ascCompareBy = (a, b, orderBy) => { let projectA, projectB; @@ -653,16 +657,14 @@ const ProjectsPage = ({ contentContainerRef }) => { projectA = a.id !== undefined && a.id !== null ? a.id : null; projectB = b.id !== undefined && b.id !== null ? b.id : null; } else if (orderBy === "calculationId") { - const aVal = a.calculationId ?? null; - const bVal = b.calculationId ?? null; + const aVal = getCalculationVersion(a, calculations); + const bVal = getCalculationVersion(b, calculations); - const isBetaA = aVal === "Beta"; - const isBetaB = bVal === "Beta"; + if (aVal === bVal) return 0; - // Beta always last (ASC) - if (isBetaA && isBetaB) return 0; - if (isBetaA) return 1; - if (isBetaB) return -1; + // natural version compare (old → new) + if (aVal === "Beta") return 1; + if (bVal === "Beta") return -1; const aParts = String(aVal).split(".").map(Number); const bParts = String(bVal).split(".").map(Number); @@ -670,12 +672,8 @@ const ProjectsPage = ({ contentContainerRef }) => { const len = Math.max(aParts.length, bParts.length); for (let i = 0; i < len; i++) { - const aNum = aParts[i] ?? 0; - const bNum = bParts[i] ?? 0; - - if (aNum !== bNum) { - return aNum - bNum; - } + const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0); + if (diff !== 0) return diff; } return 0; @@ -702,9 +700,15 @@ const ProjectsPage = ({ contentContainerRef }) => { }; const getComparator = (order, orderBy) => { - return order === "asc" - ? (a, b) => ascCompareBy(a, b, orderBy) - : (a, b) => -ascCompareBy(a, b, orderBy); + return (a, b) => { + const result = ascCompareBy(a, b, orderBy); + + if (orderBy === "calculationId") { + return order === "asc" ? -result : result; + } + + return order === "asc" ? result : -result; + }; }; const setSort = (orderBy, order, isStatus = false) => { From 7c7d6140f6163983fa6b6b264b9ff62b14c03da1 Mon Sep 17 00:00:00 2001 From: eburdekin Date: Tue, 23 Jun 2026 13:33:44 -0700 Subject: [PATCH 5/7] fix search on popup --- .../src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx b/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx index 8d9a638c6..7379a44a0 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx +++ b/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx @@ -145,6 +145,7 @@ const VersionPopup = ({ filteredOptions = [...new Set(filteredProjects.map(getValue))] .filter(value => value !== null && value !== "") + .filter(value => value.toLowerCase().includes(searchString.toLowerCase())) .sort(compareVersions); const onChangeSearchString = e => { From 05b37d31a313d45f7beaf5f9b95d01ea270d0581 Mon Sep 17 00:00:00 2001 From: eburdekin Date: Tue, 23 Jun 2026 13:37:50 -0700 Subject: [PATCH 6/7] fix filtering --- client/src/components/Projects/ProjectsPage.jsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/components/Projects/ProjectsPage.jsx b/client/src/components/Projects/ProjectsPage.jsx index 6d6de92dd..5630379ef 100644 --- a/client/src/components/Projects/ProjectsPage.jsx +++ b/client/src/components/Projects/ProjectsPage.jsx @@ -823,6 +823,15 @@ const ProjectsPage = ({ contentContainerRef }) => { return false; } + if ( + criteria.calculationIdList?.length > 0 && + !criteria.calculationIdList.includes( + calculations?.[p.calculationId]?.version ?? "Beta" + ) + ) { + return false; + } + // fullName attr allows searching by full name, not just by first or last name p["fullname"] = `${p["lastName"]}, ${p["firstName"]}`; if ( From cf2b95afa9c52a48a4eb07a6ffd7ecdcdb876993 Mon Sep 17 00:00:00 2001 From: eburdekin Date: Tue, 23 Jun 2026 13:41:05 -0700 Subject: [PATCH 7/7] remove comments --- .../components/Projects/ColumnHeaderPopups/VersionPopup.jsx | 5 ----- client/src/components/Projects/ProjectsPage.jsx | 1 - 2 files changed, 6 deletions(-) diff --git a/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx b/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx index 7379a44a0..75deae01f 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx +++ b/client/src/components/Projects/ColumnHeaderPopups/VersionPopup.jsx @@ -95,13 +95,9 @@ const VersionPopup = ({ const initiallyChecked = o => criteria[header.id + "List"].includes(o); - // To build the drop-down list, we want to apply all the criteria that - // are currently selected EXCEPT the criteria we are currently editing. const listCriteria = { ...criteria, [header.id + "List"]: [] }; const filteredProjects = projects.filter(p => filter(p, listCriteria)); - const isCalculationColumn = property === "calculationId"; - const getValue = p => { if (property === "calculationId") { return calculations?.[p.calculationId]?.version ?? "Beta"; @@ -135,7 +131,6 @@ const VersionPopup = ({ const bPart = bParts[i] ?? 0; if (aPart !== bPart) { - // 🔥 REVERSED ORDER (THIS IS THE KEY FIX) return bPart - aPart; } } diff --git a/client/src/components/Projects/ProjectsPage.jsx b/client/src/components/Projects/ProjectsPage.jsx index 5630379ef..01f38cf0b 100644 --- a/client/src/components/Projects/ProjectsPage.jsx +++ b/client/src/components/Projects/ProjectsPage.jsx @@ -662,7 +662,6 @@ const ProjectsPage = ({ contentContainerRef }) => { if (aVal === bVal) return 0; - // natural version compare (old → new) if (aVal === "Beta") return 1; if (bVal === "Beta") return -1;