Skip to content

Commit

Permalink
feat: add new filter button with state (#5556)
Browse files Browse the repository at this point in the history
[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 <kwasniewski.mateusz@gmail.com>
  • Loading branch information
sjaanus and kwasniew committed Dec 6, 2023
1 parent 12f79f9 commit b8fabbd
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 20 deletions.
Expand Up @@ -17,6 +17,7 @@ const setup = (initialState: FilterItem) => {
onChange: (value: FilterItem) => {
recordedChanges.push(value);
},
onChipClose: () => {},
state: initialState,
};

Expand Down
8 changes: 7 additions & 1 deletion frontend/src/component/common/FilterItem/FilterItem.tsx
Expand Up @@ -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;
}

Expand All @@ -29,6 +34,7 @@ export const FilterItem: FC<IFilterItemProps> = ({
label,
options,
onChange,
onChipClose,
state,
}) => {
const ref = useRef<HTMLDivElement>(null);
Expand All @@ -52,6 +58,7 @@ export const FilterItem: FC<IFilterItemProps> = ({
const onDelete = () => {
onChange({ operator: 'IS', values: [] });
onClose();
onChipClose();
};

const handleToggle = (value: string) => () => {
Expand Down Expand Up @@ -84,7 +91,6 @@ export const FilterItem: FC<IFilterItemProps> = ({
});
}
}, [state]);

return (
<>
<Box ref={ref}>
Expand Down
Expand Up @@ -105,7 +105,7 @@ export const FilterItemChip: FC<IFilterItemChipProps> = ({
)}
/>
<ConditionallyRender
condition={Boolean(onDelete && hasSelectedOptions)}
condition={Boolean(onDelete)}
show={() => (
<StyledIconButton
aria-label='delete'
Expand Down
@@ -0,0 +1,72 @@
import React, { useState } from 'react';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { IFilterItem } from './FeatureToggleFilters';
import { Box, 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 {
availableFilters: IFilterItem[];
setAvailableFilters: (filters: IFilterItem[]) => void;
}

const AddFilterButton = ({
availableFilters,
setAvailableFilters,
}: IAddFilterButtonProps) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
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 (
<div>
<StyledButton onClick={handleClick} startIcon={<Add />}>
Add Filter
</StyledButton>
<Menu
id='simple-menu'
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{availableFilters.map(
(filter) =>
!filter.enabled && (
<MenuItem
key={filter.label}
onClick={() => onClick(filter.label)}
>
{filter.label}
</MenuItem>
),
)}
</Menu>
</div>
);
};

export default AddFilterButton;
@@ -1,41 +1,116 @@
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 {
state: FeatureTogglesListFilters;
onChange: (value: FeatureTogglesListFilters) => void;
}

export interface IFilterItem {
label: string;
options: {
label: string;
value: string;
}[];
filterKey: keyof FeatureTogglesListFilters;
enabled?: boolean;
}

export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
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<IFilterItem[]>(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 (
<Box sx={(theme) => ({ padding: theme.spacing(2, 3) })}>
<StyledBox>
{availableFilters.map(
(filter) =>
filter.enabled && (
<FilterItem
key={filter.label}
label={filter.label}
state={state[filter.filterKey]}
options={filter.options}
onChange={(value) =>
onChange({ [filter.filterKey]: value })
}
onChipClose={() => removeFilter(filter.label)}
/>
),
)}
<ConditionallyRender
condition={projectsOptions.length > 1}
show={() => (
<FilterItem
label='Project'
state={state.project}
options={projectsOptions}
onChange={(value) => onChange({ project: value })}
condition={availableFilters.some((filter) => !filter.enabled)}
show={
<AddFilterButton
availableFilters={availableFilters}
setAvailableFilters={setAvailableFilters}
/>
)}
}
/>
</Box>
</StyledBox>
);
};
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/utils/serializeQueryParams.ts
Expand Up @@ -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;
};
Expand Down

0 comments on commit b8fabbd

Please sign in to comment.