feat(core): adjust collection rules (#12268)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Trashed page titles are now visually indicated with a strikethrough style in collection editor dialogs.

- **Bug Fixes**
  - Trashed pages are now properly excluded from allowed lists and filtered views.

- **Refactor**
  - Improved filtering logic for collections and page lists, separating user filters from system filters for more consistent results.
  - Enhanced filter configuration options for more flexible and maintainable filtering behavior.

- **Style**
  - Added a new style for displaying trashed items with a strikethrough effect.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN
2025-05-14 07:04:56 +00:00
parent cecf545590
commit fa3b08274c
6 changed files with 159 additions and 81 deletions

View File

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

View File

@@ -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'),

View File

@@ -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 = ({
<div
className={clsx(
styles.includeItemTitle,
page?.trash && styles.trashTitle,
styles.ellipsis
)}
>

View File

@@ -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);

View File

@@ -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<string>;
filterErrors: any[]; // errors from the filter providers
}> =
filters.length === 0
? of({
filtered: new Set<string>(extraAllowList ?? []),
filtered: new Set<string>([]),
filterErrors: [],
})
: combineLatest(
@@ -75,17 +98,51 @@ export class CollectionRulesService extends Service {
const filtered =
'error' in aggregated ? new Set<string>() : aggregated;
const finalSet = filtered.union(
new Set<string>(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<string>;
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)
);
}
}

View File

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