From 283f0cd2630853dbc997e9276affeaea6a16000c Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Fri, 7 Jul 2023 22:15:27 +0800 Subject: [PATCH] refactor: lazy load workspaces (#3091) --- apps/storybook/src/stories/card.stories.tsx | 14 +- .../src/stories/workspace-avatar.stories.tsx | 23 +-- apps/web/src/adapters/local/index.tsx | 21 +-- apps/web/src/atoms/__tests__/atom.spec.ts | 83 --------- apps/web/src/atoms/index.ts | 6 +- apps/web/src/atoms/root.ts | 160 ------------------ .../new-workspace-setting-detail/index.tsx | 7 +- .../new-workspace-setting-detail/profile.tsx | 5 +- .../new-workspace-setting-detail/publish.tsx | 15 +- .../components/affine/setting-modal/index.tsx | 49 ++---- .../setting-modal/setting-sidebar/index.tsx | 25 +-- .../setting-modal/workspace-setting/index.tsx | 14 +- .../web/src/components/page-detail-editor.tsx | 21 +-- .../src/components/pure/help-island/index.tsx | 2 +- .../pure/quick-search-modal/config.ts | 16 +- .../pure/workspace-list-modal/index.tsx | 7 +- .../WorkspaceSelector/workspace-selector.tsx | 2 +- .../src/components/root-app-sidebar/index.tsx | 11 -- apps/web/src/components/workspace-header.tsx | 6 +- apps/web/src/hooks/__tests__/index.spec.tsx | 101 +---------- .../hooks/current/use-current-workspace.ts | 61 ++++++- apps/web/src/hooks/use-get-page-info.ts | 4 +- apps/web/src/hooks/use-workspace.ts | 36 ++++ apps/web/src/hooks/use-workspaces.ts | 25 ++- apps/web/src/layouts/workspace-layout.tsx | 68 +++----- apps/web/src/pages/404.tsx | 11 +- apps/web/src/pages/index.tsx | 70 ++++---- .../workspace/[workspaceId]/[pageId].tsx | 15 +- .../src/pages/workspace/[workspaceId]/all.tsx | 2 +- .../pages/workspace/[workspaceId]/shared.tsx | 2 +- .../pages/workspace/[workspaceId]/trash.tsx | 2 +- apps/web/src/providers/modal-provider.tsx | 26 ++- apps/web/src/shared/index.ts | 10 +- .../src/components/app-sidebar/index.tsx | 6 +- .../components/card/workspace-card/index.tsx | 53 +++--- .../src/components/workspace-avatar/index.tsx | 19 +-- .../src/components/workspace-list/index.tsx | 29 ++-- packages/component/src/ui/tooltip/tooltip.tsx | 16 +- packages/env/src/workspace.ts | 23 ++- .../src/local/__tests__/crud.spec.ts | 14 +- packages/workspace/src/utils.ts | 101 +++++------ tests/parallels/blocksuite/editor.spec.ts | 9 +- .../drag-page-to-trash-folder.spec.ts | 1 + .../local-first-workspace-list.spec.ts | 1 + tests/parallels/router.spec.ts | 4 +- 45 files changed, 446 insertions(+), 750 deletions(-) delete mode 100644 apps/web/src/atoms/root.ts create mode 100644 apps/web/src/hooks/use-workspace.ts diff --git a/apps/storybook/src/stories/card.stories.tsx b/apps/storybook/src/stories/card.stories.tsx index 83d12ff6a5..ac03299ade 100644 --- a/apps/storybook/src/stories/card.stories.tsx +++ b/apps/storybook/src/stories/card.stories.tsx @@ -2,27 +2,27 @@ import { toast } from '@affine/component'; import { BlockCard } from '@affine/component/card/block-card'; import { WorkspaceCard } from '@affine/component/card/workspace-card'; import { WorkspaceFlavour } from '@affine/env/workspace'; +import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; -import { Workspace } from '@blocksuite/store'; export default { title: 'AFFiNE/Card', component: WorkspaceCard, }; -const blockSuiteWorkspace = new Workspace({ - id: 'blocksuite-local', -}); +const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( + 'blocksuite-local', + WorkspaceFlavour.LOCAL +); blockSuiteWorkspace.meta.setName('Hello World'); export const AffineWorkspaceCard = () => { return ( {}} onSettingClick={() => {}} diff --git a/apps/storybook/src/stories/workspace-avatar.stories.tsx b/apps/storybook/src/stories/workspace-avatar.stories.tsx index 3a60e0002e..7e163e49e0 100644 --- a/apps/storybook/src/stories/workspace-avatar.stories.tsx +++ b/apps/storybook/src/stories/workspace-avatar.stories.tsx @@ -1,6 +1,5 @@ import type { WorkspaceAvatarProps } from '@affine/component/workspace-avatar'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; -import { WorkspaceFlavour } from '@affine/env/workspace'; import { Workspace } from '@blocksuite/store'; import type { Meta, StoryFn } from '@storybook/react'; @@ -25,16 +24,7 @@ const basicBlockSuiteWorkspace = new Workspace({ basicBlockSuiteWorkspace.meta.setName('Hello World'); export const Basic: StoryFn = props => { - return ( - - ); + return ; }; Basic.args = { @@ -60,16 +50,7 @@ fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url)) }); export const BlobExample: StoryFn = props => { - return ( - - ); + return ; }; BlobExample.args = { diff --git a/apps/web/src/adapters/local/index.tsx b/apps/web/src/adapters/local/index.tsx index ed91d19e92..03138af9cf 100644 --- a/apps/web/src/adapters/local/index.tsx +++ b/apps/web/src/adapters/local/index.tsx @@ -17,7 +17,10 @@ import { saveWorkspaceToLocalStorage, } from '@affine/workspace/local/crud'; import { createIndexedDBDownloadProvider } from '@affine/workspace/providers'; -import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; +import { + createEmptyBlockSuiteWorkspace, + useStaticBlockSuiteWorkspace, +} from '@affine/workspace/utils'; import { nanoid } from '@blocksuite/store'; import { @@ -75,13 +78,11 @@ export const LocalAdapter: WorkspaceAdapter = { Provider: ({ children }) => { return <>{children}; }, - PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => { - const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); + PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => { + const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId); + const page = workspace.getPage(currentPageId); if (!page) { - throw new PageNotFoundError( - currentWorkspace.blockSuiteWorkspace, - currentPageId - ); + throw new PageNotFoundError(workspace, currentPageId); } return ( <> @@ -89,7 +90,7 @@ export const LocalAdapter: WorkspaceAdapter = { pageId={currentPageId} onInit={initEmptyPage} onLoad={onLoadEditor} - workspace={currentWorkspace} + workspace={workspace} /> ); @@ -105,14 +106,14 @@ export const LocalAdapter: WorkspaceAdapter = { ); }, NewSettingsDetail: ({ - currentWorkspace, + currentWorkspaceId, onDeleteWorkspace, onTransformWorkspace, }) => { return ( ); diff --git a/apps/web/src/atoms/__tests__/atom.spec.ts b/apps/web/src/atoms/__tests__/atom.spec.ts index 2b1ff0f75f..0c1873fcfd 100644 --- a/apps/web/src/atoms/__tests__/atom.spec.ts +++ b/apps/web/src/atoms/__tests__/atom.spec.ts @@ -3,34 +3,14 @@ */ import 'fake-indexeddb/auto'; -import { initEmptyPage } from '@affine/env/blocksuite'; -import type { - LocalIndexedDBBackgroundProvider, - WorkspaceAdapter, -} from '@affine/env/workspace'; -import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace'; -import { - rootCurrentWorkspaceIdAtom, - rootWorkspacesMetadataAtom, - workspaceAdaptersAtom, -} from '@affine/workspace/atom'; -import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers'; -import { - _cleanupBlockSuiteWorkspaceCache, - createEmptyBlockSuiteWorkspace, -} from '@affine/workspace/utils'; -import type { ParagraphBlockModel } from '@blocksuite/blocks/models'; -import type { Page } from '@blocksuite/store'; import { createStore } from 'jotai'; import { describe, expect, test } from 'vitest'; -import { WorkspaceAdapters } from '../../adapters/workspace'; import { pageSettingFamily, pageSettingsAtom, recentPageSettingsAtom, } from '../index'; -import { rootCurrentWorkspaceAtom } from '../root'; describe('page mode atom', () => { test('basic', () => { @@ -63,66 +43,3 @@ describe('page mode atom', () => { ]); }); }); - -describe('currentWorkspace atom', () => { - test('should be defined', async () => { - const store = createStore(); - store.set( - workspaceAdaptersAtom, - WorkspaceAdapters as Record< - WorkspaceFlavour, - WorkspaceAdapter - > - ); - let id: string; - { - const workspace = createEmptyBlockSuiteWorkspace( - 'test', - WorkspaceFlavour.LOCAL - ); - const page = workspace.createPage({ id: 'page0' }); - await initEmptyPage(page); - const frameId = page.getBlockByFlavour('affine:note').at(0)?.id as string; - id = page.addBlock( - 'affine:paragraph', - { - text: new page.Text('test 1'), - }, - frameId - ); - const provider = createIndexedDBBackgroundProvider( - workspace.id, - workspace.doc, - { - awareness: workspace.awarenessStore.awareness, - } - ) as LocalIndexedDBBackgroundProvider; - provider.connect(); - await new Promise(resolve => setTimeout(resolve, 1000)); - provider.disconnect(); - const workspaceId = await WorkspaceAdapters[ - WorkspaceFlavour.LOCAL - ].CRUD.create(workspace); - await store.set(rootWorkspacesMetadataAtom, [ - { - id: workspaceId, - flavour: WorkspaceFlavour.LOCAL, - version: WorkspaceVersion.SubDoc, - }, - ]); - _cleanupBlockSuiteWorkspaceCache(); - } - store.set( - rootCurrentWorkspaceIdAtom, - (await store.get(rootWorkspacesMetadataAtom))[0].id - ); - const workspace = await store.get(rootCurrentWorkspaceAtom); - expect(workspace).toBeDefined(); - const page = workspace.blockSuiteWorkspace.getPage('page0') as Page; - await page.waitForLoaded(); - expect(page).not.toBeNull(); - const paragraphBlock = page.getBlockById(id) as ParagraphBlockModel; - expect(paragraphBlock).not.toBeNull(); - expect(paragraphBlock.text.toString()).toBe('test 1'); - }); -}); diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index 7ad00af7e5..669ef17dc0 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -10,20 +10,18 @@ export const openCreateWorkspaceModalAtom = atom(false); export const openQuickSearchModalAtom = atom(false); export const openOnboardingModalAtom = atom(false); -export type SettingAtom = Pick & { +export type SettingAtom = Pick & { open: boolean; }; export const openSettingModalAtom = atom({ activeTab: 'appearance', - workspace: null, + workspaceId: null, open: false, }); export const openDisableCloudAlertModalAtom = atom(false); -export { workspacesAtom } from './root'; - type PageMode = 'page' | 'edgeless'; type PageLocalSetting = { mode: PageMode; diff --git a/apps/web/src/atoms/root.ts b/apps/web/src/atoms/root.ts deleted file mode 100644 index ee13f77641..0000000000 --- a/apps/web/src/atoms/root.ts +++ /dev/null @@ -1,160 +0,0 @@ -//#region async atoms that to load the real workspace data -import { DebugLogger } from '@affine/debug'; -import type { - WorkspaceAdapter, - WorkspaceRegistry, -} from '@affine/env/workspace'; -import type { WorkspaceFlavour } from '@affine/env/workspace'; -import { - rootCurrentWorkspaceIdAtom, - rootWorkspacesMetadataAtom, - workspaceAdaptersAtom, -} from '@affine/workspace/atom'; -import { assertExists } from '@blocksuite/global/utils'; -import type { ActiveDocProvider } from '@blocksuite/store'; -import { atom } from 'jotai'; - -import type { AllWorkspace } from '../shared'; - -const logger = new DebugLogger('web:atoms:root'); - -/** - * Fetch all workspaces from the Plugin CRUD - */ -export const workspacesAtom = atom>( - async (get, { signal }) => { - const WorkspaceAdapters = get(workspaceAdaptersAtom); - const flavours: string[] = Object.values(WorkspaceAdapters).map( - plugin => plugin.flavour - ); - const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom)).filter( - workspace => flavours.includes(workspace.flavour) - ); - if (jotaiWorkspaces.some(meta => !('version' in meta))) { - // wait until all workspaces have migrated to v2 - await new Promise((resolve, reject) => { - signal.addEventListener('abort', reject); - setTimeout(resolve, 1000); - }).catch(() => { - // do nothing - }); - } - const workspaces = await Promise.all( - jotaiWorkspaces.map(workspace => { - const adapter = WorkspaceAdapters[ - workspace.flavour - ] as WorkspaceAdapter; - assertExists(adapter); - const { CRUD } = adapter; - return CRUD.get(workspace.id).then(workspace => { - if (workspace === null) { - console.warn( - 'workspace is null. this should not happen. If you see this error, please report it to the developer.' - ); - } - return workspace; - }); - }) - ).then(workspaces => - workspaces.filter( - (workspace): workspace is WorkspaceRegistry['affine-cloud' | 'local'] => - workspace !== null - ) - ); - const workspaceProviders = workspaces.map(workspace => - workspace.blockSuiteWorkspace.providers.filter( - (provider): provider is ActiveDocProvider => - 'active' in provider && provider.active - ) - ); - const promises: Promise[] = []; - for (const providers of workspaceProviders) { - for (const provider of providers) { - provider.sync(); - promises.push(provider.whenReady); - } - } - // we will wait for all the necessary providers to be ready - await Promise.all(promises); - logger.info('workspaces', workspaces); - return workspaces; - } -); - -/** - * This will throw an error if the workspace is not found, - * should not be used on the root component, - * use `rootCurrentWorkspaceIdAtom` instead - */ -export const rootCurrentWorkspaceAtom = atom>( - async (get, { signal }) => { - const WorkspaceAdapters = get(workspaceAdaptersAtom); - const metadata = await get(rootWorkspacesMetadataAtom); - const targetId = get(rootCurrentWorkspaceIdAtom); - if (targetId === null) { - throw new Error( - 'current workspace id is null. this should not happen. If you see this error, please report it to the developer.' - ); - } - const targetWorkspace = metadata.find(meta => meta.id === targetId); - if (!targetWorkspace) { - throw new Error(`cannot find the workspace with id ${targetId}.`); - } - - if (!('version' in targetWorkspace)) { - // wait until the workspace has migrated to v2 - await new Promise((resolve, reject) => { - signal.addEventListener('abort', reject); - setTimeout(resolve, 1000); - }).catch(() => { - // do nothing - }); - } - - const adapter = WorkspaceAdapters[ - targetWorkspace.flavour - ] as WorkspaceAdapter; - assertExists(adapter); - - const workspace = await adapter.CRUD.get(targetWorkspace.id); - if (!workspace) { - throw new Error( - `cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.` - ); - } - - const providers = workspace.blockSuiteWorkspace.providers.filter( - (provider): provider is ActiveDocProvider => - 'active' in provider && provider.active === true - ); - for (const provider of providers) { - provider.sync(); - // we will wait for the necessary providers to be ready - await provider.whenReady; - } - logger.info('current workspace', workspace); - globalThis.currentWorkspace = workspace; - globalThis.dispatchEvent( - new CustomEvent('affine:workspace:change', { - detail: { id: workspace.id }, - }) - ); - return workspace; - } -); - -declare global { - /** - * @internal debug only - */ - // eslint-disable-next-line no-var - var currentWorkspace: AllWorkspace | undefined; - interface WindowEventMap { - 'affine:workspace:change': CustomEvent<{ id: string }>; - } -} - -// Do not add `rootCurrentWorkspacePageAtom`, this is not needed. -// It can be derived from `rootCurrentWorkspaceAtom` and `rootCurrentPageIdAtom` - -//#endregion diff --git a/apps/web/src/components/affine/new-workspace-setting-detail/index.tsx b/apps/web/src/components/affine/new-workspace-setting-detail/index.tsx index 34e912e25d..82b1860ebd 100644 --- a/apps/web/src/components/affine/new-workspace-setting-detail/index.tsx +++ b/apps/web/src/components/affine/new-workspace-setting-detail/index.tsx @@ -11,7 +11,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import type { FC } from 'react'; -import type { AffineOfficialWorkspace } from '../../../shared'; +import { useWorkspace } from '../../../hooks/use-workspace'; import { DeleteLeaveWorkspace } from './delete-leave-workspace'; import { ExportPanel } from './export'; import { ProfilePanel } from './profile'; @@ -19,7 +19,7 @@ import { PublishPanel } from './publish'; import { StoragePanel } from './storage'; export type WorkspaceSettingDetailProps = { - workspace: AffineOfficialWorkspace; + workspaceId: string; onDeleteWorkspace: (id: string) => Promise; onTransferWorkspace: < From extends WorkspaceFlavour, @@ -32,11 +32,12 @@ export type WorkspaceSettingDetailProps = { }; export const WorkspaceSettingDetail: FC = ({ - workspace, + workspaceId, onDeleteWorkspace, ...props }) => { const t = useAFFiNEI18N(); + const workspace = useWorkspace(workspaceId); const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace); return ( diff --git a/apps/web/src/components/affine/new-workspace-setting-detail/profile.tsx b/apps/web/src/components/affine/new-workspace-setting-detail/profile.tsx index 7d3fd4f3b3..7dbf9071cb 100644 --- a/apps/web/src/components/affine/new-workspace-setting-detail/profile.tsx +++ b/apps/web/src/components/affine/new-workspace-setting-detail/profile.tsx @@ -64,7 +64,10 @@ export const ProfilePanel: FC<{
- + diff --git a/apps/web/src/components/affine/new-workspace-setting-detail/publish.tsx b/apps/web/src/components/affine/new-workspace-setting-detail/publish.tsx index a95332a359..f97a29707f 100644 --- a/apps/web/src/components/affine/new-workspace-setting-detail/publish.tsx +++ b/apps/web/src/components/affine/new-workspace-setting-detail/publish.tsx @@ -18,13 +18,22 @@ import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal'; import type { WorkspaceSettingDetailProps } from './index'; import * as style from './style.css'; -export type PublishPanelProps = WorkspaceSettingDetailProps & { +export type PublishPanelProps = Omit< + WorkspaceSettingDetailProps, + 'workspaceId' +> & { workspace: AffineOfficialWorkspace; }; -export type PublishPanelLocalProps = WorkspaceSettingDetailProps & { +export type PublishPanelLocalProps = Omit< + WorkspaceSettingDetailProps, + 'workspaceId' +> & { workspace: LocalWorkspace; }; -export type PublishPanelAffineProps = WorkspaceSettingDetailProps & { +export type PublishPanelAffineProps = Omit< + WorkspaceSettingDetailProps, + 'workspaceId' +> & { workspace: AffineCloudWorkspace; }; diff --git a/apps/web/src/components/affine/setting-modal/index.tsx b/apps/web/src/components/affine/setting-modal/index.tsx index 967ddcb274..913e44939d 100644 --- a/apps/web/src/components/affine/setting-modal/index.tsx +++ b/apps/web/src/components/affine/setting-modal/index.tsx @@ -4,14 +4,13 @@ import { } from '@affine/component/setting-components'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { ContactWithUsIcon } from '@blocksuite/icons'; -import type { PassiveDocProvider } from '@blocksuite/store'; -import { noop } from 'foxact/noop'; +import { useAtomValue } from 'jotai'; import type React from 'react'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; -import { useWorkspaces } from '../../../hooks/use-workspaces'; import type { AllWorkspace } from '../../../shared'; import { AccountSetting } from './account-setting'; import { @@ -26,70 +25,52 @@ import { WorkspaceSetting } from './workspace-setting'; type ActiveTab = GeneralSettingKeys | 'workspace' | 'account'; export type SettingProps = { activeTab: ActiveTab; - workspace: AllWorkspace | null; + workspaceId: string | null; onSettingClick: (params: { activeTab: ActiveTab; - workspace: AllWorkspace | null; + workspaceId: string | null; }) => void; }; export const SettingModal: React.FC = ({ open, setOpen, activeTab = 'appearance', - workspace = null, + workspaceId = null, onSettingClick, }) => { const t = useAFFiNEI18N(); - const workspaces = useWorkspaces(); + const workspaces = useAtomValue(rootWorkspacesMetadataAtom); const [currentWorkspace] = useCurrentWorkspace(); const generalSettingList = useGeneralSettingList(); const workspaceList = useMemo(() => { return workspaces.filter( ({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC - ) as AllWorkspace[]; + ); }, [workspaces]); const onGeneralSettingClick = useCallback( (key: GeneralSettingKeys) => { onSettingClick({ activeTab: key, - workspace: null, + workspaceId: null, }); }, [onSettingClick] ); const onWorkspaceSettingClick = useCallback( - (workspace: AllWorkspace) => { + (workspaceId: string) => { onSettingClick({ activeTab: 'workspace', - workspace, + workspaceId, }); }, [onSettingClick] ); const onAccountSettingClick = useCallback(() => { - onSettingClick({ activeTab: 'account', workspace: null }); + onSettingClick({ activeTab: 'account', workspaceId: null }); }, [onSettingClick]); - useEffect(() => { - if (workspace && workspace !== currentWorkspace) { - const providers = workspace.blockSuiteWorkspace.providers.filter( - (provider): provider is PassiveDocProvider => - 'passive' in provider && provider.passive - ); - providers.forEach(provider => { - provider.connect(); - }); - return () => { - providers.forEach(provider => { - provider.disconnect(); - }); - }; - } - return noop; - }, [currentWorkspace, workspace]); - return ( = ({ workspaceList={workspaceList} onWorkspaceSettingClick={onWorkspaceSettingClick} selectedGeneralKey={activeTab} - selectedWorkspace={workspace} + selectedWorkspaceId={workspaceId} onAccountSettingClick={onAccountSettingClick} />
- {activeTab === 'workspace' && workspace ? ( - + {activeTab === 'workspace' && workspaceId ? ( + ) : null} {generalSettingList.find(v => v.key === activeTab) ? ( diff --git a/apps/web/src/components/affine/setting-modal/setting-sidebar/index.tsx b/apps/web/src/components/affine/setting-modal/setting-sidebar/index.tsx index a0d0370ce2..76ac4cc0f2 100644 --- a/apps/web/src/components/affine/setting-modal/setting-sidebar/index.tsx +++ b/apps/web/src/components/affine/setting-modal/setting-sidebar/index.tsx @@ -1,6 +1,8 @@ import { UserAvatar } from '@affine/component/user-avatar'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; +import { useStaticBlockSuiteWorkspace } from '@affine/workspace/utils'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import clsx from 'clsx'; @@ -24,17 +26,17 @@ export const SettingSidebar = ({ currentWorkspace, workspaceList, onWorkspaceSettingClick, - selectedWorkspace, + selectedWorkspaceId, selectedGeneralKey, onAccountSettingClick, }: { generalSettingList: GeneralSettingList; onGeneralSettingClick: (key: GeneralSettingKeys) => void; currentWorkspace: AllWorkspace; - workspaceList: AllWorkspace[]; - onWorkspaceSettingClick: (workspace: AllWorkspace) => void; + workspaceList: RootWorkspaceMetadata[]; + onWorkspaceSettingClick: (workspaceId: string) => void; - selectedWorkspace: AllWorkspace | null; + selectedWorkspaceId: string | null; selectedGeneralKey: string | null; onAccountSettingClick: () => void; }) => { @@ -72,12 +74,12 @@ export const SettingSidebar = ({ return ( { - onWorkspaceSettingClick(workspace); + onWorkspaceSettingClick(workspace.id); }} isCurrent={workspace.id === currentWorkspace.id} - isActive={workspace.id === selectedWorkspace?.id} + isActive={workspace.id === selectedWorkspaceId} /> ); })} @@ -107,19 +109,18 @@ export const SettingSidebar = ({ }; const WorkspaceListItem = ({ - workspace, + meta, onClick, isCurrent, isActive, }: { - workspace: AllWorkspace; + meta: RootWorkspaceMetadata; onClick: () => void; isCurrent: boolean; isActive: boolean; }) => { - const [workspaceName] = useBlockSuiteWorkspaceName( - workspace.blockSuiteWorkspace ?? null - ); + const workspace = useStaticBlockSuiteWorkspace(meta.id); + const [workspaceName] = useBlockSuiteWorkspaceName(workspace); return (
{ +export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => { + const workspace = useWorkspace(workspaceId); + usePassiveWorkspaceEffect(workspace.blockSuiteWorkspace); const helper = useAppHelper(); + const { NewSettingsDetail } = getUIAdapter(workspace.flavour); const onDeleteWorkspace = useCallback( @@ -26,7 +26,7 @@ export const WorkspaceSetting = ({ ); diff --git a/apps/web/src/components/page-detail-editor.tsx b/apps/web/src/components/page-detail-editor.tsx index 79a7ac3b17..a99bacc763 100644 --- a/apps/web/src/components/page-detail-editor.tsx +++ b/apps/web/src/components/page-detail-editor.tsx @@ -7,7 +7,7 @@ import { import { rootBlockHubAtom } from '@affine/workspace/atom'; import type { EditorContainer } from '@blocksuite/editor'; import { assertExists } from '@blocksuite/global/utils'; -import type { Page } from '@blocksuite/store'; +import type { Page, Workspace } from '@blocksuite/store'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page'; import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-block-suite-workspace-page-title'; @@ -28,14 +28,13 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { pageSettingFamily } from '../atoms'; import { contentLayoutAtom } from '../atoms/layout'; import { useAppSetting } from '../atoms/settings'; -import type { AffineOfficialWorkspace } from '../shared'; import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor'; import { editor } from './page-detail-editor.css'; import { pluginContainer } from './page-detail-editor.css'; export type PageDetailEditorProps = { isPublic?: boolean; - workspace: AffineOfficialWorkspace; + workspace: Workspace; pageId: string; onInit: (page: Page, editor: Readonly) => void; onLoad?: (page: Page, editor: EditorContainer) => () => void; @@ -53,12 +52,11 @@ const EditorWrapper = memo(function EditorWrapper({ () => Object.values(affinePluginsMap), [affinePluginsMap] ); - const blockSuiteWorkspace = workspace.blockSuiteWorkspace; - const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId); + const page = useBlockSuiteWorkspacePage(workspace, pageId); if (!page) { - throw new PageNotFoundError(blockSuiteWorkspace, pageId); + throw new PageNotFoundError(workspace, pageId); } - const meta = useBlockSuitePageMeta(blockSuiteWorkspace).find( + const meta = useBlockSuitePageMeta(workspace).find( meta => meta.id === pageId ); const pageSettingAtom = pageSettingFamily(pageId); @@ -77,7 +75,7 @@ const EditorWrapper = memo(function EditorWrapper({ className={clsx(editor, { 'full-screen': appSettings?.fullWidthLayout, })} - key={`${workspace.flavour}-${workspace.id}-${pageId}`} + key={`${workspace.id}-${pageId}`} mode={isPublic ? 'page' : currentMode} page={page} onInit={useCallback( @@ -181,12 +179,11 @@ const LayoutPanel = memo(function LayoutPanel( export const PageDetailEditor: FC = props => { const { workspace, pageId } = props; - const blockSuiteWorkspace = workspace.blockSuiteWorkspace; - const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId); + const page = useBlockSuiteWorkspacePage(workspace, pageId); if (!page) { - throw new PageNotFoundError(blockSuiteWorkspace, pageId); + throw new PageNotFoundError(workspace, pageId); } - const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, pageId); + const title = useBlockSuiteWorkspacePageTitle(workspace, pageId); const layout = useAtomValue(contentLayoutAtom); const affinePluginsMap = useAtomValue(affinePluginsAtom); diff --git a/apps/web/src/components/pure/help-island/index.tsx b/apps/web/src/components/pure/help-island/index.tsx index 78f9d070e4..9820c7134a 100644 --- a/apps/web/src/components/pure/help-island/index.tsx +++ b/apps/web/src/components/pure/help-island/index.tsx @@ -41,7 +41,7 @@ export const HelpIsland = ({ setOpenSettingModalAtom({ open: true, activeTab: 'about', - workspace: null, + workspaceId: null, }); }, [setOpenSettingModalAtom]); diff --git a/apps/web/src/components/pure/quick-search-modal/config.ts b/apps/web/src/components/pure/quick-search-modal/config.ts index cca556d661..c32d074b19 100644 --- a/apps/web/src/components/pure/quick-search-modal/config.ts +++ b/apps/web/src/components/pure/quick-search-modal/config.ts @@ -1,9 +1,5 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { - DeleteTemporarilyIcon, - FolderIcon, - SettingsIcon, -} from '@blocksuite/icons'; +import { DeleteTemporarilyIcon, FolderIcon } from '@blocksuite/icons'; import type { FC, SVGProps } from 'react'; import { useMemo } from 'react'; @@ -23,11 +19,11 @@ export const useSwitchToConfig = ( href: pathGenerator.all(workspaceId), icon: FolderIcon, }, - { - title: t['Workspace Settings'](), - href: pathGenerator.setting(workspaceId), - icon: SettingsIcon, - }, + // { + // title: t['Workspace Settings'](), + // href: pathGenerator.setting(workspaceId), + // icon: SettingsIcon, + // }, { title: t['Trash'](), href: pathGenerator.trash(workspaceId), diff --git a/apps/web/src/components/pure/workspace-list-modal/index.tsx b/apps/web/src/components/pure/workspace-list-modal/index.tsx index e172a84a8e..47b345cc63 100644 --- a/apps/web/src/components/pure/workspace-list-modal/index.tsx +++ b/apps/web/src/components/pure/workspace-list-modal/index.tsx @@ -14,6 +14,7 @@ import type { } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons'; import type { DragEndEvent } from '@dnd-kit/core'; import { useCallback, useRef } from 'react'; @@ -40,12 +41,12 @@ import { interface WorkspaceModalProps { disabled?: boolean; - workspaces: AllWorkspace[]; + workspaces: RootWorkspaceMetadata[]; currentWorkspaceId: AllWorkspace['id'] | null; open: boolean; onClose: () => void; - onClickWorkspace: (workspace: AllWorkspace) => void; - onClickWorkspaceSetting: (workspace: AllWorkspace) => void; + onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void; + onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void; onNewWorkspace: () => void; onAddWorkspace: () => void; onMoveWorkspace: (activeId: string, overId: string) => void; diff --git a/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx b/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx index 5113f477d7..b5f6ce64ca 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx @@ -57,7 +57,7 @@ export const WorkspaceSelector: React.FC = ({ data-testid="workspace-avatar" className={workspaceAvatarStyle} size={40} - workspace={currentWorkspace} + workspace={currentWorkspace?.blockSuiteWorkspace ?? null} /> diff --git a/apps/web/src/components/root-app-sidebar/index.tsx b/apps/web/src/components/root-app-sidebar/index.tsx index 9645bdb245..eebfdeb38b 100644 --- a/apps/web/src/components/root-app-sidebar/index.tsx +++ b/apps/web/src/components/root-app-sidebar/index.tsx @@ -44,7 +44,6 @@ export type RootAppSidebarProps = { paths: { all: (workspaceId: string) => string; trash: (workspaceId: string) => string; - setting: (workspaceId: string) => string; shared: (workspaceId: string) => string; }; }; @@ -173,16 +172,6 @@ export const RootAppSidebar = ({ > {t['All pages']()} - {!runtimeConfig.enableNewSettingModal && ( - } - currentPath={currentPath} - path={currentWorkspaceId && paths.setting(currentWorkspaceId)} - > - {t['Settings']()} - - )} {runtimeConfig.enableNewSettingModal ? ( ): ReactElement { const setting = useCollectionManager(); @@ -31,6 +32,9 @@ export function WorkspaceHeader({ }, [setting] ); + + const currentWorkspace = useWorkspace(currentWorkspaceId); + const getPageInfoById = useGetPageInfoById(); if ('subPath' in currentEntry) { if (currentEntry.subPath === WorkspaceSubPath.ALL) { diff --git a/apps/web/src/hooks/__tests__/index.spec.tsx b/apps/web/src/hooks/__tests__/index.spec.tsx index e6b5bcc16f..f36b66b1cc 100644 --- a/apps/web/src/hooks/__tests__/index.spec.tsx +++ b/apps/web/src/hooks/__tests__/index.spec.tsx @@ -3,14 +3,7 @@ */ import 'fake-indexeddb/auto'; -import assert from 'node:assert'; - -import type { WorkspaceAdapter } from '@affine/env/workspace'; -import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace'; -import { - rootCurrentWorkspaceIdAtom, - workspaceAdaptersAtom, -} from '@affine/workspace/atom'; +import { WorkspaceSubPath } from '@affine/env/workspace'; import type { PageBlockModel } from '@blocksuite/blocks'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { assertExists } from '@blocksuite/global/utils'; @@ -20,18 +13,12 @@ import { useBlockSuitePageMeta, usePageMetaHelper, } from '@toeverything/hooks/use-block-suite-page-meta'; -import { createStore, Provider } from 'jotai'; import routerMock from 'next-router-mock'; import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes'; import type React from 'react'; import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; -import { WorkspaceAdapters } from '../../adapters/workspace'; -import { workspacesAtom } from '../../atoms'; -import { rootCurrentWorkspaceAtom } from '../../atoms/root'; import { BlockSuiteWorkspace } from '../../shared'; -import { useCurrentWorkspace } from '../current/use-current-workspace'; -import { useAppHelper, useWorkspaces } from '../use-workspaces'; vi.mock( '../../components/blocksuite/header/editor-mode-switch/CustomLottie', @@ -56,28 +43,6 @@ beforeEach(() => { localStorage.clear(); }); -async function getJotaiContext() { - const store = createStore(); - store.set( - workspaceAdaptersAtom, - WorkspaceAdapters as Record< - WorkspaceFlavour, - WorkspaceAdapter - > - ); - const ProviderWrapper: React.FC = - function ProviderWrapper({ children }) { - return {children}; - }; - const workspaces = await store.get(workspacesAtom); - expect(workspaces.length).toBe(1); - return { - store, - ProviderWrapper, - initialWorkspaces: workspaces, - } as const; -} - beforeEach(async () => { blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test' }) .register(AffineSchemas) @@ -163,67 +128,3 @@ describe('usePageMetas', async () => { expect(result.current[0].title).toBe('test'); }); }); - -describe('useWorkspacesHelper', () => { - test('basic', async () => { - const { ProviderWrapper, store } = await getJotaiContext(); - const workspaceHelperHook = renderHook(() => useAppHelper(), { - wrapper: ProviderWrapper, - }); - const id = await workspaceHelperHook.result.current.createLocalWorkspace( - 'test' - ); - const workspaces = await store.get(workspacesAtom); - expect(workspaces.length).toBe(2); - expect(workspaces[1].id).toBe(id); - const workspacesHook = renderHook(() => useWorkspaces(), { - wrapper: ProviderWrapper, - }); - store.set(rootCurrentWorkspaceIdAtom, workspacesHook.result.current[1].id); - await store.get(rootCurrentWorkspaceAtom); - const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), { - wrapper: ProviderWrapper, - }); - currentWorkspaceHook.result.current[1](workspacesHook.result.current[1].id); - }); -}); - -describe('useWorkspaces', () => { - test('basic', async () => { - const { ProviderWrapper } = await getJotaiContext(); - const { result } = renderHook(() => useWorkspaces(), { - wrapper: ProviderWrapper, - }); - expect(result.current).toEqual([ - { - id: expect.stringContaining(''), - flavour: WorkspaceFlavour.LOCAL, - blockSuiteWorkspace: expect.anything(), - }, - ]); - }); - - test('mutation', async () => { - const { ProviderWrapper, store } = await getJotaiContext(); - const { result } = renderHook(() => useAppHelper(), { - wrapper: ProviderWrapper, - }); - { - const workspaces = await store.get(workspacesAtom); - expect(workspaces.length).toEqual(1); - } - await result.current.createLocalWorkspace('test'); - { - const workspaces = await store.get(workspacesAtom); - expect(workspaces.length).toEqual(2); - } - const { result: result2 } = renderHook(() => useWorkspaces(), { - wrapper: ProviderWrapper, - }); - expect(result2.current.length).toEqual(2); - const secondWorkspace = result2.current[1]; - expect(secondWorkspace.flavour).toBe('local'); - assert(secondWorkspace.flavour === WorkspaceFlavour.LOCAL); - expect(secondWorkspace.blockSuiteWorkspace.meta.name).toBe('test'); - }); -}); diff --git a/apps/web/src/hooks/current/use-current-workspace.ts b/apps/web/src/hooks/current/use-current-workspace.ts index 422b1e9ee8..3243973866 100644 --- a/apps/web/src/hooks/current/use-current-workspace.ts +++ b/apps/web/src/hooks/current/use-current-workspace.ts @@ -1,26 +1,47 @@ -import { isBrowser } from '@affine/env/constant'; import { rootCurrentPageIdAtom, rootCurrentWorkspaceIdAtom, } from '@affine/workspace/atom'; -import { useAtom, useAtomValue } from 'jotai'; -import { useCallback } from 'react'; +import { assertExists } from '@blocksuite/global/utils'; +import type { PassiveDocProvider, Workspace } from '@blocksuite/store'; +import { useAtom, useSetAtom } from 'jotai'; +import { useCallback, useEffect } from 'react'; -import { rootCurrentWorkspaceAtom } from '../../atoms/root'; import type { AllWorkspace } from '../../shared'; +import { useWorkspace } from '../use-workspace'; + +declare global { + /** + * @internal debug only + */ + // eslint-disable-next-line no-var + var currentWorkspace: AllWorkspace | undefined; + interface WindowEventMap { + 'affine:workspace:change': CustomEvent<{ id: string }>; + } +} export function useCurrentWorkspace(): [ AllWorkspace, (id: string | null) => void, ] { - const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom); - const [, setId] = useAtom(rootCurrentWorkspaceIdAtom); - const [, setPageId] = useAtom(rootCurrentPageIdAtom); + const [id, setId] = useAtom(rootCurrentWorkspaceIdAtom); + assertExists(id); + const currentWorkspace = useWorkspace(id); + useEffect(() => { + globalThis.currentWorkspace = currentWorkspace; + globalThis.dispatchEvent( + new CustomEvent('affine:workspace:change', { + detail: { id: currentWorkspace.id }, + }) + ); + }, [currentWorkspace]); + const setPageId = useSetAtom(rootCurrentPageIdAtom); return [ currentWorkspace, useCallback( (id: string | null) => { - if (isBrowser && id) { + if (environment.isBrowser && id) { localStorage.setItem('last_workspace_id', id); } setPageId(null); @@ -30,3 +51,27 @@ export function useCurrentWorkspace(): [ ), ]; } + +const activeWorkspaceWeakMap = new WeakMap(); + +export function usePassiveWorkspaceEffect(workspace: Workspace) { + useEffect(() => { + if (activeWorkspaceWeakMap.get(workspace) === true) { + return; + } + const providers = workspace.providers.filter( + (provider): provider is PassiveDocProvider => + 'passive' in provider && provider.passive === true + ); + providers.forEach(provider => { + provider.connect(); + }); + activeWorkspaceWeakMap.set(workspace, true); + return () => { + providers.forEach(provider => { + provider.disconnect(); + }); + activeWorkspaceWeakMap.delete(workspace); + }; + }, [workspace]); +} diff --git a/apps/web/src/hooks/use-get-page-info.ts b/apps/web/src/hooks/use-get-page-info.ts index 577009fa65..24b67a44ad 100644 --- a/apps/web/src/hooks/use-get-page-info.ts +++ b/apps/web/src/hooks/use-get-page-info.ts @@ -4,10 +4,10 @@ import { useAtomValue } from 'jotai'; import { useMemo } from 'react'; import { pageSettingsAtom } from '../atoms'; -import { rootCurrentWorkspaceAtom } from '../atoms/root'; +import { useCurrentWorkspace } from './current/use-current-workspace'; export const useGetPageInfoById = (): GetPageInfoById => { - const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom); + const [currentWorkspace] = useCurrentWorkspace(); const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace); const pageMap = useMemo( () => Object.fromEntries(pageMetas.map(page => [page.id, page])), diff --git a/apps/web/src/hooks/use-workspace.ts b/apps/web/src/hooks/use-workspace.ts new file mode 100644 index 0000000000..9046e4a18b --- /dev/null +++ b/apps/web/src/hooks/use-workspace.ts @@ -0,0 +1,36 @@ +import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; +import { useStaticBlockSuiteWorkspace } from '@affine/workspace/utils'; +import { assertExists } from '@blocksuite/global/utils'; +import type { Workspace } from '@blocksuite/store'; +import type { Atom } from 'jotai'; +import { atom, useAtomValue } from 'jotai'; + +import type { AffineOfficialWorkspace } from '../shared'; + +const workspaceWeakMap = new WeakMap< + Workspace, + Atom> +>(); + +export function useWorkspace(workspaceId: string): AffineOfficialWorkspace { + const blockSuiteWorkspace = useStaticBlockSuiteWorkspace(workspaceId); + if (!workspaceWeakMap.has(blockSuiteWorkspace)) { + const baseAtom = atom(async get => { + const metadata = await get(rootWorkspacesMetadataAtom); + const flavour = metadata.find(({ id }) => id === workspaceId)?.flavour; + assertExists(flavour); + return { + id: workspaceId, + flavour, + blockSuiteWorkspace, + }; + }); + workspaceWeakMap.set(blockSuiteWorkspace, baseAtom); + } + + return useAtomValue( + workspaceWeakMap.get(blockSuiteWorkspace) as Atom< + Promise + > + ); +} diff --git a/apps/web/src/hooks/use-workspaces.ts b/apps/web/src/hooks/use-workspaces.ts index 65f8b454b1..8c1eefc582 100644 --- a/apps/web/src/hooks/use-workspaces.ts +++ b/apps/web/src/hooks/use-workspaces.ts @@ -2,19 +2,16 @@ import { DebugLogger } from '@affine/debug'; import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud'; -import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; +import { + createEmptyBlockSuiteWorkspace, + getWorkspace, +} from '@affine/workspace/utils'; import { nanoid } from '@blocksuite/store'; import { useAtomValue, useSetAtom } from 'jotai'; import { useCallback } from 'react'; import { LocalAdapter } from '../adapters/local'; import { WorkspaceAdapters } from '../adapters/workspace'; -import { workspacesAtom } from '../atoms'; -import type { AllWorkspace } from '../shared'; - -export function useWorkspaces(): AllWorkspace[] { - return useAtomValue(workspacesAtom); -} const logger = new DebugLogger('use-workspaces'); @@ -22,12 +19,12 @@ const logger = new DebugLogger('use-workspaces'); * This hook has the permission to all workspaces. Be careful when using it. */ export function useAppHelper() { - const workspaces = useWorkspaces(); const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom); const set = useSetAtom(rootWorkspacesMetadataAtom); return { addLocalWorkspace: useCallback( async (workspaceId: string): Promise => { + createEmptyBlockSuiteWorkspace(workspaceId, WorkspaceFlavour.LOCAL); saveWorkspaceToLocalStorage(workspaceId); await set(workspaces => [ ...workspaces, @@ -68,20 +65,20 @@ export function useAppHelper() { const targetJotaiWorkspace = jotaiWorkspaces.find( ws => ws.id === workspaceId ); - const targetWorkspace = workspaces.find(ws => ws.id === workspaceId); - if (!targetJotaiWorkspace || !targetWorkspace) { + if (!targetJotaiWorkspace) { throw new Error('page cannot be found'); } + const targetWorkspace = getWorkspace(targetJotaiWorkspace.id); + // delete workspace from plugin - await WorkspaceAdapters[targetWorkspace.flavour].CRUD.delete( - // fixme: type casting - targetWorkspace as any + await WorkspaceAdapters[targetJotaiWorkspace.flavour].CRUD.delete( + targetWorkspace ); // delete workspace from jotai storage await set(workspaces => workspaces.filter(ws => ws.id !== workspaceId)); }, - [jotaiWorkspaces, set, workspaces] + [jotaiWorkspaces, set] ), }; } diff --git a/apps/web/src/layouts/workspace-layout.tsx b/apps/web/src/layouts/workspace-layout.tsx index 18c5631205..3cf3fc28f7 100644 --- a/apps/web/src/layouts/workspace-layout.tsx +++ b/apps/web/src/layouts/workspace-layout.tsx @@ -20,7 +20,6 @@ import { rootWorkspacesMetadataAtom, } from '@affine/workspace/atom'; import { assertEquals, assertExists } from '@blocksuite/global/utils'; -import type { PassiveDocProvider } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store'; import type { DragEndEvent } from '@dnd-kit/core'; import { @@ -55,10 +54,12 @@ import { RootAppSidebar, } from '../components/root-app-sidebar'; import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper'; -import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; +import { + useCurrentWorkspace, + usePassiveWorkspaceEffect, +} from '../hooks/current/use-current-workspace'; import { useRouterHelper } from '../hooks/use-router-helper'; import { useRouterTitle } from '../hooks/use-router-title'; -import { useWorkspaces } from '../hooks/use-workspaces'; import { AllWorkspaceModals, CurrentWorkspaceModals, @@ -96,13 +97,6 @@ export const QuickSearch: FC = () => { ); }; -export const AllWorkspaceContext = ({ - children, -}: PropsWithChildren): ReactElement => { - useWorkspaces(); - return <>{children}; -}; - declare global { // eslint-disable-next-line no-var var HALTING_PROBLEM_TIMEOUT: number; @@ -166,13 +160,11 @@ export const WorkspaceLayout: FC = <> {/* load all workspaces is costly, do not block the whole UI */} - - - - {/* fixme(himself65): don't re-render whole modals */} - - - + + + {/* fixme(himself65): don't re-render whole modals */} + + }> @@ -223,36 +215,24 @@ export const WorkspaceLayoutInner: FC = ({ children }) => { } //#endregion - if (currentPageId) { - const pageExist = - currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); - if (router.pathname === '/[workspaceId]/[pageId]' && !pageExist) { - router.push('/404').catch(console.error); - } - } else if ( - router.pathname === '/[workspaceId]/[pageId]' && + //#region check if page is valid + if ( typeof router.query.pageId === 'string' && - router.query.pageId !== currentPageId + router.pathname === '/workspace/[workspaceId]/[pageId]' && + currentPageId ) { - setCurrentPageId(router.query.pageId); - jumpToPage(currentWorkspace.id, router.query.pageId).catch(console.error); + if (currentPageId !== router.query.pageId) { + setCurrentPageId(router.query.pageId); + } else { + const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); + if (!page) { + router.push('/404').catch(console.error); + } + } } + //#endregion - useEffect(() => { - const backgroundProviders = - currentWorkspace.blockSuiteWorkspace.providers.filter( - (provider): provider is PassiveDocProvider => - 'passive' in provider && provider.passive - ); - backgroundProviders.forEach(provider => { - provider.connect(); - }); - return () => { - backgroundProviders.forEach(provider => { - provider.disconnect(); - }); - }; - }, [currentWorkspace]); + usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace); useEffect(() => { const page = currentWorkspace.blockSuiteWorkspace.getPage( @@ -303,7 +283,7 @@ export const WorkspaceLayoutInner: FC = ({ children }) => { const handleOpenSettingModal = useCallback(() => { setOpenSettingModalAtom({ activeTab: 'appearance', - workspace: null, + workspaceId: null, open: true, }); }, [setOpenSettingModalAtom]); diff --git a/apps/web/src/pages/404.tsx b/apps/web/src/pages/404.tsx index 2631a7bb02..fbe0e5d232 100644 --- a/apps/web/src/pages/404.tsx +++ b/apps/web/src/pages/404.tsx @@ -1,10 +1,13 @@ import { Button, displayFlex, styled } from '@affine/component'; +import { WorkspaceSubPath } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import Head from 'next/head'; import Image from 'next/legacy/image'; import { useRouter } from 'next/router'; import React from 'react'; +import { useRouterHelper } from '../hooks/use-router-helper'; + export const StyledContainer = styled('div')(() => { return { ...displayFlex('center', 'center'), @@ -26,6 +29,7 @@ export const StyledContainer = styled('div')(() => { export const NotfoundPage = () => { const t = useAFFiNEI18N(); const router = useRouter(); + const { jumpToSubPath } = useRouterHelper(router); return ( 404 @@ -34,7 +38,12 @@ export const NotfoundPage = () => {