From e0b4e258dccd1657bb5c407303466f742fdbda97 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 31 Dec 2024 10:44:48 +0100 Subject: [PATCH] feat: grouping of project level roles in autocomplete (#9046) --- .../MultipleRoleSelect.test.tsx | 53 +++++++++++++++++++ .../MultipleRoleSelect/MultipleRoleSelect.tsx | 48 +++++++++++++++-- 2 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 frontend/src/component/common/MultipleRoleSelect/MultipleRoleSelect.test.tsx diff --git a/frontend/src/component/common/MultipleRoleSelect/MultipleRoleSelect.test.tsx b/frontend/src/component/common/MultipleRoleSelect/MultipleRoleSelect.test.tsx new file mode 100644 index 0000000000..fc298a3c18 --- /dev/null +++ b/frontend/src/component/common/MultipleRoleSelect/MultipleRoleSelect.test.tsx @@ -0,0 +1,53 @@ +import { render } from 'utils/testRenderer'; +import { MultipleRoleSelect } from './MultipleRoleSelect'; +import { fireEvent, screen } from '@testing-library/react'; + +test('Display grouped project roles with names and descriptions', async () => { + render( + {}} + />, + ); + + const multiselect = await screen.findByLabelText('Role'); + + fireEvent.click(multiselect); + + expect(screen.getByText('Predefined project roles')).toBeInTheDocument(); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner description')).toBeInTheDocument(); + expect(screen.getByText('Custom project roles')).toBeInTheDocument(); + const customRoleA = screen.getByText('A Custom Role'); + const customRoleB = screen.getByText('B Custom Role'); + expect(customRoleA).toBeInTheDocument(); + expect(customRoleB).toBeInTheDocument(); + expect(customRoleA.compareDocumentPosition(customRoleB)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + expect(screen.getByText('Custom role description A')).toBeInTheDocument(); + expect(screen.getByText('Custom role description B')).toBeInTheDocument(); +}); diff --git a/frontend/src/component/common/MultipleRoleSelect/MultipleRoleSelect.tsx b/frontend/src/component/common/MultipleRoleSelect/MultipleRoleSelect.tsx index a5b2af9492..a7275432b8 100644 --- a/frontend/src/component/common/MultipleRoleSelect/MultipleRoleSelect.tsx +++ b/frontend/src/component/common/MultipleRoleSelect/MultipleRoleSelect.tsx @@ -3,8 +3,8 @@ import { type AutocompleteProps, type AutocompleteRenderOptionState, Checkbox, - TextField, styled, + TextField, } from '@mui/material'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; @@ -13,6 +13,7 @@ import { RoleDescription } from '../RoleDescription/RoleDescription'; import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; const StyledRoleOption = styled('div')(({ theme }) => ({ + paddingTop: theme.spacing(0.75), display: 'flex', flexDirection: 'column', '& > span:last-of-type': { @@ -29,6 +30,25 @@ interface IMultipleRoleSelectProps required?: boolean; } +function sortItems(items: T[]): T[] { + return items.sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'project' ? -1 : 1; + } + + if (a.type === 'custom') { + return a.name.localeCompare(b.name); + } + + return 0; + }); +} + +const StyledListItem = styled('li')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(0.5), +})); + export const MultipleRoleSelect = ({ roles, value, @@ -41,30 +61,48 @@ export const MultipleRoleSelect = ({ option: IRole, state: AutocompleteRenderOptionState, ) => ( -
  • + } checkedIcon={} - style={{ marginRight: 8 }} checked={state.selected} /> {option.name} {option.description} -
  • + ); + const sortedRoles = sortItems(roles); + return ( <> theme.spacing(0.5), + alignItems: 'flex-start', + }, + }, + }, + }, + }} multiple disableCloseOnSelect openOnFocus size='small' value={value} + groupBy={(option) => { + return option.type === 'project' + ? 'Predefined project roles' + : 'Custom project roles'; + }} onChange={(_, roles) => setValue(roles)} - options={roles} + options={sortedRoles} renderOption={renderRoleOption} getOptionLabel={(option) => option.name} renderInput={(params) => (