From dd21de9882604633eb605892d9fbc2c61739d47a Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Fri, 26 Apr 2024 16:05:54 -0500 Subject: [PATCH 01/15] Team Results page now gets settings from query parameters --- .../components/team-results/TeamResults.jsx | 34 +++++++++++++++++-- .../team-results/TeamSummaryCard.jsx | 11 +++--- .../components/team-results/TeamsActions.jsx | 14 +++----- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/web-ui/src/components/team-results/TeamResults.jsx b/web-ui/src/components/team-results/TeamResults.jsx index 4aaec41680..3712b16886 100644 --- a/web-ui/src/components/team-results/TeamResults.jsx +++ b/web-ui/src/components/team-results/TeamResults.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { styled } from '@mui/material/styles'; import TeamSummaryCard from './TeamSummaryCard'; import { AppContext } from '../../context/AppContext'; @@ -38,7 +38,9 @@ const displayName = 'TeamResults'; const TeamResults = () => { const { state } = useContext(AppContext); const loading = selectTeamsLoading(state); + const [open, setOpen] = useState(false); const [searchText, setSearchText] = useState(''); + const [selectedTeamId, setSelectedTeamId] = useState(''); const teams = selectNormalizedTeams(state, searchText); const teamCards = teams.map((team, index) => { @@ -47,10 +49,38 @@ const TeamResults = () => { key={`team-summary-${team.id}`} index={index} team={team} + onTeamSelect={setSelectedTeamId} + selectedTeamId={selectedTeamId} /> ); }); + useEffect(() => { + const url = new URL(location.href); + + const addNew = url.searchParams.get('addNew'); + setOpen(addNew === 'true'); + + const search = url.searchParams.get('search') || ''; + setSearchText(search); + + const selectedTeamId = url.searchParams.get('team') || ''; + setSelectedTeamId(selectedTeamId); + }, []); + + useEffect(() => { + const url = new URL(location.href); + let newUrl = url.origin + url.pathname; + const params = {}; + if (open) params.addNew = true; + if (searchText) params.search = searchText; + if (selectedTeamId) params.team = selectedTeamId; + if (Object.keys(params).length) { + newUrl += '?' + new URLSearchParams(params).toString(); + } + history.replaceState(params, '', newUrl); + }, [open, searchText, selectedTeamId]); + return (
@@ -63,7 +93,7 @@ const TeamResults = () => { setSearchText(e.target.value); }} /> - +
{loading diff --git a/web-ui/src/components/team-results/TeamSummaryCard.jsx b/web-ui/src/components/team-results/TeamSummaryCard.jsx index 06ff4ec5c8..18a2269d97 100644 --- a/web-ui/src/components/team-results/TeamSummaryCard.jsx +++ b/web-ui/src/components/team-results/TeamSummaryCard.jsx @@ -55,10 +55,9 @@ const propTypes = { const displayName = 'TeamSummaryCard'; -const TeamSummaryCard = ({ team, index }) => { +const TeamSummaryCard = ({ team, index, onTeamSelect, selectedTeamId }) => { const { state, dispatch } = useContext(AppContext); const { teams, userProfile, csrf } = state; - const [open, setOpen] = useState(false); const [openDelete, setOpenDelete] = useState(false); const [tooltipIsOpen, setTooltipIsOpen] = useState(false); @@ -79,10 +78,8 @@ const TeamSummaryCard = ({ team, index }) => { ? false : leads.some(lead => lead.memberId === userProfile.memberProfile.id); - const handleOpen = () => setOpen(true); const handleOpenDeleteConfirmation = () => setOpenDelete(true); - const handleClose = () => setOpen(false); const handleCloseDeleteConfirmation = () => setOpenDelete(false); const teamId = team?.id; @@ -113,7 +110,7 @@ const TeamSummaryCard = ({ team, index }) => { const handleAction = (e, index) => { if (index === 0) { - handleOpen(); + onTeamSelect(team.id); } else if (index === 1) { handleOpenDeleteConfirmation(); } @@ -216,8 +213,8 @@ const TeamSummaryCard = ({ team, index }) => { onTeamSelect('')} onSave={async editedTeam => { const res = await updateTeam(editedTeam, csrf); const data = diff --git a/web-ui/src/components/team-results/TeamsActions.jsx b/web-ui/src/components/team-results/TeamsActions.jsx index 8b21be5650..8db7309d9f 100644 --- a/web-ui/src/components/team-results/TeamsActions.jsx +++ b/web-ui/src/components/team-results/TeamsActions.jsx @@ -12,24 +12,18 @@ import './TeamResults.css'; const displayName = 'TeamsActions'; -const TeamsActions = () => { +const TeamsActions = ({ isOpen, onOpen }) => { const { state, dispatch } = useContext(AppContext); - const [open, setOpen] = useState(false); - const { csrf } = state; - const handleOpen = () => setOpen(true); - - const handleClose = () => setOpen(false); - return (
- onOpen(false)} onSave={async team => { if (csrf) { let res = await createTeam(team, csrf); From 33890075ffd30c34064bd42417a01c0f52baa335 Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Fri, 26 Apr 2024 16:08:18 -0500 Subject: [PATCH 02/15] cleanup for 2265 --- web-ui/src/components/team-results/TeamResults.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web-ui/src/components/team-results/TeamResults.jsx b/web-ui/src/components/team-results/TeamResults.jsx index 3712b16886..f6106cdbf2 100644 --- a/web-ui/src/components/team-results/TeamResults.jsx +++ b/web-ui/src/components/team-results/TeamResults.jsx @@ -38,7 +38,7 @@ const displayName = 'TeamResults'; const TeamResults = () => { const { state } = useContext(AppContext); const loading = selectTeamsLoading(state); - const [open, setOpen] = useState(false); + const [addingTeam, setAddingTeam] = useState(false); const [searchText, setSearchText] = useState(''); const [selectedTeamId, setSelectedTeamId] = useState(''); const teams = selectNormalizedTeams(state, searchText); @@ -59,7 +59,7 @@ const TeamResults = () => { const url = new URL(location.href); const addNew = url.searchParams.get('addNew'); - setOpen(addNew === 'true'); + setAddingTeam(addNew === 'true'); const search = url.searchParams.get('search') || ''; setSearchText(search); @@ -72,14 +72,14 @@ const TeamResults = () => { const url = new URL(location.href); let newUrl = url.origin + url.pathname; const params = {}; - if (open) params.addNew = true; + if (addingTeam) params.addNew = true; if (searchText) params.search = searchText; if (selectedTeamId) params.team = selectedTeamId; if (Object.keys(params).length) { newUrl += '?' + new URLSearchParams(params).toString(); } history.replaceState(params, '', newUrl); - }, [open, searchText, selectedTeamId]); + }, [addingTeam, searchText, selectedTeamId]); return ( @@ -93,7 +93,7 @@ const TeamResults = () => { setSearchText(e.target.value); }} /> - +
{loading From 4a6b6dd1c099fdc00714af76ae4a9b8fd450dd66 Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Fri, 26 Apr 2024 16:32:21 -0500 Subject: [PATCH 03/15] View Feedback page now gets settings from query parameters --- web-ui/src/pages/ViewFeedbackPage.jsx | 36 ++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/web-ui/src/pages/ViewFeedbackPage.jsx b/web-ui/src/pages/ViewFeedbackPage.jsx index 004a5872c0..8b56c0c051 100644 --- a/web-ui/src/pages/ViewFeedbackPage.jsx +++ b/web-ui/src/pages/ViewFeedbackPage.jsx @@ -107,6 +107,36 @@ const ViewFeedbackPage = () => { const [dateRange, setDateRange] = useState(DateRange.THREE_MONTHS); const [includeAll, setIncludeAll] = useState(false); + useEffect(() => { + const url = new URL(location.href); + + const dates = url.searchParams.get('dates') || ''; + setDateRange(dates); + + const search = url.searchParams.get('search') || ''; + setSearchText(search); + + const showAll = url.searchParams.get('showAll'); + setIncludeAll(showAll === 'true'); + + const sort = url.searchParams.get('sort') || ''; + setSortValue(sort); + }, []); + + useEffect(() => { + const url = new URL(location.href); + let newUrl = url.origin + url.pathname; + const params = {}; + if (dateRange) params.dates = dateRange; + if (includeAll) params.showAll = true; + if (searchText) params.search = searchText; + if (sortValue) params.sort = sortValue; + if (Object.keys(params).length) { + newUrl += '?' + new URLSearchParams(params).toString(); + } + history.replaceState(params, '', newUrl); + }, [dateRange, includeAll, searchText, sortValue]); + useEffect(() => { if (currentMembers && currentMembers.length > 0) { isAdmin && includeAll @@ -380,6 +410,7 @@ const ViewFeedbackPage = () => { ) }} + value={searchText} /> { size="small" label="Show requests sent within" onChange={e => setDateRange(e.target.value)} - defaultValue={DateRange.THREE_MONTHS} + value={dateRange} variant="outlined" > Past 3 months @@ -398,7 +429,6 @@ const ViewFeedbackPage = () => { All time - { size="small" label="Sort by" onChange={e => setSortValue(e.target.value)} - defaultValue={SortOption.SENT_DATE} + value={sortValue} variant="outlined" > From 11d7d292a419f0cd4a71995eeb78e85b38b953cc Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 08:41:33 -0500 Subject: [PATCH 04/15] added query parmeter support in ViewFeedbackPage with new approach --- web-ui/src/helpers/query-parameters.js | 53 +++++++++++++++++++++++++ web-ui/src/pages/ViewFeedbackPage.jsx | 55 +++++++++++++------------- 2 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 web-ui/src/helpers/query-parameters.js diff --git a/web-ui/src/helpers/query-parameters.js b/web-ui/src/helpers/query-parameters.js new file mode 100644 index 0000000000..d61e12c12f --- /dev/null +++ b/web-ui/src/helpers/query-parameters.js @@ -0,0 +1,53 @@ +import { useEffect } from 'react'; + +/** + * @typedef {object} QPBoolean + * @property {string} name + * @property {boolean} default + * @property {() => boolean} getter + * @property {(boolean) => void} setter + */ + +/** + * @typedef {object} QPString + * @property {string} name + * @property {string} default + * @property {() => string} getter + * @property {(string) => void} setter + */ + +/** + * @param {(QPBoolean | QPString[]} qps - query parameters + */ +export const queryParameterSetup = qps => { + useEffect(() => { + const url = new URL(location.href); + + const params = url.searchParams; + + for (const qp of qps) { + const v = params.get(qp.name); + if (typeof qp.default === 'boolean') { + qp.setter(v === 'true'); + } else { + qp.setter(v || qp.default); + } + } + }, []); + + const dependencies = qps.map(qp => qp.getter()); + + useEffect(() => { + const url = new URL(location.href); + let newUrl = url.origin + url.pathname; + const params = {}; + for (const qp of qps) { + const value = qp.getter(); + if (value && value !== qp.default) params[qp.name] = value; + } + if (Object.keys(params).length) { + newUrl += '?' + new URLSearchParams(params).toString(); + } + history.replaceState(params, '', newUrl); + }, dependencies); +}; diff --git a/web-ui/src/pages/ViewFeedbackPage.jsx b/web-ui/src/pages/ViewFeedbackPage.jsx index 8b56c0c051..68483a1447 100644 --- a/web-ui/src/pages/ViewFeedbackPage.jsx +++ b/web-ui/src/pages/ViewFeedbackPage.jsx @@ -33,6 +33,7 @@ import { } from '../context/selectors'; import { getFeedbackTemplate } from '../api/feedbacktemplate'; import SkeletonLoader from '../components/skeleton_loader/SkeletonLoader'; +import { queryParameterSetup } from '../helpers/query-parameters'; const PREFIX = 'ViewFeedbackPage'; const classes = { @@ -107,35 +108,33 @@ const ViewFeedbackPage = () => { const [dateRange, setDateRange] = useState(DateRange.THREE_MONTHS); const [includeAll, setIncludeAll] = useState(false); - useEffect(() => { - const url = new URL(location.href); - - const dates = url.searchParams.get('dates') || ''; - setDateRange(dates); - - const search = url.searchParams.get('search') || ''; - setSearchText(search); - - const showAll = url.searchParams.get('showAll'); - setIncludeAll(showAll === 'true'); - - const sort = url.searchParams.get('sort') || ''; - setSortValue(sort); - }, []); - - useEffect(() => { - const url = new URL(location.href); - let newUrl = url.origin + url.pathname; - const params = {}; - if (dateRange) params.dates = dateRange; - if (includeAll) params.showAll = true; - if (searchText) params.search = searchText; - if (sortValue) params.sort = sortValue; - if (Object.keys(params).length) { - newUrl += '?' + new URLSearchParams(params).toString(); + const qps = [ + { + name: 'dates', + default: DateRange.THREE_MONTHS, + getter: () => dateRange, + setter: setDateRange + }, + { + name: 'search', + default: '', + getter: () => searchText, + setter: setSearchText + }, + { + name: 'showAll', + default: false, + getter: () => includeAll, + setter: setIncludeAll + }, + { + name: 'sort', + default: SortOption.SENT_DATE, + getter: () => sortValue, + setter: setSortValue } - history.replaceState(params, '', newUrl); - }, [dateRange, includeAll, searchText, sortValue]); + ]; + queryParameterSetup(qps); useEffect(() => { if (currentMembers && currentMembers.length > 0) { From 6fd5d5a7131fd9143b8fc1f717fd11172da8c36e Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 08:50:44 -0500 Subject: [PATCH 05/15] more use of queryParameterSetup --- web-ui/src/components/admin/roles/Roles.jsx | 2 +- web-ui/src/components/admin/users/Users.jsx | 52 ++++++++++----------- web-ui/src/pages/ViewFeedbackPage.jsx | 5 +- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/web-ui/src/components/admin/roles/Roles.jsx b/web-ui/src/components/admin/roles/Roles.jsx index 3a8c2ba7da..156a8ea9ff 100644 --- a/web-ui/src/components/admin/roles/Roles.jsx +++ b/web-ui/src/components/admin/roles/Roles.jsx @@ -41,7 +41,7 @@ import PersonAddIcon from '@mui/icons-material/PersonAdd'; import SearchIcon from '@mui/icons-material/Search'; import AddIcon from '@mui/icons-material/Add'; -import { isArrayPresent } from './../../../helpers/checks'; +import { isArrayPresent } from '../../../helpers/checks'; import './Roles.css'; import EditIcon from '@mui/icons-material/Edit'; diff --git a/web-ui/src/components/admin/users/Users.jsx b/web-ui/src/components/admin/users/Users.jsx index 497acba449..3b1779e3ec 100644 --- a/web-ui/src/components/admin/users/Users.jsx +++ b/web-ui/src/components/admin/users/Users.jsx @@ -1,7 +1,11 @@ import fileDownload from 'js-file-download'; import React, { useContext, useEffect, useState } from 'react'; +import DownloadIcon from '@mui/icons-material/FileDownload'; +import PersonIcon from '@mui/icons-material/Person'; +import { Button, Grid, TextField } from '@mui/material'; import { styled } from '@mui/material/styles'; + import AdminMemberCard from '../../member-directory/AdminMemberCard'; import MemberModal from '../../member-directory/MemberModal'; import { createMember, reportAllMembersCsv } from '../../../api/member'; @@ -13,9 +17,7 @@ import { selectNormalizedMembersAdmin } from '../../../context/selectors'; -import { Button, Grid, TextField } from '@mui/material'; -import DownloadIcon from '@mui/icons-material/FileDownload'; -import PersonIcon from '@mui/icons-material/Person'; +import { queryParameterSetup } from '../../../helpers/query-parameters'; import './Users.css'; @@ -69,30 +71,26 @@ const Users = () => { ? selectNormalizedMembersAdmin(state, searchText) : selectNormalizedMembers(state, searchText); - useEffect(() => { - const url = new URL(location.href); - - const addUser = url.searchParams.get('addUser'); - setOpen(addUser === 'true'); - - const includeTerminated = url.searchParams.get('includeTerminated'); - setIncludeTerminated(includeTerminated === 'true'); - - const search = url.searchParams.get('search') || ''; - setSearchText(search); - }, []); - - useEffect(() => { - const url = new URL(location.href); - const params = { - addUser: open, - includeTerminated, - search: searchText - }; - const q = new URLSearchParams(params).toString(); - const newUrl = url.origin + url.pathname + '?' + q; - history.replaceState(params, '', newUrl); - }, [includeTerminated, open, searchText]); + queryParameterSetup([ + { + name: 'addUser', + default: false, + getter: () => open, + setter: setOpen + }, + { + name: 'includeTerminated', + default: false, + getter: () => includeTerminated, + setter: setIncludeTerminated + }, + { + name: 'search', + default: '', + getter: () => searchText, + setter: setSearchText + } + ]); const handleOpen = () => setOpen(true); diff --git a/web-ui/src/pages/ViewFeedbackPage.jsx b/web-ui/src/pages/ViewFeedbackPage.jsx index 68483a1447..73d57bf34e 100644 --- a/web-ui/src/pages/ViewFeedbackPage.jsx +++ b/web-ui/src/pages/ViewFeedbackPage.jsx @@ -108,7 +108,7 @@ const ViewFeedbackPage = () => { const [dateRange, setDateRange] = useState(DateRange.THREE_MONTHS); const [includeAll, setIncludeAll] = useState(false); - const qps = [ + queryParameterSetup([ { name: 'dates', default: DateRange.THREE_MONTHS, @@ -133,8 +133,7 @@ const ViewFeedbackPage = () => { getter: () => sortValue, setter: setSortValue } - ]; - queryParameterSetup(qps); + ]); useEffect(() => { if (currentMembers && currentMembers.length > 0) { From 34b887e2fb8dcd1a9f116445e2518f9148bf974a Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 09:10:15 -0500 Subject: [PATCH 06/15] improved query-parameters.js --- web-ui/src/components/admin/users/Users.jsx | 6 +++--- web-ui/src/helpers/query-parameters.js | 8 ++++---- web-ui/src/pages/ViewFeedbackPage.jsx | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/web-ui/src/components/admin/users/Users.jsx b/web-ui/src/components/admin/users/Users.jsx index 3b1779e3ec..1a87094951 100644 --- a/web-ui/src/components/admin/users/Users.jsx +++ b/web-ui/src/components/admin/users/Users.jsx @@ -75,19 +75,19 @@ const Users = () => { { name: 'addUser', default: false, - getter: () => open, + value: open, setter: setOpen }, { name: 'includeTerminated', default: false, - getter: () => includeTerminated, + value: includeTerminated, setter: setIncludeTerminated }, { name: 'search', default: '', - getter: () => searchText, + value: searchText, setter: setSearchText } ]); diff --git a/web-ui/src/helpers/query-parameters.js b/web-ui/src/helpers/query-parameters.js index d61e12c12f..a3df891b43 100644 --- a/web-ui/src/helpers/query-parameters.js +++ b/web-ui/src/helpers/query-parameters.js @@ -4,7 +4,7 @@ import { useEffect } from 'react'; * @typedef {object} QPBoolean * @property {string} name * @property {boolean} default - * @property {() => boolean} getter + * @property {boolean} value * @property {(boolean) => void} setter */ @@ -12,7 +12,7 @@ import { useEffect } from 'react'; * @typedef {object} QPString * @property {string} name * @property {string} default - * @property {() => string} getter + * @property {string} value * @property {(string) => void} setter */ @@ -35,14 +35,14 @@ export const queryParameterSetup = qps => { } }, []); - const dependencies = qps.map(qp => qp.getter()); + const dependencies = qps.map(qp => qp.value); useEffect(() => { const url = new URL(location.href); let newUrl = url.origin + url.pathname; const params = {}; for (const qp of qps) { - const value = qp.getter(); + const { value } = qp; if (value && value !== qp.default) params[qp.name] = value; } if (Object.keys(params).length) { diff --git a/web-ui/src/pages/ViewFeedbackPage.jsx b/web-ui/src/pages/ViewFeedbackPage.jsx index 73d57bf34e..e22ab94e77 100644 --- a/web-ui/src/pages/ViewFeedbackPage.jsx +++ b/web-ui/src/pages/ViewFeedbackPage.jsx @@ -112,25 +112,25 @@ const ViewFeedbackPage = () => { { name: 'dates', default: DateRange.THREE_MONTHS, - getter: () => dateRange, + value: dateRange, setter: setDateRange }, { name: 'search', default: '', - getter: () => searchText, + value: searchText, setter: setSearchText }, { name: 'showAll', default: false, - getter: () => includeAll, + value: includeAll, setter: setIncludeAll }, { name: 'sort', default: SortOption.SENT_DATE, - getter: () => sortValue, + value: sortValue, setter: setSortValue } ]); From 8f4c718037381cbc091a3807822da58b1561af5c Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 09:13:52 -0500 Subject: [PATCH 07/15] more use of queryParameterSetup --- web-ui/src/components/admin/users/Users.jsx | 1 - .../components/team-results/TeamResults.jsx | 44 +++++++++---------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/web-ui/src/components/admin/users/Users.jsx b/web-ui/src/components/admin/users/Users.jsx index 1a87094951..c863790212 100644 --- a/web-ui/src/components/admin/users/Users.jsx +++ b/web-ui/src/components/admin/users/Users.jsx @@ -16,7 +16,6 @@ import { selectNormalizedMembers, selectNormalizedMembersAdmin } from '../../../context/selectors'; - import { queryParameterSetup } from '../../../helpers/query-parameters'; import './Users.css'; diff --git a/web-ui/src/components/team-results/TeamResults.jsx b/web-ui/src/components/team-results/TeamResults.jsx index f6106cdbf2..d94fa33169 100644 --- a/web-ui/src/components/team-results/TeamResults.jsx +++ b/web-ui/src/components/team-results/TeamResults.jsx @@ -11,6 +11,7 @@ import PropTypes from 'prop-types'; import { TextField } from '@mui/material'; import './TeamResults.css'; import SkeletonLoader from '../skeleton_loader/SkeletonLoader'; +import { queryParameterSetup } from '../../helpers/query-parameters'; const PREFIX = 'TeamResults'; const classes = { @@ -55,31 +56,26 @@ const TeamResults = () => { ); }); - useEffect(() => { - const url = new URL(location.href); - - const addNew = url.searchParams.get('addNew'); - setAddingTeam(addNew === 'true'); - - const search = url.searchParams.get('search') || ''; - setSearchText(search); - - const selectedTeamId = url.searchParams.get('team') || ''; - setSelectedTeamId(selectedTeamId); - }, []); - - useEffect(() => { - const url = new URL(location.href); - let newUrl = url.origin + url.pathname; - const params = {}; - if (addingTeam) params.addNew = true; - if (searchText) params.search = searchText; - if (selectedTeamId) params.team = selectedTeamId; - if (Object.keys(params).length) { - newUrl += '?' + new URLSearchParams(params).toString(); + queryParameterSetup([ + { + name: 'addNew', + default: false, + value: addingTeam, + setter: setAddingTeam + }, + { + name: 'search', + default: '', + value: searchText, + setter: setSearchText + }, + { + name: 'team', + default: '', + value: selectedTeamId, + setter: setSelectedTeamId } - history.replaceState(params, '', newUrl); - }, [addingTeam, searchText, selectedTeamId]); + ]); return ( From 558473fc708e8ca20dd6b7793d4c1e9f95e70f65 Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 10:13:16 -0500 Subject: [PATCH 08/15] more use of queryParameterSetup --- web-ui/src/helpers/query-parameters.js | 9 +++++--- web-ui/src/pages/PermissionsPage.jsx | 30 +++++++++++++------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/web-ui/src/helpers/query-parameters.js b/web-ui/src/helpers/query-parameters.js index a3df891b43..2e185cd0f2 100644 --- a/web-ui/src/helpers/query-parameters.js +++ b/web-ui/src/helpers/query-parameters.js @@ -5,7 +5,8 @@ import { useEffect } from 'react'; * @property {string} name * @property {boolean} default * @property {boolean} value - * @property {(boolean) => void} setter + * @property {(boolean) => void} setter - takes query parameter value and updates state + * @property {(any) => string} toQP - takes state value and returns query parameter value */ /** @@ -13,7 +14,8 @@ import { useEffect } from 'react'; * @property {string} name * @property {string} default * @property {string} value - * @property {(string) => void} setter + * @property {(string) => void} setter - takes query parameter value and updates state + * @property {(any) => string} toQP - takes state value and returns query parameter value */ /** @@ -42,7 +44,8 @@ export const queryParameterSetup = qps => { let newUrl = url.origin + url.pathname; const params = {}; for (const qp of qps) { - const { value } = qp; + let { toQP, value } = qp; + if (toQP) value = toQP(value); if (value && value !== qp.default) params[qp.name] = value; } if (Object.keys(params).length) { diff --git a/web-ui/src/pages/PermissionsPage.jsx b/web-ui/src/pages/PermissionsPage.jsx index 0733ce45cc..17c92a88f4 100644 --- a/web-ui/src/pages/PermissionsPage.jsx +++ b/web-ui/src/pages/PermissionsPage.jsx @@ -19,6 +19,7 @@ import { selectRoles, selectHasPermissionAssignmentPermission } from '../context/selectors'; +import { queryParameterSetup } from '../helpers/query-parameters'; import './PermissionsPage.css'; @@ -72,21 +73,20 @@ const EditPermissionsPage = () => { const [rolePermissions, setRolePermissions] = useState([]); const [refresh, setRefresh] = useState(true); - useEffect(() => { - const url = new URL(location.href); - const roleName = url.searchParams.get('role'); - const role = roles.find(r => r.role === roleName); - setSelectedRole(role || roles[0]); - }, []); - - useEffect(() => { - if (!selectedRole) return; - const url = new URL(location.href); - const params = { role: selectedRole.role }; - const q = new URLSearchParams(params).toString(); - const newUrl = url.origin + url.pathname + '?' + q; - history.replaceState(params, '', newUrl); - }, [selectedRole]); + queryParameterSetup([ + { + name: 'role', + default: roles[0], + value: selectedRole, + setter(value) { + const role = roles.find(r => r.role === value); + setSelectedRole(role); + }, + toQP(selectedRole) { + return selectedRole.role; + } + } + ]); useEffect(() => { const getRolePermissions = async () => { From b35933fb59888aafd0de86db5971c3672d278b99 Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 10:23:05 -0500 Subject: [PATCH 09/15] more use of queryParameterSetup --- web-ui/src/components/admin/roles/Roles.jsx | 51 +++++++++++---------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/web-ui/src/components/admin/roles/Roles.jsx b/web-ui/src/components/admin/roles/Roles.jsx index 156a8ea9ff..99901f3a77 100644 --- a/web-ui/src/components/admin/roles/Roles.jsx +++ b/web-ui/src/components/admin/roles/Roles.jsx @@ -37,14 +37,15 @@ import { FormHelperText, Divider } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import EditIcon from '@mui/icons-material/Edit'; import PersonAddIcon from '@mui/icons-material/PersonAdd'; import SearchIcon from '@mui/icons-material/Search'; -import AddIcon from '@mui/icons-material/Add'; import { isArrayPresent } from '../../../helpers/checks'; +import { queryParameterSetup } from '../../../helpers/query-parameters'; import './Roles.css'; -import EditIcon from '@mui/icons-material/Edit'; const Roles = () => { const { state, dispatch } = useContext(AppContext); @@ -62,29 +63,31 @@ const Roles = () => { memberProfiles?.sort((a, b) => a.name.localeCompare(b.name)); - useEffect(() => { - const url = new URL(location.href); - const selectedRoles = url.searchParams.get('roles'); - if (selectedRoles?.length > 0) { - // Select only the roles specified in the URL. - setSelectedRoles(selectedRoles.split(',')); - } else { - // Select all possible roles. - setSelectedRoles(roles.map(r => r.role)); + queryParameterSetup([ + { + name: 'roles', + default: [], + value: selectedRoles, + setter(value) { + if (value?.length > 0) { + // Select only the roles specified in the URL. + setSelectedRoles(value.split(',').sort()); + } else { + // Select all possible roles. + setSelectedRoles(roles.map(r => r.role)); + } + }, + toQP() { + return selectedRoles.join(','); + } + }, + { + name: 'search', + default: '', + value: searchText, + setter: setSearchText } - setSearchText(url.searchParams.get('search') ?? ''); - }, []); - - useEffect(() => { - const url = new URL(location.href); - const params = { - roles: selectedRoles.join(','), - search: searchText - }; - const q = new URLSearchParams(params).toString(); - const newUrl = url.origin + url.pathname + '?' + q; - history.replaceState(params, '', newUrl); - }, [searchText, selectedRoles]); + ]); useEffect(() => { const memberMap = {}; From e4f64167acc3f52b8071d978aabee73bca33d1e5 Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 11:34:49 -0500 Subject: [PATCH 10/15] improved query-parameters.js --- web-ui/src/helpers/query-parameters.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web-ui/src/helpers/query-parameters.js b/web-ui/src/helpers/query-parameters.js index 2e185cd0f2..956e6b093a 100644 --- a/web-ui/src/helpers/query-parameters.js +++ b/web-ui/src/helpers/query-parameters.js @@ -6,7 +6,7 @@ import { useEffect } from 'react'; * @property {boolean} default * @property {boolean} value * @property {(boolean) => void} setter - takes query parameter value and updates state - * @property {(any) => string} toQP - takes state value and returns query parameter value + * @property {[(any) => string]} toQP - takes state value and returns query parameter value */ /** @@ -15,22 +15,20 @@ import { useEffect } from 'react'; * @property {string} default * @property {string} value * @property {(string) => void} setter - takes query parameter value and updates state - * @property {(any) => string} toQP - takes state value and returns query parameter value + * @property {[(any) => string]} toQP - takes state value and returns query parameter value */ /** - * @param {(QPBoolean | QPString[]} qps - query parameters + * @param {(QPBoolean | QPString)[]} qps - query parameters */ export const queryParameterSetup = qps => { useEffect(() => { const url = new URL(location.href); - const params = url.searchParams; - for (const qp of qps) { const v = params.get(qp.name); if (typeof qp.default === 'boolean') { - qp.setter(v === 'true'); + qp.setter(v ? v === 'true' : qp.default); } else { qp.setter(v || qp.default); } @@ -39,6 +37,8 @@ export const queryParameterSetup = qps => { const dependencies = qps.map(qp => qp.value); + // This assumes the app does not use query parameters for any other purposes. + // It will drop any that are not set by this useEffect. useEffect(() => { const url = new URL(location.href); let newUrl = url.origin + url.pathname; From 78a24a1d60467597fccdb84844fe7e43f2301e26 Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 12:51:37 -0500 Subject: [PATCH 11/15] renamed queryParameterSetup to useQueryParameters --- web-ui/src/components/admin/roles/Roles.jsx | 17 ++++++++--------- web-ui/src/components/admin/users/Users.jsx | 4 ++-- .../src/components/team-results/TeamResults.jsx | 4 ++-- web-ui/src/helpers/query-parameters.js | 11 ++++++++--- web-ui/src/pages/PermissionsPage.jsx | 4 ++-- web-ui/src/pages/ViewFeedbackPage.jsx | 4 ++-- 6 files changed, 24 insertions(+), 20 deletions(-) diff --git a/web-ui/src/components/admin/roles/Roles.jsx b/web-ui/src/components/admin/roles/Roles.jsx index 99901f3a77..ecf7f6d6c1 100644 --- a/web-ui/src/components/admin/roles/Roles.jsx +++ b/web-ui/src/components/admin/roles/Roles.jsx @@ -43,7 +43,7 @@ import PersonAddIcon from '@mui/icons-material/PersonAdd'; import SearchIcon from '@mui/icons-material/Search'; import { isArrayPresent } from '../../../helpers/checks'; -import { queryParameterSetup } from '../../../helpers/query-parameters'; +import { useQueryParameters } from '../../../helpers/query-parameters'; import './Roles.css'; @@ -63,18 +63,19 @@ const Roles = () => { memberProfiles?.sort((a, b) => a.name.localeCompare(b.name)); - queryParameterSetup([ + if (!roles) console.error('Roles.jsx: state.roles is not set!'); + const allRoles = roles.map(r => r.role).sort(); + useQueryParameters([ { name: 'roles', - default: [], + default: allRoles, value: selectedRoles, setter(value) { if (value?.length > 0) { // Select only the roles specified in the URL. - setSelectedRoles(value.split(',').sort()); + setSelectedRoles(value.sort()); } else { - // Select all possible roles. - setSelectedRoles(roles.map(r => r.role)); + setSelectedRoles(allRoles); } }, toQP() { @@ -230,9 +231,7 @@ const Roles = () => { value={selectedRoles} onChange={event => { const value = event.target.value; - setSelectedRoles( - typeof value === 'string' ? value.split(',') : value - ); + setSelectedRoles(value.sort()); }} input={} renderValue={selected => selected.join(', ')} diff --git a/web-ui/src/components/admin/users/Users.jsx b/web-ui/src/components/admin/users/Users.jsx index c863790212..68dbe9ec1e 100644 --- a/web-ui/src/components/admin/users/Users.jsx +++ b/web-ui/src/components/admin/users/Users.jsx @@ -16,7 +16,7 @@ import { selectNormalizedMembers, selectNormalizedMembersAdmin } from '../../../context/selectors'; -import { queryParameterSetup } from '../../../helpers/query-parameters'; +import { useQueryParameters } from '../../../helpers/query-parameters'; import './Users.css'; @@ -70,7 +70,7 @@ const Users = () => { ? selectNormalizedMembersAdmin(state, searchText) : selectNormalizedMembers(state, searchText); - queryParameterSetup([ + useQueryParameters([ { name: 'addUser', default: false, diff --git a/web-ui/src/components/team-results/TeamResults.jsx b/web-ui/src/components/team-results/TeamResults.jsx index d94fa33169..c4f4ffae92 100644 --- a/web-ui/src/components/team-results/TeamResults.jsx +++ b/web-ui/src/components/team-results/TeamResults.jsx @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; import { TextField } from '@mui/material'; import './TeamResults.css'; import SkeletonLoader from '../skeleton_loader/SkeletonLoader'; -import { queryParameterSetup } from '../../helpers/query-parameters'; +import { useQueryParameters } from '../../helpers/query-parameters'; const PREFIX = 'TeamResults'; const classes = { @@ -56,7 +56,7 @@ const TeamResults = () => { ); }); - queryParameterSetup([ + useQueryParameters([ { name: 'addNew', default: false, diff --git a/web-ui/src/helpers/query-parameters.js b/web-ui/src/helpers/query-parameters.js index 956e6b093a..f4dc07af12 100644 --- a/web-ui/src/helpers/query-parameters.js +++ b/web-ui/src/helpers/query-parameters.js @@ -21,15 +21,16 @@ import { useEffect } from 'react'; /** * @param {(QPBoolean | QPString)[]} qps - query parameters */ -export const queryParameterSetup = qps => { +export const useQueryParameters = qps => { useEffect(() => { const url = new URL(location.href); const params = url.searchParams; for (const qp of qps) { - const v = params.get(qp.name); + let v = params.get(qp.name); if (typeof qp.default === 'boolean') { qp.setter(v ? v === 'true' : qp.default); } else { + if (v && Array.isArray(qp.default)) v = v.split(','); qp.setter(v || qp.default); } } @@ -46,7 +47,8 @@ export const queryParameterSetup = qps => { for (const qp of qps) { let { toQP, value } = qp; if (toQP) value = toQP(value); - if (value && value !== qp.default) params[qp.name] = value; + if (!value) continue; + if (!compare(value, qp.default)) params[qp.name] = value; } if (Object.keys(params).length) { newUrl += '?' + new URLSearchParams(params).toString(); @@ -54,3 +56,6 @@ export const queryParameterSetup = qps => { history.replaceState(params, '', newUrl); }, dependencies); }; + +const compare = (a, b) => stringValue(a) === stringValue(b); +const stringValue = v => (Array.isArray(v) ? v.sort().join(',') : v); diff --git a/web-ui/src/pages/PermissionsPage.jsx b/web-ui/src/pages/PermissionsPage.jsx index 17c92a88f4..4a2fe4f27b 100644 --- a/web-ui/src/pages/PermissionsPage.jsx +++ b/web-ui/src/pages/PermissionsPage.jsx @@ -19,7 +19,7 @@ import { selectRoles, selectHasPermissionAssignmentPermission } from '../context/selectors'; -import { queryParameterSetup } from '../helpers/query-parameters'; +import { useQueryParameters } from '../helpers/query-parameters'; import './PermissionsPage.css'; @@ -73,7 +73,7 @@ const EditPermissionsPage = () => { const [rolePermissions, setRolePermissions] = useState([]); const [refresh, setRefresh] = useState(true); - queryParameterSetup([ + useQueryParameters([ { name: 'role', default: roles[0], diff --git a/web-ui/src/pages/ViewFeedbackPage.jsx b/web-ui/src/pages/ViewFeedbackPage.jsx index e22ab94e77..0be9a0567f 100644 --- a/web-ui/src/pages/ViewFeedbackPage.jsx +++ b/web-ui/src/pages/ViewFeedbackPage.jsx @@ -33,7 +33,7 @@ import { } from '../context/selectors'; import { getFeedbackTemplate } from '../api/feedbacktemplate'; import SkeletonLoader from '../components/skeleton_loader/SkeletonLoader'; -import { queryParameterSetup } from '../helpers/query-parameters'; +import { useQueryParameters } from '../helpers/query-parameters'; const PREFIX = 'ViewFeedbackPage'; const classes = { @@ -108,7 +108,7 @@ const ViewFeedbackPage = () => { const [dateRange, setDateRange] = useState(DateRange.THREE_MONTHS); const [includeAll, setIncludeAll] = useState(false); - queryParameterSetup([ + useQueryParameters([ { name: 'dates', default: DateRange.THREE_MONTHS, From a4848d6aa5b55ccf3556ee5fc81177af6e39b923 Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 13:19:22 -0500 Subject: [PATCH 12/15] query-parameters cleanup --- web-ui/src/components/admin/roles/Roles.jsx | 7 +------ web-ui/src/helpers/query-parameters.js | 3 +-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/web-ui/src/components/admin/roles/Roles.jsx b/web-ui/src/components/admin/roles/Roles.jsx index ecf7f6d6c1..c3b33f4ed7 100644 --- a/web-ui/src/components/admin/roles/Roles.jsx +++ b/web-ui/src/components/admin/roles/Roles.jsx @@ -71,12 +71,7 @@ const Roles = () => { default: allRoles, value: selectedRoles, setter(value) { - if (value?.length > 0) { - // Select only the roles specified in the URL. - setSelectedRoles(value.sort()); - } else { - setSelectedRoles(allRoles); - } + setSelectedRoles(isArrayPresent(value) ? value.sort() : allRoles); }, toQP() { return selectedRoles.join(','); diff --git a/web-ui/src/helpers/query-parameters.js b/web-ui/src/helpers/query-parameters.js index f4dc07af12..9a2a8b8ae3 100644 --- a/web-ui/src/helpers/query-parameters.js +++ b/web-ui/src/helpers/query-parameters.js @@ -47,8 +47,7 @@ export const useQueryParameters = qps => { for (const qp of qps) { let { toQP, value } = qp; if (toQP) value = toQP(value); - if (!value) continue; - if (!compare(value, qp.default)) params[qp.name] = value; + if (value && !compare(value, qp.default)) params[qp.name] = value; } if (Object.keys(params).length) { newUrl += '?' + new URLSearchParams(params).toString(); From e14885ce36f0e2de87f3d2fb5ebc69f5cf0d69cc Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 13:26:05 -0500 Subject: [PATCH 13/15] fixed query-parameters.js to retain other query parameters --- web-ui/src/helpers/query-parameters.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web-ui/src/helpers/query-parameters.js b/web-ui/src/helpers/query-parameters.js index 9a2a8b8ae3..920add31be 100644 --- a/web-ui/src/helpers/query-parameters.js +++ b/web-ui/src/helpers/query-parameters.js @@ -38,17 +38,23 @@ export const useQueryParameters = qps => { const dependencies = qps.map(qp => qp.value); - // This assumes the app does not use query parameters for any other purposes. - // It will drop any that are not set by this useEffect. useEffect(() => { const url = new URL(location.href); let newUrl = url.origin + url.pathname; const params = {}; + + // Add query parameters listed in qps that do not have their default value. for (const qp of qps) { let { toQP, value } = qp; if (toQP) value = toQP(value); if (value && !compare(value, qp.default)) params[qp.name] = value; } + + // Add query parameters that are not listed in qps. + for (const [k, v] of url.searchParams) { + if (!qps.some(qp => qp.name === k)) params[k] = v; + } + if (Object.keys(params).length) { newUrl += '?' + new URLSearchParams(params).toString(); } From d1c30a276d150d851056b08828b78c9f7a7d4193 Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 13:40:07 -0500 Subject: [PATCH 14/15] fixed a failing test --- web-ui/src/pages/PermissionsPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/pages/PermissionsPage.jsx b/web-ui/src/pages/PermissionsPage.jsx index 4a2fe4f27b..24144df376 100644 --- a/web-ui/src/pages/PermissionsPage.jsx +++ b/web-ui/src/pages/PermissionsPage.jsx @@ -83,7 +83,7 @@ const EditPermissionsPage = () => { setSelectedRole(role); }, toQP(selectedRole) { - return selectedRole.role; + return selectedRole ? selectedRole.role : ''; } } ]); From fda463930b8090964b16b7d670aa76c0a5050bfd Mon Sep 17 00:00:00 2001 From: Mark Volkmann Date: Mon, 29 Apr 2024 13:41:15 -0500 Subject: [PATCH 15/15] better test fix --- web-ui/src/pages/PermissionsPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/pages/PermissionsPage.jsx b/web-ui/src/pages/PermissionsPage.jsx index 24144df376..5ae6ca6305 100644 --- a/web-ui/src/pages/PermissionsPage.jsx +++ b/web-ui/src/pages/PermissionsPage.jsx @@ -83,7 +83,7 @@ const EditPermissionsPage = () => { setSelectedRole(role); }, toQP(selectedRole) { - return selectedRole ? selectedRole.role : ''; + return selectedRole?.role ?? ''; } } ]);