diff --git a/packages/frontend/core/src/components/page-list/docs/select-page.tsx b/packages/frontend/core/src/components/page-list/docs/select-page.tsx index e67246ae17..0c7d798ab9 100644 --- a/packages/frontend/core/src/components/page-list/docs/select-page.tsx +++ b/packages/frontend/core/src/components/page-list/docs/select-page.tsx @@ -114,21 +114,34 @@ export const SelectPage = ({ useEffect(() => { const subscription = collectionRulesService - .watch([ - ...filters, - { - type: 'system', - key: 'empty-journal', - method: 'is', - value: 'false', - }, - { - type: 'system', - key: 'trash', - method: 'is', - value: 'false', - }, - ]) + .watch({ + filters: + filters.length > 0 + ? filters + : [ + // if no filters are present, match all non-trash documents + { + type: 'system', + key: 'trash', + method: 'is', + value: 'false', + }, + ], + extraFilters: [ + { + type: 'system', + key: 'empty-journal', + method: 'is', + value: 'false', + }, + { + type: 'system', + key: 'trash', + method: 'is', + value: 'false', + }, + ], + }) .subscribe(result => { setFilteredDocIds(result.groups.flatMap(group => group.items)); }); diff --git a/packages/frontend/core/src/desktop/dialogs/collection-editor/edit-collection.css.ts b/packages/frontend/core/src/desktop/dialogs/collection-editor/edit-collection.css.ts index 1f65274d5d..d10aa4e827 100644 --- a/packages/frontend/core/src/desktop/dialogs/collection-editor/edit-collection.css.ts +++ b/packages/frontend/core/src/desktop/dialogs/collection-editor/edit-collection.css.ts @@ -37,6 +37,9 @@ export const includeItemTitle = style({ overflow: 'hidden', fontWeight: 600, }); +export const trashTitle = style({ + textDecoration: 'line-through', +}); export const includeItemContentIs = style({ padding: '0 8px', color: cssVar('textSecondaryColor'), diff --git a/packages/frontend/core/src/desktop/dialogs/collection-editor/rules-mode.tsx b/packages/frontend/core/src/desktop/dialogs/collection-editor/rules-mode.tsx index d3567ca4b8..05619941e7 100644 --- a/packages/frontend/core/src/desktop/dialogs/collection-editor/rules-mode.tsx +++ b/packages/frontend/core/src/desktop/dialogs/collection-editor/rules-mode.tsx @@ -49,21 +49,23 @@ export const RulesMode = ({ useEffect(() => { const subscription = collectionRulesService - .watch( - collection.rules.filters.length > 0 - ? [ - ...collection.rules.filters, - { - type: 'system', - key: 'trash', - method: 'is', - value: 'false', - }, - ] - : [], - undefined, - undefined - ) + .watch({ + filters: collection.rules.filters, + extraFilters: [ + { + type: 'system', + key: 'trash', + method: 'is', + value: 'false', + }, + { + type: 'system', + key: 'empty-journal', + method: 'is', + value: 'false', + }, + ], + }) .subscribe(rules => { setRulesPageIds(rules.groups.flatMap(group => group.items)); }); @@ -82,7 +84,8 @@ export const RulesMode = ({ return allPageListConfig.allPages.filter(meta => { return ( collection.allowList.includes(meta.id) && - !rulesPageIds.includes(meta.id) + !rulesPageIds.includes(meta.id) && + !meta.trash ); }); }, [allPageListConfig.allPages, collection.allowList, rulesPageIds]); @@ -196,6 +199,7 @@ export const RulesMode = ({
diff --git a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx index 8162348a87..adc954851f 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx @@ -143,9 +143,22 @@ export const AllPage = () => { const collectionRulesService = useService(CollectionRulesService); useEffect(() => { const subscription = collectionRulesService - .watch( - [ - ...(filters ?? []), + .watch({ + filters: + filters && filters.length > 0 + ? filters + : [ + // if no filters are present, match all non-trash documents + { + type: 'system', + key: 'trash', + method: 'is', + value: 'false', + }, + ], + groupBy, + orderBy, + extraFilters: [ { type: 'system', key: 'empty-journal', @@ -159,9 +172,7 @@ export const AllPage = () => { value: 'false', }, ], - groupBy, - orderBy - ) + }) .subscribe({ next: result => { explorerContextValue.groups$.next(result.groups); diff --git a/packages/frontend/core/src/modules/collection-rules/services/collection-rules.ts b/packages/frontend/core/src/modules/collection-rules/services/collection-rules.ts index 799ce8dda1..fcb3abdc41 100644 --- a/packages/frontend/core/src/modules/collection-rules/services/collection-rules.ts +++ b/packages/frontend/core/src/modules/collection-rules/services/collection-rules.ts @@ -3,7 +3,6 @@ import { catchError, combineLatest, distinctUntilChanged, - firstValueFrom, map, type Observable, of, @@ -19,27 +18,51 @@ export class CollectionRulesService extends Service { super(); } - watch( - filters: FilterParams[], - groupBy?: GroupByParams, - orderBy?: OrderByParams, - extraAllowList?: string[] - ): Observable<{ + watch(options: { + /** + * Primary filters + * + * If filters.length === 0, no items will be matched + */ + filters?: FilterParams[]; + groupBy?: GroupByParams; + orderBy?: OrderByParams; + /** + * Additional allowed items that bypass primary filters but are still subject to extraFilters + */ + extraAllowList?: string[]; + /** + * Additional filters that will be applied after the primary filters and extraAllowList + * + * Useful for applying system-level filters such as trash, empty journal, etc. + * + * Note: If the primary filters match no items, these extraFilters will not be applied. + */ + extraFilters?: FilterParams[]; + }): Observable<{ groups: { key: string; items: string[]; }[]; filterErrors: any[]; }> { + const { + filters = [], + groupBy, + orderBy, + extraAllowList, + extraFilters = [], + } = options; + // STEP 1: FILTER const filterProviders = this.framework.getAll(FilterProvider); - const filtered$: Observable<{ + const primaryFiltered$: Observable<{ filtered: Set; filterErrors: any[]; // errors from the filter providers }> = filters.length === 0 ? of({ - filtered: new Set(extraAllowList ?? []), + filtered: new Set([]), filterErrors: [], }) : combineLatest( @@ -75,17 +98,51 @@ export class CollectionRulesService extends Service { const filtered = 'error' in aggregated ? new Set() : aggregated; - const finalSet = filtered.union( - new Set(extraAllowList ?? []) - ); - return { - filtered: finalSet, + filtered: filtered, filterErrors: results.map(i => ('error' in i ? i.error : null)), }; }) ); + const extraFiltered$ = + extraFilters.length === 0 + ? of(null) + : combineLatest( + extraFilters.map(filter => { + const provider = filterProviders.get(filter.type); + if (!provider) { + throw new Error(`Unsupported filter type: ${filter.type}`); + } + return provider.filter$(filter).pipe( + distinctUntilChanged((prev, curr) => { + return prev.isSubsetOf(curr) && curr.isSubsetOf(prev); + }) + ); + }) + ).pipe( + map(results => { + return results.reduce((acc, result) => { + return acc.intersection(result); + }); + }) + ); + + const finalFiltered$ = combineLatest([ + primaryFiltered$, + extraFiltered$, + ]).pipe( + map(([primary, extra]) => ({ + filtered: + extra === null + ? primary.filtered.union(new Set(extraAllowList ?? [])) + : primary.filtered + .union(new Set(extraAllowList ?? [])) + .intersection(extra), + filterErrors: primary.filterErrors, + })) + ); + // STEP 2: ORDER BY const orderByProvider = orderBy ? this.framework.getOptional(OrderByProvider(orderBy.type)) @@ -94,7 +151,7 @@ export class CollectionRulesService extends Service { ordered: string[]; filtered: Set; filterErrors: any[]; - }> = filtered$.pipe(last$ => { + }> = finalFiltered$.pipe(last$ => { if (orderBy && orderByProvider) { const shared$ = last$.pipe(share()); const items$ = shared$.pipe( @@ -171,7 +228,7 @@ export class CollectionRulesService extends Service { }[]; filterErrors: any[]; }> = grouped$.pipe( - throttleTime(300, undefined, { leading: false, trailing: true }), // throttle the results to avoid too many re-renders + throttleTime(300, undefined, { leading: true, trailing: true }), // throttle the results to avoid too many re-renders map(({ grouped, ordered, filtered, filterErrors }) => { const result: { key: string; items: string[] }[] = []; @@ -216,15 +273,4 @@ export class CollectionRulesService extends Service { return final$; } - - compute( - filters: FilterParams[], - groupBy?: GroupByParams, - orderBy?: OrderByParams, - extraAllowList?: string[] - ) { - return firstValueFrom( - this.watch(filters, groupBy, orderBy, extraAllowList) - ); - } } diff --git a/packages/frontend/core/src/modules/collection/entities/collection.ts b/packages/frontend/core/src/modules/collection/entities/collection.ts index b0b67b5f54..21ff0db5a3 100644 --- a/packages/frontend/core/src/modules/collection/entities/collection.ts +++ b/packages/frontend/core/src/modules/collection/entities/collection.ts @@ -48,23 +48,24 @@ export class Collection extends Entity<{ id: string }> { return this.info$.pipe( switchMap(info => { return this.rulesService - .watch( - info.rules.filters.length > 0 - ? [ - ...info.rules.filters, - // if we have more than one filter, we need to add a system filter to exclude trash - { - type: 'system', - key: 'trash', - method: 'is', - value: 'false', - }, - ] - : [], // If no filters are provided, an empty filter list will match no documents - undefined, - undefined, - info.allowList - ) + .watch({ + filters: info.rules.filters, + extraAllowList: info.allowList, + extraFilters: [ + { + type: 'system', + key: 'trash', + method: 'is', + value: 'false', + }, + { + type: 'system', + key: 'empty-journal', + method: 'is', + value: 'false', + }, + ], + }) .pipe(map(result => result.groups.map(group => group.items).flat())); }) );