mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00: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(() => {
|
||||
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));
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user