Skip to content

Commit b33df2c

Browse files
authored
Merge pull request #2276 from objectcomputing/feature-2265-team-directory-query-parameters
Team Results page now gets settings from query parameters
2 parents c55a80a + fda4639 commit b33df2c

File tree

8 files changed

+198
-93
lines changed

8 files changed

+198
-93
lines changed

web-ui/src/components/admin/roles/Roles.jsx

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ import {
3737
FormHelperText,
3838
Divider
3939
} from '@mui/material';
40+
import AddIcon from '@mui/icons-material/Add';
41+
import EditIcon from '@mui/icons-material/Edit';
4042
import PersonAddIcon from '@mui/icons-material/PersonAdd';
4143
import SearchIcon from '@mui/icons-material/Search';
42-
import AddIcon from '@mui/icons-material/Add';
4344

44-
import { isArrayPresent } from './../../../helpers/checks';
45+
import { isArrayPresent } from '../../../helpers/checks';
46+
import { useQueryParameters } from '../../../helpers/query-parameters';
4547

4648
import './Roles.css';
47-
import EditIcon from '@mui/icons-material/Edit';
4849

4950
const Roles = () => {
5051
const { state, dispatch } = useContext(AppContext);
@@ -62,29 +63,27 @@ const Roles = () => {
6263

6364
memberProfiles?.sort((a, b) => a.name.localeCompare(b.name));
6465

65-
useEffect(() => {
66-
const url = new URL(location.href);
67-
const selectedRoles = url.searchParams.get('roles');
68-
if (selectedRoles?.length > 0) {
69-
// Select only the roles specified in the URL.
70-
setSelectedRoles(selectedRoles.split(','));
71-
} else {
72-
// Select all possible roles.
73-
setSelectedRoles(roles.map(r => r.role));
66+
if (!roles) console.error('Roles.jsx: state.roles is not set!');
67+
const allRoles = roles.map(r => r.role).sort();
68+
useQueryParameters([
69+
{
70+
name: 'roles',
71+
default: allRoles,
72+
value: selectedRoles,
73+
setter(value) {
74+
setSelectedRoles(isArrayPresent(value) ? value.sort() : allRoles);
75+
},
76+
toQP() {
77+
return selectedRoles.join(',');
78+
}
79+
},
80+
{
81+
name: 'search',
82+
default: '',
83+
value: searchText,
84+
setter: setSearchText
7485
}
75-
setSearchText(url.searchParams.get('search') ?? '');
76-
}, []);
77-
78-
useEffect(() => {
79-
const url = new URL(location.href);
80-
const params = {
81-
roles: selectedRoles.join(','),
82-
search: searchText
83-
};
84-
const q = new URLSearchParams(params).toString();
85-
const newUrl = url.origin + url.pathname + '?' + q;
86-
history.replaceState(params, '', newUrl);
87-
}, [searchText, selectedRoles]);
86+
]);
8887

8988
useEffect(() => {
9089
const memberMap = {};
@@ -227,9 +226,7 @@ const Roles = () => {
227226
value={selectedRoles}
228227
onChange={event => {
229228
const value = event.target.value;
230-
setSelectedRoles(
231-
typeof value === 'string' ? value.split(',') : value
232-
);
229+
setSelectedRoles(value.sort());
233230
}}
234231
input={<OutlinedInput label="Roles" />}
235232
renderValue={selected => selected.join(', ')}

web-ui/src/components/admin/users/Users.jsx

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import fileDownload from 'js-file-download';
22
import React, { useContext, useEffect, useState } from 'react';
33

4+
import DownloadIcon from '@mui/icons-material/FileDownload';
5+
import PersonIcon from '@mui/icons-material/Person';
6+
import { Button, Grid, TextField } from '@mui/material';
47
import { styled } from '@mui/material/styles';
8+
59
import AdminMemberCard from '../../member-directory/AdminMemberCard';
610
import MemberModal from '../../member-directory/MemberModal';
711
import { createMember, reportAllMembersCsv } from '../../../api/member';
@@ -12,10 +16,7 @@ import {
1216
selectNormalizedMembers,
1317
selectNormalizedMembersAdmin
1418
} from '../../../context/selectors';
15-
16-
import { Button, Grid, TextField } from '@mui/material';
17-
import DownloadIcon from '@mui/icons-material/FileDownload';
18-
import PersonIcon from '@mui/icons-material/Person';
19+
import { useQueryParameters } from '../../../helpers/query-parameters';
1920

2021
import './Users.css';
2122

@@ -69,30 +70,26 @@ const Users = () => {
6970
? selectNormalizedMembersAdmin(state, searchText)
7071
: selectNormalizedMembers(state, searchText);
7172

72-
useEffect(() => {
73-
const url = new URL(location.href);
74-
75-
const addUser = url.searchParams.get('addUser');
76-
setOpen(addUser === 'true');
77-
78-
const includeTerminated = url.searchParams.get('includeTerminated');
79-
setIncludeTerminated(includeTerminated === 'true');
80-
81-
const search = url.searchParams.get('search') || '';
82-
setSearchText(search);
83-
}, []);
84-
85-
useEffect(() => {
86-
const url = new URL(location.href);
87-
const params = {
88-
addUser: open,
89-
includeTerminated,
90-
search: searchText
91-
};
92-
const q = new URLSearchParams(params).toString();
93-
const newUrl = url.origin + url.pathname + '?' + q;
94-
history.replaceState(params, '', newUrl);
95-
}, [includeTerminated, open, searchText]);
73+
useQueryParameters([
74+
{
75+
name: 'addUser',
76+
default: false,
77+
value: open,
78+
setter: setOpen
79+
},
80+
{
81+
name: 'includeTerminated',
82+
default: false,
83+
value: includeTerminated,
84+
setter: setIncludeTerminated
85+
},
86+
{
87+
name: 'search',
88+
default: '',
89+
value: searchText,
90+
setter: setSearchText
91+
}
92+
]);
9693

9794
const handleOpen = () => setOpen(true);
9895

web-ui/src/components/team-results/TeamResults.jsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, useState } from 'react';
1+
import React, { useContext, useEffect, useState } from 'react';
22
import { styled } from '@mui/material/styles';
33
import TeamSummaryCard from './TeamSummaryCard';
44
import { AppContext } from '../../context/AppContext';
@@ -11,6 +11,7 @@ import PropTypes from 'prop-types';
1111
import { TextField } from '@mui/material';
1212
import './TeamResults.css';
1313
import SkeletonLoader from '../skeleton_loader/SkeletonLoader';
14+
import { useQueryParameters } from '../../helpers/query-parameters';
1415

1516
const PREFIX = 'TeamResults';
1617
const classes = {
@@ -38,7 +39,9 @@ const displayName = 'TeamResults';
3839
const TeamResults = () => {
3940
const { state } = useContext(AppContext);
4041
const loading = selectTeamsLoading(state);
42+
const [addingTeam, setAddingTeam] = useState(false);
4143
const [searchText, setSearchText] = useState('');
44+
const [selectedTeamId, setSelectedTeamId] = useState('');
4245
const teams = selectNormalizedTeams(state, searchText);
4346

4447
const teamCards = teams.map((team, index) => {
@@ -47,10 +50,33 @@ const TeamResults = () => {
4750
key={`team-summary-${team.id}`}
4851
index={index}
4952
team={team}
53+
onTeamSelect={setSelectedTeamId}
54+
selectedTeamId={selectedTeamId}
5055
/>
5156
);
5257
});
5358

59+
useQueryParameters([
60+
{
61+
name: 'addNew',
62+
default: false,
63+
value: addingTeam,
64+
setter: setAddingTeam
65+
},
66+
{
67+
name: 'search',
68+
default: '',
69+
value: searchText,
70+
setter: setSearchText
71+
},
72+
{
73+
name: 'team',
74+
default: '',
75+
value: selectedTeamId,
76+
setter: setSelectedTeamId
77+
}
78+
]);
79+
5480
return (
5581
<Root>
5682
<div className="team-search">
@@ -63,7 +89,7 @@ const TeamResults = () => {
6389
setSearchText(e.target.value);
6490
}}
6591
/>
66-
<TeamsActions />
92+
<TeamsActions isOpen={addingTeam} onOpen={setAddingTeam} />
6793
</div>
6894
<div className="teams">
6995
{loading

web-ui/src/components/team-results/TeamSummaryCard.jsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,9 @@ const propTypes = {
5555

5656
const displayName = 'TeamSummaryCard';
5757

58-
const TeamSummaryCard = ({ team, index }) => {
58+
const TeamSummaryCard = ({ team, index, onTeamSelect, selectedTeamId }) => {
5959
const { state, dispatch } = useContext(AppContext);
6060
const { teams, userProfile, csrf } = state;
61-
const [open, setOpen] = useState(false);
6261
const [openDelete, setOpenDelete] = useState(false);
6362
const [tooltipIsOpen, setTooltipIsOpen] = useState(false);
6463

@@ -79,10 +78,8 @@ const TeamSummaryCard = ({ team, index }) => {
7978
? false
8079
: leads.some(lead => lead.memberId === userProfile.memberProfile.id);
8180

82-
const handleOpen = () => setOpen(true);
8381
const handleOpenDeleteConfirmation = () => setOpenDelete(true);
8482

85-
const handleClose = () => setOpen(false);
8683
const handleCloseDeleteConfirmation = () => setOpenDelete(false);
8784

8885
const teamId = team?.id;
@@ -113,7 +110,7 @@ const TeamSummaryCard = ({ team, index }) => {
113110

114111
const handleAction = (e, index) => {
115112
if (index === 0) {
116-
handleOpen();
113+
onTeamSelect(team.id);
117114
} else if (index === 1) {
118115
handleOpenDeleteConfirmation();
119116
}
@@ -216,8 +213,8 @@ const TeamSummaryCard = ({ team, index }) => {
216213
</CardActions>
217214
<EditTeamModal
218215
team={team}
219-
open={open}
220-
onClose={handleClose}
216+
open={team.id === selectedTeamId}
217+
onClose={() => onTeamSelect('')}
221218
onSave={async editedTeam => {
222219
const res = await updateTeam(editedTeam, csrf);
223220
const data =

web-ui/src/components/team-results/TeamsActions.jsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,18 @@ import './TeamResults.css';
1212

1313
const displayName = 'TeamsActions';
1414

15-
const TeamsActions = () => {
15+
const TeamsActions = ({ isOpen, onOpen }) => {
1616
const { state, dispatch } = useContext(AppContext);
17-
const [open, setOpen] = useState(false);
18-
1917
const { csrf } = state;
2018

21-
const handleOpen = () => setOpen(true);
22-
23-
const handleClose = () => setOpen(false);
24-
2519
return (
2620
<div className="team-actions">
27-
<Button startIcon={<GroupIcon />} onClick={handleOpen}>
21+
<Button startIcon={<GroupIcon />} onClick={() => onOpen(true)}>
2822
Add Team
2923
</Button>
3024
<AddTeamModal
31-
open={open}
32-
onClose={handleClose}
25+
open={isOpen}
26+
onClose={() => onOpen(false)}
3327
onSave={async team => {
3428
if (csrf) {
3529
let res = await createTeam(team, csrf);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useEffect } from 'react';
2+
3+
/**
4+
* @typedef {object} QPBoolean
5+
* @property {string} name
6+
* @property {boolean} default
7+
* @property {boolean} value
8+
* @property {(boolean) => void} setter - takes query parameter value and updates state
9+
* @property {[(any) => string]} toQP - takes state value and returns query parameter value
10+
*/
11+
12+
/**
13+
* @typedef {object} QPString
14+
* @property {string} name
15+
* @property {string} default
16+
* @property {string} value
17+
* @property {(string) => void} setter - takes query parameter value and updates state
18+
* @property {[(any) => string]} toQP - takes state value and returns query parameter value
19+
*/
20+
21+
/**
22+
* @param {(QPBoolean | QPString)[]} qps - query parameters
23+
*/
24+
export const useQueryParameters = qps => {
25+
useEffect(() => {
26+
const url = new URL(location.href);
27+
const params = url.searchParams;
28+
for (const qp of qps) {
29+
let v = params.get(qp.name);
30+
if (typeof qp.default === 'boolean') {
31+
qp.setter(v ? v === 'true' : qp.default);
32+
} else {
33+
if (v && Array.isArray(qp.default)) v = v.split(',');
34+
qp.setter(v || qp.default);
35+
}
36+
}
37+
}, []);
38+
39+
const dependencies = qps.map(qp => qp.value);
40+
41+
useEffect(() => {
42+
const url = new URL(location.href);
43+
let newUrl = url.origin + url.pathname;
44+
const params = {};
45+
46+
// Add query parameters listed in qps that do not have their default value.
47+
for (const qp of qps) {
48+
let { toQP, value } = qp;
49+
if (toQP) value = toQP(value);
50+
if (value && !compare(value, qp.default)) params[qp.name] = value;
51+
}
52+
53+
// Add query parameters that are not listed in qps.
54+
for (const [k, v] of url.searchParams) {
55+
if (!qps.some(qp => qp.name === k)) params[k] = v;
56+
}
57+
58+
if (Object.keys(params).length) {
59+
newUrl += '?' + new URLSearchParams(params).toString();
60+
}
61+
history.replaceState(params, '', newUrl);
62+
}, dependencies);
63+
};
64+
65+
const compare = (a, b) => stringValue(a) === stringValue(b);
66+
const stringValue = v => (Array.isArray(v) ? v.sort().join(',') : v);

0 commit comments

Comments
 (0)