diff --git a/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.css b/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.css index 464874ec46..d7f8f4b6f6 100644 --- a/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.css +++ b/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.css @@ -1,3 +1,7 @@ +.member-selector-dialog .custom-tenure-picker { + width: 8rem; +} + .member-selector-dialog .toolbar-title-container { display: flex; flex-direction: row; @@ -30,15 +34,15 @@ } .member-selector-dialog .name-search-field { - width: 350px; - margin-right: 3rem; + width: 300px; } .member-selector-dialog .filter-input-container { display: flex; flex-direction: row; - gap: 0.5rem; + gap: 1rem; flex-wrap: wrap; + width: 100%; } .member-selector-dialog .direct-reports-only-checkbox { diff --git a/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.jsx b/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.jsx index eaf0b7452a..6c49459abc 100644 --- a/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.jsx +++ b/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.jsx @@ -1,14 +1,20 @@ +import { differenceInMonths } from 'date-fns'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { AppBar, + Autocomplete, Avatar, Button, Checkbox, Dialog, DialogContent, + Divider, + FormControl, + FormControlLabel, FormGroup, IconButton, + InputAdornment, InputLabel, List, ListItem, @@ -17,16 +23,15 @@ import { ListItemText, MenuItem, Select, + Slide, TextField, Toolbar, Typography } from '@mui/material'; -import Slide from '@mui/material/Slide'; import CloseIcon from '@mui/icons-material/Close'; -import InputAdornment from '@mui/material/InputAdornment'; import SearchIcon from '@mui/icons-material/Search'; -import Autocomplete from '@mui/material/Autocomplete'; -import FormControl from '@mui/material/FormControl'; +import { DatePicker } from '@mui/x-date-pickers'; + import { getAvatarURL } from '../../../api/api'; import { AppContext } from '../../../context/AppContext'; import { @@ -47,8 +52,6 @@ import { getMembersByGuild } from '../../../api/guild'; import { getSkillMembers } from '../../../api/memberskill'; import './MemberSelectorDialog.css'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Divider from '@mui/material/Divider'; const DialogTransition = React.forwardRef((props, ref) => ( @@ -64,9 +67,29 @@ export const FilterType = Object.freeze({ MANAGER: 'Manager' }); +export const Tenures = Object.freeze({ + All: 'All', + Months6: '6 Months', + Years1: '1 Year', + Years5: '5 Years', + Years10: '10 Years', + Years20: '20 Years', + Custom: 'Custom' +}); + +const tenureToMonths = { + [Tenures.Months6]: 6, + [Tenures.Years1]: 12, + [Tenures.Years5]: 60, + [Tenures.Years10]: 120, + [Tenures.Years20]: 240, + [Tenures.Custom]: 0 +}; + const propTypes = { initialFilters: PropTypes.arrayOf( PropTypes.shape({ + tenure: PropTypes.oneOf(Object.values(Tenures)), type: PropTypes.oneOf(Object.values(FilterType)), value: PropTypes.oneOfType([ PropTypes.string, @@ -108,8 +131,9 @@ const MemberSelectorDialog = ({ const [filter, setFilter] = useState(null); const [filteredMembers, setFilteredMembers] = useState([]); const [directReportsOnly, setDirectReportsOnly] = useState(false); - const [selectableMembers, setSelectableMembers] = useState([]); + const [tenure, setTenure] = useState(initialFilter?.tenure || Tenures.All); + const [customTenure, setCustomTenure] = useState(new Date()); const handleSubmit = useCallback(() => { const membersToAdd = members.filter(member => checked.has(member.id)); @@ -230,6 +254,22 @@ const MemberSelectorDialog = ({ member => !selectedMembers.includes(member) ); + // Exclude members that don't have the selected tenure. + if (tenure === Tenures.Custom) { + filteredMemberList = members.filter(member => { + const start = new Date(member.startDate); + return start <= customTenure; + }); + } else if (tenure !== Tenures.All) { + const now = new Date(); + const requiredMonths = tenureToMonths[tenure]; + filteredMemberList = members.filter(member => { + const start = new Date(member.startDate); + const diffMonths = differenceInMonths(now, start); + return diffMonths >= requiredMonths; + }); + } + // If a filter is selected, use it to filter the list of selectable members if (filter) { switch (filterType) { @@ -330,15 +370,17 @@ const MemberSelectorDialog = ({ }); } }, [ - state, csrf, - members, - filterType, + customTenure, + directReportsOnly, filter, + filterType, + members, + open, selectedMembers, showError, - directReportsOnly, - open + state, + tenure ]); useEffect(() => { @@ -438,13 +480,32 @@ const MemberSelectorDialog = ({ ) }} /> + + Filter Type + + ( )} @@ -458,53 +519,71 @@ const MemberSelectorDialog = ({ value={filter} onChange={(_, value) => setFilter(value)} /> + + handleToggleAll(event.target.checked)} + checked={ + selectableMembers.length > 0 && + visibleChecked().length === selectableMembers.length + } + indeterminate={ + visibleChecked().length > 0 && + visibleChecked().length !== selectableMembers.length + } + disabled={selectableMembers.length === 0} + /> + + +
+ {filterType === FilterType.MANAGER && ( + + setDirectReportsOnly(event.target.checked) + } + /> + } + label="Direct reports only" + /> + )} - Filter by + Required Tenure - {filterType === FilterType.MANAGER && ( - - setDirectReportsOnly(event.target.checked) - } - /> - } - label="Direct reports only" + {tenure === Tenures.Custom && ( + )}
- handleToggleAll(event.target.checked)} - checked={ - selectableMembers.length > 0 && - visibleChecked().length === selectableMembers.length - } - indeterminate={ - visibleChecked().length > 0 && - visibleChecked().length !== selectableMembers.length - } - disabled={selectableMembers.length === 0} - />
diff --git a/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.spec.jsx b/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.spec.jsx index 7a6f2d1621..5e5f8cc157 100644 --- a/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.spec.jsx +++ b/web-ui/src/components/member_selector/member_selector_dialog/MemberSelectorDialog.spec.jsx @@ -122,10 +122,10 @@ describe('MemberSelectorDialog', () => { expect(memberList).toHaveLength(initialState.memberProfiles.length); const filterField = await screen.findByRole('combobox', { - name: /filter members/i + name: /filter value/i }); let filterTypeField = await screen.findByRole('combobox', { - name: /filter by/i + name: /filter type/i }); expect(filterField.innerHTML).toBe(''); expect(filterTypeField.innerHTML).toBe('Team'); @@ -155,10 +155,10 @@ describe('MemberSelectorDialog', () => { expect(memberList).toHaveLength(initialState.memberProfiles.length); const filterField = await screen.findByRole('combobox', { - name: /filter members/i + name: /filter value/i }); let filterTypeField = await screen.findByRole('combobox', { - name: /filter by/i + name: /filter type/i }); expect(filterField.innerHTML).toBe(''); expect(filterTypeField.innerHTML).toBe('Team'); diff --git a/web-ui/src/components/member_selector/member_selector_dialog/__snapshots__/MemberSelectorDialog.test.jsx.snap b/web-ui/src/components/member_selector/member_selector_dialog/__snapshots__/MemberSelectorDialog.test.jsx.snap index c34574635c..560c0170df 100644 --- a/web-ui/src/components/member_selector/member_selector_dialog/__snapshots__/MemberSelectorDialog.test.jsx.snap +++ b/web-ui/src/components/member_selector/member_selector_dialog/__snapshots__/MemberSelectorDialog.test.jsx.snap @@ -145,6 +145,62 @@ exports[`MemberSelectorDialog > renders correctly 1`] = ` +
+ +
+ + + + +
+
@@ -154,10 +210,10 @@ exports[`MemberSelectorDialog > renders correctly 1`] = `
renders correctly 1`] = ` autocapitalize="none" autocomplete="off" class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputAdornedEnd MuiAutocomplete-input MuiAutocomplete-inputFocused css-nxo287-MuiInputBase-input-MuiOutlinedInput-input" - id=":r2:" + id=":r3:" placeholder="Search for team" role="combobox" spellcheck="false" @@ -210,13 +266,44 @@ exports[`MemberSelectorDialog > renders correctly 1`] = ` class="css-yjsfm1" > - Filter Members + Filter Value
+ + + + + + +
+
@@ -225,13 +312,13 @@ exports[`MemberSelectorDialog > renders correctly 1`] = ` data-shrink="true" id="member-filter-label" > - Filter by + Required Tenure
- Filter by + Required Tenure
- - - -