Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 25 additions & 28 deletions web-ui/src/components/admin/roles/Roles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { isArrayPresent } from '../../../helpers/checks';
import { useQueryParameters } from '../../../helpers/query-parameters';

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

const Roles = () => {
const { state, dispatch } = useContext(AppContext);
Expand All @@ -62,29 +63,27 @@ 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));
if (!roles) console.error('Roles.jsx: state.roles is not set!');
const allRoles = roles.map(r => r.role).sort();
useQueryParameters([
{
name: 'roles',
default: allRoles,
value: selectedRoles,
setter(value) {
setSelectedRoles(isArrayPresent(value) ? value.sort() : allRoles);
},
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 = {};
Expand Down Expand Up @@ -227,9 +226,7 @@ const Roles = () => {
value={selectedRoles}
onChange={event => {
const value = event.target.value;
setSelectedRoles(
typeof value === 'string' ? value.split(',') : value
);
setSelectedRoles(value.sort());
}}
input={<OutlinedInput label="Roles" />}
renderValue={selected => selected.join(', ')}
Expand Down
53 changes: 25 additions & 28 deletions web-ui/src/components/admin/users/Users.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,10 +16,7 @@ import {
selectNormalizedMembers,
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 { useQueryParameters } from '../../../helpers/query-parameters';

import './Users.css';

Expand Down Expand Up @@ -69,30 +70,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]);
useQueryParameters([
{
name: 'addUser',
default: false,
value: open,
setter: setOpen
},
{
name: 'includeTerminated',
default: false,
value: includeTerminated,
setter: setIncludeTerminated
},
{
name: 'search',
default: '',
value: searchText,
setter: setSearchText
}
]);

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

Expand Down
30 changes: 28 additions & 2 deletions web-ui/src/components/team-results/TeamResults.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,6 +11,7 @@ import PropTypes from 'prop-types';
import { TextField } from '@mui/material';
import './TeamResults.css';
import SkeletonLoader from '../skeleton_loader/SkeletonLoader';
import { useQueryParameters } from '../../helpers/query-parameters';

const PREFIX = 'TeamResults';
const classes = {
Expand Down Expand Up @@ -38,7 +39,9 @@ const displayName = 'TeamResults';
const TeamResults = () => {
const { state } = useContext(AppContext);
const loading = selectTeamsLoading(state);
const [addingTeam, setAddingTeam] = useState(false);
const [searchText, setSearchText] = useState('');
const [selectedTeamId, setSelectedTeamId] = useState('');
const teams = selectNormalizedTeams(state, searchText);

const teamCards = teams.map((team, index) => {
Expand All @@ -47,10 +50,33 @@ const TeamResults = () => {
key={`team-summary-${team.id}`}
index={index}
team={team}
onTeamSelect={setSelectedTeamId}
selectedTeamId={selectedTeamId}
/>
);
});

useQueryParameters([
{
name: 'addNew',
default: false,
value: addingTeam,
setter: setAddingTeam
},
{
name: 'search',
default: '',
value: searchText,
setter: setSearchText
},
{
name: 'team',
default: '',
value: selectedTeamId,
setter: setSelectedTeamId
}
]);

return (
<Root>
<div className="team-search">
Expand All @@ -63,7 +89,7 @@ const TeamResults = () => {
setSearchText(e.target.value);
}}
/>
<TeamsActions />
<TeamsActions isOpen={addingTeam} onOpen={setAddingTeam} />
</div>
<div className="teams">
{loading
Expand Down
11 changes: 4 additions & 7 deletions web-ui/src/components/team-results/TeamSummaryCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This state is tracked in TeamResults now.

const [openDelete, setOpenDelete] = useState(false);
const [tooltipIsOpen, setTooltipIsOpen] = useState(false);

Expand All @@ -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;
Expand Down Expand Up @@ -113,7 +110,7 @@ const TeamSummaryCard = ({ team, index }) => {

const handleAction = (e, index) => {
if (index === 0) {
handleOpen();
onTeamSelect(team.id);
} else if (index === 1) {
handleOpenDeleteConfirmation();
}
Expand Down Expand Up @@ -216,8 +213,8 @@ const TeamSummaryCard = ({ team, index }) => {
</CardActions>
<EditTeamModal
team={team}
open={open}
onClose={handleClose}
open={team.id === selectedTeamId}
onClose={() => onTeamSelect('')}
onSave={async editedTeam => {
const res = await updateTeam(editedTeam, csrf);
const data =
Expand Down
14 changes: 4 additions & 10 deletions web-ui/src/components/team-results/TeamsActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="team-actions">
<Button startIcon={<GroupIcon />} onClick={handleOpen}>
<Button startIcon={<GroupIcon />} onClick={() => onOpen(true)}>
Add Team
</Button>
<AddTeamModal
open={open}
onClose={handleClose}
open={isOpen}
onClose={() => onOpen(false)}
onSave={async team => {
if (csrf) {
let res = await createTeam(team, csrf);
Expand Down
66 changes: 66 additions & 0 deletions web-ui/src/helpers/query-parameters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useEffect } from 'react';

/**
* @typedef {object} QPBoolean
* @property {string} name
* @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
*/

/**
* @typedef {object} QPString
* @property {string} name
* @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
*/

/**
* @param {(QPBoolean | QPString)[]} qps - query parameters
*/
export const useQueryParameters = qps => {
useEffect(() => {
const url = new URL(location.href);
const params = url.searchParams;
for (const qp of qps) {
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);
}
}
}, []);

const dependencies = qps.map(qp => qp.value);

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();
}
history.replaceState(params, '', newUrl);
}, dependencies);
};

const compare = (a, b) => stringValue(a) === stringValue(b);
const stringValue = v => (Array.isArray(v) ? v.sort().join(',') : v);
Loading