From 34686f3d854526b76c983b1616da209f760667e1 Mon Sep 17 00:00:00 2001 From: EYHN Date: Mon, 19 May 2025 04:24:36 +0000 Subject: [PATCH] feat(core): edit and delete pinned collections in all docs (#12296) ## Summary by CodeRabbit - **New Features** - Added the ability to edit and remove pinned collections directly from the workspace UI. - Improved filter management with clearer handling of temporary filters and editing workflows. - Enhanced synchronization and readiness tracking for collections and pinned collections, resulting in more responsive and reliable updates. - **Style** - Updated pinned collection item styles for better interaction feedback, including new edit and remove button visuals. --- .../pages/workspace/all-page/all-page.tsx | 141 ++++++++++++------ .../all-page/pinned-collections.css.ts | 32 ++++ .../workspace/all-page/pinned-collections.tsx | 76 ++++++++-- .../modules/collection/services/collection.ts | 5 + .../collection/services/pinned-collection.ts | 5 + .../modules/collection/stores/collection.ts | 13 +- .../collection/stores/pinned-collection.ts | 4 + .../core/src/modules/db/entities/table.ts | 17 ++- 8 files changed, 228 insertions(+), 65 deletions(-) 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 419d5e6641..f7b6cb70d9 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 @@ -35,6 +35,18 @@ export const AllPage = () => { const collectionService = useService(CollectionService); const pinnedCollectionService = useService(PinnedCollectionService); + const isCollectionDataReady = useLiveData( + collectionService.collectionDataReady$ + ); + + const isPinnedCollectionDataReady = useLiveData( + pinnedCollectionService.pinnedCollectionDataReady$ + ); + + const pinnedCollections = useLiveData( + pinnedCollectionService.pinnedCollections$ + ); + const [selectedCollectionId, setSelectedCollectionId] = useState< string | null >(null); @@ -45,17 +57,28 @@ export const AllPage = () => { ); useEffect(() => { - // if selected collection is not found, set selected collection id to null - if (!selectedCollection && selectedCollectionId) { + // if selected collection is not in pinned collections, set selected collection id to null + if ( + isPinnedCollectionDataReady && + selectedCollectionId && + !pinnedCollections.some(c => c.collectionId === selectedCollectionId) + ) { setSelectedCollectionId(null); } - }, [selectedCollection, selectedCollectionId]); + }, [isPinnedCollectionDataReady, pinnedCollections, selectedCollectionId]); + + useEffect(() => { + // if selected collection is not found, set selected collection id to null + if (!selectedCollection && selectedCollectionId && isCollectionDataReady) { + setSelectedCollectionId(null); + } + }, [isCollectionDataReady, selectedCollection, selectedCollectionId]); const selectedCollectionInfo = useLiveData( selectedCollection ? selectedCollection.info$ : null ); - const [tempFilters, setTempFilters] = useState([]); + const [tempFilters, setTempFilters] = useState(null); const [explorerContextValue] = useState(createDocExplorerContext); @@ -68,10 +91,9 @@ export const AllPage = () => { useEffect(() => { const subscription = collectionRulesService .watch( - // collection filters and temp filters can't exist at the same time selectedCollectionInfo ? { - filters: selectedCollectionInfo.rules.filters, + filters: tempFilters ?? selectedCollectionInfo.rules.filters, groupBy, orderBy, extraAllowList: selectedCollectionInfo.allowList, @@ -160,42 +182,74 @@ export const AllPage = () => { setTempFilters(filters); }, []); + const handleSelectCollection = useCallback((collectionId: string) => { + setSelectedCollectionId(collectionId); + setTempFilters(null); + }, []); + + const handleEditCollection = useCallback( + (collectionId: string) => { + const collection = collectionService.collection$(collectionId).value; + if (!collection) { + return; + } + setSelectedCollectionId(collectionId); + setTempFilters(collection.info$.value.rules.filters); + }, + [collectionService] + ); + const handleSaveFilters = useCallback(() => { - openPromptModal({ - title: t['com.affine.editCollection.saveCollection'](), - label: t['com.affine.editCollectionName.name'](), - inputOptions: { - placeholder: t['com.affine.editCollectionName.name.placeholder'](), - }, - children: t['com.affine.editCollectionName.createTips'](), - confirmText: t['com.affine.editCollection.save'](), - cancelText: t['com.affine.editCollection.button.cancel'](), - confirmButtonOptions: { - variant: 'primary', - }, - onConfirm(name) { - const id = collectionService.createCollection({ - name, - rules: { - filters: tempFilters, - }, - }); - pinnedCollectionService.addPinnedCollection({ - collectionId: id, - index: pinnedCollectionService.indexAt('after'), - }); - setTempFilters([]); - setSelectedCollectionId(id); - }, - }); + if (selectedCollectionId) { + collectionService.updateCollection(selectedCollectionId, { + rules: { + filters: tempFilters ?? [], + }, + }); + setTempFilters(null); + } else { + openPromptModal({ + title: t['com.affine.editCollection.saveCollection'](), + label: t['com.affine.editCollectionName.name'](), + inputOptions: { + placeholder: t['com.affine.editCollectionName.name.placeholder'](), + }, + children: t['com.affine.editCollectionName.createTips'](), + confirmText: t['com.affine.editCollection.save'](), + cancelText: t['com.affine.editCollection.button.cancel'](), + confirmButtonOptions: { + variant: 'primary', + }, + onConfirm(name) { + const id = collectionService.createCollection({ + name, + rules: { + filters: tempFilters ?? [], + }, + }); + pinnedCollectionService.addPinnedCollection({ + collectionId: id, + index: pinnedCollectionService.indexAt('after'), + }); + setTempFilters(null); + setSelectedCollectionId(id); + }, + }); + } }, [ collectionService, openPromptModal, pinnedCollectionService, + selectedCollectionId, t, tempFilters, ]); + const handleNewTempFilter = useCallback((params: FilterParams) => { + setSelectedCollectionId(null); + setTempFilters([params]); + }, []); + return ( @@ -209,29 +263,24 @@ export const AllPage = () => {
setSelectedCollectionId(null)} - onClickCollection={collectionId => { - setSelectedCollectionId(collectionId); - setTempFilters([]); - }} - onAddFilter={params => { - setSelectedCollectionId(null); - setTempFilters([...(tempFilters ?? []), params]); - }} - hiddenAdd={tempFilters.length > 0} + onActiveAll={() => setSelectedCollectionId(null)} + onActiveCollection={handleSelectCollection} + onAddFilter={handleNewTempFilter} + onEditCollection={handleEditCollection} + hiddenAdd={tempFilters !== null} />
- {tempFilters.length > 0 && ( + {tempFilters !== null && (
); }; export const PinnedCollections = ({ activeCollectionId, - onClickAll, - onClickCollection, + onActiveAll, + onActiveCollection, onAddFilter, + onEditCollection, hiddenAdd, }: { activeCollectionId: string | null; - onClickAll: () => void; - onClickCollection: (collectionId: string) => void; + onActiveAll: () => void; + onActiveCollection: (collectionId: string) => void; onAddFilter: (params: FilterParams) => void; + onEditCollection: (collectionId: string) => void; hiddenAdd?: boolean; }) => { const t = useI18n(); @@ -74,22 +108,32 @@ export const PinnedCollections = ({
{t['com.affine.all-docs.pinned-collection.all']()}
- {pinnedCollections.map(record => ( + {pinnedCollections.map((record, index) => ( onClickCollection(record.collectionId)} + onClick={() => onActiveCollection(record.collectionId)} + onClickEdit={() => onEditCollection(record.collectionId)} + onClickRemove={() => { + const nextCollectionId = pinnedCollections[index - 1]?.collectionId; + if (nextCollectionId) { + onActiveCollection(nextCollectionId); + } else { + onActiveAll(); + } + pinnedCollectionService.removePinnedCollection(record.collectionId); + }} /> ))} {!hiddenAdd && ( )} @@ -98,17 +142,17 @@ export const PinnedCollections = ({ }; export const AddPinnedCollection = ({ - onAddPinnedCollection, + onPinCollection, onAddFilter, }: { - onAddPinnedCollection: (collectionId: string) => void; + onPinCollection: (collectionId: string) => void; onAddFilter: (params: FilterParams) => void; }) => { return ( } @@ -121,10 +165,10 @@ export const AddPinnedCollection = ({ }; export const AddPinnedCollectionMenuContent = ({ - onAddPinnedCollection, + onPinCollection, onAddFilter, }: { - onAddPinnedCollection: (collectionId: string) => void; + onPinCollection: (collectionId: string) => void; onAddFilter: (params: FilterParams) => void; }) => { const [addingFilter, setAddingFilter] = useState(false); @@ -167,7 +211,7 @@ export const AddPinnedCollectionMenuContent = ({ prefixIcon={} suffixIcon={} onClick={() => { - onAddPinnedCollection(meta.id); + onPinCollection(meta.id); }} > {meta.name ?? t['Untitled']()} diff --git a/packages/frontend/core/src/modules/collection/services/collection.ts b/packages/frontend/core/src/modules/collection/services/collection.ts index d511a59adf..88d08218b7 100644 --- a/packages/frontend/core/src/modules/collection/services/collection.ts +++ b/packages/frontend/core/src/modules/collection/services/collection.ts @@ -19,6 +19,11 @@ export class CollectionService extends Service { }, }); + readonly collectionDataReady$ = LiveData.from( + this.store.watchCollectionDataReady(), + false + ); + // collection metas used in collection list, only include `id` and `name`, without `rules` and `allowList` readonly collectionMetas$ = LiveData.from( this.store.watchCollectionMetas(), diff --git a/packages/frontend/core/src/modules/collection/services/pinned-collection.ts b/packages/frontend/core/src/modules/collection/services/pinned-collection.ts index 495ebbde3c..6ade90b739 100644 --- a/packages/frontend/core/src/modules/collection/services/pinned-collection.ts +++ b/packages/frontend/core/src/modules/collection/services/pinned-collection.ts @@ -14,6 +14,11 @@ export class PinnedCollectionService extends Service { super(); } + pinnedCollectionDataReady$ = LiveData.from( + this.pinnedCollectionStore.watchPinnedCollectionDataReady(), + false + ); + pinnedCollections$ = LiveData.from( this.pinnedCollectionStore.watchPinnedCollections(), [] diff --git a/packages/frontend/core/src/modules/collection/stores/collection.ts b/packages/frontend/core/src/modules/collection/stores/collection.ts index da07aefab6..622ef4f9ca 100644 --- a/packages/frontend/core/src/modules/collection/stores/collection.ts +++ b/packages/frontend/core/src/modules/collection/stores/collection.ts @@ -7,7 +7,7 @@ import { } from '@toeverything/infra'; import dayjs from 'dayjs'; import { nanoid } from 'nanoid'; -import { map, type Observable, switchMap } from 'rxjs'; +import { distinctUntilChanged, map, type Observable, switchMap } from 'rxjs'; import { Array as YArray } from 'yjs'; import type { FilterParams } from '../../collection-rules'; @@ -35,6 +35,17 @@ export class CollectionStore extends Store { return this.rootYDoc.getMap('setting'); } + watchCollectionDataReady() { + return this.workspaceService.workspace.engine.doc + .docState$(this.workspaceService.workspace.id) + .pipe( + map(docState => { + return docState.ready; + }), + distinctUntilChanged() + ); + } + watchCollectionMetas() { return yjsGetPath(this.workspaceSettingYMap, 'collections').pipe( switchMap(yjsObserveDeep), diff --git a/packages/frontend/core/src/modules/collection/stores/pinned-collection.ts b/packages/frontend/core/src/modules/collection/stores/pinned-collection.ts index 3e5afa44c5..8663c91673 100644 --- a/packages/frontend/core/src/modules/collection/stores/pinned-collection.ts +++ b/packages/frontend/core/src/modules/collection/stores/pinned-collection.ts @@ -13,6 +13,10 @@ export class PinnedCollectionStore extends Store { super(); } + watchPinnedCollectionDataReady() { + return this.workspaceDBService.db.pinnedCollections.isReady$; + } + watchPinnedCollections(): Observable { return this.workspaceDBService.db.pinnedCollections.find$(); } diff --git a/packages/frontend/core/src/modules/db/entities/table.ts b/packages/frontend/core/src/modules/db/entities/table.ts index 37621a7ceb..60db9d2a30 100644 --- a/packages/frontend/core/src/modules/db/entities/table.ts +++ b/packages/frontend/core/src/modules/db/entities/table.ts @@ -3,7 +3,7 @@ import type { TableSchemaBuilder, } from '@toeverything/infra'; import { Entity, LiveData } from '@toeverything/infra'; -import { map } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs'; import type { WorkspaceService } from '../../workspace'; @@ -19,10 +19,23 @@ export class WorkspaceDBTable< super(); } + isReady$ = LiveData.from( + this.workspaceService.workspace.engine.doc + .docState$(this.props.storageDocId) + .pipe( + map(docState => docState.ready), + distinctUntilChanged() + ), + false + ); + isSyncing$ = LiveData.from( this.workspaceService.workspace.engine.doc .docState$(this.props.storageDocId) - .pipe(map(docState => docState.syncing)), + .pipe( + map(docState => docState.syncing), + distinctUntilChanged() + ), false );