diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index fc1942d43..cd2c39141 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -229,7 +229,7 @@ export const RootAppSidebar = memo((): ReactElement => { diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/layouts/collapsible-section.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/layouts/collapsible-section.tsx index 33d0ff58f..f73b98053 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/layouts/collapsible-section.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/layouts/collapsible-section.tsx @@ -1,8 +1,5 @@ import { CategoryDivider } from '@affine/core/modules/app-sidebar/views'; -import { - type CollapsibleSectionName, - NavigationPanelService, -} from '@affine/core/modules/navigation-panel'; +import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import * as Collapsible from '@radix-ui/react-collapsible'; import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; @@ -17,7 +14,7 @@ import { import { content, header, root } from './collapsible-section.css'; interface CollapsibleSectionProps extends PropsWithChildren { - name: CollapsibleSectionName; + path: string[]; title: string; actions?: ReactNode; @@ -33,7 +30,7 @@ interface CollapsibleSectionProps extends PropsWithChildren { } export const CollapsibleSection = ({ - name, + path, title, actions, children, @@ -48,15 +45,15 @@ export const CollapsibleSection = ({ contentClassName, contentStyle, }: CollapsibleSectionProps) => { - const section = useService(NavigationPanelService).sections[name]; + const navigationPanelService = useService(NavigationPanelService); - const collapsed = useLiveData(section.collapsed$); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); const setCollapsed = useCallback( (v: boolean) => { - section.setCollapsed(v); + navigationPanelService.setCollapsed(path, v); }, - [section] + [navigationPanelService, path] ); return ( diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/collection/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/collection/index.tsx index 94bef235f..a89cc62f2 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/collection/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/collection/index.tsx @@ -11,6 +11,7 @@ import { } from '@affine/core/modules/collection'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { GlobalContextService } from '@affine/core/modules/global-context'; +import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import type { AffineDNDData } from '@affine/core/types/dnd'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; @@ -47,6 +48,7 @@ export const NavigationPanelCollectionNode = ({ operations: additionalOperations, canDrop, dropEffect, + parentPath, }: { collectionId: string; } & GenericNavigationPanelNode) => { @@ -55,10 +57,21 @@ export const NavigationPanelCollectionNode = ({ GlobalContextService, WorkspaceDialogService, }); + const navigationPanelService = useService(NavigationPanelService); const active = useLiveData(globalContextService.globalContext.collectionId.$) === collectionId; - const [collapsed, setCollapsed] = useState(true); + const path = useMemo( + () => [...(parentPath ?? []), `collection-${collectionId}`], + [parentPath, collectionId] + ); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); + const setCollapsed = useCallback( + (value: boolean) => { + navigationPanelService.setCollapsed(path, value); + }, + [navigationPanelService, path] + ); const collectionService = useService(CollectionService); const collection = useLiveData(collectionService.collection$(collectionId)); @@ -160,7 +173,7 @@ export const NavigationPanelCollectionNode = ({ const handleOpenCollapsed = useCallback(() => { setCollapsed(false); - }, []); + }, [setCollapsed]); const handleEditCollection = useCallback(() => { if (!collection) { @@ -217,15 +230,20 @@ export const NavigationPanelCollectionNode = ({ dropEffect={handleDropEffectOnCollection} data-testid={`navigation-panel-collection-${collectionId}`} > - + ); }; const NavigationPanelCollectionNodeChildren = ({ collection, + path, }: { collection: Collection; + path: string[]; }) => { const t = useI18n(); const { collectionService } = useServices({ @@ -264,6 +282,7 @@ const NavigationPanelCollectionNodeChildren = ({ at: 'navigation-panel:collection:filtered-docs', collectionId: collection.id, }} + parentPath={path} operations={ allowList.has(docId) ? [ diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/doc/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/doc/index.tsx index 37bc00e9b..3612efc8d 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/doc/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/doc/index.tsx @@ -14,6 +14,7 @@ import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; import { DocsSearchService } from '@affine/core/modules/docs-search'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { GlobalContextService } from '@affine/core/modules/global-context'; +import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import { GuardService } from '@affine/core/modules/permissions'; import type { AffineDNDData } from '@affine/core/types/dnd'; import { useI18n } from '@affine/i18n'; @@ -46,6 +47,7 @@ export const NavigationPanelDocNode = ({ canDrop, operations: additionalOperations, dropEffect, + parentPath, }: { docId: string; isLinked?: boolean; @@ -67,11 +69,22 @@ export const NavigationPanelDocNode = ({ FeatureFlagService, GuardService, }); + const navigationPanelService = useService(NavigationPanelService); const { appSettings } = useAppSettingHelper(); const active = useLiveData(globalContextService.globalContext.docId.$) === docId; - const [collapsed, setCollapsed] = useState(true); + const path = useMemo( + () => [...(parentPath ?? []), `doc-${docId}`], + [parentPath, docId] + ); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); + const setCollapsed = useCallback( + (value: boolean) => { + navigationPanelService.setCollapsed(path, value); + }, + [navigationPanelService, path] + ); const isCollapsed = appSettings.showLinkedDocInSidebar ? collapsed : true; const docRecord = useLiveData(docsService.list.doc$(docId)); @@ -227,7 +240,7 @@ export const NavigationPanelDocNode = ({ openInfoModal: () => workspaceDialogService.open('doc-info', { docId }), openNodeCollapsed: () => setCollapsed(false), }), - [docId, workspaceDialogService] + [docId, setCollapsed, workspaceDialogService] ) ); @@ -302,6 +315,7 @@ export const NavigationPanelDocNode = ({ at: 'navigation-panel:doc:linked-docs', docId, }} + parentPath={path} isLinked /> )) diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/folder/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/folder/index.tsx index 28190448e..9387652e9 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/folder/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/folder/index.tsx @@ -13,6 +13,7 @@ import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/uti import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import { type FolderNode, OrganizeService, @@ -31,7 +32,7 @@ import { RemoveFolderIcon, TagsIcon, } from '@blocksuite/icons/rc'; -import { useLiveData, useServices } from '@toeverything/infra'; +import { useLiveData, useService, useServices } from '@toeverything/infra'; import { difference } from 'lodash-es'; import { useCallback, useMemo, useState } from 'react'; @@ -57,6 +58,7 @@ export const NavigationPanelFolderNode = ({ dropEffect, canDrop, reorderable, + parentPath, }: { defaultRenaming?: boolean; nodeId: string; @@ -104,6 +106,7 @@ export const NavigationPanelFolderNode = ({ dropEffect={dropEffect} reorderable={reorderable} canDrop={canDrop} + parentPath={parentPath} /> ); } else if (type === 'doc') { @@ -117,6 +120,7 @@ export const NavigationPanelFolderNode = ({ canDrop={canDrop} dropEffect={dropEffect} operations={additionalOperations} + parentPath={parentPath} /> ) ); @@ -131,6 +135,7 @@ export const NavigationPanelFolderNode = ({ reorderable={reorderable} dropEffect={dropEffect} operations={additionalOperations} + parentPath={parentPath} /> ) ); @@ -145,6 +150,7 @@ export const NavigationPanelFolderNode = ({ reorderable dropEffect={dropEffect} operations={additionalOperations} + parentPath={parentPath} /> ) ); @@ -177,6 +183,7 @@ const NavigationPanelFolderNodeFolder = ({ canDrop, dropEffect, reorderable, + parentPath, }: { defaultRenaming?: boolean; node: FolderNode; @@ -189,11 +196,22 @@ const NavigationPanelFolderNodeFolder = ({ FeatureFlagService, WorkspaceDialogService, }); + const navigationPanelService = useService(NavigationPanelService); const name = useLiveData(node.name$); const enableEmojiIcon = useLiveData( featureFlagService.flags.enable_emoji_folder_icon.$ ); - const [collapsed, setCollapsed] = useState(true); + const path = useMemo( + () => [...(parentPath ?? []), `folder-${node.id}`], + [parentPath, node.id] + ); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); + const setCollapsed = useCallback( + (value: boolean) => { + navigationPanelService.setCollapsed(path, value); + }, + [navigationPanelService, path] + ); const [newFolderId, setNewFolderId] = useState(null); const { createPage } = usePageHelper( @@ -575,7 +593,7 @@ const NavigationPanelFolderNodeFolder = ({ target: 'doc', }); setCollapsed(false); - }, [createPage, node]); + }, [createPage, node, setCollapsed]); const handleCreateSubfolder = useCallback(() => { const newFolderId = node.createFolder( @@ -585,7 +603,7 @@ const NavigationPanelFolderNodeFolder = ({ track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' }); setCollapsed(false); setNewFolderId(newFolderId); - }, [node, t]); + }, [node, setCollapsed, t]); const handleAddToFolder = useCallback( (type: 'doc' | 'collection' | 'tag') => { @@ -628,7 +646,7 @@ const NavigationPanelFolderNodeFolder = ({ target: type, }); }, - [children, node, workspaceDialogService] + [children, node, setCollapsed, workspaceDialogService] ); const folderOperations = useMemo(() => { @@ -761,14 +779,17 @@ const NavigationPanelFolderNodeFolder = ({ [t] ); - const handleCollapsedChange = useCallback((collapsed: boolean) => { - if (collapsed) { - setNewFolderId(null); // reset new folder id to clear the renaming state - setCollapsed(true); - } else { - setCollapsed(false); - } - }, []); + const handleCollapsedChange = useCallback( + (collapsed: boolean) => { + if (collapsed) { + setNewFolderId(null); // reset new folder id to clear the renaming state + setCollapsed(true); + } else { + setCollapsed(false); + } + }, + [setCollapsed] + ); return ( ))} diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/tag/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/tag/index.tsx index 6ad920674..264ddb1b0 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/tag/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/tag/index.tsx @@ -4,14 +4,15 @@ import { toast, } from '@affine/component'; import { GlobalContextService } from '@affine/core/modules/global-context'; +import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import type { Tag } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag'; import type { AffineDNDData } from '@affine/core/types/dnd'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; -import { useLiveData, useServices } from '@toeverything/infra'; +import { useLiveData, useService, useServices } from '@toeverything/infra'; import clsx from 'clsx'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { NavigationPanelTreeNode, @@ -31,6 +32,7 @@ export const NavigationPanelTagNode = ({ operations: additionalOperations, dropEffect, canDrop, + parentPath, }: { tagId: string; } & GenericNavigationPanelNode) => { @@ -39,9 +41,20 @@ export const NavigationPanelTagNode = ({ TagService, GlobalContextService, }); + const navigationPanelService = useService(NavigationPanelService); const active = useLiveData(globalContextService.globalContext.tagId.$) === tagId; - const [collapsed, setCollapsed] = useState(true); + const path = useMemo( + () => [...(parentPath ?? []), `tag-${tagId}`], + [parentPath, tagId] + ); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); + const setCollapsed = useCallback( + (value: boolean) => { + navigationPanelService.setCollapsed(path, value); + }, + [navigationPanelService, path] + ); const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId)); const tagColor = useLiveData(tagRecord?.color$); @@ -154,7 +167,7 @@ export const NavigationPanelTagNode = ({ () => ({ openNodeCollapsed: () => setCollapsed(false), }), - [] + [setCollapsed] ) ); @@ -188,7 +201,7 @@ export const NavigationPanelTagNode = ({ dropEffect={handleDropEffectOnTag} data-testid={`navigation-panel-tag-${tagId}`} > - + ); }; @@ -198,7 +211,13 @@ export const NavigationPanelTagNode = ({ * so we split the tag node children into a separate component, * so it won't be rendered when the tag node is collapsed. */ -export const NavigationPanelTagNodeDocs = ({ tag }: { tag: Tag }) => { +export const NavigationPanelTagNodeDocs = ({ + tag, + path, +}: { + tag: Tag; + path: string[]; +}) => { const tagDocIds = useLiveData(tag.pageIds$); return tagDocIds.map(docId => ( @@ -209,6 +228,7 @@ export const NavigationPanelTagNodeDocs = ({ tag }: { tag: Tag }) => { location={{ at: 'navigation-panel:tags:docs', }} + parentPath={path} /> )); }; diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/types.ts b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/types.ts index 4ec9b5090..05de76479 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/nodes/types.ts +++ b/packages/frontend/core/src/desktop/components/navigation-panel/nodes/types.ts @@ -46,4 +46,9 @@ export interface GenericNavigationPanelNode { * The drop effect to be used when an element is dropped over the node. */ dropEffect?: NavigationPanelTreeNodeDropEffect; + /** + * The path segments to the parent node in the navigation tree. + * Used to persist the node's collapsed/expanded state in cache storage. + */ + parentPath: string[]; } diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/sections/collections/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/sections/collections/index.tsx index e10c1ac8c..a1b809d97 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/sections/collections/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/sections/collections/index.tsx @@ -6,7 +6,7 @@ import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { AddCollectionIcon } from '@blocksuite/icons/rc'; import { useLiveData, useServices } from '@toeverything/infra'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { NavigationPanelCollectionNode } from '../../nodes/collection'; @@ -22,10 +22,9 @@ export const NavigationPanelCollections = () => { WorkbenchService, NavigationPanelService, }); - const navigationPanelSection = navigationPanelService.sections.collections; const collections = useLiveData(collectionService.collections$); const { openPromptModal } = usePromptModal(); - + const path = useMemo(() => ['collections'], []); const handleCreateCollection = useCallback(() => { openPromptModal({ title: t['com.affine.editCollection.saveCollection'](), @@ -49,20 +48,21 @@ export const NavigationPanelCollections = () => { type: 'collection', }); workbenchService.workbench.openCollection(id); - navigationPanelSection.setCollapsed(false); + navigationPanelService.setCollapsed(path, false); }, }); }, [ collectionService, - navigationPanelSection, + navigationPanelService, openPromptModal, + path, t, workbenchService.workbench, ]); return ( { location={{ at: 'navigation-panel:collection:list', }} + parentPath={path} /> ))} diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/sections/favorites/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/sections/favorites/index.tsx index cb6bfcd6a..5272e4a5f 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/sections/favorites/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/sections/favorites/index.tsx @@ -17,7 +17,7 @@ import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { PlusIcon } from '@blocksuite/icons/rc'; import { useLiveData, useServices } from '@toeverything/infra'; -import { type MouseEventHandler, useCallback } from 'react'; +import { type MouseEventHandler, useCallback, useMemo } from 'react'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { NavigationPanelCollectionNode } from '../../nodes/collection'; @@ -41,7 +41,7 @@ export const NavigationPanelFavorites = () => { NavigationPanelService, }); - const navigationPanelSection = navigationPanelService.sections.favorites; + const path = useMemo(() => ['favorites'], []); const favorites = useLiveData(favoriteService.favoriteList.sortedList$); @@ -71,10 +71,10 @@ export const NavigationPanelFavorites = () => { track.$.navigationPanel.favorites.drop({ type: data.source.data.entity.type, }); - navigationPanelSection.setCollapsed(false); + navigationPanelService.setCollapsed(path, false); } }, - [navigationPanelSection, favoriteService.favoriteList] + [navigationPanelService, favoriteService.favoriteList, path] ); const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback( @@ -85,9 +85,9 @@ export const NavigationPanelFavorites = () => { newDoc.id, favoriteService.favoriteList.indexAt('before') ); - navigationPanelSection.setCollapsed(false); + navigationPanelService.setCollapsed(path, false); }, - [createPage, navigationPanelSection, favoriteService.favoriteList] + [createPage, navigationPanelService, favoriteService.favoriteList, path] ); const handleOnChildrenDrop = useCallback( @@ -162,7 +162,7 @@ export const NavigationPanelFavorites = () => { return ( { key={favorite.id} favorite={favorite} onDrop={handleOnChildrenDrop} + parentPath={path} /> ))} @@ -215,11 +216,13 @@ const childLocation = { const NavigationPanelFavoriteNode = ({ favorite, onDrop, + parentPath, }: { favorite: { id: string; type: FavoriteSupportTypeUnion; }; + parentPath: string[]; onDrop: ( favorite: { id: string; @@ -242,6 +245,7 @@ const NavigationPanelFavoriteNode = ({ onDrop={handleOnChildrenDrop} dropEffect={favoriteChildrenDropEffect} canDrop={favoriteChildrenCanDrop} + parentPath={parentPath} /> ) : favorite.type === 'tag' ? ( ) : favorite.type === 'folder' ? ( ) : ( ); }; diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/sections/migration-favorites/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/sections/migration-favorites/index.tsx index 8cc1b2779..118391e4a 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/sections/migration-favorites/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/sections/migration-favorites/index.tsx @@ -5,7 +5,7 @@ import { Trans, useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { BroomIcon, HelpIcon } from '@blocksuite/icons/rc'; import { useLiveData, useServices } from '@toeverything/infra'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { NavigationPanelCollectionNode } from '../../nodes/collection'; @@ -25,6 +25,7 @@ export const NavigationPanelMigrationFavorites = () => { const trashDocs = useLiveData(docsService.list.trashDocs$); const migrated = useLiveData(migrationFavoriteItemsAdapter.migrated$); const { openConfirmModal } = useConfirmModal(); + const path = useMemo(() => ['migration-favorites'], []); const favorites = useLiveData( migrationFavoriteItemsAdapter.favorites$.map(favs => { @@ -99,7 +100,7 @@ export const NavigationPanelMigrationFavorites = () => { return ( { ))} @@ -138,11 +140,13 @@ const childLocation = { }; const NavigationPanelMigrationFavoriteNode = ({ favorite, + parentPath, }: { favorite: { id: string; type: 'collection' | 'doc'; }; + parentPath: string[]; }) => { return favorite.type === 'doc' ? ( ) : ( ); }; diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/sections/organize/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/sections/organize/index.tsx index f9c8dea06..24e3bd114 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/sections/organize/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/sections/organize/index.tsx @@ -27,10 +27,9 @@ export const NavigationPanelOrganize = () => { OrganizeService, NavigationPanelService, }); - const navigationPanelSection = navigationPanelService.sections.organize; - const collapsed = useLiveData(navigationPanelSection.collapsed$); + const path = useMemo(() => ['organize'], []); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); const [newFolderId, setNewFolderId] = useState(null); - const t = useI18n(); const folderTree = organizeService.folderTree; @@ -46,9 +45,9 @@ export const NavigationPanelOrganize = () => { ); track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' }); setNewFolderId(newFolderId); - navigationPanelSection.setCollapsed(false); + navigationPanelService.setCollapsed(path, false); return newFolderId; - }, [navigationPanelSection, rootFolder]); + }, [navigationPanelService, path, rootFolder]); const handleOnChildrenDrop = useCallback( (data: DropTargetDropEvent, node?: FolderNode) => { @@ -105,7 +104,7 @@ export const NavigationPanelOrganize = () => { return ( { at: 'navigation-panel:organize:folder-node', nodeId: child.id as string, }} + parentPath={path} /> ))} diff --git a/packages/frontend/core/src/desktop/components/navigation-panel/sections/tags/index.tsx b/packages/frontend/core/src/desktop/components/navigation-panel/sections/tags/index.tsx index 232506dd4..3ee2549ba 100644 --- a/packages/frontend/core/src/desktop/components/navigation-panel/sections/tags/index.tsx +++ b/packages/frontend/core/src/desktop/components/navigation-panel/sections/tags/index.tsx @@ -5,7 +5,7 @@ import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { AddTagIcon } from '@blocksuite/icons/rc'; import { useLiveData, useServices } from '@toeverything/infra'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { NavigationPanelTagNode } from '../../nodes/tag'; @@ -19,8 +19,8 @@ export const NavigationPanelTags = () => { TagService, NavigationPanelService, }); - const navigationPanelSection = navigationPanelService.sections.tags; - const collapsed = useLiveData(navigationPanelSection.collapsed$); + const path = useMemo(() => ['tags'], []); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); const [creating, setCreating] = useState(false); const tags = useLiveData(tagService.tagList.tags$); @@ -30,9 +30,9 @@ export const NavigationPanelTags = () => { (name: string) => { tagService.tagList.createTag(name, tagService.randomTagColor()); track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' }); - navigationPanelSection.setCollapsed(false); + navigationPanelService.setCollapsed(path, false); }, - [navigationPanelSection, tagService] + [navigationPanelService, path, tagService] ); useEffect(() => { @@ -45,7 +45,7 @@ export const NavigationPanelTags = () => { return ( { location={{ at: 'navigation-panel:tags:list', }} + parentPath={path} /> ))} diff --git a/packages/frontend/core/src/mobile/components/navigation/layouts/collapsible-section.tsx b/packages/frontend/core/src/mobile/components/navigation/layouts/collapsible-section.tsx index 2fd3fc9e6..ad0efed80 100644 --- a/packages/frontend/core/src/mobile/components/navigation/layouts/collapsible-section.tsx +++ b/packages/frontend/core/src/mobile/components/navigation/layouts/collapsible-section.tsx @@ -1,7 +1,4 @@ -import { - type CollapsibleSectionName, - NavigationPanelService, -} from '@affine/core/modules/navigation-panel'; +import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import { ToggleRightIcon } from '@blocksuite/icons/rc'; import * as Collapsible from '@radix-ui/react-collapsible'; import { useLiveData, useService } from '@toeverything/infra'; @@ -22,7 +19,7 @@ import { } from './collapsible-section.css'; interface CollapsibleSectionProps extends HTMLAttributes { - name: CollapsibleSectionName; + path: string[]; title: string; actions?: ReactNode; testId?: string; @@ -76,7 +73,7 @@ const CollapsibleSectionTrigger = forwardRef< }); export const CollapsibleSection = ({ - name, + path, title, actions, testId, @@ -86,12 +83,12 @@ export const CollapsibleSection = ({ children, ...attrs }: CollapsibleSectionProps) => { - const section = useService(NavigationPanelService).sections[name]; - const collapsed = useLiveData(section.collapsed$); + const navigationPanelService = useService(NavigationPanelService); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); const setCollapsed = useCallback( - (v: boolean) => section.setCollapsed(v), - [section] + (v: boolean) => navigationPanelService.setCollapsed(path, v), + [navigationPanelService, path] ); return ( diff --git a/packages/frontend/core/src/mobile/components/navigation/nodes/collection/index.tsx b/packages/frontend/core/src/mobile/components/navigation/nodes/collection/index.tsx index bf2ad5904..01c8cf8fc 100644 --- a/packages/frontend/core/src/mobile/components/navigation/nodes/collection/index.tsx +++ b/packages/frontend/core/src/mobile/components/navigation/nodes/collection/index.tsx @@ -6,11 +6,12 @@ import { } from '@affine/core/modules/collection'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { GlobalContextService } from '@affine/core/modules/global-context'; +import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import { ShareDocsListService } from '@affine/core/modules/share-doc'; import { useI18n } from '@affine/i18n'; import track from '@affine/track'; import { FilterMinusIcon, ViewLayersIcon } from '@blocksuite/icons/rc'; -import { useLiveData, useServices } from '@toeverything/infra'; +import { useLiveData, useService, useServices } from '@toeverything/infra'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { AddItemPlaceholder } from '../../layouts/add-item-placeholder'; @@ -26,9 +27,11 @@ const CollectionIcon = () => ; export const NavigationPanelCollectionNode = ({ collectionId, operations: additionalOperations, + parentPath, }: { collectionId: string; operations?: NodeOperation[]; + parentPath: string[]; }) => { const t = useI18n(); const { globalContextService, collectionService, workspaceDialogService } = @@ -37,17 +40,28 @@ export const NavigationPanelCollectionNode = ({ CollectionService, WorkspaceDialogService, }); + const navigationPanelService = useService(NavigationPanelService); const active = useLiveData(globalContextService.globalContext.collectionId.$) === collectionId; - const [collapsed, setCollapsed] = useState(true); + const path = useMemo( + () => [...parentPath, `collection-${collectionId}`], + [parentPath, collectionId] + ); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); + const setCollapsed = useCallback( + (value: boolean) => { + navigationPanelService.setCollapsed(path, value); + }, + [navigationPanelService, path] + ); const collection = useLiveData(collectionService.collection$(collectionId)); const name = useLiveData(collection?.name$); const handleOpenCollapsed = useCallback(() => { setCollapsed(false); - }, []); + }, [setCollapsed]); const handleEditCollection = useCallback(() => { if (!collection) { @@ -95,6 +109,7 @@ export const NavigationPanelCollectionNode = ({ ); @@ -103,9 +118,11 @@ export const NavigationPanelCollectionNode = ({ const NavigationPanelCollectionNodeChildren = ({ collection, onAddDoc, + path, }: { collection: Collection; onAddDoc?: () => void; + path: string[]; }) => { const t = useI18n(); const { shareDocsListService, collectionService } = useServices({ @@ -147,6 +164,7 @@ const NavigationPanelCollectionNodeChildren = ({ { const t = useI18n(); const { @@ -48,9 +51,20 @@ export const NavigationPanelDocNode = ({ DocDisplayMetaService, FeatureFlagService, }); + const navigationPanelService = useService(NavigationPanelService); const active = useLiveData(globalContextService.globalContext.docId.$) === docId; - const [collapsed, setCollapsed] = useState(true); + const path = useMemo( + () => [...parentPath, `doc-${docId}`], + [parentPath, docId] + ); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); + const setCollapsed = useCallback( + (value: boolean) => { + navigationPanelService.setCollapsed(path, value); + }, + [navigationPanelService, path] + ); const docRecord = useLiveData(docsService.list.doc$(docId)); const DocIcon = useLiveData( @@ -103,7 +117,7 @@ export const NavigationPanelDocNode = ({ openInfoModal: () => workspaceDialogService.open('doc-info', { docId }), openNodeCollapsed: () => setCollapsed(false), }), - [docId, workspaceDialogService] + [docId, setCollapsed, workspaceDialogService] ); const operations = useNavigationPanelDocNodeOperationsMenu(docId, option); const { handleAddLinkedPage } = useNavigationPanelDocNodeOperations( @@ -150,6 +164,7 @@ export const NavigationPanelDocNode = ({ key={`${child.docId}-${index}`} docId={child.docId} isLinked + parentPath={path} /> )) : null diff --git a/packages/frontend/core/src/mobile/components/navigation/nodes/folder/index.tsx b/packages/frontend/core/src/mobile/components/navigation/nodes/folder/index.tsx index 778fc0b65..7d70af5aa 100644 --- a/packages/frontend/core/src/mobile/components/navigation/nodes/folder/index.tsx +++ b/packages/frontend/core/src/mobile/components/navigation/nodes/folder/index.tsx @@ -14,6 +14,7 @@ import type { import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import { NavigationPanelService } from '@affine/core/modules/navigation-panel'; import { type FolderNode, OrganizeService, @@ -31,9 +32,9 @@ import { RemoveFolderIcon, TagsIcon, } from '@blocksuite/icons/rc'; -import { useLiveData, useServices } from '@toeverything/infra'; +import { useLiveData, useService, useServices } from '@toeverything/infra'; import { difference } from 'lodash-es'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { AddItemPlaceholder } from '../../layouts/add-item-placeholder'; import { NavigationPanelTreeNode } from '../../tree/node'; @@ -46,11 +47,13 @@ import { FavoriteFolderOperation } from './operations'; export const NavigationPanelFolderNode = ({ nodeId, operations, + parentPath, }: { nodeId: string; operations?: | NodeOperation[] | ((type: string, node: FolderNode) => NodeOperation[]); + parentPath: string[]; }) => { const { organizeService } = useServices({ OrganizeService, @@ -78,24 +81,34 @@ export const NavigationPanelFolderNode = ({ ); } if (!data) return null; if (type === 'doc') { return ( - + ); } else if (type === 'collection') { return ( ); } else if (type === 'tag') { return ( - + ); } @@ -119,9 +132,11 @@ const NavigationPanelFolderIcon: NavigationPanelTreeNodeIcon = ({ const NavigationPanelFolderNodeFolder = ({ node, operations: additionalOperations, + parentPath, }: { node: FolderNode; operations?: NodeOperation[]; + parentPath: string[]; }) => { const t = useI18n(); const { workspaceService, featureFlagService, workspaceDialogService } = @@ -135,7 +150,18 @@ const NavigationPanelFolderNodeFolder = ({ const enableEmojiIcon = useLiveData( featureFlagService.flags.enable_emoji_folder_icon.$ ); - const [collapsed, setCollapsed] = useState(true); + const navigationPanelService = useService(NavigationPanelService); + const path = useMemo( + () => [...parentPath, `folder-${node.id}`], + [parentPath, node.id] + ); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); + const setCollapsed = useCallback( + (value: boolean) => { + navigationPanelService.setCollapsed(path, value); + }, + [navigationPanelService, path] + ); const { createPage } = usePageHelper( workspaceService.workspace.docCollection @@ -171,7 +197,7 @@ const NavigationPanelFolderNodeFolder = ({ target: 'doc', }); setCollapsed(false); - }, [createPage, node]); + }, [createPage, node, setCollapsed]); const handleCreateSubfolder = useCallback( (name: string) => { @@ -179,7 +205,7 @@ const NavigationPanelFolderNodeFolder = ({ track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' }); setCollapsed(false); }, - [node] + [node, setCollapsed] ); const handleAddToFolder = useCallback( @@ -223,7 +249,7 @@ const NavigationPanelFolderNodeFolder = ({ target: type, }); }, - [children, node, workspaceDialogService] + [children, node, setCollapsed, workspaceDialogService] ); const createSubTipRenderer = useCallback( @@ -388,13 +414,16 @@ const NavigationPanelFolderNodeFolder = ({ [t] ); - const handleCollapsedChange = useCallback((collapsed: boolean) => { - if (collapsed) { - setCollapsed(true); - } else { - setCollapsed(false); - } - }, []); + const handleCollapsedChange = useCallback( + (collapsed: boolean) => { + if (collapsed) { + setCollapsed(true); + } else { + setCollapsed(false); + } + }, + [setCollapsed] + ); return ( ))} { const t = useI18n(); const { tagService, globalContextService } = useServices({ TagService, GlobalContextService, }); + const navigationPanelService = useService(NavigationPanelService); const active = useLiveData(globalContextService.globalContext.tagId.$) === tagId; - const [collapsed, setCollapsed] = useState(true); + const path = useMemo( + () => [...parentPath, `tag-${tagId}`], + [parentPath, tagId] + ); + const collapsed = useLiveData(navigationPanelService.collapsed$(path)); + const setCollapsed = useCallback( + (value: boolean) => { + navigationPanelService.setCollapsed(path, value); + }, + [navigationPanelService, path] + ); const tagRecord = useLiveData(tagService.tagList.tagByTagId$(tagId)); const tagColor = useLiveData(tagRecord?.color$); @@ -57,7 +71,7 @@ export const NavigationPanelTagNode = ({ () => ({ openNodeCollapsed: () => setCollapsed(false), }), - [] + [setCollapsed] ); const operations = useNavigationPanelTagNodeOperationsMenu(tagId, option); const { handleNewDoc } = useNavigationPanelTagNodeOperations(tagId, option); @@ -86,7 +100,11 @@ export const NavigationPanelTagNode = ({ aria-label={tagName} data-role="navigation-panel-tag" > - + ); }; @@ -99,9 +117,11 @@ export const NavigationPanelTagNode = ({ export const NavigationPanelTagNodeDocs = ({ tag, onNewDoc, + path, }: { tag: Tag; onNewDoc?: () => void; + path: string[]; }) => { const t = useI18n(); const tagDocIds = useLiveData(tag.pageIds$); @@ -109,7 +129,7 @@ export const NavigationPanelTagNodeDocs = ({ return ( <> {tagDocIds.map(docId => ( - + ))} diff --git a/packages/frontend/core/src/mobile/components/navigation/sections/collections/index.tsx b/packages/frontend/core/src/mobile/components/navigation/sections/collections/index.tsx index 436fd3e79..b00bddfb6 100644 --- a/packages/frontend/core/src/mobile/components/navigation/sections/collections/index.tsx +++ b/packages/frontend/core/src/mobile/components/navigation/sections/collections/index.tsx @@ -7,7 +7,7 @@ import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { AddCollectionIcon } from '@blocksuite/icons/rc'; import { useLiveData, useServices } from '@toeverything/infra'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { AddItemPlaceholder } from '../../layouts/add-item-placeholder'; import { CollapsibleSection } from '../../layouts/collapsible-section'; @@ -22,7 +22,7 @@ export const NavigationPanelCollections = () => { WorkbenchService, NavigationPanelService, }); - const navigationPanelSection = navigationPanelService.sections.collections; + const path = useMemo(() => ['collections'], []); const collectionMetas = useLiveData(collectionService.collectionMetas$); const { openPromptModal } = usePromptModal(); @@ -49,12 +49,13 @@ export const NavigationPanelCollections = () => { type: 'collection', }); workbenchService.workbench.openCollection(id); - navigationPanelSection.setCollapsed(false); + navigationPanelService.setCollapsed(path, false); }, }); }, [ collectionService, - navigationPanelSection, + navigationPanelService, + path, openPromptModal, t, workbenchService.workbench, @@ -62,7 +63,7 @@ export const NavigationPanelCollections = () => { return ( @@ -71,6 +72,7 @@ export const NavigationPanelCollections = () => { ))} { }); const t = useI18n(); - const navigationPanelSection = navigationPanelService.sections.favorites; + const path = useMemo(() => ['favorites'], []); const favorites = useLiveData(favoriteService.favoriteList.sortedList$); const isLoading = useLiveData(favoriteService.favoriteList.isLoading$); const { createPage } = usePageHelper( @@ -38,19 +38,23 @@ export const NavigationPanelFavorites = () => { newDoc.id, favoriteService.favoriteList.indexAt('before') ); - navigationPanelSection.setCollapsed(false); - }, [createPage, navigationPanelSection, favoriteService.favoriteList]); + navigationPanelService.setCollapsed(path, false); + }, [createPage, favoriteService.favoriteList, navigationPanelService, path]); return ( {favorites.map(favorite => ( - + ))} { export const FavoriteNode = ({ favorite, + parentPath, }: { favorite: { id: string; type: FavoriteSupportTypeUnion; }; + parentPath: string[]; }) => { return favorite.type === 'doc' ? ( - + ) : favorite.type === 'tag' ? ( - + ) : favorite.type === 'folder' ? ( - + ) : ( - + ); }; diff --git a/packages/frontend/core/src/mobile/components/navigation/sections/organize/index.tsx b/packages/frontend/core/src/mobile/components/navigation/sections/organize/index.tsx index 579581d22..6cb57dc72 100644 --- a/packages/frontend/core/src/mobile/components/navigation/sections/organize/index.tsx +++ b/packages/frontend/core/src/mobile/components/navigation/sections/organize/index.tsx @@ -6,7 +6,7 @@ import { useI18n } from '@affine/i18n'; import track from '@affine/track'; import { AddOrganizeIcon } from '@blocksuite/icons/rc'; import { useLiveData, useServices } from '@toeverything/infra'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { AddItemPlaceholder } from '../../layouts/add-item-placeholder'; import { CollapsibleSection } from '../../layouts/collapsible-section'; @@ -18,7 +18,7 @@ export const NavigationPanelOrganize = () => { OrganizeService, NavigationPanelService, }); - const navigationPanelSection = navigationPanelService.sections.organize; + const path = useMemo(() => ['organize'], []); const [openNewFolderDialog, setOpenNewFolderDialog] = useState(false); const t = useI18n(); @@ -36,15 +36,15 @@ export const NavigationPanelOrganize = () => { rootFolder.indexAt('before') ); track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' }); - navigationPanelSection.setCollapsed(false); + navigationPanelService.setCollapsed(path, false); return newFolderId; }, - [navigationPanelSection, rootFolder] + [navigationPanelService, path, rootFolder] ); return ( {/* TODO(@CatsJuice): Organize loading UI */} @@ -53,6 +53,7 @@ export const NavigationPanelOrganize = () => { ))} { TagService, NavigationPanelService, }); - const navigationPanelSection = navigationPanelService.sections.tags; + const path = useMemo(() => ['tags'], []); const tags = useLiveData(tagService.tagList.tags$); const [showNewTagDialog, setShowNewTagDialog] = useState(false); @@ -36,19 +36,23 @@ export const NavigationPanelTags = () => { setShowNewTagDialog(false); tagService.tagList.createTag(name, color); track.$.navigationPanel.organize.createOrganizeItem({ type: 'tag' }); - navigationPanelSection.setCollapsed(false); + navigationPanelService.setCollapsed(path, false); }, - [navigationPanelSection, tagService] + [navigationPanelService, path, tagService] ); return ( {tags.map(tag => ( - + ))} } diff --git a/packages/frontend/core/src/mobile/views/recent-docs/index.tsx b/packages/frontend/core/src/mobile/views/recent-docs/index.tsx index b2eb46bbb..87e07f003 100644 --- a/packages/frontend/core/src/mobile/views/recent-docs/index.tsx +++ b/packages/frontend/core/src/mobile/views/recent-docs/index.tsx @@ -24,7 +24,7 @@ export const RecentDocs = ({ max = 5 }: { max?: number }) => { return ( = { - recent: true, - favorites: false, - organize: false, - collections: true, - tags: true, - favoritesOld: true, - migrationFavorites: true, - others: false, -}; - -export class NavigationPanelSection extends Entity<{ - name: CollapsibleSectionName; -}> { - name: CollapsibleSectionName = this.props.name; - key = `explorer.section.${this.name}`; - defaultValue = DEFAULT_COLLAPSABLE_STATE[this.name]; - - constructor(private readonly globalCache: GlobalCache) { - super(); - } - - collapsed$ = LiveData.from( - this.globalCache - .watch(this.key) - .pipe(map(v => v ?? this.defaultValue)), - this.defaultValue - ); - - setCollapsed(collapsed: boolean) { - this.globalCache.set(this.key, collapsed); - } -} diff --git a/packages/frontend/core/src/modules/navigation-panel/index.ts b/packages/frontend/core/src/modules/navigation-panel/index.ts index aabe76403..06422e59a 100644 --- a/packages/frontend/core/src/modules/navigation-panel/index.ts +++ b/packages/frontend/core/src/modules/navigation-panel/index.ts @@ -1,15 +1,12 @@ import { type Framework } from '@toeverything/infra'; import { GlobalCache } from '../storage'; -import { WorkspaceScope } from '../workspace'; -import { NavigationPanelSection } from './entities/navigation-panel-section'; +import { WorkspaceScope, WorkspaceService } from '../workspace'; import { NavigationPanelService } from './services/navigation-panel'; export { NavigationPanelService } from './services/navigation-panel'; -export type { CollapsibleSectionName } from './types'; export function configureNavigationPanelModule(framework: Framework) { framework .scope(WorkspaceScope) - .service(NavigationPanelService) - .entity(NavigationPanelSection, [GlobalCache]); + .service(NavigationPanelService, [GlobalCache, WorkspaceService]); } diff --git a/packages/frontend/core/src/modules/navigation-panel/services/navigation-panel.ts b/packages/frontend/core/src/modules/navigation-panel/services/navigation-panel.ts index b7f3f1c12..b3a035b3c 100644 --- a/packages/frontend/core/src/modules/navigation-panel/services/navigation-panel.ts +++ b/packages/frontend/core/src/modules/navigation-panel/services/navigation-panel.ts @@ -1,25 +1,47 @@ -import { Service } from '@toeverything/infra'; +import { LiveData, Service } from '@toeverything/infra'; -import { NavigationPanelSection } from '../entities/navigation-panel-section'; -import type { CollapsibleSectionName } from '../types'; +import type { GlobalCache } from '../../storage/providers/global'; +import type { WorkspaceService } from '../../workspace'; -const allSectionName: Array = [ - 'recent', // mobile only - 'favorites', - 'organize', - 'collections', - 'tags', - 'favoritesOld', - 'migrationFavorites', - 'others', -]; +const DEFAULT_COLLAPSABLE_STATE: Record = { + recent: true, + favorites: false, + organize: false, + collections: true, + tags: true, + favoritesOld: true, + migrationFavorites: true, + others: false, +}; export class NavigationPanelService extends Service { - readonly sections = allSectionName.reduce( - (prev, name) => - Object.assign(prev, { - [name]: this.framework.createEntity(NavigationPanelSection, { name }), - }), - {} as Record - ); + constructor( + private readonly globalCache: GlobalCache, + private readonly workspaceService: WorkspaceService + ) { + super(); + } + + private readonly collapsedCache = new Map>(); + + collapsed$(path: string[]) { + const pathKey = path.join(':'); + const key = `navigation:${this.workspaceService.workspace.id}:${pathKey}`; + const cached$ = this.collapsedCache.get(key); + if (!cached$) { + const liveData$ = LiveData.from( + this.globalCache.watch(key), + undefined + ).map(v => v ?? DEFAULT_COLLAPSABLE_STATE[pathKey] ?? true); + this.collapsedCache.set(key, liveData$); + return liveData$; + } + return cached$; + } + + setCollapsed(path: string[], collapsed: boolean) { + const pathKey = path.join(':'); + const key = `navigation:${this.workspaceService.workspace.id}:${pathKey}`; + this.globalCache.set(key, collapsed); + } } diff --git a/packages/frontend/core/src/modules/navigation-panel/types.ts b/packages/frontend/core/src/modules/navigation-panel/types.ts deleted file mode 100644 index b2423fb67..000000000 --- a/packages/frontend/core/src/modules/navigation-panel/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type CollapsibleSectionName = - | 'recent' - | 'collections' - | 'favorites' - | 'tags' - | 'organize' - | 'favoritesOld' - | 'migrationFavorites' - | 'others';