1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-27 01:19:00 +02:00

chore(ux): If there's only a single available filter, always show that filter (don't hide it beneath a filters button) (#10127)

This PR offers a little QoL upgrade in cases where you have only a
single available filter: Instead of having to first click "filters" and
then select the only option, we'll now always show the available filter,
whether it's marked as persistent or not.

The filter still gets a 'delete filter' button that clears the filter
(which is more convenient than having to deselect every one of the
options one by one), but the filter won't disappear when you clear it.

Additionally, because the `state` of the filter item will be undefined
if it has no value, I've added a `preventAutoOpen` prop to the
underlying Filter component.

~~I don't understand why we want to auto-open the filter, but it was
added by @kwasniew in https://github.com/Unleash/unleash/pull/5611, so
it appears to be deliberate.~~ The fact that we auto-open the filter
when state is undefined makes this a little tricky. I realized during
this that the reason we do it is that we want the filter to auto-open
when you select it from the dropdown. Maybe there's a better way to do
that than useEffect, but maybe not 🤷🏼

Further, the filter handling logic is quite complex (what filters to
show, ordering, etc), so I've moved as much of that into the multifilter
component, leaving the singlefilter as simple as possible.

I'm ... not particularly proud of the code here, so I'm happy to take
any suggestions for improvements. Happy to throw it all away if you have
a better way to achieve this goal.

## Rendered

The lifecycle insights use the persistent, single filter, the
performance insights do not:

<img width="1629" alt="image"
src="https://github.com/user-attachments/assets/b8599883-97dc-428e-a98f-ad59ad1c74ab"
/>

<img width="1556" alt="image"
src="https://github.com/user-attachments/assets/eacdc4bf-bc60-4e26-a88c-8be7dc5e31be"
/>
This commit is contained in:
Thomas Heartman 2025-06-12 14:25:58 +02:00 committed by GitHub
parent e010f31a15
commit d7da5fe6a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 157 additions and 65 deletions

View File

@ -21,6 +21,7 @@ export interface IFilterDateItemProps {
onChipClose?: () => void; onChipClose?: () => void;
state: FilterItemParams | null | undefined; state: FilterItemParams | null | undefined;
operators: [string, ...string[]]; operators: [string, ...string[]];
initMode?: 'auto-open' | 'manual';
} }
export const FilterDateItem: FC<IFilterDateItemProps> = ({ export const FilterDateItem: FC<IFilterDateItemProps> = ({
@ -31,6 +32,7 @@ export const FilterDateItem: FC<IFilterDateItemProps> = ({
onChipClose, onChipClose,
state, state,
operators, operators,
initMode = 'auto-open',
}) => { }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
@ -41,7 +43,7 @@ export const FilterDateItem: FC<IFilterDateItemProps> = ({
}; };
useEffect(() => { useEffect(() => {
if (!state) { if (!state && initMode === 'auto-open') {
open(); open();
} }
}, []); }, []);

View File

@ -15,10 +15,11 @@ export interface IFilterItemProps {
label: ReactNode; label: ReactNode;
options: Array<{ label: string; value: string }>; options: Array<{ label: string; value: string }>;
onChange: (value: FilterItemParams) => void; onChange: (value: FilterItemParams) => void;
onChipClose: () => void; onChipClose?: () => void;
state: FilterItemParams | null | undefined; state: FilterItemParams | null | undefined;
singularOperators: [string, ...string[]]; singularOperators: [string, ...string[]];
pluralOperators: [string, ...string[]]; pluralOperators: [string, ...string[]];
initMode?: 'auto-open' | 'manual';
} }
export type FilterItemParams = { export type FilterItemParams = {
@ -80,6 +81,7 @@ export const FilterItem: FC<IFilterItemProps> = ({
state, state,
singularOperators, singularOperators,
pluralOperators, pluralOperators,
initMode = 'auto-open',
}) => { }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(); const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>();
@ -93,7 +95,7 @@ export const FilterItem: FC<IFilterItemProps> = ({
}; };
useEffect(() => { useEffect(() => {
if (!state) { if (!state && initMode === 'auto-open') {
open(); open();
} }
}, []); }, []);
@ -108,11 +110,13 @@ export const FilterItem: FC<IFilterItemProps> = ({
.filter((label): label is string => label !== undefined); .filter((label): label is string => label !== undefined);
const currentOperator = state ? state.operator : currentOperators[0]; const currentOperator = state ? state.operator : currentOperators[0];
const onDelete = () => { const onDelete = onChipClose
onChange({ operator: singularOperators[0], values: [] }); ? () => {
onClose(); onChange({ operator: singularOperators[0], values: [] });
onChipClose(); onClose();
}; onChipClose();
}
: undefined;
const filteredOptions = options.filter((option) => const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(searchText.toLowerCase()), option.label.toLowerCase().includes(searchText.toLowerCase()),

View File

@ -35,6 +35,7 @@ type IBaseFilterItem = {
value: string; value: string;
}[]; }[];
filterKey: string; filterKey: string;
persistent?: boolean;
}; };
type ITextFilterItem = IBaseFilterItem & { type ITextFilterItem = IBaseFilterItem & {
@ -46,7 +47,6 @@ export type IDateFilterItem = IBaseFilterItem & {
dateOperators: [string, ...string[]]; dateOperators: [string, ...string[]];
fromFilterKey?: string; fromFilterKey?: string;
toFilterKey?: string; toFilterKey?: string;
persistent?: boolean;
}; };
export type IFilterItem = ITextFilterItem | IDateFilterItem; export type IFilterItem = ITextFilterItem | IDateFilterItem;
@ -64,10 +64,116 @@ const StyledIcon = styled(Icon)(({ theme }) => ({
}, },
})); }));
export const Filters: FC<IFilterProps> = ({ type RangeChangeHandler = (filter: IDateFilterItem) =>
| ((value: {
from: FilterItemParams;
to: FilterItemParams;
}) => void)
| undefined;
type RenderFilterProps = {
onChipClose?: (label: string) => void;
state: FilterItemParams | null | undefined;
onChange: (value: FilterItemParamHolder) => void;
filter: ITextFilterItem | IDateFilterItem;
rangeChangeHandler: RangeChangeHandler;
initMode?: 'auto-open' | 'manual';
};
const RenderFilter: FC<RenderFilterProps> = ({
filter,
onChipClose,
onChange,
state,
rangeChangeHandler,
initMode,
}) => {
const label = (
<>
<StyledCategoryIconWrapper>
<StyledIcon>{filter.icon}</StyledIcon>
</StyledCategoryIconWrapper>
{filter.label}
</>
);
if ('dateOperators' in filter) {
return (
<FilterDateItem
key={filter.label}
initMode={initMode}
label={label}
name={filter.label}
state={state}
onChange={(value) => {
onChange({ [filter.filterKey]: value });
}}
onRangeChange={rangeChangeHandler?.(filter)}
operators={filter.dateOperators}
onChipClose={
filter.persistent
? undefined
: () => onChipClose?.(filter.label)
}
/>
);
}
return (
<FilterItem
initMode={initMode}
key={filter.label}
label={label}
name={filter.label}
state={state}
options={filter.options}
onChange={(value) => onChange({ [filter.filterKey]: value })}
singularOperators={filter.singularOperators}
pluralOperators={filter.pluralOperators}
onChipClose={
filter.persistent
? undefined
: () => onChipClose?.(filter.label)
}
/>
);
};
type SingleFilterProps = Omit<IFilterProps, 'availableFilters'> & {
filter: IFilterItem;
rangeChangeHandler: RangeChangeHandler;
};
const SingleFilter: FC<SingleFilterProps> = ({
state,
onChange,
className,
filter,
rangeChangeHandler,
}) => {
return (
<StyledBox className={className}>
<RenderFilter
filter={filter}
state={state[filter.filterKey]}
onChange={onChange}
rangeChangeHandler={rangeChangeHandler}
onChipClose={undefined}
initMode='manual'
/>
</StyledBox>
);
};
type MultiFilterProps = IFilterProps & {
rangeChangeHandler: RangeChangeHandler;
};
const MultiFilter: FC<MultiFilterProps> = ({
state, state,
onChange, onChange,
availableFilters, availableFilters,
rangeChangeHandler,
className, className,
}) => { }) => {
const [unselectedFilters, setUnselectedFilters] = useState<string[]>([]); const [unselectedFilters, setUnselectedFilters] = useState<string[]>([]);
@ -123,21 +229,6 @@ export const Filters: FC<IFilterProps> = ({
const hasAvailableFilters = unselectedFilters.length > 0; const hasAvailableFilters = unselectedFilters.length > 0;
const rangeChangeHandler = (filter: IDateFilterItem) => {
const fromKey = filter.fromFilterKey;
const toKey = filter.toFilterKey;
if (fromKey && toKey) {
return (value: {
from: FilterItemParams;
to: FilterItemParams;
}) => {
onChange({ [fromKey]: value.from });
onChange({ [toKey]: value.to });
};
}
return undefined;
};
return ( return (
<StyledBox className={className}> <StyledBox className={className}>
{selectedFilters.map((selectedFilter) => { {selectedFilters.map((selectedFilter) => {
@ -149,48 +240,13 @@ export const Filters: FC<IFilterProps> = ({
return null; return null;
} }
const label = (
<>
<StyledCategoryIconWrapper>
<StyledIcon>{filter.icon}</StyledIcon>
</StyledCategoryIconWrapper>
{filter.label}
</>
);
if ('dateOperators' in filter) {
return (
<FilterDateItem
key={filter.label}
label={label}
name={filter.label}
state={state[filter.filterKey]}
onChange={(value) => {
onChange({ [filter.filterKey]: value });
}}
onRangeChange={rangeChangeHandler(filter)}
operators={filter.dateOperators}
onChipClose={
filter.persistent
? undefined
: () => deselectFilter(filter.label)
}
/>
);
}
return ( return (
<FilterItem <RenderFilter
key={filter.label} key={filter.filterKey}
label={label} filter={filter}
name={filter.label}
state={state[filter.filterKey]} state={state[filter.filterKey]}
options={filter.options} onChange={onChange}
onChange={(value) => rangeChangeHandler={rangeChangeHandler}
onChange({ [filter.filterKey]: value })
}
singularOperators={filter.singularOperators}
pluralOperators={filter.pluralOperators}
onChipClose={() => deselectFilter(filter.label)} onChipClose={() => deselectFilter(filter.label)}
/> />
); );
@ -211,3 +267,33 @@ export const Filters: FC<IFilterProps> = ({
</StyledBox> </StyledBox>
); );
}; };
export const Filters: FC<IFilterProps> = (props) => {
const rangeChangeHandler = (filter: IDateFilterItem) => {
const fromKey = filter.fromFilterKey;
const toKey = filter.toFilterKey;
if (fromKey && toKey) {
return (value: {
from: FilterItemParams;
to: FilterItemParams;
}) => {
props.onChange({ [fromKey]: value.from });
props.onChange({ [toKey]: value.to });
};
}
return undefined;
};
if (props.availableFilters.length === 1) {
const filter = props.availableFilters[0];
return (
<SingleFilter
rangeChangeHandler={rangeChangeHandler}
filter={filter}
{...props}
/>
);
}
return <MultiFilter rangeChangeHandler={rangeChangeHandler} {...props} />;
};