Skip to content

Commit a05eda3

Browse files
committed
Enonic UI: Externalize Filter State management #9332
1 parent 1c34ed5 commit a05eda3

File tree

5 files changed

+229
-194
lines changed

5 files changed

+229
-194
lines changed

modules/lib/src/main/resources/assets/js/app/browse/filter/ContentBrowseFilterPanel.ts

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
1+
import {AggregationGroupView} from '@enonic/lib-admin-ui/aggregation/AggregationGroupView';
2+
import {Bucket} from '@enonic/lib-admin-ui/aggregation/Bucket';
3+
import {BucketAggregation} from '@enonic/lib-admin-ui/aggregation/BucketAggregation';
4+
import {BrowseFilterPanel} from '@enonic/lib-admin-ui/app/browse/filter/BrowseFilterPanel';
5+
import {TextSearchField} from '@enonic/lib-admin-ui/app/browse/filter/TextSearchField';
6+
import {AuthContext} from '@enonic/lib-admin-ui/auth/AuthContext';
7+
import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl';
8+
import {Element} from '@enonic/lib-admin-ui/dom/Element';
19
import {SearchInputValues} from '@enonic/lib-admin-ui/query/SearchInputValues';
2-
import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper';
3-
import Q from 'q';
410
import {i18n} from '@enonic/lib-admin-ui/util/Messages';
5-
import {Router} from '../../Router';
6-
import {ContentServerEventsHandler} from '../../event/ContentServerEventsHandler';
7-
import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus';
11+
import {cn} from '@enonic/ui';
12+
import Q from 'q';
13+
import {
14+
$contentFilterState,
15+
addSelectedBucket,
16+
deselectAllFilterBuckets,
17+
getFilterSelection,
18+
getFilterValue,
19+
hasFilterSet,
20+
hasFilterValueSet,
21+
resetContentFilter
22+
} from '../../../v6/features/store/contentFilter.store';
23+
import {ContentId} from '../../content/ContentId';
824
import {ContentQuery} from '../../content/ContentQuery';
9-
import {AggregationGroupView} from '@enonic/lib-admin-ui/aggregation/AggregationGroupView';
10-
import {BrowseFilterPanel} from '@enonic/lib-admin-ui/app/browse/filter/BrowseFilterPanel';
11-
import {BucketAggregation} from '@enonic/lib-admin-ui/aggregation/BucketAggregation';
12-
import {BucketAggregationView} from '@enonic/lib-admin-ui/aggregation/BucketAggregationView';
25+
import {ContentSummary} from '../../content/ContentSummary';
26+
import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus';
1327
import {ContentServerChangeItem} from '../../event/ContentServerChangeItem';
28+
import {ContentServerEventsHandler} from '../../event/ContentServerEventsHandler';
1429
import {ProjectContext} from '../../project/ProjectContext';
15-
import {ContentSummary} from '../../content/ContentSummary';
16-
import {ContentId} from '../../content/ContentId';
30+
import {Router} from '../../Router';
1731
import {ContentBrowseFilterComponent} from '../../ui2/filter/ContentBrowseFilterComponent';
18-
import {DependenciesSection} from './DependenciesSection';
19-
import {ContentAggregation} from './ContentAggregation';
32+
import {Branch} from '../../versioning/Branch';
2033
import {AggregationsDisplayNamesResolver} from './AggregationsDisplayNamesResolver';
21-
import {ContentAggregationsFetcher} from './ContentAggregationsFetcher';
2234
import {AggregationsQueryResult} from './AggregationsQueryResult';
23-
import {Element} from '@enonic/lib-admin-ui/dom/Element';
24-
import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl';
25-
import {ContentExportElement} from './ContentExportElement';
35+
import {ContentAggregation} from './ContentAggregation';
36+
import {ContentAggregationsFetcher} from './ContentAggregationsFetcher';
2637
import {ContentDependency} from './ContentDependency';
27-
import {TextSearchField} from '@enonic/lib-admin-ui/app/browse/filter/TextSearchField';
28-
import {Branch} from '../../versioning/Branch';
29-
import {AuthContext} from '@enonic/lib-admin-ui/auth/AuthContext';
30-
import {cn} from '@enonic/ui';
38+
import {ContentExportElement} from './ContentExportElement';
39+
import {DependenciesSection} from './DependenciesSection'
3140

