From b8fabbd7266dbed57ddbd82875654a2296ef254a Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Wed, 6 Dec 2023 12:50:33 +0200 Subject: [PATCH] feat: add new filter button with state (#5556) [Screencast from 2023-12-05 16-59-28.webm](https://github.com/Unleash/unleash/assets/964450/793c771b-6246-4e28-8c13-920696a48bd5) --------- Co-authored-by: kwasniew --- .../common/FilterItem/FilterItem.test.tsx | 1 + .../common/FilterItem/FilterItem.tsx | 8 +- .../FilterItemChip/FilterItemChip.tsx | 2 +- .../FeatureToggleFilters/AddFilterButton.tsx | 72 ++++++++++++ .../FeatureToggleFilters.tsx | 109 +++++++++++++++--- .../FeatureToggleListTable.tsx | 1 + frontend/src/utils/serializeQueryParams.ts | 2 +- 7 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/AddFilterButton.tsx diff --git a/frontend/src/component/common/FilterItem/FilterItem.test.tsx b/frontend/src/component/common/FilterItem/FilterItem.test.tsx index e1dc637e5b..3169762b1c 100644 --- a/frontend/src/component/common/FilterItem/FilterItem.test.tsx +++ b/frontend/src/component/common/FilterItem/FilterItem.test.tsx @@ -17,6 +17,7 @@ const setup = (initialState: FilterItem) => { onChange: (value: FilterItem) => { recordedChanges.push(value); }, + onChipClose: () => {}, state: initialState, }; diff --git a/frontend/src/component/common/FilterItem/FilterItem.tsx b/frontend/src/component/common/FilterItem/FilterItem.tsx index 637a8b3ba4..8566ff1f98 100644 --- a/frontend/src/component/common/FilterItem/FilterItem.tsx +++ b/frontend/src/component/common/FilterItem/FilterItem.tsx @@ -9,11 +9,16 @@ import { StyledTextField, } from './FilterItem.styles'; import { FilterItemChip } from './FilterItemChip/FilterItemChip'; +import { + FeatureTogglesListFilters, + IFilterItem, +} from '../../feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters'; interface IFilterItemProps { label: string; options: Array<{ label: string; value: string }>; onChange: (value: FilterItem) => void; + onChipClose: () => void; state: FilterItem | null | undefined; } @@ -29,6 +34,7 @@ export const FilterItem: FC = ({ label, options, onChange, + onChipClose, state, }) => { const ref = useRef(null); @@ -52,6 +58,7 @@ export const FilterItem: FC = ({ const onDelete = () => { onChange({ operator: 'IS', values: [] }); onClose(); + onChipClose(); }; const handleToggle = (value: string) => () => { @@ -84,7 +91,6 @@ export const FilterItem: FC = ({ }); } }, [state]); - return ( <> diff --git a/frontend/src/component/common/FilterItem/FilterItemChip/FilterItemChip.tsx b/frontend/src/component/common/FilterItem/FilterItemChip/FilterItemChip.tsx index ce017f33ae..2c0dcd99e0 100644 --- a/frontend/src/component/common/FilterItem/FilterItemChip/FilterItemChip.tsx +++ b/frontend/src/component/common/FilterItem/FilterItemChip/FilterItemChip.tsx @@ -105,7 +105,7 @@ export const FilterItemChip: FC = ({ )} /> ( ({ + margin: theme.spacing(-1, 0, -1, 0), + padding: theme.spacing(1.25), +})); +interface IAddFilterButtonProps { + availableFilters: IFilterItem[]; + setAvailableFilters: (filters: IFilterItem[]) => void; +} + +const AddFilterButton = ({ + availableFilters, + setAvailableFilters, +}: IAddFilterButtonProps) => { + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + const onClick = (label: string) => { + const filters = availableFilters.map((filter) => + filter.label === label + ? { + ...filter, + enabled: true, + } + : filter, + ); + setAvailableFilters(filters); + handleClose(); + }; + + return ( +
+ }> + Add Filter + + + {availableFilters.map( + (filter) => + !filter.enabled && ( + onClick(filter.label)} + > + {filter.label} + + ), + )} + +
+ ); +}; + +export default AddFilterButton; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx index c1638b9768..1c3035d6d2 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx @@ -1,11 +1,19 @@ -import { VFC } from 'react'; -import { Box } from '@mui/material'; +import { useEffect, useState, VFC } from 'react'; +import { Box, styled } from '@mui/material'; import { FilterItem } from 'component/common/FilterItem/FilterItem'; import useProjects from 'hooks/api/getters/useProjects/useProjects'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import AddFilterButton from './AddFilterButton'; + +const StyledBox = styled(Box)(({ theme }) => ({ + display: 'flex', + padding: theme.spacing(2, 3), + gap: theme.spacing(1), +})); export type FeatureTogglesListFilters = { - project: FilterItem | null | undefined; + project?: FilterItem | null | undefined; + state?: FilterItem | null | undefined; }; interface IFeatureToggleFiltersProps { @@ -13,29 +21,96 @@ interface IFeatureToggleFiltersProps { onChange: (value: FeatureTogglesListFilters) => void; } +export interface IFilterItem { + label: string; + options: { + label: string; + value: string; + }[]; + filterKey: keyof FeatureTogglesListFilters; + enabled?: boolean; +} + export const FeatureToggleFilters: VFC = ({ state, onChange, }) => { const { projects } = useProjects(); - const projectsOptions = (projects || []).map((project) => ({ - label: project.name, - value: project.id, - })); + const stateOptions = [ + { + label: 'Active', + value: 'active', + }, + { + label: 'Stale', + value: 'stale', + }, + ]; + const defaultFilterItems: IFilterItem[] = [ + { + label: 'State', + options: stateOptions, + filterKey: 'state', + }, + ]; + const [availableFilters, setAvailableFilters] = + useState(defaultFilterItems); + const removeFilter = (label: string) => { + const filters = availableFilters.map((filter) => + filter.label === label + ? { + ...filter, + enabled: false, + } + : filter, + ); + setAvailableFilters(filters); + }; + + useEffect(() => { + const projectsOptions = (projects || []).map((project) => ({ + label: project.name, + value: project.id, + })); + + const newFilterItems = [ + ...defaultFilterItems, + { + label: 'Project', + options: projectsOptions, + filterKey: 'project', + } as const, + ]; + + setAvailableFilters(newFilterItems); + }, [JSON.stringify(projects)]); return ( - ({ padding: theme.spacing(2, 3) })}> + + {availableFilters.map( + (filter) => + filter.enabled && ( + + onChange({ [filter.filterKey]: value }) + } + onChipClose={() => removeFilter(filter.label)} + /> + ), + )} 1} - show={() => ( - onChange({ project: value })} + condition={availableFilters.some((filter) => !filter.enabled)} + show={ + - )} + } /> - + ); }; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index e0d9e46bfc..5ee6c4a442 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -81,6 +81,7 @@ export const FeatureToggleListTable: VFC = () => { sortBy: withDefault(StringParam, 'createdAt'), sortOrder: withDefault(StringParam, 'desc'), project: FilterItemParam, + state: FilterItemParam, }; const [tableState, setTableState] = usePersistentTableState( 'features-list-table', diff --git a/frontend/src/utils/serializeQueryParams.ts b/frontend/src/utils/serializeQueryParams.ts index 7cc2031435..3aaf3c500b 100644 --- a/frontend/src/utils/serializeQueryParams.ts +++ b/frontend/src/utils/serializeQueryParams.ts @@ -37,7 +37,7 @@ export type FilterItem = { const encodeFilterItem = ( filterItem: FilterItem | null | undefined, ): string | undefined => { - return filterItem && filterItem.values.length + return filterItem?.values.length ? `${filterItem.operator}:${filterItem.values.join(',')}` : undefined; };