1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

Basic filter label (#5387)

This commit is contained in:
Tymoteusz Czech 2023-11-24 10:07:42 +01:00 committed by GitHub
parent b0c05111c6
commit ffe37ac709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 433 additions and 0 deletions

View File

@ -0,0 +1,37 @@
import { Checkbox, ListItem, Popover, TextField, styled } from '@mui/material';
export const StyledDropdown = styled('div')(({ theme }) => ({
padding: theme.spacing(1.5),
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
}));
export const StyledListItem = styled(ListItem)(({ theme }) => ({
paddingLeft: theme.spacing(1),
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}));
export const StyledCheckbox = styled(Checkbox)(({ theme }) => ({
padding: theme.spacing(1, 1, 1, 1.5),
}));
export const StyledPopover = styled(Popover)(({ theme }) => ({
'& .MuiPaper-root': {
borderRadius: `${theme.shape.borderRadiusMedium}px`,
},
}));
export const StyledTextField = styled(TextField)(({ theme }) => ({
'& .MuiInputBase-root': {
padding: theme.spacing(0, 1.5),
borderRadius: `${theme.shape.borderRadiusMedium}px`,
},
'& .MuiInputBase-input': {
padding: theme.spacing(0.75, 0),
fontSize: theme.typography.body2.fontSize,
},
}));

View File

@ -0,0 +1,159 @@
import { Search } from '@mui/icons-material';
import { List, ListItemText, Box, InputAdornment } from '@mui/material';
import { FC, useEffect, useRef, useState } from 'react';
import {
StyledCheckbox,
StyledDropdown,
StyledListItem,
StyledPopover,
StyledTextField,
} from './FilterItem.styles';
import { FilterItemChip } from './FilterItemChip/FilterItemChip';
interface IFilterItemProps {
label: string;
options: Array<{ label: string; value: string }>;
}
const singularOperators = ['IS', 'IS_NOT'];
const pluralOperators = ['IS_IN', 'IS_NOT_IN'];
export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => {
const ref = useRef<HTMLDivElement>(null);
const [selectedOptions, setSelectedOptions] = useState<typeof options>([]);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
const [searchText, setSearchText] = useState('');
const currentOperators =
selectedOptions?.length > 1 ? pluralOperators : singularOperators;
const [operator, setOperator] = useState(currentOperators[0]);
const onClick = () => {
setAnchorEl(ref.current);
};
const onClose = () => {
setAnchorEl(null);
};
const onDelete = () => {
setSelectedOptions([]);
onClose();
};
const handleToggle = (value: string) => () => {
if (
selectedOptions?.some(
(selectedOption) => selectedOption.value === value,
)
) {
setSelectedOptions(
selectedOptions?.filter(
(selectedOption) => selectedOption.value !== value,
),
);
} else {
setSelectedOptions([
...(selectedOptions ?? []),
options.find((option) => option.value === value) ?? {
label: '',
value: '',
},
]);
}
};
useEffect(() => {
if (!currentOperators.includes(operator)) {
setOperator(currentOperators[0]);
}
}, [currentOperators, operator]);
return (
<>
<Box ref={ref}>
<FilterItemChip
label={label}
selectedOptions={selectedOptions?.map(
(option) => option?.label,
)}
onDelete={onDelete}
onClick={onClick}
operator={operator}
operatorOptions={currentOperators}
onChangeOperator={setOperator}
/>
</Box>
<StyledPopover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<StyledDropdown>
<StyledTextField
variant='outlined'
size='small'
value={searchText}
onChange={(event) => setSearchText(event.target.value)}
placeholder='Search'
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<Search fontSize='small' />
</InputAdornment>
),
}}
/>
<List disablePadding>
{options
?.filter((option) =>
option.label
.toLowerCase()
.includes(searchText.toLowerCase()),
)
.map((option) => {
const labelId = `checkbox-list-label-${option.value}`;
return (
<StyledListItem
key={option.value}
dense
disablePadding
onClick={handleToggle(option.value)}
>
<StyledCheckbox
edge='start'
checked={
selectedOptions?.some(
(selectedOption) =>
selectedOption.value ===
option.value,
) ?? false
}
tabIndex={-1}
inputProps={{
'aria-labelledby': labelId,
}}
size='small'
disableRipple
/>
<ListItemText
id={labelId}
primary={option.label}
/>
</StyledListItem>
);
})}
</List>
</StyledDropdown>
</StyledPopover>
</>
);
};

View File

