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:
parent
e010f31a15
commit
d7da5fe6a4
@ -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();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -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()),
|
||||||
|
@ -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} />;
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user