refactor(core): refactor collection to use new filter system (#12228)

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

- **New Features**
  - Introduced a new Collection entity and store with reactive management and real-time updates.
  - Added reactive favorite and shared filters with expanded filtering options.
- **Refactor**
  - Overhauled collection and filtering logic for better performance and maintainability.
  - Replaced legacy filtering UI and logic with a streamlined, service-driven rules system.
  - Updated collection components to use reactive data streams and simplified props.
  - Simplified collection creation by delegating ID generation and instantiation to the service layer.
  - Removed deprecated hooks and replaced state-based filtering with observable-driven filtering.
- **Bug Fixes**
  - Improved accuracy and consistency of tag and favorite filtering in collections.
- **Chores**
  - Removed deprecated and unused filter-related files, types, components, and styles to reduce complexity.
  - Cleaned up imports and removed unused code across multiple components.
- **Documentation**
  - Corrected inline documentation for improved clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN
2025-05-13 08:28:02 +00:00
parent 5dbe6ff68b
commit 13d882d6d8
96 changed files with 1274 additions and 3161 deletions

View File

@@ -14,7 +14,7 @@ export class CreatedByFilterProvider extends Service implements FilterProvider {
filter$(params: FilterParams): Observable<Set<string>> {
const method = params.method as WorkspacePropertyFilter<'createdBy'>;
if (method === 'include') {
const userIds = params.value?.split(',') ?? [];
const userIds = params.value?.split(',').filter(Boolean) ?? [];
return this.docsService.propertyValues$('createdBy').pipe(
map(o => {

View File

@@ -0,0 +1,49 @@
import type { DocsService } from '@affine/core/modules/doc';
import type { FavoriteService } from '@affine/core/modules/favorite';
import { Service } from '@toeverything/infra';
import { combineLatest, map, type Observable } from 'rxjs';
import type { FilterProvider } from '../../provider';
import type { FilterParams } from '../../types';
export class FavoriteFilterProvider extends Service implements FilterProvider {
constructor(
private readonly favoriteService: FavoriteService,
private readonly docsService: DocsService
) {
super();
}
filter$(params: FilterParams): Observable<Set<string>> {
const method = params.method;
if (method === 'is') {
return combineLatest([
this.favoriteService.favoriteList.list$,
this.docsService.allDocIds$(),
]).pipe(
map(([favoriteList, allDocIds]) => {
const favoriteDocIds = new Set<string>();
for (const { id, type } of favoriteList) {
if (type === 'doc') {
favoriteDocIds.add(id);
}
}
if (params.value === 'true') {
return favoriteDocIds;
} else if (params.value === 'false') {
const notFavoriteDocIds = new Set<string>();
for (const id of allDocIds) {
if (!favoriteDocIds.has(id)) {
notFavoriteDocIds.add(id);
}
}
return notFavoriteDocIds;
} else {
throw new Error(`Unsupported value: ${params.value}`);
}
})
);
}
throw new Error(`Unsupported method: ${params.method}`);
}
}

View File

@@ -0,0 +1,48 @@
import type { DocsService } from '@affine/core/modules/doc';
import type { ShareDocsListService } from '@affine/core/modules/share-doc';
import { onStart, Service } from '@toeverything/infra';
import { combineLatest, map, type Observable, of } from 'rxjs';
import type { FilterProvider } from '../../provider';
import type { FilterParams } from '../../types';
export class SharedFilterProvider extends Service implements FilterProvider {
constructor(
private readonly shareDocsListService: ShareDocsListService,
private readonly docsService: DocsService
) {
super();
}
filter$(params: FilterParams): Observable<Set<string>> {
const method = params.method;
if (method === 'is') {
return combineLatest([
this.shareDocsListService.shareDocs?.list$ ??
(of([]) as Observable<{ id: string }[]>),
this.docsService.allDocIds$(),
]).pipe(
onStart(() => {
this.shareDocsListService.shareDocs?.revalidate();
}),
map(([shareDocsList, allDocIds]) => {
const shareDocIds = new Set(shareDocsList.map(item => item.id));
if (params.value === 'true') {
return shareDocIds;
} else if (params.value === 'false') {
const notShareDocIds = new Set<string>();
for (const id of allDocIds) {
if (!shareDocIds.has(id)) {
notShareDocIds.add(id);
}
}
return notShareDocIds;
} else {
throw new Error(`Unsupported value: ${params.value}`);
}
})
);
}
throw new Error(`Unsupported method: ${params.method}`);
}
}

View File

@@ -1,5 +1,6 @@
import type { DocsService } from '@affine/core/modules/doc';
import type { TagService } from '@affine/core/modules/tag';
import type { WorkspacePropertyFilter } from '@affine/core/modules/workspace-property';
import { Service } from '@toeverything/infra';
import { combineLatest, map, type Observable, of, switchMap } from 'rxjs';
@@ -15,23 +16,66 @@ export class TagsFilterProvider extends Service implements FilterProvider {
}
filter$(params: FilterParams): Observable<Set<string>> {
if (params.method === 'include') {
const tagIds = params.value?.split(',') ?? [];
const tags = tagIds.map(id => this.tagService.tagList.tagByTagId$(id));
const method = params.method as WorkspacePropertyFilter<'tags'>;
const tagIds = params.value?.split(',').filter(Boolean) ?? [];
const tags = tagIds.map(id => this.tagService.tagList.tagByTagId$(id));
if (method === 'include-all' || method === 'not-include-all') {
if (tags.length === 0) {
return of(new Set<string>());
}
return combineLatest(tags).pipe(
const includeDocIds$ = combineLatest(tags).pipe(
switchMap(tags =>
combineLatest(
tags
.filter(tag => tag !== undefined)
.map(tag => tag.pageIds$.map(ids => new Set(ids)))
).pipe(
map(pageIds =>
pageIds.reduce((acc, curr) => acc.intersection(curr))
)
)
)
);
if (method === 'include-all') {
return includeDocIds$;
} else {
return combineLatest([
this.docsService.allDocIds$(),
includeDocIds$,
]).pipe(
map(
([docIds, includeDocIds]) =>
new Set(docIds.filter(id => !includeDocIds.has(id)))
)
);
}
} else if (method === 'include-any-of' || method === 'not-include-any-of') {
if (tags.length === 0) {
return of(new Set<string>());
}
const includeAnyOfDocIds$ = combineLatest(tags).pipe(
switchMap(tags =>
combineLatest(
tags.filter(tag => tag !== undefined).map(tag => tag.pageIds$)
).pipe(map(pageIds => new Set(pageIds.flat())))
)
);
} else if (params.method === 'is-not-empty') {
if (method === 'include-any-of') {
return includeAnyOfDocIds$;
} else {
return combineLatest([
this.docsService.allDocIds$(),
includeAnyOfDocIds$,
]).pipe(
map(
([docIds, includeAnyOfDocIds]) =>
new Set(docIds.filter(id => !includeAnyOfDocIds.has(id)))
)
);
}
} else if (method === 'is-not-empty') {
return combineLatest([
this.tagService.tagList.tags$.map(tags => new Set(tags.map(t => t.id))),
this.docsService.allDocsTagIds$(),
@@ -49,7 +93,7 @@ export class TagsFilterProvider extends Service implements FilterProvider {
)
)
);
} else if (params.method === 'is-empty') {
} else if (method === 'is-empty') {
return this.tagService.tagList.tags$
.map(tags => new Set(tags.map(t => t.id)))
.pipe(
@@ -70,6 +114,6 @@ export class TagsFilterProvider extends Service implements FilterProvider {
)
);
}
throw new Error(`Unsupported method: ${params.method}`);
throw new Error(`Unsupported method: ${method}`);
}
}

View File

@@ -13,7 +13,7 @@ export class UpdatedByFilterProvider extends Service implements FilterProvider {
filter$(params: FilterParams): Observable<Set<string>> {
const method = params.method as WorkspacePropertyFilter<'updatedBy'>;
if (method === 'include') {
const userIds = params.value?.split(',') ?? [];
const userIds = params.value?.split(',').filter(Boolean) ?? [];
return this.docsService.propertyValues$('updatedBy').pipe(
map(o => {

View File

@@ -1,6 +1,8 @@
import type { Framework } from '@toeverything/infra';
import { DocsService } from '../doc';
import { FavoriteService } from '../favorite';
import { ShareDocsListService } from '../share-doc';
import { TagService } from '../tag';
import { WorkspaceScope } from '../workspace';
import { WorkspacePropertyService } from '../workspace-property';
@@ -10,8 +12,10 @@ import { CreatedByFilterProvider } from './impls/filters/created-by';
import { DatePropertyFilterProvider } from './impls/filters/date';
import { DocPrimaryModeFilterProvider } from './impls/filters/doc-primary-mode';
import { EmptyJournalFilterProvider } from './impls/filters/empty-journal';
import { FavoriteFilterProvider } from './impls/filters/favorite';
import { JournalFilterProvider } from './impls/filters/journal';
import { PropertyFilterProvider } from './impls/filters/property';
import { SharedFilterProvider } from './impls/filters/shared';
import { SystemFilterProvider } from './impls/filters/system';
import { TagsFilterProvider } from './impls/filters/tags';
import { TextPropertyFilterProvider } from './impls/filters/text';
@@ -118,6 +122,14 @@ export function configureCollectionRulesModule(framework: Framework) {
.impl(FilterProvider('system:empty-journal'), EmptyJournalFilterProvider, [
DocsService,
])
.impl(FilterProvider('system:favorite'), FavoriteFilterProvider, [
FavoriteService,
DocsService,
])
.impl(FilterProvider('system:shared'), SharedFilterProvider, [
ShareDocsListService,
DocsService,
])
// --------------- Group By ---------------
.impl(GroupByProvider('system'), SystemGroupByProvider)
.impl(GroupByProvider('property'), PropertyGroupByProvider, [

View File

@@ -3,6 +3,7 @@ import {
catchError,
combineLatest,
distinctUntilChanged,
firstValueFrom,
map,
type Observable,
of,
@@ -21,7 +22,8 @@ export class CollectionRulesService extends Service {
watch(
filters: FilterParams[],
groupBy?: GroupByParams,
orderBy?: OrderByParams
orderBy?: OrderByParams,
extraAllowList?: string[]
): Observable<{
groups: {
key: string;
@@ -36,7 +38,10 @@ export class CollectionRulesService extends Service {
filterErrors: any[]; // errors from the filter providers
}> =
filters.length === 0
? of({ filtered: new Set<string>(), filterErrors: [] })
? of({
filtered: new Set<string>(extraAllowList ?? []),
filterErrors: [],
})
: combineLatest(
filters.map(filter => {
const provider = filterProviders.get(filter.type);
@@ -57,7 +62,7 @@ export class CollectionRulesService extends Service {
})
).pipe(
map(results => {
const finalSet = results.reduce((acc, result) => {
const aggregated = results.reduce((acc, result) => {
if ('error' in acc) {
return acc;
}
@@ -67,8 +72,15 @@ export class CollectionRulesService extends Service {
return acc.intersection(result);
});
const filtered =
'error' in aggregated ? new Set<string>() : aggregated;
const finalSet = filtered.union(
new Set<string>(extraAllowList ?? [])
);
return {
filtered: 'error' in finalSet ? new Set<string>() : finalSet,
filtered: finalSet,
filterErrors: results.map(i => ('error' in i ? i.error : null)),
};
})
@@ -204,4 +216,15 @@ 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

@@ -0,0 +1,88 @@
import { Entity, LiveData } from '@toeverything/infra';
import { uniq } from 'lodash-es';
import { map, switchMap } from 'rxjs';
import type { CollectionRulesService } from '../../collection-rules';
import type { CollectionInfo, CollectionStore } from '../stores/collection';
export class Collection extends Entity<{ id: string }> {
constructor(
private readonly store: CollectionStore,
private readonly rulesService: CollectionRulesService
) {
super();
}
id = this.props.id;
info$ = LiveData.from<CollectionInfo>(
this.store.watchCollectionInfo(this.id).pipe(
map(
info =>
({
// default fields in case collection info is not found
name: '',
id: this.id,
rules: {
filters: [],
},
allowList: [],
...info,
}) as CollectionInfo
)
),
{} as CollectionInfo
);
name$ = this.info$.map(info => info.name);
allowList$ = this.info$.map(info => info.allowList);
rules$ = this.info$.map(info => info.rules);
/**
* Returns a list of document IDs that match the collection rules and allow list.
*
* For performance optimization,
* Developers must explicitly call `watch()` to retrieve the result and properly manage the subscription lifecycle.
*/
watch() {
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
)
.pipe(map(result => result.groups.map(group => group.items).flat()));
})
);
}
updateInfo(info: Partial<CollectionInfo>) {
this.store.updateCollectionInfo(this.id, info);
}
addDoc(...docIds: string[]) {
this.store.updateCollectionInfo(this.id, {
allowList: uniq([...this.info$.value.allowList, ...docIds]),
});
}
removeDoc(...docIds: string[]) {
this.store.updateCollectionInfo(this.id, {
allowList: this.info$.value.allowList.filter(id => !docIds.includes(id)),
});
}
}

View File

@@ -1,12 +1,20 @@
export { Collection } from './entities/collection';
export type { CollectionMeta } from './services/collection';
export { CollectionService } from './services/collection';
export type { CollectionInfo } from './stores/collection';
import { type Framework } from '@toeverything/infra';
import { CollectionRulesService } from '../collection-rules';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { Collection } from './entities/collection';
import { CollectionService } from './services/collection';
import { CollectionStore } from './stores/collection';
export function configureCollectionModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(CollectionService, [WorkspaceService]);
.service(CollectionService, [CollectionStore])
.store(CollectionStore, [WorkspaceService])
.entity(Collection, [CollectionStore, CollectionRulesService]);
}

View File

@@ -1,193 +1,78 @@
import type {
Collection,
DeleteCollectionInfo,
DeletedCollection,
} from '@affine/env/filter';
import { LiveData, Service } from '@toeverything/infra';
import { Observable } from 'rxjs';
import { Array as YArray } from 'yjs';
import { LiveData, ObjectPool, Service } from '@toeverything/infra';
import { map } from 'rxjs';
import type { WorkspaceService } from '../../workspace';
import { Collection } from '../entities/collection';
import type { CollectionInfo, CollectionStore } from '../stores/collection';
const SETTING_KEY = 'setting';
const COLLECTIONS_KEY = 'collections';
const COLLECTIONS_TRASH_KEY = 'collections_trash';
export interface CollectionMeta extends Pick<CollectionInfo, 'id' | 'name'> {
title: string;
}
export class CollectionService extends Service {
constructor(private readonly workspaceService: WorkspaceService) {
constructor(private readonly store: CollectionStore) {
super();
}
private get doc() {
return this.workspaceService.workspace.docCollection.doc;
}
pool = new ObjectPool<string, Collection>({
onDelete(obj) {
obj.dispose();
},
});
private get setting() {
return this.workspaceService.workspace.docCollection.doc.getMap(
SETTING_KEY
);
}
private get collectionsYArray(): YArray<Collection> | undefined {
return this.setting.get(COLLECTIONS_KEY) as YArray<Collection>;
}
private get collectionsTrashYArray(): YArray<DeletedCollection> | undefined {
return this.setting.get(COLLECTIONS_TRASH_KEY) as YArray<DeletedCollection>;
}
// collection metas used in collection list, only include `id` and `name`, without `rules` and `allowList`
readonly collectionMetas$ = LiveData.from(
this.store.watchCollectionMetas(),
[]
);
readonly collections$ = LiveData.from(
new Observable<Collection[]>(subscriber => {
subscriber.next(this.collectionsYArray?.toArray() ?? []);
const fn = () => {
subscriber.next(this.collectionsYArray?.toArray() ?? []);
};
this.setting.observeDeep(fn);
return () => {
this.setting.unobserveDeep(fn);
};
}),
[]
this.store.watchCollectionIds().pipe(
map(
ids =>
new Map<string, Collection>(
ids.map(id => {
const exists = this.pool.get(id);
if (exists) {
return [id, exists.obj];
}
const record = this.framework.createEntity(Collection, { id });
this.pool.put(id, record);
return [id, record] as const;
})
)
)
),
new Map<string, Collection>()
);
collection$(id: string) {
return this.collections$.map(collections => {
return collections.find(v => v.id === id);
return this.collections$.selector(collections => {
return collections.get(id);
});
}
readonly collectionsTrash$ = LiveData.from(
new Observable<DeletedCollection[]>(subscriber => {
subscriber.next(this.collectionsTrashYArray?.toArray() ?? []);
const fn = () => {
subscriber.next(this.collectionsTrashYArray?.toArray() ?? []);
};
this.setting.observeDeep(fn);
return () => {
this.setting.unobserveDeep(fn);
};
}),
[]
);
addCollection(...collections: Collection[]) {
if (!this.setting.has(COLLECTIONS_KEY)) {
this.setting.set(COLLECTIONS_KEY, new YArray());
}
this.doc.transact(() => {
this.collectionsYArray?.insert(0, collections);
});
createCollection(collectionInfo: Partial<Omit<CollectionInfo, 'id'>>) {
return this.store.createCollection(collectionInfo);
}
updateCollection(id: string, updater: (value: Collection) => Collection) {
if (this.collectionsYArray) {
updateFirstOfYArray(
this.collectionsYArray,
v => v.id === id,
v => {
return updater(v);
}
);
}
}
addPageToCollection(collectionId: string, pageId: string) {
this.updateCollection(collectionId, old => {
return {
...old,
allowList: [pageId, ...(old.allowList ?? [])],
};
});
}
deletePageFromCollection(collectionId: string, pageId: string) {
this.updateCollection(collectionId, old => {
return {
...old,
allowList: old.allowList?.filter(id => id !== pageId),
};
});
}
deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) {
const collectionsYArray = this.collectionsYArray;
if (!collectionsYArray) {
return;
}
const set = new Set(ids);
this.workspaceService.workspace.docCollection.doc.transact(() => {
const indexList: number[] = [];
const list: Collection[] = [];
collectionsYArray.forEach((collection, i) => {
if (set.has(collection.id)) {
set.delete(collection.id);
indexList.unshift(i);
list.push(JSON.parse(JSON.stringify(collection)));
}
});
indexList.forEach(i => {
collectionsYArray.delete(i);
});
if (!this.collectionsTrashYArray) {
this.setting.set(COLLECTIONS_TRASH_KEY, new YArray());
}
const collectionsTrashYArray = this.collectionsTrashYArray;
if (!collectionsTrashYArray) {
return;
}
collectionsTrashYArray.insert(
0,
list.map(collection => ({
userId: info?.userId,
userName: info ? info.userName : 'Local User',
collection,
}))
);
if (collectionsTrashYArray.length > 10) {
collectionsTrashYArray.delete(10, collectionsTrashYArray.length - 10);
}
});
}
private deletePagesFromCollection(
collection: Collection,
idSet: Set<string>
updateCollection(
id: string,
collectionInfo: Partial<Omit<CollectionInfo, 'id'>>
) {
const newAllowList = collection.allowList.filter(id => !idSet.has(id));
if (newAllowList.length !== collection.allowList.length) {
this.updateCollection(collection.id, old => {
return {
...old,
allowList: newAllowList,
};
});
}
return this.store.updateCollectionInfo(id, collectionInfo);
}
deletePagesFromCollections(ids: string[]) {
const idSet = new Set(ids);
this.doc.transact(() => {
this.collections$.value.forEach(collection => {
this.deletePagesFromCollection(collection, idSet);
});
});
addDocToCollection(collectionId: string, docId: string) {
const collection = this.collection$(collectionId).value;
collection?.addDoc(docId);
}
removeDocFromCollection(collectionId: string, docId: string) {
const collection = this.collection$(collectionId).value;
collection?.removeDoc(docId);
}
deleteCollection(id: string) {
this.store.deleteCollection(id);
}
}
const updateFirstOfYArray = <T>(
array: YArray<T>,
p: (value: T) => boolean,
update: (value: T) => T
) => {
array.doc?.transact(() => {
for (let i = 0; i < array.length; i++) {
const ele = array.get(i);
if (p(ele)) {
array.delete(i);
array.insert(i, [update(ele)]);
return;
}
}
});
};

View File

@@ -0,0 +1,291 @@
import type { Collection as LegacyCollectionInfo } from '@affine/env/filter';
import {
Store,
yjsGetPath,
yjsObserve,
yjsObserveDeep,
} from '@toeverything/infra';
import dayjs from 'dayjs';
import { nanoid } from 'nanoid';
import { map, type Observable, switchMap } from 'rxjs';
import { Array as YArray } from 'yjs';
import type { FilterParams } from '../../collection-rules';
import type { WorkspaceService } from '../../workspace';
export interface CollectionInfo {
id: string;
name: string;
rules: {
filters: FilterParams[];
};
allowList: string[];
}
export class CollectionStore extends Store {
constructor(private readonly workspaceService: WorkspaceService) {
super();
}
private get rootYDoc() {
return this.workspaceService.workspace.rootYDoc;
}
private get workspaceSettingYMap() {
return this.rootYDoc.getMap('setting');
}
watchCollectionMetas() {
return yjsGetPath(this.workspaceSettingYMap, 'collections').pipe(
switchMap(yjsObserveDeep),
map(yjs => {
if (yjs instanceof YArray) {
return yjs.map(v => {
return {
id: v.id as string,
name: v.name as string,
// for old code compatibility
title: v.name as string,
};
});
} else {
return [];
}
})
);
}
watchCollectionIds() {
return yjsGetPath(this.workspaceSettingYMap, 'collections').pipe(
switchMap(yjsObserve),
map(yjs => {
if (yjs instanceof YArray) {
return yjs.map(v => {
return v.id as string;
});
} else {
return [];
}
})
);
}
watchCollectionInfo(id: string): Observable<CollectionInfo | null> {
return yjsGetPath(this.workspaceSettingYMap, 'collections').pipe(
switchMap(yjsObserve),
map(meta => {
if (meta instanceof YArray) {
// meta is YArray, `for-of` is faster then `for`
for (const doc of meta) {
if (doc && doc.id === id) {
return doc;
}
}
return null;
} else {
return null;
}
}),
switchMap(yjsObserveDeep),
map(yjs => {
if (yjs) {
return this.migrateCollectionInfo(yjs as LegacyCollectionInfo);
} else {
return null;
}
})
);
}
createCollection(info: Partial<Omit<CollectionInfo, 'id'>>) {
const id = nanoid();
let yArray = this.rootYDoc.getMap('setting').get('collections') as
| YArray<any>
| undefined;
if (!(yArray instanceof YArray)) {
// if collections list is not a YArray, create a new one
yArray = new YArray<any>();
this.rootYDoc.getMap('setting').set('collections', yArray);
}
yArray.push([
{
id: id,
name: info.name ?? '',
rules: info.rules ?? { filters: [] },
allowList: info.allowList ?? [],
},
]);
return id;
}
deleteCollection(id: string) {
const yArray = this.rootYDoc.getMap('setting').get('collections') as
| YArray<any>
| undefined;
if (!(yArray instanceof YArray)) {
throw new Error('Collections is not a YArray');
}
for (let i = 0; i < yArray.length; i++) {
const collection = yArray.get(i);
if (collection.id === id) {
yArray.delete(i);
return;
}
}
}
updateCollectionInfo(id: string, info: Partial<Omit<CollectionInfo, 'id'>>) {
const yArray = this.rootYDoc.getMap('setting').get('collections') as
| YArray<any>
| undefined;
if (!(yArray instanceof YArray)) {
throw new Error('Collections is not a YArray');
}
let collectionIndex = 0;
for (const collection of yArray) {
if (collection.id === id) {
this.rootYDoc.transact(() => {
yArray.delete(collectionIndex, 1);
yArray.insert(collectionIndex, [
{
id: collection.id,
name: info.name ?? collection.name,
rules: info.rules ?? collection.rules,
allowList: info.allowList ?? collection.allowList,
},
]);
});
return;
}
collectionIndex++;
}
}
migrateCollectionInfo(
legacyCollectionInfo: LegacyCollectionInfo
): CollectionInfo {
if ('rules' in legacyCollectionInfo) {
return legacyCollectionInfo as CollectionInfo;
}
return {
id: legacyCollectionInfo.id,
name: legacyCollectionInfo.name,
rules: {
filters: this.migrateFilterList(legacyCollectionInfo.filterList),
},
allowList: legacyCollectionInfo.allowList,
};
}
migrateFilterList(
filterList: LegacyCollectionInfo['filterList']
): FilterParams[] {
return filterList.map(filter => {
const leftValue = filter.left.name;
const method = filter.funcName;
const args = filter.args.map(arg => arg.value);
const arg0 = args[0];
if (leftValue === 'Created' || leftValue === 'Updated') {
const key = leftValue === 'Created' ? 'createdAt' : 'updatedAt';
if (method === 'after' && typeof arg0 === 'number') {
return {
type: 'system',
key,
method: 'after',
value: dayjs(arg0).format('YYYY-MM-DD'),
};
} else if (method === 'before' && typeof arg0 === 'number') {
return {
type: 'system',
key,
method: 'before',
value: dayjs(arg0).format('YYYY-MM-DD'),
};
} else if (method === 'last' && typeof arg0 === 'number') {
return {
type: 'system',
key,
method: 'last',
value: dayjs().subtract(arg0, 'day').format('YYYY-MM-DD'),
};
}
} else if (leftValue === 'Is Favourited') {
if (method === 'is') {
const value = arg0 ? 'true' : 'false';
return {
type: 'system',
key: 'favorite',
method: 'is',
value,
};
}
} else if (leftValue === 'Tags') {
if (method === 'is not empty') {
return {
type: 'system',
key: 'tags',
method: 'is-not-empty',
};
} else if (method === 'is empty') {
return {
type: 'system',
key: 'tags',
method: 'is-empty',
};
} else if (method === 'contains all' && Array.isArray(arg0)) {
return {
type: 'system',
key: 'tags',
method: 'include-all',
value: arg0.join(','),
};
} else if (method === 'contains one of' && Array.isArray(arg0)) {
return {
type: 'system',
key: 'tags',
method: 'include-any-of',
value: arg0.join(','),
};
} else if (method === 'does not contains all' && Array.isArray(arg0)) {
return {
type: 'system',
key: 'tags',
method: 'not-include-all',
value: arg0.join(','),
};
} else if (
method === 'does not contains one of' &&
Array.isArray(arg0)
) {
return {
type: 'system',
key: 'tags',
method: 'not-include-any-of',
value: arg0.join(','),
};
}
} else if (leftValue === 'Is Public' && method === 'is') {
return {
type: 'system',
key: 'shared',
method: 'is',
value: arg0 ? 'true' : 'false',
};
}
return {
type: 'unknown',
key: 'unknown',
method: 'unknown',
};
});
}
}

View File

@@ -69,6 +69,10 @@ export class DocsService extends Service {
return this.store.watchAllDocTagIds();
}
allDocIds$() {
return this.store.watchDocIds();
}
allNonTrashDocIds$() {
return this.store.watchNonTrashDocIds();
}

View File

@@ -30,7 +30,7 @@ export class CollectionsQuickSearchSession
LiveData.computed(get => {
const query = get(this.query$);
const collections = get(this.collectionService.collections$);
const collections = get(this.collectionService.collectionMetas$);
const fuse = new Fuse(collections, {
keys: ['name'],

View File

@@ -1,7 +1,4 @@
import type {
CollectionMeta,
TagMeta,
} from '@affine/core/components/page-list';
import type { TagMeta } from '@affine/core/components/page-list';
import { I18n } from '@affine/i18n';
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
import type { DocMeta } from '@blocksuite/affine/store';
@@ -18,7 +15,7 @@ import { html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { map } from 'rxjs';
import type { CollectionService } from '../../collection';
import type { CollectionMeta, CollectionService } from '../../collection';
import type { DocDisplayMetaService } from '../../doc-display-meta';
import type { DocsSearchService } from '../../docs-search';
import { type RecentDocsService } from '../../quicksearch';
@@ -298,7 +295,7 @@ export class SearchMenuService extends Service {
action: SearchCollectionMenuAction,
_abortSignal: AbortSignal
): LinkedMenuGroup {
const collections = this.collectionService.collections$.value;
const collections = this.collectionService.collectionMetas$.value;
if (query.trim().length === 0) {
return {
name: I18n.t('com.affine.editor.at-menu.collections', {

View File

@@ -11,7 +11,7 @@ import {
onStart,
smartRetry,
} from '@toeverything/infra';
import { tap } from 'rxjs';
import { map, tap } from 'rxjs';
import type { GlobalCache } from '../../storage';
import type { WorkspaceService } from '../../workspace';
@@ -22,7 +22,12 @@ type ShareDocListType = GetWorkspacePublicPagesQuery['workspace']['publicDocs'];
export const logger = new DebugLogger('affine:share-doc-list');
export class ShareDocsList extends Entity {
list$ = LiveData.from(this.cache.watch<ShareDocListType>('share-docs'), []);
list$ = LiveData.from(
this.cache
.watch<ShareDocListType>('share-docs')
.pipe(map(list => list ?? [])),
[]
);
isLoading$ = new LiveData<boolean>(false);
error$ = new LiveData<any>(null);

View File

@@ -13,7 +13,13 @@ type DateFilters =
export type WorkspacePropertyTypes = {
tags: {
filter: 'include' | 'is-not-empty' | 'is-empty';
filter:
| 'include-all'
| 'include-any-of'
| 'not-include-all'
| 'not-include-any-of'
| 'is-not-empty'
| 'is-empty';
};
text: {
filter: 'is' | 'is-not' | 'is-not-empty' | 'is-empty';