diff --git a/frontend/src/component/common/FilterDateItem/FilterDateItem.test.tsx b/frontend/src/component/common/FilterDateItem/FilterDateItem.test.tsx index c9cb4cb2ae..c6d07393a4 100644 --- a/frontend/src/component/common/FilterDateItem/FilterDateItem.test.tsx +++ b/frontend/src/component/common/FilterDateItem/FilterDateItem.test.tsx @@ -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); diff --git a/frontend/src/component/common/FilterDateItem/FilterDateItem.tsx b/frontend/src/component/common/FilterDateItem/FilterDateItem.tsx index a0e7f69659..c40d3ed6c3 100644 --- a/frontend/src/component/common/FilterDateItem/FilterDateItem.tsx +++ b/frontend/src/component/common/FilterDateItem/FilterDateItem.tsx @@ -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; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx index f443594a5c..e7f5393a84 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx @@ -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 = ({ state, onChange, @@ -71,31 +33,6 @@ export const FeatureToggleFilters: VFC = ({ ]; const [availableFilters, setAvailableFilters] = useState([]); - const [unselectedFilters, setUnselectedFilters] = useState([]); - const [selectedFilters, setSelectedFilters] = useState([]); - - 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 = ({ 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 ( - - {selectedFilters.map((selectedFilter) => { - const filter = availableFilters.find( - (filter) => filter.label === selectedFilter, - ); - - if (!filter) { - return null; - } - - if ('dateOperators' in filter) { - return ( - - onChange({ [filter.filterKey]: value }) - } - operators={filter.dateOperators} - onChipClose={() => deselectFilter(filter.label)} - /> - ); - } - - return ( - - onChange({ [filter.filterKey]: value }) - } - singularOperators={filter.singularOperators} - pluralOperators={filter.pluralOperators} - onChipClose={() => deselectFilter(filter.label)} - /> - ); - })} - - - } - /> - + ); }; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 355b40ee23..c5d4549475 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -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 = () => { } > - + diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/AddFilterButton/AddFilterButton.tsx b/frontend/src/component/filter/AddFilterButton.tsx similarity index 100% rename from frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/AddFilterButton/AddFilterButton.tsx rename to frontend/src/component/filter/AddFilterButton.tsx diff --git a/frontend/src/component/common/FilterItem/FilterItem.styles.tsx b/frontend/src/component/filter/FilterItem/FilterItem.styles.tsx similarity index 100% rename from frontend/src/component/common/FilterItem/FilterItem.styles.tsx rename to frontend/src/component/filter/FilterItem/FilterItem.styles.tsx diff --git a/frontend/src/component/common/FilterItem/FilterItem.test.tsx b/frontend/src/component/filter/FilterItem/FilterItem.test.tsx similarity index 100% rename from frontend/src/component/common/FilterItem/FilterItem.test.tsx rename to frontend/src/component/filter/FilterItem/FilterItem.test.tsx diff --git a/frontend/src/component/common/FilterItem/FilterItem.tsx b/frontend/src/component/filter/FilterItem/FilterItem.tsx similarity index 100% rename from frontend/src/component/common/FilterItem/FilterItem.tsx rename to frontend/src/component/filter/FilterItem/FilterItem.tsx diff --git a/frontend/src/component/common/FilterItem/FilterItemChip/FilterItemChip.tsx b/frontend/src/component/filter/FilterItem/FilterItemChip/FilterItemChip.tsx similarity index 98% rename from frontend/src/component/common/FilterItem/FilterItemChip/FilterItemChip.tsx rename to frontend/src/component/filter/FilterItem/FilterItemChip/FilterItemChip.tsx index 734bdb93da..2feb1e95c3 100644 --- a/frontend/src/component/common/FilterItem/FilterItemChip/FilterItemChip.tsx +++ b/frontend/src/component/filter/FilterItem/FilterItemChip/FilterItemChip.tsx @@ -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'; diff --git a/frontend/src/component/common/FilterItem/FilterItemChip/FilterItemOperator/FilterItemOperator.tsx b/frontend/src/component/filter/FilterItem/FilterItemChip/FilterItemOperator/FilterItemOperator.tsx similarity index 100% rename from frontend/src/component/common/FilterItem/FilterItemChip/FilterItemOperator/FilterItemOperator.tsx rename to frontend/src/component/filter/FilterItem/FilterItemChip/FilterItemOperator/FilterItemOperator.tsx diff --git a/frontend/src/component/filter/Filters.tsx b/frontend/src/component/filter/Filters.tsx new file mode 100644 index 0000000000..67b6256992 --- /dev/null +++ b/frontend/src/component/filter/Filters.tsx @@ -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 = ({ + state, + onChange, + availableFilters, +}) => { + const [unselectedFilters, setUnselectedFilters] = useState([]); + const [selectedFilters, setSelectedFilters] = useState([]); + + 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 ( + + {selectedFilters.map((selectedFilter) => { + const filter = availableFilters.find( + (filter) => filter.label === selectedFilter, + ); + + if (!filter) { + return null; + } + + if ('dateOperators' in filter) { + return ( + + onChange({ [filter.filterKey]: value }) + } + operators={filter.dateOperators} + onChipClose={() => deselectFilter(filter.label)} + /> + ); + } + + return ( + + onChange({ [filter.filterKey]: value }) + } + singularOperators={filter.singularOperators} + pluralOperators={filter.pluralOperators} + onChipClose={() => deselectFilter(filter.label)} + /> + ); + })} + + + } + /> + + ); +}; diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectOverviewFilters.tsx b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectOverviewFilters.tsx index 2c6d472b20..da93b62c70 100644 --- a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectOverviewFilters.tsx +++ b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectOverviewFilters.tsx @@ -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',