1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

feat: keep filters ordered based on user selection (#5609)

This commit is contained in:
Jaanus Sellin 2023-12-12 13:01:23 +02:00 committed by GitHub
parent 850b78a699
commit 386c4baa86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 153 additions and 73 deletions

View File

@ -4,13 +4,14 @@ import { ArrowDropDown, Close, TopicOutlined } from '@mui/icons-material';
import { ConditionallyRender } from '../../ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from '../../ConditionallyRender/ConditionallyRender';
import { Chip, IconButton, styled } from '@mui/material'; import { Chip, IconButton, styled } from '@mui/material';
import { FilterItemOperator } from './FilterItemOperator/FilterItemOperator'; import { FilterItemOperator } from './FilterItemOperator/FilterItemOperator';
import { FILTER_ITEM } from 'utils/testIds';
const StyledChip = styled( const StyledChip = styled(
({ ({
isActive, isActive,
...props ...props
}: { isActive: boolean } & ComponentProps<typeof Chip>) => ( }: { isActive: boolean } & ComponentProps<typeof Chip>) => (
<Chip {...props} /> <Chip data-testid={FILTER_ITEM} {...props} />
), ),
)(({ theme, isActive = false }) => ({ )(({ theme, isActive = false }) => ({
borderRadius: `${theme.shape.borderRadius}px`, borderRadius: `${theme.shape.borderRadius}px`,

View File

@ -2,22 +2,26 @@ import React, { useState } from 'react';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { IFilterVisibility, IFilterItem } from '../FeatureToggleFilters'; import { styled } from '@mui/material';
import { Box, styled } from '@mui/material';
import { Add } from '@mui/icons-material'; import { Add } from '@mui/icons-material';
const StyledButton = styled(Button)(({ theme }) => ({ const StyledButton = styled(Button)(({ theme }) => ({
margin: theme.spacing(-1, 0, -1, 0), margin: theme.spacing(-1, 0, -1, 0),
padding: theme.spacing(1.25), padding: theme.spacing(1.25),
})); }));
interface IAddFilterButtonProps { interface IAddFilterButtonProps {
visibleFilters: IFilterVisibility; visibleOptions: string[];
setVisibleFilters: (filters: IFilterVisibility) => void; setVisibleOptions: (filters: string[]) => void;
hiddenOptions: string[];
setHiddenOptions: (filters: string[]) => void;
} }
const AddFilterButton = ({ const AddFilterButton = ({
visibleFilters, visibleOptions,
setVisibleFilters, setVisibleOptions,
hiddenOptions,
setHiddenOptions,
}: IAddFilterButtonProps) => { }: IAddFilterButtonProps) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
@ -28,12 +32,12 @@ const AddFilterButton = ({
setAnchorEl(null); setAnchorEl(null);
}; };
const onClick = (label: string) => { const onSelect = (label: string) => {
const filterVisibility = { const newVisibleOptions = visibleOptions.filter((f) => f !== label);
...visibleFilters, const newHiddenOptions = [...hiddenOptions, label];
[label]: true,
}; setHiddenOptions(newHiddenOptions);
setVisibleFilters(filterVisibility); setVisibleOptions(newVisibleOptions);
handleClose(); handleClose();
}; };
@ -49,13 +53,11 @@ const AddFilterButton = ({
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
onClose={handleClose} onClose={handleClose}
> >
{Object.entries(visibleFilters).map(([label, enabled]) => {visibleOptions.map((label) => (
!enabled ? ( <MenuItem key={label} onClick={() => onSelect(label)}>
<MenuItem key={label} onClick={() => onClick(label)}> {label}
{label} </MenuItem>
</MenuItem> ))}
) : null,
)}
</Menu> </Menu>
</div> </div>
); );

View File

@ -2,6 +2,7 @@ import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer'; import { render } from 'utils/testRenderer';
import { testServerRoute, testServerSetup } from 'utils/testServer'; import { testServerRoute, testServerSetup } from 'utils/testServer';
import { FeatureToggleFilters } from './FeatureToggleFilters'; import { FeatureToggleFilters } from './FeatureToggleFilters';
import { FILTER_ITEM } from 'utils/testIds';
const server = testServerSetup(); const server = testServerSetup();
@ -38,3 +39,21 @@ test('should not render projects filters when less than two project', async () =
expect(screen.queryByText('Projects')).not.toBeInTheDocument(); expect(screen.queryByText('Projects')).not.toBeInTheDocument();
}); });
test('should keep filters order when adding a new filter', async () => {
render(<FeatureToggleFilters onChange={() => {}} state={{}} />);
const valuesElement = await screen.findByText('Tags');
expect(valuesElement).toBeInTheDocument();
valuesElement.click();
const stateElement = await screen.findByText('State');
expect(stateElement).toBeInTheDocument();
stateElement.click();
const filterItems = screen.getAllByTestId(FILTER_ITEM);
const filterTexts = filterItems.map((item) => item.textContent);
expect(filterTexts).toEqual(['Tags', 'State']);
});

View File

@ -10,6 +10,7 @@ import {
FilterItemParams, FilterItemParams,
} from 'component/common/FilterItem/FilterItem'; } from 'component/common/FilterItem/FilterItem';
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags'; import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
import { FILTER_ITEM } from 'utils/testIds';
const StyledBox = styled(Box)(({ theme }) => ({ const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
@ -42,10 +43,6 @@ export interface IFilterItem {
pluralOperators: [string, ...string[]]; pluralOperators: [string, ...string[]];
} }
export type IFilterVisibility = {
[key: string]: boolean | undefined;
};
export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
state, state,
onChange, onChange,
@ -66,14 +63,30 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
]; ];
const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]); const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]);
const [visibleFilters, setVisibleFilters] = useState<IFilterVisibility>({}); const [unselectedFilters, setUnselectedFilters] = useState<string[]>([]);
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
const hideFilter = (label: string) => { const deselectFilter = (label: string) => {
const filterVisibility = { const newSelectedFilters = selectedFilters.filter((f) => f !== label);
...visibleFilters, const newUnselectedFilters = [...unselectedFilters, label].sort();
[label]: false,
}; setSelectedFilters(newSelectedFilters);
setVisibleFilters(filterVisibility); setUnselectedFilters(newUnselectedFilters);
};
const mergeArraysKeepingOrder = (
firstArray: string[],
secondArray: string[],
): string[] => {
const elementsSet = new Set(firstArray);
secondArray.forEach((element) => {
if (!elementsSet.has(element)) {
firstArray.push(element);
}
});
return firstArray;
}; };
useEffect(() => { useEffect(() => {
@ -147,59 +160,102 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
useEffect(() => { useEffect(() => {
const hasMultipleProjects = projects.length > 1; const hasMultipleProjects = projects.length > 1;
const filterVisibility: IFilterVisibility = { const fieldsMapping = [
State: Boolean(state.state), {
...(hasMultipleProjects && { stateField: 'state',
Project: Boolean(state.project), label: 'State',
}), },
Tags: Boolean(state.tag), ...(hasMultipleProjects
Segment: Boolean(state.segment), ? [
'Created date': Boolean(state.createdAt), {
}; stateField: 'project',
setVisibleFilters(filterVisibility); label: 'Project',
},
]
: []),
{
stateField: 'tag',
label: 'Tags',
},
{
stateField: 'segment',
label: 'Segment',
},
{
stateField: 'createdAt',
label: 'Created date',
},
];
const newSelectedFilters = fieldsMapping
.filter((field) =>
Boolean(
state[field.stateField as keyof FeatureTogglesListFilters],
),
)
.map((field) => field.label);
const newUnselectedFilters = fieldsMapping
.filter(
(field) =>
!state[field.stateField as keyof FeatureTogglesListFilters],
)
.map((field) => field.label)
.sort();
setSelectedFilters(
mergeArraysKeepingOrder(selectedFilters, newSelectedFilters),
);
setUnselectedFilters(newUnselectedFilters);
}, [JSON.stringify(state), JSON.stringify(projects)]); }, [JSON.stringify(state), JSON.stringify(projects)]);
const hasAvailableFilters = Object.values(visibleFilters).some( const hasAvailableFilters = unselectedFilters.length > 0;
(value) => !value,
);
return ( return (
<StyledBox> <StyledBox>
{availableFilters.map( {selectedFilters.map((selectedFilter) => {
(filter) => if (selectedFilter === 'Created date') {
visibleFilters[filter.label] && ( return (
<FilterItem <FilterDateItem
key={filter.label} label={'Created date'}
label={filter.label} state={state.createdAt}
state={state[filter.filterKey]} onChange={(value) => onChange({ createdAt: value })}
options={filter.options} operators={['IS_ON_OR_AFTER', 'IS_BEFORE']}
onChange={(value) => onChipClose={() => deselectFilter('Created date')}
onChange({ [filter.filterKey]: value })
}
singularOperators={filter.singularOperators}
pluralOperators={filter.pluralOperators}
onChipClose={() => hideFilter(filter.label)}
/> />
), );
)}
<ConditionallyRender
condition={Boolean(visibleFilters['Created date'])}
show={
<FilterDateItem
label={'Created date'}
state={state.createdAt}
onChange={(value) => onChange({ createdAt: value })}
operators={['IS_ON_OR_AFTER', 'IS_BEFORE']}
onChipClose={() => hideFilter('Created date')}
/>
} }
/>
const filter = availableFilters.find(
(filter) => filter.label === selectedFilter,
);
if (!filter) {
return null;
}
return (
<FilterItem
key={filter.label}
label={filter.label}
state={state[filter.filterKey]}
options={filter.options}
onChange={(value) =>
onChange({ [filter.filterKey]: value })
}
singularOperators={filter.singularOperators}
pluralOperators={filter.pluralOperators}
onChipClose={() => deselectFilter(filter.label)}
/>
);
})}
<ConditionallyRender <ConditionallyRender
condition={hasAvailableFilters} condition={hasAvailableFilters}
show={ show={
<AddFilterButton <AddFilterButton
visibleFilters={visibleFilters} visibleOptions={unselectedFilters}
setVisibleFilters={setVisibleFilters} setVisibleOptions={setUnselectedFilters}
hiddenOptions={selectedFilters}
setHiddenOptions={setSelectedFilters}
/> />
} }
/> />

View File

@ -98,3 +98,5 @@ export const BATCH_ACTIONS_BAR = 'BATCH_ACTIONS_BAR';
export const BATCH_SELECTED_COUNT = 'BATCH_SELECTED_COUNT'; export const BATCH_SELECTED_COUNT = 'BATCH_SELECTED_COUNT';
export const BATCH_SELECT = 'BATCH_SELECT'; export const BATCH_SELECT = 'BATCH_SELECT';
export const MORE_BATCH_ACTIONS = 'MORE_BATCH_ACTIONS'; export const MORE_BATCH_ACTIONS = 'MORE_BATCH_ACTIONS';
export const FILTER_ITEM = 'FILTER_ITEM';