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()));
})
);