1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

refactor: filter abstraction (#5625)

This commit is contained in:
Jaanus Sellin 2023-12-13 11:43:02 +02:00 committed by GitHub
parent 17b747ea8f
commit ed4a182e7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 178 additions and 153 deletions

View File

@ -1,6 +1,6 @@
import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import { FilterItemParams } from '../FilterItem/FilterItem';
import { FilterItemParams } from 'component/filter/FilterItem/FilterItem';
import { FilterDateItem, IFilterDateItemProps } from './FilterDateItem';
const getDate = (option: string) => screen.getByText(option);

View File

@ -1,13 +1,13 @@
import { Box } from '@mui/material';
import React, { FC, useEffect, useRef, useState } from 'react';
import { StyledPopover } from '../FilterItem/FilterItem.styles';
import { FilterItemChip } from '../FilterItem/FilterItemChip/FilterItemChip';
import { StyledPopover } from 'component/filter/FilterItem/FilterItem.styles';
import { FilterItemChip } from 'component/filter/FilterItem/FilterItemChip/FilterItemChip';
import { DateCalendar, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { format } from 'date-fns';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { getLocalizedDateString } from '../util';
import { FilterItemParams } from '../FilterItem/FilterItem';
import { FilterItemParams } from 'component/filter/FilterItem/FilterItem';
export interface IFilterDateItemProps {
label: string;

View File

@ -1,56 +1,18 @@
import { useEffect, useState, VFC } from 'react';
import { Box, styled } from '@mui/material';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AddFilterButton from './AddFilterButton/AddFilterButton';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { FilterDateItem } from 'component/common/FilterDateItem/FilterDateItem';
import {
FilterItem,
FilterItemParams,
} from 'component/common/FilterItem/FilterItem';
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',
padding: theme.spacing(2, 3),
gap: theme.spacing(1),
flexWrap: 'wrap',
}));
type FeatureTogglesListFilters = {
project?: FilterItemParams | null | undefined;
tag?: FilterItemParams | null | undefined;
state?: FilterItemParams | null | undefined;
segment?: FilterItemParams | null | undefined;
createdAt?: FilterItemParams | null | undefined;
};
import {
FilterItemParamHolder,
Filters,
IFilterItem,
} from 'component/filter/Filters';
interface IFeatureToggleFiltersProps {
state: FeatureTogglesListFilters;
onChange: (value: FeatureTogglesListFilters) => void;
state: FilterItemParamHolder;
onChange: (value: FilterItemParamHolder) => void;
}
type IBaseFilterItem = {
label: string;
options: {
label: string;
value: string;
}[];
filterKey: keyof FeatureTogglesListFilters;
};
type ITextFilterItem = IBaseFilterItem & {
singularOperators: [string, ...string[]];
pluralOperators: [string, ...string[]];
};
type IDateFilterItem = IBaseFilterItem & {
dateOperators: [string, ...string[]];
};
type IFilterItem = ITextFilterItem | IDateFilterItem;
export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
state,
onChange,
@ -71,31 +33,6 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
];
const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]);
const [unselectedFilters, setUnselectedFilters] = useState<string[]>([]);
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
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(() => {
const projectsOptions = (projects || []).map((project) => ({
@ -171,81 +108,11 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
JSON.stringify(tags),
]);
useEffect(() => {
const newSelectedFilters = availableFilters
.filter((field) =>
Boolean(
state[field.filterKey as keyof FeatureTogglesListFilters],
),
)
.map((field) => field.label);
const newUnselectedFilters = availableFilters
.filter(
(field) =>
!state[field.filterKey as keyof FeatureTogglesListFilters],
)
.map((field) => field.label)
.sort();
setSelectedFilters(
mergeArraysKeepingOrder(selectedFilters, newSelectedFilters),
);
setUnselectedFilters(newUnselectedFilters);
}, [JSON.stringify(state), JSON.stringify(availableFilters)]);
const hasAvailableFilters = unselectedFilters.length > 0;
return (
<StyledBox>
{selectedFilters.map((selectedFilter) => {
const filter = availableFilters.find(
(filter) => filter.label === selectedFilter,
);
if (!filter) {
return null;
}
if ('dateOperators' in filter) {
return (
<FilterDateItem
label={filter.label}
state={state[filter.filterKey]}
onChange={(value) =>
onChange({ [filter.filterKey]: value })
}
operators={filter.dateOperators}
onChipClose={() => deselectFilter(filter.label)}
<Filters
availableFilters={availableFilters}
state={state}
onChange={onChange}
/>
);
}
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
visibleOptions={unselectedFilters}
setVisibleOptions={setUnselectedFilters}
hiddenOptions={selectedFilters}
setHiddenOptions={setSelectedFilters}
/>
}
/>
</StyledBox>
);
};

View File

@ -93,6 +93,14 @@ export const FeatureToggleListTable: VFC = () => {
stateConfig,
);
const filterState = {
project: tableState.project,
tag: tableState.tag,
state: tableState.state,
segment: tableState.segment,
createdAt: tableState.createdAt,
};
const {
features = [],
total,
@ -327,7 +335,10 @@ export const FeatureToggleListTable: VFC = () => {
</PageHeader>
}
>
<FeatureToggleFilters onChange={setTableState} state={tableState} />
<FeatureToggleFilters
onChange={setTableState}
state={filterState}
/>
<SearchHighlightProvider value={tableState.query || ''}>
<PaginatedTable tableInstance={table} totalItems={total} />
</SearchHighlightProvider>

View File

@ -1,6 +1,6 @@
import { ComponentProps, FC } from 'react';
import { ArrowDropDown, Close, TopicOutlined } from '@mui/icons-material';
import { ConditionallyRender } from '../../ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Chip, IconButton, styled } from '@mui/material';
import { FilterItemOperator } from './FilterItemOperator/FilterItemOperator';
import { FILTER_ITEM } from 'utils/testIds';

View File

@ -0,0 +1,147 @@
import { useEffect, useState, VFC } from 'react';
import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AddFilterButton from './AddFilterButton';
import { FilterDateItem } from 'component/common/FilterDateItem/FilterDateItem';
import { FilterItem, FilterItemParams } from './FilterItem/FilterItem';
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',
padding: theme.spacing(2, 3),
gap: theme.spacing(1),
flexWrap: 'wrap',
}));
export type FilterItemParamHolder = Record<
string,
FilterItemParams | null | undefined
>;
interface IFilterProps {
state: FilterItemParamHolder;
onChange: (value: FilterItemParamHolder) => void;
availableFilters: IFilterItem[];
}
type IBaseFilterItem = {
label: string;
options: {
label: string;
value: string;
}[];
filterKey: string;
};
type ITextFilterItem = IBaseFilterItem & {
singularOperators: [string, ...string[]];
pluralOperators: [string, ...string[]];
};
type IDateFilterItem = IBaseFilterItem & {
dateOperators: [string, ...string[]];
};
export type IFilterItem = ITextFilterItem | IDateFilterItem;
export const Filters: VFC<IFilterProps> = ({
state,
onChange,
availableFilters,
}) => {
const [unselectedFilters, setUnselectedFilters] = useState<string[]>([]);
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
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(() => {
const newSelectedFilters = availableFilters
.filter((field) => Boolean(state[field.filterKey]))
.map((field) => field.label);
const newUnselectedFilters = availableFilters
.filter((field) => !state[field.filterKey])
.map((field) => field.label)
.sort();
setSelectedFilters(
mergeArraysKeepingOrder(selectedFilters, newSelectedFilters),
);
setUnselectedFilters(newUnselectedFilters);
}, [JSON.stringify(state), JSON.stringify(availableFilters)]);
const hasAvailableFilters = unselectedFilters.length > 0;
return (
<StyledBox>
{selectedFilters.map((selectedFilter) => {
const filter = availableFilters.find(
(filter) => filter.label === selectedFilter,
);
if (!filter) {
return null;
}
if ('dateOperators' in filter) {
return (
<FilterDateItem
label={filter.label}
state={state[filter.filterKey]}
onChange={(value) =>
onChange({ [filter.filterKey]: value })
}
operators={filter.dateOperators}
onChipClose={() => deselectFilter(filter.label)}
/>
);
}
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
visibleOptions={unselectedFilters}
setVisibleOptions={setUnselectedFilters}
hiddenOptions={selectedFilters}
setHiddenOptions={setSelectedFilters}
/>
}
/>
</StyledBox>
);
};

View File

@ -5,9 +5,9 @@ import { FilterDateItem } from 'component/common/FilterDateItem/FilterDateItem';
import {
FilterItem,
FilterItemParams,
} from 'component/common/FilterItem/FilterItem';
} from 'component/filter/FilterItem/FilterItem';
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
import AddFilterButton from 'component/feature/FeatureToggleList/FeatureToggleFilters/AddFilterButton/AddFilterButton';
import AddFilterButton from 'component/filter/AddFilterButton';
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',