mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
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:
@@ -114,21 +114,34 @@ export const SelectPage = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = collectionRulesService
|
const subscription = collectionRulesService
|
||||||
.watch([
|
.watch({
|
||||||
...filters,
|
filters:
|
||||||
{
|
filters.length > 0
|
||||||
type: 'system',
|
? filters
|
||||||
key: 'empty-journal',
|
: [
|
||||||
method: 'is',
|
// if no filters are present, match all non-trash documents
|
||||||
value: 'false',
|
{
|
||||||
},
|
type: 'system',
|
||||||
{
|
key: 'trash',
|
||||||
type: 'system',
|
method: 'is',
|
||||||
key: 'trash',
|
value: 'false',
|
||||||
method: 'is',
|
},
|
||||||
value: 'false',
|
],
|
||||||
},
|
extraFilters: [
|
||||||
])
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'empty-journal',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'trash',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
.subscribe(result => {
|
.subscribe(result => {
|
||||||
setFilteredDocIds(result.groups.flatMap(group => group.items));
|
setFilteredDocIds(result.groups.flatMap(group => group.items));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export const includeItemTitle = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
});
|
});
|
||||||
|
export const trashTitle = style({
|
||||||
|
textDecoration: 'line-through',
|
||||||
|
});
|
||||||
export const includeItemContentIs = style({
|
export const includeItemContentIs = style({
|
||||||
padding: '0 8px',
|
padding: '0 8px',
|
||||||
color: cssVar('textSecondaryColor'),
|
color: cssVar('textSecondaryColor'),
|
||||||
|
|||||||
@@ -49,21 +49,23 @@ export const RulesMode = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = collectionRulesService
|
const subscription = collectionRulesService
|
||||||
.watch(
|
.watch({
|
||||||
collection.rules.filters.length > 0
|
filters: collection.rules.filters,
|
||||||
? [
|
extraFilters: [
|
||||||
...collection.rules.filters,
|
{
|
||||||
{
|
type: 'system',
|
||||||
type: 'system',
|
key: 'trash',
|
||||||
key: 'trash',
|
method: 'is',
|
||||||
method: 'is',
|
value: 'false',
|
||||||
value: 'false',
|
},
|
||||||
},
|
{
|
||||||
]
|
type: 'system',
|
||||||
: [],
|
key: 'empty-journal',
|
||||||
undefined,
|
method: 'is',
|
||||||
undefined
|
value: 'false',
|
||||||
)
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
.subscribe(rules => {
|
.subscribe(rules => {
|
||||||
setRulesPageIds(rules.groups.flatMap(group => group.items));
|
setRulesPageIds(rules.groups.flatMap(group => group.items));
|
||||||
});
|
});
|
||||||
@@ -82,7 +84,8 @@ export const RulesMode = ({
|
|||||||
return allPageListConfig.allPages.filter(meta => {
|
return allPageListConfig.allPages.filter(meta => {
|
||||||
return (
|
return (
|
||||||
collection.allowList.includes(meta.id) &&
|
collection.allowList.includes(meta.id) &&
|
||||||
!rulesPageIds.includes(meta.id)
|
!rulesPageIds.includes(meta.id) &&
|
||||||
|
!meta.trash
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [allPageListConfig.allPages, collection.allowList, rulesPageIds]);
|
}, [allPageListConfig.allPages, collection.allowList, rulesPageIds]);
|
||||||
@@ -196,6 +199,7 @@ export const RulesMode = ({
|
|||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
styles.includeItemTitle,
|
styles.includeItemTitle,
|
||||||
|
page?.trash && styles.trashTitle,
|
||||||
styles.ellipsis
|
styles.ellipsis
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -143,9 +143,22 @@ export const AllPage = () => {
|
|||||||
const collectionRulesService = useService(CollectionRulesService);
|
const collectionRulesService = useService(CollectionRulesService);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = collectionRulesService
|
const subscription = collectionRulesService
|
||||||
.watch(
|
.watch({
|
||||||
[
|
filters:
|
||||||
...(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',
|
type: 'system',
|
||||||
key: 'empty-journal',
|
key: 'empty-journal',
|
||||||
@@ -159,9 +172,7 @@ export const AllPage = () => {
|
|||||||
value: 'false',
|
value: 'false',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
groupBy,
|
})
|
||||||
orderBy
|
|
||||||
)
|
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: result => {
|
next: result => {
|
||||||
explorerContextValue.groups$.next(result.groups);
|
explorerContextValue.groups$.next(result.groups);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
catchError,
|
catchError,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
firstValueFrom,
|
|
||||||
map,
|
map,
|
||||||
type Observable,
|
type Observable,
|
||||||
of,
|
of,
|
||||||
@@ -19,27 +18,51 @@ export class CollectionRulesService extends Service {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(options: {
|
||||||
filters: FilterParams[],
|
/**
|
||||||
groupBy?: GroupByParams,
|
* Primary filters
|
||||||
orderBy?: OrderByParams,
|
*
|
||||||
extraAllowList?: string[]
|
* If filters.length === 0, no items will be matched
|
||||||
): Observable<{
|
*/
|
||||||
|
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: {
|
groups: {
|
||||||
key: string;
|
key: string;
|
||||||
items: string[];
|
items: string[];
|
||||||
}[];
|
}[];
|
||||||
filterErrors: any[];
|
filterErrors: any[];
|
||||||
}> {
|
}> {
|
||||||
|
const {
|
||||||
|
filters = [],
|
||||||
|
groupBy,
|
||||||
|
orderBy,
|
||||||
|
extraAllowList,
|
||||||
|
extraFilters = [],
|
||||||
|
} = options;
|
||||||
|
|
||||||
// STEP 1: FILTER
|
// STEP 1: FILTER
|
||||||
const filterProviders = this.framework.getAll(FilterProvider);
|
const filterProviders = this.framework.getAll(FilterProvider);
|
||||||
const filtered$: Observable<{
|
const primaryFiltered$: Observable<{
|
||||||
filtered: Set<string>;
|
filtered: Set<string>;
|
||||||
filterErrors: any[]; // errors from the filter providers
|
filterErrors: any[]; // errors from the filter providers
|
||||||
}> =
|
}> =
|
||||||
filters.length === 0
|
filters.length === 0
|
||||||
? of({
|
? of({
|
||||||
filtered: new Set<string>(extraAllowList ?? []),
|
filtered: new Set<string>([]),
|
||||||
filterErrors: [],
|
filterErrors: [],
|
||||||
})
|
})
|
||||||
: combineLatest(
|
: combineLatest(
|
||||||
@@ -75,17 +98,51 @@ export class CollectionRulesService extends Service {
|
|||||||
const filtered =
|
const filtered =
|
||||||
'error' in aggregated ? new Set<string>() : aggregated;
|
'error' in aggregated ? new Set<string>() : aggregated;
|
||||||
|
|
||||||
const finalSet = filtered.union(
|
|
||||||
new Set<string>(extraAllowList ?? [])
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filtered: finalSet,
|
filtered: filtered,
|
||||||
filterErrors: results.map(i => ('error' in i ? i.error : null)),
|
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
|
// STEP 2: ORDER BY
|
||||||
const orderByProvider = orderBy
|
const orderByProvider = orderBy
|
||||||
? this.framework.getOptional(OrderByProvider(orderBy.type))
|
? this.framework.getOptional(OrderByProvider(orderBy.type))
|
||||||
@@ -94,7 +151,7 @@ export class CollectionRulesService extends Service {
|
|||||||
ordered: string[];
|
ordered: string[];
|
||||||
filtered: Set<string>;
|
filtered: Set<string>;
|
||||||
filterErrors: any[];
|
filterErrors: any[];
|
||||||
}> = filtered$.pipe(last$ => {
|
}> = finalFiltered$.pipe(last$ => {
|
||||||
if (orderBy && orderByProvider) {
|
if (orderBy && orderByProvider) {
|
||||||
const shared$ = last$.pipe(share());
|
const shared$ = last$.pipe(share());
|
||||||
const items$ = shared$.pipe(
|
const items$ = shared$.pipe(
|
||||||
@@ -171,7 +228,7 @@ export class CollectionRulesService extends Service {
|
|||||||
}[];
|
}[];
|
||||||
filterErrors: any[];
|
filterErrors: any[];
|
||||||
}> = grouped$.pipe(
|
}> = 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 }) => {
|
map(({ grouped, ordered, filtered, filterErrors }) => {
|
||||||
const result: { key: string; items: string[] }[] = [];
|
const result: { key: string; items: string[] }[] = [];
|
||||||
|
|
||||||
@@ -216,15 +273,4 @@ export class CollectionRulesService extends Service {
|
|||||||
|
|
||||||
return final$;
|
return final$;
|
||||||
}
|
}
|
||||||
|
|
||||||
compute(
|
|
||||||
filters: FilterParams[],
|
|
||||||
groupBy?: GroupByParams,
|
|
||||||
orderBy?: OrderByParams,
|
|
||||||
extraAllowList?: string[]
|
|
||||||
) {
|
|
||||||
return firstValueFrom(
|
|
||||||
this.watch(filters, groupBy, orderBy, extraAllowList)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,23 +48,24 @@ export class Collection extends Entity<{ id: string }> {
|
|||||||
return this.info$.pipe(
|
return this.info$.pipe(
|
||||||
switchMap(info => {
|
switchMap(info => {
|
||||||
return this.rulesService
|
return this.rulesService
|
||||||
.watch(
|
.watch({
|
||||||
info.rules.filters.length > 0
|
filters: info.rules.filters,
|
||||||
? [
|
extraAllowList: info.allowList,
|
||||||
...info.rules.filters,
|
extraFilters: [
|
||||||
// if we have more than one filter, we need to add a system filter to exclude trash
|
{
|
||||||
{
|
type: 'system',
|
||||||
type: 'system',
|
key: 'trash',
|
||||||
key: 'trash',
|
method: 'is',
|
||||||
method: 'is',
|
value: 'false',
|
||||||
value: 'false',
|
},
|
||||||
},
|
{
|
||||||
]
|
type: 'system',
|
||||||
: [], // If no filters are provided, an empty filter list will match no documents
|
key: 'empty-journal',
|
||||||
undefined,
|
method: 'is',
|
||||||
undefined,
|
value: 'false',
|
||||||
info.allowList
|
},
|
||||||
)
|
],
|
||||||
|
})
|
||||||
.pipe(map(result => result.groups.map(group => group.items).flat()));
|
.pipe(map(result => result.groups.map(group => group.items).flat()));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user