mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	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 <kwasniewski.mateusz@gmail.com>
This commit is contained in:
		
							parent
							
								
									12f79f90bb
								
							
						
					
					
						commit
						b8fabbd726
					
				@ -17,6 +17,7 @@ const setup = (initialState: FilterItem) => {
 | 
			
		||||
        onChange: (value: FilterItem) => {
 | 
			
		||||
            recordedChanges.push(value);
 | 
			
		||||
        },
 | 
			
		||||
        onChipClose: () => {},
 | 
			
		||||
        state: initialState,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<IFilterItemProps> = ({
 | 
			
		||||
    label,
 | 
			
		||||
    options,
 | 
			
		||||
    onChange,
 | 
			
		||||
    onChipClose,
 | 
			
		||||
    state,
 | 
			
		||||
}) => {
 | 
			
		||||
    const ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
@ -52,6 +58,7 @@ export const FilterItem: FC<IFilterItemProps> = ({
 | 
			
		||||
    const onDelete = () => {
 | 
			
		||||
        onChange({ operator: 'IS', values: [] });
 | 
			
		||||
        onClose();
 | 
			
		||||
        onChipClose();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleToggle = (value: string) => () => {
 | 
			
		||||
@ -84,7 +91,6 @@ export const FilterItem: FC<IFilterItemProps> = ({
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }, [state]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <Box ref={ref}>
 | 
			
		||||
 | 
			
		||||
@ -105,7 +105,7 @@ export const FilterItemChip: FC<IFilterItemChipProps> = ({
 | 
			
		||||
                        )}
 | 
			
		||||
                    />
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={Boolean(onDelete && hasSelectedOptions)}
 | 
			
		||||
                        condition={Boolean(onDelete)}
 | 
			
		||||
                        show={() => (
 | 
			
		||||
                            <StyledIconButton
 | 
			
		||||
                                aria-label='delete'
 | 
			
		||||
 | 
			
		||||
@ -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,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<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>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user