3241
export class ContentBrowseFilterPanel<T extends ContentSummaryAndCompareStatus = ContentSummaryAndCompareStatus>
3342
extends BrowseFilterPanel<T> {
@@ -50,22 +59,16 @@ export class ContentBrowseFilterPanel<T extends ContentSummaryAndCompareStatus =
5059
this.aggregationsFetcher = this.createAggregationFetcher();
5160
this.displayNamesResolver = new AggregationsDisplayNamesResolver();
5261
this.dependenciesSection = new DependenciesSection();
53-
this.initElementsAndListeners();
5462

5563
this.filterComponent = new ContentBrowseFilterComponent({
5664
bucketAggregations: [],
57-
onChange: () => {
58-
this.search();
59-
},
60-
onSelectionChange: () => {
61-
this.search();
62-
},
6365
filterableAggregations: this.getFilterableAggregations(),
6466
exportOptions: this.getExportOptions(),
6567
});
6668

6769
this.appendChild(this.filterComponent);
6870

71+
this.handleEvents();
6972
this.getAndUpdateAggregations();
7073
}
7174

@@ -89,19 +92,15 @@ export class ContentBrowseFilterPanel<T extends ContentSummaryAndCompareStatus =
8992
});
9093
}
9194

92-
protected initElementsAndListeners() {
93-
if (this.isExportAllowed()) {
94-
this.exportElement = new ContentExportElement().setEnabled(false).setTitle(i18n('action.export')) as ContentExportElement;
95-
}
96-
97-
this.handleEvents();
98-
}
99-
10095
protected handleEvents() {
10196
this.onRendered(() => {
10297
super.appendChild(this.elementsContainer);
10398
});
10499

100+
$contentFilterState.listen(() => {
101+
this.search();
102+
});
103+
105104
this.handleEventsForDependenciesSection();
106105
}
107106

@@ -175,11 +174,6 @@ export class ContentBrowseFilterPanel<T extends ContentSummaryAndCompareStatus =
175174
ContentAggregation.MODIFIED_BY.toString();
176175
}
177176

178-
protected isExportAllowed(): boolean {
179-
// add more checks here if needed
180-
return true;
181-
}
182-
183177
private removeDependencyItem() {
184178
this.dependenciesSection.reset();
185179
this.search();
@@ -215,7 +209,7 @@ export class ContentBrowseFilterPanel<T extends ContentSummaryAndCompareStatus =
215209
}
216210

217211
private selectContentTypeBucket(key: string): void {
218-
this.filterComponent.selectBucketViewByKey(ContentAggregation.CONTENT_TYPE, key);
212+
addSelectedBucket(ContentAggregation.CONTENT_TYPE, new Bucket(key, 0));
219213
}
220214

