mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-12 13:48:35 +02:00
refactor: filter abstraction (#5625)
This commit is contained in:
parent
17b747ea8f
commit
ed4a182e7e
@ -1,6 +1,6 @@
|
|||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import { FilterItemParams } from '../FilterItem/FilterItem';
|
import { FilterItemParams } from 'component/filter/FilterItem/FilterItem';
|
||||||
import { FilterDateItem, IFilterDateItemProps } from './FilterDateItem';
|
import { FilterDateItem, IFilterDateItemProps } from './FilterDateItem';
|
||||||
|
|
||||||
const getDate = (option: string) => screen.getByText(option);
|
const getDate = (option: string) => screen.getByText(option);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||||
import { StyledPopover } from '../FilterItem/FilterItem.styles';
|
import { StyledPopover } from 'component/filter/FilterItem/FilterItem.styles';
|
||||||
import { FilterItemChip } from '../FilterItem/FilterItemChip/FilterItemChip';
|
import { FilterItemChip } from 'component/filter/FilterItem/FilterItemChip/FilterItemChip';
|
||||||
import { DateCalendar, LocalizationProvider } from '@mui/x-date-pickers';
|
import { DateCalendar, LocalizationProvider } from '@mui/x-date-pickers';
|
||||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { getLocalizedDateString } from '../util';
|
import { getLocalizedDateString } from '../util';
|
||||||
import { FilterItemParams } from '../FilterItem/FilterItem';
|
import { FilterItemParams } from 'component/filter/FilterItem/FilterItem';
|
||||||
|
|
||||||
export interface IFilterDateItemProps {
|
export interface IFilterDateItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -1,56 +1,18 @@
|
|||||||
import { useEffect, useState, VFC } from 'react';
|
import { useEffect, useState, VFC } from 'react';
|
||||||
import { Box, styled } from '@mui/material';
|
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
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 { 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';
|
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
|
||||||
|
import {
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
FilterItemParamHolder,
|
||||||
display: 'flex',
|
Filters,
|
||||||
padding: theme.spacing(2, 3),
|
IFilterItem,
|
||||||
gap: theme.spacing(1),
|
} from 'component/filter/Filters';
|
||||||
flexWrap: 'wrap',
|
|
||||||
}));
|
|
||||||
|
|
||||||
type FeatureTogglesListFilters = {
|
|
||||||
project?: FilterItemParams | null | undefined;
|
|
||||||
tag?: FilterItemParams | null | undefined;
|
|
||||||
state?: FilterItemParams | null | undefined;
|
|
||||||
segment?: FilterItemParams | null | undefined;
|
|
||||||
createdAt?: FilterItemParams | null | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IFeatureToggleFiltersProps {
|
interface IFeatureToggleFiltersProps {
|
||||||
state: FeatureTogglesListFilters;
|
state: FilterItemParamHolder;
|
||||||
onChange: (value: FeatureTogglesListFilters) => void;
|
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> = ({
|
export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
|
||||||
state,
|
state,
|
||||||
onChange,
|
onChange,
|
||||||
@ -71,31 +33,6 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
const projectsOptions = (projects || []).map((project) => ({
|
const projectsOptions = (projects || []).map((project) => ({
|
||||||
@ -171,81 +108,11 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
|
|||||||
JSON.stringify(tags),
|
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 (
|
return (
|
||||||
<StyledBox>
|
<Filters
|
||||||
{selectedFilters.map((selectedFilter) => {
|
availableFilters={availableFilters}
|
||||||
const filter = availableFilters.find(
|
state={state}
|
||||||
(filter) => filter.label === selectedFilter,
|
onChange={onChange}
|
||||||
);
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -93,6 +93,14 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
stateConfig,
|
stateConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filterState = {
|
||||||
|
project: tableState.project,
|
||||||
|
tag: tableState.tag,
|
||||||
|
state: tableState.state,
|
||||||
|
segment: tableState.segment,
|
||||||
|
createdAt: tableState.createdAt,
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
features = [],
|
features = [],
|
||||||
total,
|
total,
|
||||||
@ -327,7 +335,10 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FeatureToggleFilters onChange={setTableState} state={tableState} />
|
<FeatureToggleFilters
|
||||||
|
onChange={setTableState}
|
||||||
|
state={filterState}
|
||||||
|
/>
|
||||||
<SearchHighlightProvider value={tableState.query || ''}>
|
<SearchHighlightProvider value={tableState.query || ''}>
|
||||||
<PaginatedTable tableInstance={table} totalItems={total} />
|
<PaginatedTable tableInstance={table} totalItems={total} />
|
||||||
</SearchHighlightProvider>
|
</SearchHighlightProvider>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ComponentProps, FC } from 'react';
|
import { ComponentProps, FC } from 'react';
|
||||||
import { ArrowDropDown, Close, TopicOutlined } from '@mui/icons-material';
|
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 { Chip, IconButton, styled } from '@mui/material';
|
||||||
import { FilterItemOperator } from './FilterItemOperator/FilterItemOperator';
|
import { FilterItemOperator } from './FilterItemOperator/FilterItemOperator';
|
||||||
import { FILTER_ITEM } from 'utils/testIds';
|
import { FILTER_ITEM } from 'utils/testIds';
|
147
frontend/src/component/filter/Filters.tsx
Normal file
147
frontend/src/component/filter/Filters.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -5,9 +5,9 @@ import { FilterDateItem } from 'component/common/FilterDateItem/FilterDateItem';
|
|||||||
import {
|
import {
|
||||||
FilterItem,
|
FilterItem,
|
||||||
FilterItemParams,
|
FilterItemParams,
|
||||||
} from 'component/common/FilterItem/FilterItem';
|
} from 'component/filter/FilterItem/FilterItem';
|
||||||
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
|
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 }) => ({
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
Loading…
Reference in New Issue
Block a user