Skip to content

Commit 72a47aa

Browse files
refactor: update filter handling to support multiple active filters
1 parent aa82950 commit 72a47aa

File tree

5 files changed

+111
-51
lines changed

5 files changed

+111
-51
lines changed

packages/toolbar/src/core/tests/FilterOptions.test.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ describe('FilterOptions', () => {
1818
isLoading: false,
1919
};
2020

21-
const renderWithContext = (props: FilterOptionsProps = defaultProps, activeFilter: FlagFilterMode = 'all') => {
22-
const onFilterChange = vi.fn();
21+
const renderWithContext = (
22+
props: FilterOptionsProps = defaultProps,
23+
activeFilters: Set<FlagFilterMode> = new Set(['all']),
24+
) => {
25+
const onFilterToggle = vi.fn();
2326

2427
return {
25-
onFilterChange,
28+
onFilterToggle,
2629
...render(
27-
<FlagFilterOptionsContext.Provider value={{ activeFilter, onFilterChange }}>
30+
<FlagFilterOptionsContext.Provider value={{ activeFilters, onFilterToggle }}>
2831
<FilterOptions {...props} />
2932
</FlagFilterOptionsContext.Provider>,
3033
),
@@ -33,67 +36,77 @@ describe('FilterOptions', () => {
3336

3437
describe('Status Text', () => {
3538
test('shows "Showing all X flags" when on All filter with no search', () => {
36-
renderWithContext({ ...defaultProps, filteredFlags: 5, totalFlags: 5 }, 'all');
39+
renderWithContext({ ...defaultProps, filteredFlags: 5, totalFlags: 5 }, new Set(['all']));
3740

3841
expect(screen.getByText('Showing all 5 flags')).toBeInTheDocument();
3942
});
4043

4144
test('shows "Showing X of Y flags" when searching on All filter', () => {
42-
renderWithContext({ ...defaultProps, filteredFlags: 2, totalFlags: 5 }, 'all');
45+
renderWithContext({ ...defaultProps, filteredFlags: 2, totalFlags: 5 }, new Set(['all']));
4346

4447
expect(screen.getByText('Showing 2 of 5 flags')).toBeInTheDocument();
4548
});
4649

4750
test('shows "Showing X of Y flags" when on Overrides filter', () => {
48-
renderWithContext({ ...defaultProps, filteredFlags: 2, totalFlags: 5 }, 'overrides');
51+
renderWithContext({ ...defaultProps, filteredFlags: 2, totalFlags: 5 }, new Set(['overrides']));
4952

5053
expect(screen.getByText('Showing 2 of 5 flags')).toBeInTheDocument();
5154
});
5255

5356
test('shows "Showing X of Y flags" when on Starred filter', () => {
54-
renderWithContext({ ...defaultProps, filteredFlags: 1, totalFlags: 5 }, 'starred');
57+
renderWithContext({ ...defaultProps, filteredFlags: 1, totalFlags: 5 }, new Set(['starred']));
5558

5659
expect(screen.getByText('Showing 1 of 5 flags')).toBeInTheDocument();
5760
});
5861
});
5962

6063
describe('Clear Buttons', () => {
6164
test('shows clear button on Overrides filter when totalOverriddenFlags > 0', () => {
62-
renderWithContext({ ...defaultProps, totalOverriddenFlags: 2 }, 'overrides');
65+
renderWithContext({ ...defaultProps, totalOverriddenFlags: 2 }, new Set(['overrides']));
6366

6467
expect(screen.getByText(/Clear Overrides \(2\)/)).toBeInTheDocument();
6568
});
6669

6770
test('does not show clear button on Overrides filter when totalOverriddenFlags = 0', () => {
68-
renderWithContext({ ...defaultProps, totalOverriddenFlags: 0 }, 'overrides');
71+
renderWithContext({ ...defaultProps, totalOverriddenFlags: 0 }, new Set(['overrides']));
6972

7073
expect(screen.queryByText(/Clear Overrides/)).not.toBeInTheDocument();
7174
});
7275

7376
test('shows clear button on Starred filter when starredCount > 0', () => {
74-
renderWithContext({ ...defaultProps, starredCount: 3 }, 'starred');
77+
renderWithContext({ ...defaultProps, starredCount: 3 }, new Set(['starred']));
7578

7679
expect(screen.getByText(/Clear Starred \(3\)/)).toBeInTheDocument();
7780
});
7881

7982
test('does not show clear button on Starred filter when starredCount = 0', () => {
80-
renderWithContext({ ...defaultProps, starredCount: 0 }, 'starred');
83+
renderWithContext({ ...defaultProps, starredCount: 0 }, new Set(['starred']));
8184

8285
expect(screen.queryByText(/Clear Starred/)).not.toBeInTheDocument();
8386
});
8487

8588
test('does not show clear button on All filter', () => {
86-
renderWithContext(defaultProps, 'all');
89+
renderWithContext(defaultProps, new Set(['all']));
8790

8891
expect(screen.queryByText(/Clear Overrides/)).not.toBeInTheDocument();
8992
expect(screen.queryByText(/Clear Starred/)).not.toBeInTheDocument();
9093
});
9194

9295
test('disables clear button when isLoading is true', () => {
93-
renderWithContext({ ...defaultProps, isLoading: true }, 'overrides');
96+
renderWithContext({ ...defaultProps, isLoading: true }, new Set(['overrides']));
9497

9598
const clearButton = screen.getByText(/Clear Overrides/);
9699
expect(clearButton).toBeDisabled();
97100
});
101+
102+
test('hides clear buttons when multiple filters are selected', () => {
103+
renderWithContext(
104+
{ ...defaultProps, totalOverriddenFlags: 2, starredCount: 3 },
105+
new Set(['overrides', 'starred']),
106+
);
107+
108+
expect(screen.queryByText(/Clear Overrides/)).not.toBeInTheDocument();
109+
expect(screen.queryByText(/Clear Starred/)).not.toBeInTheDocument();
110+
});
98111
});
99112
});

packages/toolbar/src/core/ui/Toolbar/TabContent/FlagDevServerTabContent.tsx

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,31 @@ export function FlagDevServerTabContent(props: FlagDevServerTabContentProps) {
2828
const { flags } = state;
2929
const { isStarred, toggleStarred, clearAllStarred, starredCount } = useStarredFlags();
3030

31-
const [activeFilter, setActiveFilter] = useState<FlagFilterMode>('all');
31+
const [activeFilters, setActiveFilters] = useState<Set<FlagFilterMode>>(new Set(['all']));
3232
const parentRef = useRef<HTMLDivElement>(null);
3333

34+
const handleFilterToggle = useCallback((filter: FlagFilterMode) => {
35+
setActiveFilters((prev) => {
36+
// Clicking "All" resets to default state
37+
if (filter === 'all') {
38+
return new Set(['all']);
39+
}
40+
41+
const next = new Set(prev);
42+
next.delete('all'); // Remove "All" when selecting specific filters
43+
44+
// Toggle the selected filter
45+
if (next.has(filter)) {
46+
next.delete(filter);
47+
} else {
48+
next.add(filter);
49+
}
50+
51+
// Default to "All" if no filters remain
52+
return next.size === 0 ? new Set(['all']) : next;
53+
});
54+
}, []);
55+
3456
const flagEntries = Object.entries(flags);
3557
const filteredFlags = useMemo(() => {
3658
return flagEntries.filter(([flagKey, flag]) => {
@@ -39,17 +61,18 @@ export function FlagDevServerTabContent(props: FlagDevServerTabContentProps) {
3961
flag.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
4062
flagKey.toLowerCase().includes(searchTerm.trim().toLowerCase());
4163

42-
// Apply active filter
64+
// Apply active filters (OR logic)
4365
let matchesFilter = true;
44-
if (activeFilter === 'overrides') {
45-
matchesFilter = flag.isOverridden;
46-
} else if (activeFilter === 'starred') {
47-
matchesFilter = isStarred(flagKey);
66+
if (activeFilters.has('all')) {
67+
matchesFilter = true;
68+
} else {
69+
matchesFilter =
70+
(activeFilters.has('overrides') && flag.isOverridden) || (activeFilters.has('starred') && isStarred(flagKey));
4871
}
4972

5073
return matchesSearch && matchesFilter;
5174
});
52-
}, [flagEntries, searchTerm, activeFilter, isStarred]);
75+
}, [flagEntries, searchTerm, activeFilters, isStarred]);
5376

5477
const virtualizer = useVirtualizer({
5578
count: filteredFlags.length,
@@ -92,24 +115,24 @@ export function FlagDevServerTabContent(props: FlagDevServerTabContentProps) {
92115

93116
const onRemoveAllOverrides = async () => {
94117
await clearAllOverrides();
95-
setActiveFilter('all');
118+
setActiveFilters(new Set(['all']));
96119
if (reloadOnFlagChangeIsEnabled) {
97120
window.location.reload();
98121
}
99122
};
100123

101124
const onClearOverride = useCallback(
102125
(flagKey: string) => {
103-
if (totalOverriddenFlags <= 1 && activeFilter === 'overrides') {
104-
setActiveFilter('all');
126+
if (totalOverriddenFlags <= 1 && activeFilters.has('overrides') && !activeFilters.has('starred')) {
127+
setActiveFilters(new Set(['all']));
105128
}
106129
clearOverride(flagKey).then(() => {
107130
if (reloadOnFlagChangeIsEnabled) {
108131
window.location.reload();
109132
}
110133
});
111134
},
112-
[totalOverriddenFlags, activeFilter, clearOverride, reloadOnFlagChangeIsEnabled],
135+
[totalOverriddenFlags, activeFilters, clearOverride, reloadOnFlagChangeIsEnabled],
113136
);
114137

115138
const handleHeightChange = useCallback(
@@ -126,10 +149,10 @@ export function FlagDevServerTabContent(props: FlagDevServerTabContentProps) {
126149
);
127150

128151
const getGenericHelpText = () => {
129-
if (activeFilter === 'overrides' && totalOverriddenFlags === 0) {
152+
if (activeFilters.has('overrides') && !activeFilters.has('starred') && totalOverriddenFlags === 0) {
130153
return { title: 'No overridden flags found', subtitle: 'You have not set any overrides yet' };
131154
}
132-
if (activeFilter === 'starred' && starredCount === 0) {
155+
if (activeFilters.has('starred') && !activeFilters.has('overrides') && starredCount === 0) {
133156
return { title: 'No starred flags found', subtitle: 'Star flags to see them here' };
134157
}
135158
return { title: 'No flags found', subtitle: 'Try adjusting your search' };
@@ -138,7 +161,7 @@ export function FlagDevServerTabContent(props: FlagDevServerTabContentProps) {
138161
const { title: genericHelpTitle, subtitle: genericHelpSubtitle } = getGenericHelpText();
139162

140163
return (
141-
<FlagFilterOptionsContext.Provider value={{ activeFilter, onFilterChange: setActiveFilter }}>
164+
<FlagFilterOptionsContext.Provider value={{ activeFilters, onFilterToggle: handleFilterToggle }}>
142165
<div data-testid="flag-dev-server-tab-content">
143166
<>
144167
<FilterOptions
@@ -151,7 +174,7 @@ export function FlagDevServerTabContent(props: FlagDevServerTabContentProps) {
151174
isLoading={state.isLoading}
152175
/>
153176

154-
{filteredFlags.length === 0 && (searchTerm.trim() || activeFilter !== 'all') ? (
177+
{filteredFlags.length === 0 && (searchTerm.trim() || !activeFilters.has('all')) ? (
155178
<GenericHelpText title={genericHelpTitle} subtitle={genericHelpSubtitle} />
156179
) : (
157180
<div ref={parentRef} className={styles.virtualContainer}>

packages/toolbar/src/core/ui/Toolbar/TabContent/FlagSdkOverrideTabContent.tsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,31 @@ function FlagSdkOverrideTabContentInner(props: FlagSdkOverrideTabContentInnerPro
3131
const analytics = useAnalytics();
3232
const { flags, isLoading } = useFlagSdkOverrideContext();
3333
const { isStarred, toggleStarred, clearAllStarred, starredCount } = useStarredFlags();
34-
const [activeFilter, setActiveFilter] = useState<FlagFilterMode>('all');
34+
const [activeFilters, setActiveFilters] = useState<Set<FlagFilterMode>>(new Set(['all']));
3535
const parentRef = useRef<HTMLDivElement>(null);
3636

37+
const handleFilterToggle = useCallback((filter: FlagFilterMode) => {
38+
setActiveFilters((prev) => {
39+
// Clicking "All" resets to default state
40+
if (filter === 'all') {
41+
return new Set(['all']);
42+
}
43+
44+
const next = new Set(prev);
45+
next.delete('all'); // Remove "All" when selecting specific filters
46+
47+
// Toggle the selected filter
48+
if (next.has(filter)) {
49+
next.delete(filter);
50+
} else {
51+
next.add(filter);
52+
}
53+
54+
// Default to "All" if no filters remain
55+
return next.size === 0 ? new Set(['all']) : next;
56+
});
57+
}, []);
58+
3759
const handleClearOverride = useCallback(
3860
(flagKey: string) => {
3961
if (flagOverridePlugin) {
@@ -62,17 +84,18 @@ function FlagSdkOverrideTabContentInner(props: FlagSdkOverrideTabContentInnerPro
6284
flag.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
6385
flagKey.toLowerCase().includes(searchTerm.trim().toLowerCase());
6486

65-
// Apply active filter
87+
// Apply active filters (OR logic)
6688
let matchesFilter = true;
67-
if (activeFilter === 'overrides') {
68-
matchesFilter = flag.isOverridden;
69-
} else if (activeFilter === 'starred') {
70-
matchesFilter = isStarred(flagKey);
89+
if (activeFilters.has('all')) {
90+
matchesFilter = true;
91+
} else {
92+
matchesFilter =
93+
(activeFilters.has('overrides') && flag.isOverridden) || (activeFilters.has('starred') && isStarred(flagKey));
7194
}
7295

7396
return matchesSearch && matchesFilter;
7497
});
75-
}, [flagEntries, searchTerm, activeFilter, isStarred]);
98+
}, [flagEntries, searchTerm, activeFilters, isStarred]);
7699

77100
const virtualizer = useVirtualizer({
78101
count: filteredFlags.length,
@@ -108,7 +131,7 @@ function FlagSdkOverrideTabContentInner(props: FlagSdkOverrideTabContentInnerPro
108131

109132
flagOverridePlugin.clearAllOverrides();
110133
analytics.trackFlagOverride('*', { count: overrideCount }, 'clear_all');
111-
setActiveFilter('all');
134+
setActiveFilters(new Set(['all']));
112135

113136
if (reloadOnFlagChangeIsEnabled) {
114137
window.location.reload();
@@ -164,10 +187,10 @@ function FlagSdkOverrideTabContentInner(props: FlagSdkOverrideTabContentInnerPro
164187
}
165188

166189
const getGenericHelpText = () => {
167-
if (activeFilter === 'overrides' && totalOverriddenFlags === 0) {
190+
if (activeFilters.has('overrides') && !activeFilters.has('starred') && totalOverriddenFlags === 0) {
168191
return { title: 'No overridden flags found', subtitle: 'You have not set any overrides yet' };
169192
}
170-
if (activeFilter === 'starred' && starredCount === 0) {
193+
if (activeFilters.has('starred') && !activeFilters.has('overrides') && starredCount === 0) {
171194
return { title: 'No starred flags found', subtitle: 'Star flags to see them here' };
172195
}
173196
return { title: 'No flags found', subtitle: 'Try adjusting your search' };
@@ -176,7 +199,7 @@ function FlagSdkOverrideTabContentInner(props: FlagSdkOverrideTabContentInnerPro
176199
const { title: genericHelpTitle, subtitle: genericHelpSubtitle } = getGenericHelpText();
177200

178201
return (
179-
<FlagFilterOptionsContext.Provider value={{ activeFilter, onFilterChange: setActiveFilter }}>
202+
<FlagFilterOptionsContext.Provider value={{ activeFilters, onFilterToggle: handleFilterToggle }}>
180203
<div data-testid="flag-sdk-tab-content">
181204
<>
182205
<FilterOptions
@@ -189,7 +212,7 @@ function FlagSdkOverrideTabContentInner(props: FlagSdkOverrideTabContentInnerPro
189212
isLoading={isLoading}
190213
/>
191214

192-
{filteredFlags.length === 0 && (searchTerm.trim() || activeFilter !== 'all') ? (
215+
{filteredFlags.length === 0 && (searchTerm.trim() || !activeFilters.has('all')) ? (
193216
<GenericHelpText title={genericHelpTitle} subtitle={genericHelpSubtitle} />
194217
) : (
195218
<div ref={parentRef} className={sharedStyles.virtualContainer}>

packages/toolbar/src/core/ui/Toolbar/components/FilterOptions/FilterOptions.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,23 @@ export interface FilterOptionsProps {
2121
export function FilterOptions(props: FilterOptionsProps) {
2222
const { totalFlags, filteredFlags, totalOverriddenFlags, starredCount, onClearOverrides, onClearStarred, isLoading } =
2323
props;
24-
const { activeFilter, onFilterChange } = useFlagFilterOptions();
24+
const { activeFilters, onFilterToggle } = useFlagFilterOptions();
2525

26-
const isAllActive = activeFilter === 'all';
27-
const isOverridesActive = activeFilter === 'overrides';
28-
const isStarredActive = activeFilter === 'starred';
26+
const isAllActive = activeFilters.has('all');
27+
const isOverridesActive = activeFilters.has('overrides');
28+
const isStarredActive = activeFilters.has('starred');
29+
const hasMultipleFilters = activeFilters.size > 1;
2930

3031
return (
3132
<div className={styles.container}>
3233
<div className={styles.topRow}>
3334
{FILTER_OPTIONS.map((filter) => {
34-
const isActive = activeFilter === filter.id;
35+
const isActive = activeFilters.has(filter.id);
3536
return (
3637
<button
3738
key={filter.id}
3839
className={`${styles.option} ${isActive ? styles.activeOption : ''}`}
39-
onClick={() => onFilterChange(filter.id)}
40+
onClick={() => onFilterToggle(filter.id)}
4041
data-active={isActive}
4142
aria-label={`Show ${filter.label.toLowerCase()} flags`}
4243
>
@@ -52,15 +53,15 @@ export function FilterOptions(props: FilterOptionsProps) {
5253
? `Showing all ${totalFlags} flags`
5354
: `Showing ${filteredFlags} of ${totalFlags} flags`}
5455
</div>
55-
{isOverridesActive && totalOverriddenFlags > 0 && onClearOverrides && (
56+
{!hasMultipleFilters && isOverridesActive && totalOverriddenFlags > 0 && onClearOverrides && (
5657
<ClearButton
5758
label="Overrides"
5859
count={totalOverriddenFlags}
5960
onClick={onClearOverrides}
6061
isLoading={isLoading}
6162
/>
6263
)}
63-
{isStarredActive && starredCount > 0 && onClearStarred && (
64+
{!hasMultipleFilters && isStarredActive && starredCount > 0 && onClearStarred && (
6465
<ClearButton label="Starred" count={starredCount} onClick={onClearStarred} isLoading={isLoading} />
6566
)}
6667
</div>

packages/toolbar/src/core/ui/Toolbar/components/FilterOptions/useFlagFilterOptions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { createContext, useContext } from 'react';
33
export type FlagFilterMode = 'all' | 'overrides' | 'starred';
44

55
interface FlagFilterOptionsContextType {
6-
activeFilter: FlagFilterMode;
7-
onFilterChange: (filter: FlagFilterMode) => void;
6+
activeFilters: Set<FlagFilterMode>;
7+
onFilterToggle: (filter: FlagFilterMode) => void;
88
}
99

1010
export const FlagFilterOptionsContext = createContext<FlagFilterOptionsContextType | undefined>(undefined);

0 commit comments

Comments
 (0)