221215
searchItemById(id: ContentId): void {
@@ -252,26 +246,26 @@ export class ContentBrowseFilterPanel<T extends ContentSummaryAndCompareStatus =
252246
getSearchInputValues(): SearchInputValues {
253247
const searchInputValues: SearchInputValues = new SearchInputValues();
254248

255-
searchInputValues.setAggregationSelections(this.filterComponent.getSelectedBuckets());
256-
searchInputValues.setTextSearchFieldValue(this.filterComponent.getValue());
249+
searchInputValues.setAggregationSelections(getFilterSelection());
250+
searchInputValues.setTextSearchFieldValue(getFilterValue());
257251

258252
return searchInputValues;
259253
}
260254

261255
hasFilterSet(): boolean {
262-
return this.filterComponent.hasSelectedBuckets() || this.hasSearchStringSet();
256+
return hasFilterSet();
263257
}
264258

265259
hasSearchStringSet(): boolean {
266-
return !StringHelper.isBlank(this.filterComponent.getValue());
260+
return hasFilterValueSet();
267261
}
268262

269263
resetControls() {
270-
this.filterComponent.reset();
264+
resetContentFilter();
271265
}
272266

273267
deselectAll() {
274-
this.filterComponent.deselectAll();
268+
deselectAllFilterBuckets();
275269
}
276270

277271
updateHitsCounter(hits: number) {

modules/lib/src/main/resources/assets/js/app/ui2/filter/BucketAggregation.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import {Bucket} from '@enonic/lib-admin-ui/aggregation/Bucket';
22
import {BucketAggregation} from '@enonic/lib-admin-ui/aggregation/BucketAggregation';
33
import {Button, Checkbox, CheckboxChecked, useControlledState} from '@enonic/ui';
4-
import {ReactElement, useCallback, useMemo, useState} from 'react';
4+
import {useStore} from '@nanostores/preact';
5+
import {ReactElement, useCallback, useMemo} from 'react';
6+
import {
7+
$contentFilterState,
8+
addSelectedBucket,
9+
removeSelectedBucket
10+
} from '../../../v6/features/store/contentFilter.store';
511
import {useI18n} from '../hooks/useI18n';
612
import {toSafeKey} from '../util/filter';
713

814
export type BucketAggregationProps = {
915
aggregation: BucketAggregation;
10-
selection?: Bucket[];
11-
onSelectionChange?: (selection: Bucket[]) => void;
1216
showAll?: boolean;
1317
showMoreLabel?: string;
1418
showLessLabel?: string;
@@ -17,32 +21,33 @@ export type BucketAggregationProps = {
1721

1822
export const BucketAggregationComponent = ({
1923
aggregation,
20-
selection,
21-
onSelectionChange,
2224
showAll,
2325
showMoreLabel = 'Show more',
2426
showLessLabel = 'Show less',
2527
maxVisibleBuckets = 5,
2628
}: BucketAggregationProps): ReactElement => {
29+
const {selection} = useStore($contentFilterState);
30+
const isBucketSelected = (bucketKey: string): boolean => {
31+
return selection.some((s) => s.getName() === aggregation.getName() && s.getSelectedBuckets().some((b) => b.getKey() === bucketKey));
32+
}
33+
2734
const [showAllState, setShowAllState] = useControlledState(showAll, false);
2835
const handleShowMoreLessClick = () => {
2936
setShowAllState(!showAllState);
3037
};
3138

32-
const [selectionState, setSelectionState] = useControlledState(selection, [], onSelectionChange);
33-
const isSelected = (bucket: Bucket) => selectionState.some(b => b.getKey() === bucket.getKey());
34-
3539
const buckets = aggregation.getBuckets().filter((b) => b.getDocCount() > 0);
3640
const visibleBuckets = showAllState ? buckets : buckets.slice(0, maxVisibleBuckets);
3741
const hasHiddenBuckets = buckets.length > maxVisibleBuckets;
3842

39-
const handleSelectionChange = useCallback((bucket: Bucket, checkedState: CheckboxChecked) => {
40-
const isChecked = checkedState === true;
41-
const newSelection = isChecked ? [...selectionState, bucket] : selectionState.filter(b => b.getKey() !== bucket.getKey());
42-
setSelectionState(newSelection);
43-
}, [selectionState, setSelectionState]);
44-
4543
const displayName = useMemo(() => useI18n(`field.${aggregation.getName()}`), [aggregation]);
44+
const handleSelectionChange = useCallback((bucket: Bucket, checked: CheckboxChecked) => {
45+
if (checked) {
46+
addSelectedBucket(aggregation.getName(), bucket);
47+
} else {
48+
removeSelectedBucket(aggregation.getName(), bucket);
49+
}
50+
}, [aggregation]);
4651

4752
return (
4853
<div className=''>
@@ -56,7 +61,7 @@ export const BucketAggregationComponent = ({
5661
id={safeKey}
5762
key={safeKey}
5863
className={'px-2.5'}
59-
checked={isSelected(bucket)}
64+
checked={isBucketSelected(bucket.getKey())}
6065
defaultChecked={false}
6166
label={label}
6267
onCheckedChange={(checked: CheckboxChecked) => handleSelectionChange(bucket, checked)}
Lines changed: 9 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
import {AggregationSelection} from '@enonic/lib-admin-ui/aggregation/AggregationSelection';
2-
import {Bucket} from '@enonic/lib-admin-ui/aggregation/Bucket';
31
import {BucketAggregation} from '@enonic/lib-admin-ui/aggregation/BucketAggregation';
42
import {LegacyElement} from '@enonic/lib-admin-ui/ui2/LegacyElement';
5-
import {Button, SearchInput, useControlledState} from '@enonic/ui';
3+
import {Button, SearchInput} from '@enonic/ui';
4+
import {useStore} from '@nanostores/preact';
65
import {Download} from 'lucide-react';
7-
import {useCallback, useMemo} from 'react';
6+
import {useMemo} from 'react';
7+
import {$contentFilterState, setContentFilterValue} from '../../../v6/features/store/contentFilter.store';
88
import {BucketAggregationComponent} from './BucketAggregation';
99
import {FilterableBucketAggregation} from './FilterableBucketAggregation';
1010

1111
export interface ContentBrowseFilterPanelComponentProps {
12-
value?: string;
13-
onChange: (value: string) => void;
14-
selection?: AggregationSelection[];
15-
onSelectionChange?: (selection: AggregationSelection[]) => void;
1612
hits?: number;
1713
bucketAggregations: BucketAggregation[];
1814
filterableAggregations?: {
@@ -26,11 +22,7 @@ export interface ContentBrowseFilterPanelComponentProps {
2622
}
2723

2824
const ContentBrowseFilterPanelComponent = ({
29-
value,
30-
onChange,
3125
hits = 0,
32-
selection,
33-
onSelectionChange,
3426
bucketAggregations,
3527
filterableAggregations,
3628
exportOptions,
@@ -39,26 +31,12 @@ const ContentBrowseFilterPanelComponent = ({
3931
() => bucketAggregations.filter(ba => ba.getBuckets().some(b => b.getDocCount() > 0)),
4032
[bucketAggregations]
4133
);
42-
43-
const [inputValue, setInputValue] = useControlledState(value, '', onChange);
44-
const [selectionState, setSelectionState] = useControlledState(selection, [], onSelectionChange);
45-
46-
const getBucketSelection = useCallback(
47-
(ba: BucketAggregation) => selectionState.find(s => s.getName() === ba.getName())?.getSelectedBuckets() || [], [selectionState]);
48-
const onBucketSelectionChange = useCallback((name: string, newBucketSelection: Bucket[]) => {
49-
const newSelection = selectionState.filter(s => s.getName() !== name);
50-
if (newBucketSelection.length > 0) {
51-
const aggrSel = new AggregationSelection(name);
52-
aggrSel.setValues(newBucketSelection);
53-
newSelection.push(aggrSel);
54-
}
55-
setSelectionState(newSelection);
56-
}, [selectionState, setSelectionState]);
34+
const {value} = useStore($contentFilterState);
5735

5836
return (
5937
<div className='bg-surface-neutral'>
60-
<SearchInput id={`bfc-input-${new Date().getDate()}`} className={'h-11.5'} showSearchIcon={false} value={inputValue}
61-
onChange={setInputValue} placeholder='Type to search...'/>
38+
<SearchInput id={`bfc-input-${new Date().getDate()}`} className={'h-11.5'} showSearchIcon={false} value={value}
39+
onChange={setContentFilterValue} placeholder='Type to search...'/>
6240
<div className='flex mt-2 mb-8 items-center'>
6341
<div className='grow'>
6442
<span className='text-lg pl-4.5 pr-4.5'>{hits} hits</span>
@@ -78,14 +56,9 @@ const ContentBrowseFilterPanelComponent = ({
7856
filterableOptions ?
7957
<FilterableBucketAggregation
8058
key={safeKey}
81-
selection={getBucketSelection(ba)}
8259
idsToKeepOnTop={filterableOptions.idsToKeepOnTop}
83-
onSelectionChange={(bucketSel) => onBucketSelectionChange(ba.getName(), bucketSel)}
8460
aggregation={ba}/> :
85-
<BucketAggregationComponent key={safeKey}
86-
selection={getBucketSelection(ba)}
87-
onSelectionChange={(bucketSel) => onBucketSelectionChange(ba.getName(), bucketSel)}
88-
aggregation={ba}/>
61+
<BucketAggregationComponent key={safeKey} aggregation={ba}/>
8962
);
9063
})}
9164
</div>
@@ -96,73 +69,16 @@ const ContentBrowseFilterPanelComponent = ({
9669
export class ContentBrowseFilterComponent
9770
extends LegacyElement<typeof ContentBrowseFilterPanelComponent, ContentBrowseFilterPanelComponentProps> {
9871

99-
private currentValue: string;
100-
101-
private selection: AggregationSelection[] = [];
10272

10373
constructor(props: ContentBrowseFilterPanelComponentProps) {
104-
const {onChange, onSelectionChange, ...rest} = props;
105-
106-
super({
107-
onChange: (value: string) => {
108-
this.currentValue = value;
109-
props.onChange?.(value);
110-
},
111-
onSelectionChange: (selection: AggregationSelection[]) => {
112-
this.selection = selection;
113-
props.onSelectionChange?.(selection);
114-
},
115-
...rest,
116-
}, ContentBrowseFilterPanelComponent);
117-
118-
this.currentValue = props.value ?? '';
119-
}
120-
121-
reset(): void {
122-
this.props.setKey('value', '');
123-
this.deselectAll();
124-
}
125-
126-
deselectAll(): void {
127-
this.props.setKey('selection', []);
74+
super(props, ContentBrowseFilterPanelComponent);
12875
}
12976

13077
updateAggregations(aggregations: BucketAggregation[]): void {
13178
this.props.setKey('bucketAggregations', aggregations);
13279
}
13380

134-
getSelectedBuckets(): AggregationSelection[] {
135-
return this.selection;
136-
}
137-
138-
getValue(): string {
139-
return this.currentValue;
140-
}
141-
142-
hasSelectedBuckets(): boolean {
143-
return this.selection.length > 0;
144-
}
145-
14681
updateHitsCounter(hits: number): void {
14782
this.props.setKey('hits', hits);
14883
}
149-
150-
selectBucketViewByKey(aggregationName: string, bucketKey: string): void {
151-
const aggregationSelection = this.selection.find(s => s.getName() === aggregationName);
152-
153-
if (aggregationSelection) {
154-
const bucket = aggregationSelection.getSelectedBuckets().find(b => b.getKey() === bucketKey);
155-
156-
if (!bucket) {
157-
const bucketToSelect = this.props.get().bucketAggregations
158-
.find(ba => ba.getName() === aggregationName)?.getBuckets()
159-
.find(b => b.getKey() === bucketKey);
160-
161-
if (bucketToSelect) {
162-
aggregationSelection.setValues([...aggregationSelection.getSelectedBuckets(), bucketToSelect]);
163-
this.props.setKey('selection', this.selection);
164-
}
165-
}
166-
}
167-
}
16884
}

0 commit comments

Comments
 (0)