@ -0,0 +1,132 @@
import { ComponentProps, FC } from 'react';
import {} from '../FilterItem.styles';
import { ArrowDropDown, Close, TopicOutlined } from '@mui/icons-material';
import { ConditionallyRender } from '../../ConditionallyRender/ConditionallyRender';
import { Chip, IconButton, styled } from '@mui/material';
import { FilterItemOperator } from './FilterItemOperator/FilterItemOperator';
const StyledChip = styled(
({
isActive,
...props
}: { isActive: boolean } & ComponentProps<typeof Chip>) => (
<Chip {...props} />
),
)(({ theme, isActive = false }) => ({
borderRadius: `${theme.shape.borderRadius}px`,
padding: 0,
margin: theme.spacing(0, 0, 1, 0),
fontSize: theme.typography.body2.fontSize,
...(isActive
? {
backgroundColor: theme.palette.secondary.light,
}
: {}),
}));
const StyledLabel = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontWeight: theme.typography.fontWeightBold,
}));
const StyledCategoryIconWrapper = styled('div')(({ theme }) => ({
marginRight: theme.spacing(1),
display: 'flex',
alignItems: 'center',
fontSize: theme.typography.h2.fontSize,
}));
const StyledOptions = styled('span')(({ theme }) => ({
color: theme.palette.text.primary,
fontWeight: theme.typography.fontWeightRegular,
}));
const StyledIconButton = styled(IconButton)(({ theme }) => ({
marginLeft: theme.spacing(0.5),
marginRight: theme.spacing(-1.25),
}));
const Arrow = () => (
<ArrowDropDown
fontSize='small'
color='action'
sx={(theme) => ({
marginRight: theme.spacing(-1),
marginLeft: theme.spacing(0.5),
})}
/>
);
interface IFilterItemChipProps {
label: string;
selectedOptions?: string[];
operatorOptions: string[];
operator: string;
onChangeOperator: (value: string) => void;
onClick?: () => void;
onDelete?: () => void;
}
export const FilterItemChip: FC<IFilterItemChipProps> = ({
label,
selectedOptions = [],
operatorOptions,
operator,
onChangeOperator,
onClick,
onDelete,
}) => {
const hasSelectedOptions = selectedOptions.length > 0;
return (
<StyledChip
isActive={hasSelectedOptions}
label={
<StyledLabel>
<StyledCategoryIconWrapper>
<TopicOutlined fontSize='inherit' />
</StyledCategoryIconWrapper>
{label}
<ConditionallyRender
condition={!hasSelectedOptions}
show={() => <Arrow />}
elseShow={() => (
<>
<FilterItemOperator
options={operatorOptions}
value={operator}
onChange={onChangeOperator}
/>
<StyledOptions>
{selectedOptions.join(', ')}
</StyledOptions>
</>
)}
/>
<ConditionallyRender
condition={Boolean(onDelete && hasSelectedOptions)}
show={() => (
<StyledIconButton
aria-label='delete'
color='primary'
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onDelete?.();
}}
size='small'
>
<Close fontSize='inherit' color='action' />
</StyledIconButton>
)}
/>
</StyledLabel>
}
color='primary'
variant='outlined'
onClick={onClick}
/>
);
};

View File

@ -0,0 +1,80 @@
import { styled, Menu, MenuItem } from '@mui/material';
import { FC, useState, MouseEvent } from 'react';
interface IFilterItemOperatorProps {
options: string[];
value: string;
onChange: (value: string) => void;
}
const StyledOperator = styled('button')(({ theme }) => ({
borderRadius: 0,
border: 'none',
cursor: 'pointer',
color: theme.palette.text.disabled,
fontSize: theme.typography.body2.fontSize,
padding: theme.spacing(0, 0.75),
margin: theme.spacing(0, 0.75),
height: theme.spacing(3.75),
display: 'flex',
alignItems: 'center',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}));
const StyledMenu = styled(Menu)(({ theme }) => ({
transform: `translateY(${theme.spacing(0.5)})`,
}));
const formatOption = (option: string) =>
option.replaceAll('_', ' ').toLocaleLowerCase();
export const FilterItemOperator: FC<IFilterItemOperatorProps> = ({
options,
value,
onChange,
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const handleClose = () => {
setAnchorEl(null);
};
const handleClick = (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const handleMenuItemClick =
(option: string) => (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
onChange(option);
handleClose();
};
return (
<>
<StyledOperator onClick={handleClick}>
{formatOption(value)}
</StyledOperator>
<StyledMenu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
anchorPosition={{ left: 0, top: 1 }}
onClick={(event) => event.stopPropagation()}
>
{options.map((option) => (
<MenuItem
key={option}
onClick={handleMenuItemClick(option)}
>
{formatOption(option)}
</MenuItem>
))}
</StyledMenu>
</>
);
};

View File

@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
import {
Box,
IconButton,
Link,
Tooltip,
@ -39,6 +40,8 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { focusable } from 'themes/themeStyles';
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import useToast from 'hooks/useToast';
import { FilterItem } from 'component/common/FilterItem/FilterItem';
import { useUiFlag } from 'hooks/useUiFlag';
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
name: 'Name of the feature',
@ -73,6 +76,7 @@ export const FeatureToggleListTable: VFC = () => {
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
const [initialState] = useState(() => ({
sortBy: [
{
@ -367,6 +371,27 @@ export const FeatureToggleListTable: VFC = () => {
</PageHeader>
}
>
{featureSearchFrontend && (
<Box sx={(theme) => ({ marginBottom: theme.spacing(2) })}>
<FilterItem
label='Project'
options={[
{
label: 'Project 1',
value: '1',
},
{
label: 'Test',
value: '2',
},
{
label: 'Default',
value: '3',
},
]}
/>
</Box>
)}
<SearchHighlightProvider value={getSearchText(searchValue)}>
<VirtualizedTable
rows={rows}