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:
parent
850b78a699
commit
386c4baa86
@ -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`,
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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']);
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user