1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01: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 { Chip, IconButton, styled } from '@mui/material';
import { FilterItemOperator } from './FilterItemOperator/FilterItemOperator';
import { FILTER_ITEM } from 'utils/testIds';
const StyledChip = styled(
({
isActive,
...props
}: { isActive: boolean } & ComponentProps<typeof Chip>) => (
<Chip {...props} />
<Chip data-testid={FILTER_ITEM} {...props} />
),
)(({ theme, isActive = false }) => ({
borderRadius: `${theme.shape.borderRadius}px`,

View File

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

View File

@ -2,6 +2,7 @@ import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { FeatureToggleFilters } from './FeatureToggleFilters';
import { FILTER_ITEM } from 'utils/testIds';
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();
});
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,
} from 'component/common/FilterItem/FilterItem';
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
import { FILTER_ITEM } from 'utils/testIds';
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',
@ -42,10 +43,6 @@ export interface IFilterItem {
pluralOperators: [string, ...string[]];
}
export type IFilterVisibility = {
[key: string]: boolean | undefined;
};
export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
state,
onChange,
@ -66,14 +63,30 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
];
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 filterVisibility = {
...visibleFilters,
[label]: false,
};
setVisibleFilters(filterVisibility);
const deselectFilter = (label: string) => {
const newSelectedFilters = selectedFilters.filter((f) => f !== label);
const newUnselectedFilters = [...unselectedFilters, label].sort();
setSelectedFilters(newSelectedFilters);
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(() => {
@ -147,59 +160,102 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
useEffect(() => {
const hasMultipleProjects = projects.length > 1;
const filterVisibility: IFilterVisibility = {
State: Boolean(state.state),
...(hasMultipleProjects && {
Project: Boolean(state.project),
}),
Tags: Boolean(state.tag),
Segment: Boolean(state.segment),
'Created date': Boolean(state.createdAt),
};
setVisibleFilters(filterVisibility);
const fieldsMapping = [
{
stateField: 'state',
label: 'State',
},
...(hasMultipleProjects
? [
{
stateField: 'project',
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)]);
const hasAvailableFilters = Object.values(visibleFilters).some(
(value) => !value,
);
const hasAvailableFilters = unselectedFilters.length > 0;
return (
<StyledBox>
{availableFilters.map(
(filter) =>
visibleFilters[filter.label] && (
<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={() => hideFilter(filter.label)}
{selectedFilters.map((selectedFilter) => {
if (selectedFilter === 'Created date') {
return (
<FilterDateItem
label={'Created date'}
state={state.createdAt}
onChange={(value) => onChange({ createdAt: value })}
operators={['IS_ON_OR_AFTER', 'IS_BEFORE']}
onChipClose={() => deselectFilter('Created date')}
/>
),
)}
<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
condition={hasAvailableFilters}
show={
<AddFilterButton
visibleFilters={visibleFilters}
setVisibleFilters={setVisibleFilters}
visibleOptions={unselectedFilters}
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_SELECT = 'BATCH_SELECT';
export const MORE_BATCH_ACTIONS = 'MORE_BATCH_ACTIONS';
export const FILTER_ITEM = 'FILTER_ITEM';