refactor: lazy load workspaces (#3091)

This commit is contained in:
Alex Yang
2023-07-07 22:15:27 +08:00
committed by GitHub
parent 66152401be
commit 283f0cd263
45 changed files with 446 additions and 750 deletions
+7 -7
View File
@@ -2,27 +2,27 @@ import { toast } from '@affine/component';
import { BlockCard } from '@affine/component/card/block-card'; import { BlockCard } from '@affine/component/card/block-card';
import { WorkspaceCard } from '@affine/component/card/workspace-card'; import { WorkspaceCard } from '@affine/component/card/workspace-card';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { Workspace } from '@blocksuite/store';
export default { export default {
title: 'AFFiNE/Card', title: 'AFFiNE/Card',
component: WorkspaceCard, component: WorkspaceCard,
}; };
const blockSuiteWorkspace = new Workspace({ const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
id: 'blocksuite-local', 'blocksuite-local',
}); WorkspaceFlavour.LOCAL
);
blockSuiteWorkspace.meta.setName('Hello World'); blockSuiteWorkspace.meta.setName('Hello World');
export const AffineWorkspaceCard = () => { export const AffineWorkspaceCard = () => {
return ( return (
<WorkspaceCard <WorkspaceCard
workspace={{ meta={{
id: 'blocksuite-local',
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
id: 'local',
blockSuiteWorkspace,
}} }}
onClick={() => {}} onClick={() => {}}
onSettingClick={() => {}} onSettingClick={() => {}}
@@ -1,6 +1,5 @@
import type { WorkspaceAvatarProps } from '@affine/component/workspace-avatar'; import type { WorkspaceAvatarProps } from '@affine/component/workspace-avatar';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Workspace } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store';
import type { Meta, StoryFn } from '@storybook/react'; import type { Meta, StoryFn } from '@storybook/react';
@@ -25,16 +24,7 @@ const basicBlockSuiteWorkspace = new Workspace({
basicBlockSuiteWorkspace.meta.setName('Hello World'); basicBlockSuiteWorkspace.meta.setName('Hello World');
export const Basic: StoryFn<WorkspaceAvatarProps> = props => { export const Basic: StoryFn<WorkspaceAvatarProps> = props => {
return ( return <WorkspaceAvatar {...props} workspace={basicBlockSuiteWorkspace} />;
<WorkspaceAvatar
{...props}
workspace={{
flavour: WorkspaceFlavour.LOCAL,
id: 'local',
blockSuiteWorkspace: basicBlockSuiteWorkspace,
}}
/>
);
}; };
Basic.args = { Basic.args = {
@@ -60,16 +50,7 @@ fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url))
}); });
export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => { export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => {
return ( return <WorkspaceAvatar {...props} workspace={avatarBlockSuiteWorkspace} />;
<WorkspaceAvatar
{...props}
workspace={{
flavour: WorkspaceFlavour.LOCAL,
id: 'local',
blockSuiteWorkspace: avatarBlockSuiteWorkspace,
}}
/>
);
}; };
BlobExample.args = { BlobExample.args = {
+11 -10
View File
@@ -17,7 +17,10 @@ import {
saveWorkspaceToLocalStorage, saveWorkspaceToLocalStorage,
} from '@affine/workspace/local/crud'; } from '@affine/workspace/local/crud';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers'; 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 { nanoid } from '@blocksuite/store';
import { import {
@@ -75,13 +78,11 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
Provider: ({ children }) => { Provider: ({ children }) => {
return <>{children}</>; return <>{children}</>;
}, },
PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => { PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
const page = workspace.getPage(currentPageId);
if (!page) { if (!page) {
throw new PageNotFoundError( throw new PageNotFoundError(workspace, currentPageId);
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
} }
return ( return (
<> <>
@@ -89,7 +90,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
pageId={currentPageId} pageId={currentPageId}
onInit={initEmptyPage} onInit={initEmptyPage}
onLoad={onLoadEditor} onLoad={onLoadEditor}
workspace={currentWorkspace} workspace={workspace}
/> />
</> </>
); );
@@ -105,14 +106,14 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
); );
}, },
NewSettingsDetail: ({ NewSettingsDetail: ({
currentWorkspace, currentWorkspaceId,
onDeleteWorkspace, onDeleteWorkspace,
onTransformWorkspace, onTransformWorkspace,
}) => { }) => {
return ( return (
<NewWorkspaceSettingDetail <NewWorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace} onDeleteWorkspace={onDeleteWorkspace}
workspace={currentWorkspace} workspaceId={currentWorkspaceId}
onTransferWorkspace={onTransformWorkspace} onTransferWorkspace={onTransformWorkspace}
/> />
); );
-83
View File
@@ -3,34 +3,14 @@
*/ */
import 'fake-indexeddb/auto'; 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 { createStore } from 'jotai';
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import { WorkspaceAdapters } from '../../adapters/workspace';
import { import {
pageSettingFamily, pageSettingFamily,
pageSettingsAtom, pageSettingsAtom,
recentPageSettingsAtom, recentPageSettingsAtom,
} from '../index'; } from '../index';
import { rootCurrentWorkspaceAtom } from '../root';
describe('page mode atom', () => { describe('page mode atom', () => {
test('basic', () => { 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<WorkspaceFlavour>
>
);
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');
});
});
+2 -4
View File
@@ -10,20 +10,18 @@ export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
export const openQuickSearchModalAtom = atom(false); export const openQuickSearchModalAtom = atom(false);
export const openOnboardingModalAtom = atom(false); export const openOnboardingModalAtom = atom(false);
export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspace'> & { export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & {
open: boolean; open: boolean;
}; };
export const openSettingModalAtom = atom<SettingAtom>({ export const openSettingModalAtom = atom<SettingAtom>({
activeTab: 'appearance', activeTab: 'appearance',
workspace: null, workspaceId: null,
open: false, open: false,
}); });
export const openDisableCloudAlertModalAtom = atom(false); export const openDisableCloudAlertModalAtom = atom(false);
export { workspacesAtom } from './root';
type PageMode = 'page' | 'edgeless'; type PageMode = 'page' | 'edgeless';
type PageLocalSetting = { type PageLocalSetting = {
mode: PageMode; mode: PageMode;
-160
View File
@@ -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<Promise<AllWorkspace[]>>(
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<WorkspaceFlavour>;
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<void>[] = [];
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<Promise<AllWorkspace>>(
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<WorkspaceFlavour>;
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
@@ -11,7 +11,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import type { FC } from 'react'; import type { FC } from 'react';
import type { AffineOfficialWorkspace } from '../../../shared'; import { useWorkspace } from '../../../hooks/use-workspace';
import { DeleteLeaveWorkspace } from './delete-leave-workspace'; import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { ExportPanel } from './export'; import { ExportPanel } from './export';
import { ProfilePanel } from './profile'; import { ProfilePanel } from './profile';
@@ -19,7 +19,7 @@ import { PublishPanel } from './publish';
import { StoragePanel } from './storage'; import { StoragePanel } from './storage';
export type WorkspaceSettingDetailProps = { export type WorkspaceSettingDetailProps = {
workspace: AffineOfficialWorkspace; workspaceId: string;
onDeleteWorkspace: (id: string) => Promise<void>; onDeleteWorkspace: (id: string) => Promise<void>;
onTransferWorkspace: < onTransferWorkspace: <
From extends WorkspaceFlavour, From extends WorkspaceFlavour,
@@ -32,11 +32,12 @@ export type WorkspaceSettingDetailProps = {
}; };
export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({ export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
workspace, workspaceId,
onDeleteWorkspace, onDeleteWorkspace,
...props ...props
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workspace = useWorkspace(workspaceId);
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace); const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
return ( return (
@@ -64,7 +64,10 @@ export const ProfilePanel: FC<{
<div className="camera-icon-wrapper"> <div className="camera-icon-wrapper">
<CameraIcon /> <CameraIcon />
</div> </div>
<WorkspaceAvatar size={56} workspace={workspace} /> <WorkspaceAvatar
size={56}
workspace={workspace.blockSuiteWorkspace}
/>
</> </>
</Upload> </Upload>
</div> </div>
@@ -18,13 +18,22 @@ import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
import type { WorkspaceSettingDetailProps } from './index'; import type { WorkspaceSettingDetailProps } from './index';
import * as style from './style.css'; import * as style from './style.css';
export type PublishPanelProps = WorkspaceSettingDetailProps & { export type PublishPanelProps = Omit<
WorkspaceSettingDetailProps,
'workspaceId'
> & {
workspace: AffineOfficialWorkspace; workspace: AffineOfficialWorkspace;
}; };
export type PublishPanelLocalProps = WorkspaceSettingDetailProps & { export type PublishPanelLocalProps = Omit<
WorkspaceSettingDetailProps,
'workspaceId'
> & {
workspace: LocalWorkspace; workspace: LocalWorkspace;
}; };
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & { export type PublishPanelAffineProps = Omit<
WorkspaceSettingDetailProps,
'workspaceId'
> & {
workspace: AffineCloudWorkspace; workspace: AffineCloudWorkspace;
}; };
@@ -4,14 +4,13 @@ import {
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { ContactWithUsIcon } from '@blocksuite/icons'; import { ContactWithUsIcon } from '@blocksuite/icons';
import type { PassiveDocProvider } from '@blocksuite/store'; import { useAtomValue } from 'jotai';
import { noop } from 'foxact/noop';
import type React from 'react'; 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 { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useWorkspaces } from '../../../hooks/use-workspaces';
import type { AllWorkspace } from '../../../shared'; import type { AllWorkspace } from '../../../shared';
import { AccountSetting } from './account-setting'; import { AccountSetting } from './account-setting';
import { import {
@@ -26,70 +25,52 @@ import { WorkspaceSetting } from './workspace-setting';
type ActiveTab = GeneralSettingKeys | 'workspace' | 'account'; type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
export type SettingProps = { export type SettingProps = {
activeTab: ActiveTab; activeTab: ActiveTab;
workspace: AllWorkspace | null; workspaceId: string | null;
onSettingClick: (params: { onSettingClick: (params: {
activeTab: ActiveTab; activeTab: ActiveTab;
workspace: AllWorkspace | null; workspaceId: string | null;
}) => void; }) => void;
}; };
export const SettingModal: React.FC<SettingModalProps & SettingProps> = ({ export const SettingModal: React.FC<SettingModalProps & SettingProps> = ({
open, open,
setOpen, setOpen,
activeTab = 'appearance', activeTab = 'appearance',
workspace = null, workspaceId = null,
onSettingClick, onSettingClick,
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workspaces = useWorkspaces(); const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const generalSettingList = useGeneralSettingList(); const generalSettingList = useGeneralSettingList();
const workspaceList = useMemo(() => { const workspaceList = useMemo(() => {
return workspaces.filter( return workspaces.filter(
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC ({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
) as AllWorkspace[]; );
}, [workspaces]); }, [workspaces]);
const onGeneralSettingClick = useCallback( const onGeneralSettingClick = useCallback(
(key: GeneralSettingKeys) => { (key: GeneralSettingKeys) => {
onSettingClick({ onSettingClick({
activeTab: key, activeTab: key,
workspace: null, workspaceId: null,
}); });
}, },
[onSettingClick] [onSettingClick]
); );
const onWorkspaceSettingClick = useCallback( const onWorkspaceSettingClick = useCallback(
(workspace: AllWorkspace) => { (workspaceId: string) => {
onSettingClick({ onSettingClick({
activeTab: 'workspace', activeTab: 'workspace',
workspace, workspaceId,
}); });
}, },
[onSettingClick] [onSettingClick]
); );
const onAccountSettingClick = useCallback(() => { const onAccountSettingClick = useCallback(() => {
onSettingClick({ activeTab: 'account', workspace: null }); onSettingClick({ activeTab: 'account', workspaceId: null });
}, [onSettingClick]); }, [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 ( return (
<SettingModalBase open={open} setOpen={setOpen}> <SettingModalBase open={open} setOpen={setOpen}>
<SettingSidebar <SettingSidebar
@@ -99,15 +80,15 @@ export const SettingModal: React.FC<SettingModalProps & SettingProps> = ({
workspaceList={workspaceList} workspaceList={workspaceList}
onWorkspaceSettingClick={onWorkspaceSettingClick} onWorkspaceSettingClick={onWorkspaceSettingClick}
selectedGeneralKey={activeTab} selectedGeneralKey={activeTab}
selectedWorkspace={workspace} selectedWorkspaceId={workspaceId}
onAccountSettingClick={onAccountSettingClick} onAccountSettingClick={onAccountSettingClick}
/> />
<div className={settingContent}> <div className={settingContent}>
<div className="wrapper"> <div className="wrapper">
<div className="content"> <div className="content">
{activeTab === 'workspace' && workspace ? ( {activeTab === 'workspace' && workspaceId ? (
<WorkspaceSetting key={workspace.id} workspace={workspace} /> <WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
) : null} ) : null}
{generalSettingList.find(v => v.key === activeTab) ? ( {generalSettingList.find(v => v.key === activeTab) ? (
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} /> <GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
@@ -1,6 +1,8 @@
import { UserAvatar } from '@affine/component/user-avatar'; import { UserAvatar } from '@affine/component/user-avatar';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; 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 { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -24,17 +26,17 @@ export const SettingSidebar = ({
currentWorkspace, currentWorkspace,
workspaceList, workspaceList,
onWorkspaceSettingClick, onWorkspaceSettingClick,
selectedWorkspace, selectedWorkspaceId,
selectedGeneralKey, selectedGeneralKey,
onAccountSettingClick, onAccountSettingClick,
}: { }: {
generalSettingList: GeneralSettingList; generalSettingList: GeneralSettingList;
onGeneralSettingClick: (key: GeneralSettingKeys) => void; onGeneralSettingClick: (key: GeneralSettingKeys) => void;
currentWorkspace: AllWorkspace; currentWorkspace: AllWorkspace;
workspaceList: AllWorkspace[]; workspaceList: RootWorkspaceMetadata[];
onWorkspaceSettingClick: (workspace: AllWorkspace) => void; onWorkspaceSettingClick: (workspaceId: string) => void;
selectedWorkspace: AllWorkspace | null; selectedWorkspaceId: string | null;
selectedGeneralKey: string | null; selectedGeneralKey: string | null;
onAccountSettingClick: () => void; onAccountSettingClick: () => void;
}) => { }) => {
@@ -72,12 +74,12 @@ export const SettingSidebar = ({
return ( return (
<WorkspaceListItem <WorkspaceListItem
key={workspace.id} key={workspace.id}
workspace={workspace} meta={workspace}
onClick={() => { onClick={() => {
onWorkspaceSettingClick(workspace); onWorkspaceSettingClick(workspace.id);
}} }}
isCurrent={workspace.id === currentWorkspace.id} isCurrent={workspace.id === currentWorkspace.id}
isActive={workspace.id === selectedWorkspace?.id} isActive={workspace.id === selectedWorkspaceId}
/> />
); );
})} })}
@@ -107,19 +109,18 @@ export const SettingSidebar = ({
}; };
const WorkspaceListItem = ({ const WorkspaceListItem = ({
workspace, meta,
onClick, onClick,
isCurrent, isCurrent,
isActive, isActive,
}: { }: {
workspace: AllWorkspace; meta: RootWorkspaceMetadata;
onClick: () => void; onClick: () => void;
isCurrent: boolean; isCurrent: boolean;
isActive: boolean; isActive: boolean;
}) => { }) => {
const [workspaceName] = useBlockSuiteWorkspaceName( const workspace = useStaticBlockSuiteWorkspace(meta.id);
workspace.blockSuiteWorkspace ?? null const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
);
return ( return (
<div <div
className={clsx(sidebarSelectItem, { active: isActive })} className={clsx(sidebarSelectItem, { active: isActive })}
@@ -1,16 +1,16 @@
import { Suspense, useCallback } from 'react'; import { Suspense, useCallback } from 'react';
import { getUIAdapter } from '../../../../adapters/workspace'; import { getUIAdapter } from '../../../../adapters/workspace';
import { usePassiveWorkspaceEffect } from '../../../../hooks/current/use-current-workspace';
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace'; import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
import { useWorkspace } from '../../../../hooks/use-workspace';
import { useAppHelper } from '../../../../hooks/use-workspaces'; import { useAppHelper } from '../../../../hooks/use-workspaces';
import type { AllWorkspace } from '../../../../shared';
export const WorkspaceSetting = ({ export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
workspace, const workspace = useWorkspace(workspaceId);
}: { usePassiveWorkspaceEffect(workspace.blockSuiteWorkspace);
workspace: AllWorkspace;
}) => {
const helper = useAppHelper(); const helper = useAppHelper();
const { NewSettingsDetail } = getUIAdapter(workspace.flavour); const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
const onDeleteWorkspace = useCallback( const onDeleteWorkspace = useCallback(
@@ -26,7 +26,7 @@ export const WorkspaceSetting = ({
<NewSettingsDetail <NewSettingsDetail
onTransformWorkspace={onTransformWorkspace} onTransformWorkspace={onTransformWorkspace}
onDeleteWorkspace={onDeleteWorkspace} onDeleteWorkspace={onDeleteWorkspace}
currentWorkspace={workspace} currentWorkspaceId={workspaceId}
/> />
</Suspense> </Suspense>
); );
+9 -12
View File
@@ -7,7 +7,7 @@ import {
import { rootBlockHubAtom } from '@affine/workspace/atom'; import { rootBlockHubAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils'; 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 { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page'; import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-block-suite-workspace-page-title'; 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 { pageSettingFamily } from '../atoms';
import { contentLayoutAtom } from '../atoms/layout'; import { contentLayoutAtom } from '../atoms/layout';
import { useAppSetting } from '../atoms/settings'; import { useAppSetting } from '../atoms/settings';
import type { AffineOfficialWorkspace } from '../shared';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor'; import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import { editor } from './page-detail-editor.css'; import { editor } from './page-detail-editor.css';
import { pluginContainer } from './page-detail-editor.css'; import { pluginContainer } from './page-detail-editor.css';
export type PageDetailEditorProps = { export type PageDetailEditorProps = {
isPublic?: boolean; isPublic?: boolean;
workspace: AffineOfficialWorkspace; workspace: Workspace;
pageId: string; pageId: string;
onInit: (page: Page, editor: Readonly<EditorContainer>) => void; onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
onLoad?: (page: Page, editor: EditorContainer) => () => void; onLoad?: (page: Page, editor: EditorContainer) => () => void;
@@ -53,12 +52,11 @@ const EditorWrapper = memo(function EditorWrapper({
() => Object.values(affinePluginsMap), () => Object.values(affinePluginsMap),
[affinePluginsMap] [affinePluginsMap]
); );
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const page = useBlockSuiteWorkspacePage(workspace, pageId);
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
if (!page) { 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 meta => meta.id === pageId
); );
const pageSettingAtom = pageSettingFamily(pageId); const pageSettingAtom = pageSettingFamily(pageId);
@@ -77,7 +75,7 @@ const EditorWrapper = memo(function EditorWrapper({
className={clsx(editor, { className={clsx(editor, {
'full-screen': appSettings?.fullWidthLayout, 'full-screen': appSettings?.fullWidthLayout,
})} })}
key={`${workspace.flavour}-${workspace.id}-${pageId}`} key={`${workspace.id}-${pageId}`}
mode={isPublic ? 'page' : currentMode} mode={isPublic ? 'page' : currentMode}
page={page} page={page}
onInit={useCallback( onInit={useCallback(
@@ -181,12 +179,11 @@ const LayoutPanel = memo(function LayoutPanel(
export const PageDetailEditor: FC<PageDetailEditorProps> = props => { export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
const { workspace, pageId } = props; const { workspace, pageId } = props;
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const page = useBlockSuiteWorkspacePage(workspace, pageId);
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
if (!page) { 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 layout = useAtomValue(contentLayoutAtom);
const affinePluginsMap = useAtomValue(affinePluginsAtom); const affinePluginsMap = useAtomValue(affinePluginsAtom);
@@ -41,7 +41,7 @@ export const HelpIsland = ({
setOpenSettingModalAtom({ setOpenSettingModalAtom({
open: true, open: true,
activeTab: 'about', activeTab: 'about',
workspace: null, workspaceId: null,
}); });
}, [setOpenSettingModalAtom]); }, [setOpenSettingModalAtom]);
@@ -1,9 +1,5 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import { DeleteTemporarilyIcon, FolderIcon } from '@blocksuite/icons';
DeleteTemporarilyIcon,
FolderIcon,
SettingsIcon,
} from '@blocksuite/icons';
import type { FC, SVGProps } from 'react'; import type { FC, SVGProps } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -23,11 +19,11 @@ export const useSwitchToConfig = (
href: pathGenerator.all(workspaceId), href: pathGenerator.all(workspaceId),
icon: FolderIcon, icon: FolderIcon,
}, },
{ // {
title: t['Workspace Settings'](), // title: t['Workspace Settings'](),
href: pathGenerator.setting(workspaceId), // href: pathGenerator.setting(workspaceId),
icon: SettingsIcon, // icon: SettingsIcon,
}, // },
{ {
title: t['Trash'](), title: t['Trash'](),
href: pathGenerator.trash(workspaceId), href: pathGenerator.trash(workspaceId),
@@ -14,6 +14,7 @@ import type {
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons'; import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
@@ -40,12 +41,12 @@ import {
interface WorkspaceModalProps { interface WorkspaceModalProps {
disabled?: boolean; disabled?: boolean;
workspaces: AllWorkspace[]; workspaces: RootWorkspaceMetadata[];
currentWorkspaceId: AllWorkspace['id'] | null; currentWorkspaceId: AllWorkspace['id'] | null;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onClickWorkspace: (workspace: AllWorkspace) => void; onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
onClickWorkspaceSetting: (workspace: AllWorkspace) => void; onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
onNewWorkspace: () => void; onNewWorkspace: () => void;
onAddWorkspace: () => void; onAddWorkspace: () => void;
onMoveWorkspace: (activeId: string, overId: string) => void; onMoveWorkspace: (activeId: string, overId: string) => void;
@@ -57,7 +57,7 @@ export const WorkspaceSelector: React.FC<WorkspaceSelectorProps> = ({
data-testid="workspace-avatar" data-testid="workspace-avatar"
className={workspaceAvatarStyle} className={workspaceAvatarStyle}
size={40} size={40}
workspace={currentWorkspace} workspace={currentWorkspace?.blockSuiteWorkspace ?? null}
/> />
<StyledSelectorWrapper> <StyledSelectorWrapper>
<StyledWorkspaceName data-testid="workspace-name"> <StyledWorkspaceName data-testid="workspace-name">
@@ -44,7 +44,6 @@ export type RootAppSidebarProps = {
paths: { paths: {
all: (workspaceId: string) => string; all: (workspaceId: string) => string;
trash: (workspaceId: string) => string; trash: (workspaceId: string) => string;
setting: (workspaceId: string) => string;
shared: (workspaceId: string) => string; shared: (workspaceId: string) => string;
}; };
}; };
@@ -173,16 +172,6 @@ export const RootAppSidebar = ({
> >
<span data-testid="all-pages">{t['All pages']()}</span> <span data-testid="all-pages">{t['All pages']()}</span>
</RouteMenuLinkItem> </RouteMenuLinkItem>
{!runtimeConfig.enableNewSettingModal && (
<RouteMenuLinkItem
data-testid="slider-bar-workspace-setting-button"
icon={<SettingsIcon />}
currentPath={currentPath}
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
>
<span data-testid="settings">{t['Settings']()}</span>
</RouteMenuLinkItem>
)}
{runtimeConfig.enableNewSettingModal ? ( {runtimeConfig.enableNewSettingModal ? (
<MenuItem <MenuItem
data-testid="slider-bar-workspace-setting-button" data-testid="slider-bar-workspace-setting-button"
+5 -1
View File
@@ -14,12 +14,13 @@ import type { ReactElement } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useGetPageInfoById } from '../hooks/use-get-page-info'; import { useGetPageInfoById } from '../hooks/use-get-page-info';
import { useWorkspace } from '../hooks/use-workspace';
import { BlockSuiteEditorHeader } from './blocksuite/workspace-header'; import { BlockSuiteEditorHeader } from './blocksuite/workspace-header';
import { filterContainerStyle } from './filter-container.css'; import { filterContainerStyle } from './filter-container.css';
import { WorkspaceModeFilterTab, WorkspaceTitle } from './pure/workspace-title'; import { WorkspaceModeFilterTab, WorkspaceTitle } from './pure/workspace-title';
export function WorkspaceHeader({ export function WorkspaceHeader({
currentWorkspace, currentWorkspaceId,
currentEntry, currentEntry,
}: WorkspaceHeaderProps<WorkspaceFlavour>): ReactElement { }: WorkspaceHeaderProps<WorkspaceFlavour>): ReactElement {
const setting = useCollectionManager(); const setting = useCollectionManager();
@@ -31,6 +32,9 @@ export function WorkspaceHeader({
}, },
[setting] [setting]
); );
const currentWorkspace = useWorkspace(currentWorkspaceId);
const getPageInfoById = useGetPageInfoById(); const getPageInfoById = useGetPageInfoById();
if ('subPath' in currentEntry) { if ('subPath' in currentEntry) {
if (currentEntry.subPath === WorkspaceSubPath.ALL) { if (currentEntry.subPath === WorkspaceSubPath.ALL) {
+1 -100
View File
@@ -3,14 +3,7 @@
*/ */
import 'fake-indexeddb/auto'; import 'fake-indexeddb/auto';
import assert from 'node:assert'; import { WorkspaceSubPath } from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import {
rootCurrentWorkspaceIdAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import type { PageBlockModel } from '@blocksuite/blocks'; import type { PageBlockModel } from '@blocksuite/blocks';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
@@ -20,18 +13,12 @@ import {
useBlockSuitePageMeta, useBlockSuitePageMeta,
usePageMetaHelper, usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta'; } from '@toeverything/hooks/use-block-suite-page-meta';
import { createStore, Provider } from 'jotai';
import routerMock from 'next-router-mock'; import routerMock from 'next-router-mock';
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes'; import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
import type React from 'react'; import type React from 'react';
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 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 { BlockSuiteWorkspace } from '../../shared';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { useAppHelper, useWorkspaces } from '../use-workspaces';
vi.mock( vi.mock(
'../../components/blocksuite/header/editor-mode-switch/CustomLottie', '../../components/blocksuite/header/editor-mode-switch/CustomLottie',
@@ -56,28 +43,6 @@ beforeEach(() => {
localStorage.clear(); localStorage.clear();
}); });
async function getJotaiContext() {
const store = createStore();
store.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
const ProviderWrapper: React.FC<React.PropsWithChildren> =
function ProviderWrapper({ children }) {
return <Provider store={store}>{children}</Provider>;
};
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toBe(1);
return {
store,
ProviderWrapper,
initialWorkspaces: workspaces,
} as const;
}
beforeEach(async () => { beforeEach(async () => {
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test' }) blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test' })
.register(AffineSchemas) .register(AffineSchemas)
@@ -163,67 +128,3 @@ describe('usePageMetas', async () => {
expect(result.current[0].title).toBe('test'); 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');
});
});
@@ -1,26 +1,47 @@
import { isBrowser } from '@affine/env/constant';
import { import {
rootCurrentPageIdAtom, rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom, rootCurrentWorkspaceIdAtom,
} from '@affine/workspace/atom'; } from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai'; import { assertExists } from '@blocksuite/global/utils';
import { useCallback } from 'react'; 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 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(): [ export function useCurrentWorkspace(): [
AllWorkspace, AllWorkspace,
(id: string | null) => void, (id: string | null) => void,
] { ] {
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom); const [id, setId] = useAtom(rootCurrentWorkspaceIdAtom);
const [, setId] = useAtom(rootCurrentWorkspaceIdAtom); assertExists(id);
const [, setPageId] = useAtom(rootCurrentPageIdAtom); 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 [ return [
currentWorkspace, currentWorkspace,
useCallback( useCallback(
(id: string | null) => { (id: string | null) => {
if (isBrowser && id) { if (environment.isBrowser && id) {
localStorage.setItem('last_workspace_id', id); localStorage.setItem('last_workspace_id', id);
} }
setPageId(null); setPageId(null);
@@ -30,3 +51,27 @@ export function useCurrentWorkspace(): [
), ),
]; ];
} }
const activeWorkspaceWeakMap = new WeakMap<Workspace, boolean>();
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]);
}
+2 -2
View File
@@ -4,10 +4,10 @@ import { useAtomValue } from 'jotai';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { pageSettingsAtom } from '../atoms'; import { pageSettingsAtom } from '../atoms';
import { rootCurrentWorkspaceAtom } from '../atoms/root'; import { useCurrentWorkspace } from './current/use-current-workspace';
export const useGetPageInfoById = (): GetPageInfoById => { export const useGetPageInfoById = (): GetPageInfoById => {
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom); const [currentWorkspace] = useCurrentWorkspace();
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace); const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const pageMap = useMemo( const pageMap = useMemo(
() => Object.fromEntries(pageMetas.map(page => [page.id, page])), () => Object.fromEntries(pageMetas.map(page => [page.id, page])),
+36
View File
@@ -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<Promise<AffineOfficialWorkspace>>
>();
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<AffineOfficialWorkspace>
>
);
}
+11 -14
View File
@@ -2,19 +2,16 @@ import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud'; 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 { nanoid } from '@blocksuite/store';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { LocalAdapter } from '../adapters/local'; import { LocalAdapter } from '../adapters/local';
import { WorkspaceAdapters } from '../adapters/workspace'; 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'); 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. * This hook has the permission to all workspaces. Be careful when using it.
*/ */
export function useAppHelper() { export function useAppHelper() {
const workspaces = useWorkspaces();
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom); const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom); const set = useSetAtom(rootWorkspacesMetadataAtom);
return { return {
addLocalWorkspace: useCallback( addLocalWorkspace: useCallback(
async (workspaceId: string): Promise<string> => { async (workspaceId: string): Promise<string> => {
createEmptyBlockSuiteWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
saveWorkspaceToLocalStorage(workspaceId); saveWorkspaceToLocalStorage(workspaceId);
await set(workspaces => [ await set(workspaces => [
...workspaces, ...workspaces,
@@ -68,20 +65,20 @@ export function useAppHelper() {
const targetJotaiWorkspace = jotaiWorkspaces.find( const targetJotaiWorkspace = jotaiWorkspaces.find(
ws => ws.id === workspaceId ws => ws.id === workspaceId
); );
const targetWorkspace = workspaces.find(ws => ws.id === workspaceId); if (!targetJotaiWorkspace) {
if (!targetJotaiWorkspace || !targetWorkspace) {
throw new Error('page cannot be found'); throw new Error('page cannot be found');
} }
const targetWorkspace = getWorkspace(targetJotaiWorkspace.id);
// delete workspace from plugin // delete workspace from plugin
await WorkspaceAdapters[targetWorkspace.flavour].CRUD.delete( await WorkspaceAdapters[targetJotaiWorkspace.flavour].CRUD.delete(
// fixme: type casting targetWorkspace
targetWorkspace as any
); );
// delete workspace from jotai storage // delete workspace from jotai storage
await set(workspaces => workspaces.filter(ws => ws.id !== workspaceId)); await set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
}, },
[jotaiWorkspaces, set, workspaces] [jotaiWorkspaces, set]
), ),
}; };
} }
+24 -44
View File
@@ -20,7 +20,6 @@ import {
rootWorkspacesMetadataAtom, rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom'; } from '@affine/workspace/atom';
import { assertEquals, assertExists } from '@blocksuite/global/utils'; import { assertEquals, assertExists } from '@blocksuite/global/utils';
import type { PassiveDocProvider } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { import {
@@ -55,10 +54,12 @@ import {
RootAppSidebar, RootAppSidebar,
} from '../components/root-app-sidebar'; } from '../components/root-app-sidebar';
import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper'; 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 { useRouterHelper } from '../hooks/use-router-helper';
import { useRouterTitle } from '../hooks/use-router-title'; import { useRouterTitle } from '../hooks/use-router-title';
import { useWorkspaces } from '../hooks/use-workspaces';
import { import {
AllWorkspaceModals, AllWorkspaceModals,
CurrentWorkspaceModals, CurrentWorkspaceModals,
@@ -96,13 +97,6 @@ export const QuickSearch: FC = () => {
); );
}; };
export const AllWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
useWorkspaces();
return <>{children}</>;
};
declare global { declare global {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var HALTING_PROBLEM_TIMEOUT: number; var HALTING_PROBLEM_TIMEOUT: number;
@@ -166,13 +160,11 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
<> <>
{/* load all workspaces is costly, do not block the whole UI */} {/* load all workspaces is costly, do not block the whole UI */}
<Suspense fallback={null}> <Suspense fallback={null}>
<AllWorkspaceContext> <AllWorkspaceModals />
<AllWorkspaceModals /> <CurrentWorkspaceContext>
<CurrentWorkspaceContext> {/* fixme(himself65): don't re-render whole modals */}
{/* fixme(himself65): don't re-render whole modals */} <CurrentWorkspaceModals key={currentWorkspaceId} />
<CurrentWorkspaceModals key={currentWorkspaceId} /> </CurrentWorkspaceContext>
</CurrentWorkspaceContext>
</AllWorkspaceContext>
</Suspense> </Suspense>
<CurrentWorkspaceContext> <CurrentWorkspaceContext>
<Suspense fallback={<WorkspaceFallback />}> <Suspense fallback={<WorkspaceFallback />}>
@@ -223,36 +215,24 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
} }
//#endregion //#endregion
if (currentPageId) { //#region check if page is valid
const pageExist = if (
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (router.pathname === '/[workspaceId]/[pageId]' && !pageExist) {
router.push('/404').catch(console.error);
}
} else if (
router.pathname === '/[workspaceId]/[pageId]' &&
typeof router.query.pageId === 'string' && typeof router.query.pageId === 'string' &&
router.query.pageId !== currentPageId router.pathname === '/workspace/[workspaceId]/[pageId]' &&
currentPageId
) { ) {
setCurrentPageId(router.query.pageId); if (currentPageId !== router.query.pageId) {
jumpToPage(currentWorkspace.id, router.query.pageId).catch(console.error); setCurrentPageId(router.query.pageId);
} else {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
router.push('/404').catch(console.error);
}
}
} }
//#endregion
useEffect(() => { usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
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]);
useEffect(() => { useEffect(() => {
const page = currentWorkspace.blockSuiteWorkspace.getPage( const page = currentWorkspace.blockSuiteWorkspace.getPage(
@@ -303,7 +283,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const handleOpenSettingModal = useCallback(() => { const handleOpenSettingModal = useCallback(() => {
setOpenSettingModalAtom({ setOpenSettingModalAtom({
activeTab: 'appearance', activeTab: 'appearance',
workspace: null, workspaceId: null,
open: true, open: true,
}); });
}, [setOpenSettingModalAtom]); }, [setOpenSettingModalAtom]);
+10 -1
View File
@@ -1,10 +1,13 @@
import { Button, displayFlex, styled } from '@affine/component'; import { Button, displayFlex, styled } from '@affine/component';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/legacy/image'; import Image from 'next/legacy/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { useRouterHelper } from '../hooks/use-router-helper';
export const StyledContainer = styled('div')(() => { export const StyledContainer = styled('div')(() => {
return { return {
...displayFlex('center', 'center'), ...displayFlex('center', 'center'),
@@ -26,6 +29,7 @@ export const StyledContainer = styled('div')(() => {
export const NotfoundPage = () => { export const NotfoundPage = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const router = useRouter(); const router = useRouter();
const { jumpToSubPath } = useRouterHelper(router);
return ( return (
<StyledContainer data-testid="notFound"> <StyledContainer data-testid="notFound">
<Image alt="404" src="/imgs/invite-error.svg" width={360} height={270} /> <Image alt="404" src="/imgs/invite-error.svg" width={360} height={270} />
@@ -34,7 +38,12 @@ export const NotfoundPage = () => {
<Button <Button
shape="round" shape="round"
onClick={() => { onClick={() => {
router.push('/').catch(err => console.error(err)); const id = localStorage.getItem('last_workspace_id');
if (id) {
jumpToSubPath(id, WorkspaceSubPath.ALL).catch(console.error);
} else {
router.push('/').catch(err => console.error(err));
}
}} }}
> >
{t['Back Home']()} {t['Back Home']()}
+40 -30
View File
@@ -1,22 +1,33 @@
import { WorkspaceFallback } from '@affine/component/workspace'; import { WorkspaceFallback } from '@affine/component/workspace';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceSubPath } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getWorkspace } from '@affine/workspace/utils';
import { useAtomValue } from 'jotai';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Suspense, useEffect } from 'react'; import { Suspense, useEffect } from 'react';
import { PageLoading } from '../components/pure/loading';
import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper'; import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper';
import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces'; import { useWorkspace } from '../hooks/use-workspace';
import { AllWorkspaceContext } from '../layouts/workspace-layout'; import { useAppHelper } from '../hooks/use-workspaces';
import { AllWorkspaceModals } from '../providers/modal-provider'; import { AllWorkspaceModals } from '../providers/modal-provider';
const logger = new DebugLogger('index-page'); const logger = new DebugLogger('index:router');
type AllWorkspaceLoaderProps = {
id: string;
};
const WorkspaceLoader = (props: AllWorkspaceLoaderProps): null => {
useWorkspace(props.id);
return null;
};
const IndexPageInner = () => { const IndexPageInner = () => {
const router = useRouter(); const router = useRouter();
const { jumpToPage, jumpToSubPath } = useRouterHelper(router); const { jumpToPage, jumpToSubPath } = useRouterHelper(router);
const workspaces = useWorkspaces(); const meta = useAtomValue(rootWorkspacesMetadataAtom);
const helper = useAppHelper(); const helper = useAppHelper();
useEffect(() => { useEffect(() => {
@@ -25,15 +36,14 @@ const IndexPageInner = () => {
} }
const lastId = localStorage.getItem('last_workspace_id'); const lastId = localStorage.getItem('last_workspace_id');
const lastPageId = localStorage.getItem('last_page_id'); const lastPageId = localStorage.getItem('last_page_id');
const targetWorkspace =
(lastId && workspaces.find(({ id }) => id === lastId)) ||
workspaces.at(0);
if (targetWorkspace) { const target =
const nonTrashPages = (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
targetWorkspace.blockSuiteWorkspace.meta.pageMetas.filter( if (target) {
({ trash }) => !trash const targetWorkspace = getWorkspace(target.id);
); const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
({ trash }) => !trash
);
const pageId = const pageId =
nonTrashPages.find(({ id }) => id === lastPageId)?.id ?? nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
nonTrashPages.at(0)?.id; nonTrashPages.at(0)?.id;
@@ -56,38 +66,38 @@ const IndexPageInner = () => {
console.error(err); console.error(err);
}); });
}, 1000); }, 1000);
const dispose = const dispose = targetWorkspace.slots.pageAdded.once(pageId => {
targetWorkspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => { clearTimeout(clearId);
clearTimeout(clearId); jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE).catch(
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE).catch( err => {
err => { console.error(err);
console.error(err); }
} );
); });
});
return () => { return () => {
clearTimeout(clearId); clearTimeout(clearId);
dispose.dispose(); dispose.dispose();
}; };
} }
} else { } else {
console.warn('No target workspace. This should not happen in production'); console.warn('No workspace found');
} }
return; return;
}, [helper, jumpToPage, jumpToSubPath, router, workspaces]); }, [meta, helper, jumpToPage, jumpToSubPath, router]);
return ( return (
<Suspense fallback={<WorkspaceFallback />}> <>
<AllWorkspaceContext> {meta.map(({ id }) => (
<AllWorkspaceModals /> <WorkspaceLoader key={id} id={id} />
</AllWorkspaceContext> ))}
</Suspense> <AllWorkspaceModals />
</>
); );
}; };
const IndexPage: NextPage = () => { const IndexPage: NextPage = () => {
return ( return (
<Suspense fallback={<PageLoading />}> <Suspense fallback={<WorkspaceFallback />}>
<IndexPageInner /> <IndexPageInner />
</Suspense> </Suspense>
); );
@@ -8,7 +8,6 @@ import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type React from 'react'; import type React from 'react';
@@ -16,7 +15,6 @@ import { useCallback } from 'react';
import { getUIAdapter } from '../../../adapters/workspace'; import { getUIAdapter } from '../../../adapters/workspace';
import { pageSettingFamily } from '../../../atoms'; import { pageSettingFamily } from '../../../atoms';
import { rootCurrentWorkspaceAtom } from '../../../atoms/root';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper'; import { useRouterHelper } from '../../../hooks/use-router-helper';
import { WorkspaceLayout } from '../../../layouts/workspace-layout'; import { WorkspaceLayout } from '../../../layouts/workspace-layout';
@@ -65,13 +63,13 @@ const WorkspaceDetail: React.FC = () => {
return ( return (
<> <>
<Header <Header
currentWorkspace={currentWorkspace} currentWorkspaceId={currentWorkspace.id}
currentEntry={{ currentEntry={{
pageId: currentPageId, pageId: currentPageId,
}} }}
/> />
<PageDetail <PageDetail
currentWorkspace={currentWorkspace} currentWorkspaceId={currentWorkspace.id}
currentPageId={currentPageId} currentPageId={currentPageId}
onLoadEditor={onLoad} onLoadEditor={onLoad}
/> />
@@ -81,12 +79,11 @@ const WorkspaceDetail: React.FC = () => {
const WorkspaceDetailPage: NextPageWithLayout = () => { const WorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom); const [currentWorkspace] = useCurrentWorkspace();
const currentPageId = useAtomValue(rootCurrentPageIdAtom); const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const page = useBlockSuiteWorkspacePage( const page = currentPageId
currentWorkspace.blockSuiteWorkspace, ? currentWorkspace.blockSuiteWorkspace.getPage(currentPageId)
currentPageId : null;
);
if (!router.isReady) { if (!router.isReady) {
return <PageDetailSkeleton key="router-not-ready" />; return <PageDetailSkeleton key="router-not-ready" />;
} else if (!currentPageId || !page) { } else if (!currentPageId || !page) {
@@ -45,7 +45,7 @@ const AllPage: NextPageWithLayout = () => {
<title>{t['All pages']()} - AFFiNE</title> <title>{t['All pages']()} - AFFiNE</title>
</Head> </Head>
<Header <Header
currentWorkspace={currentWorkspace} currentWorkspaceId={currentWorkspace.id}
currentEntry={{ currentEntry={{
subPath: WorkspaceSubPath.ALL, subPath: WorkspaceSubPath.ALL,
}} }}
@@ -40,7 +40,7 @@ const SharedPages: NextPageWithLayout = () => {
<title>{t['Shared Pages']()} - AFFiNE</title> <title>{t['Shared Pages']()} - AFFiNE</title>
</Head> </Head>
<Header <Header
currentWorkspace={currentWorkspace} currentWorkspaceId={currentWorkspace.id}
currentEntry={{ currentEntry={{
subPath: WorkspaceSubPath.SHARED, subPath: WorkspaceSubPath.SHARED,
}} }}
@@ -44,7 +44,7 @@ const TrashPage: NextPageWithLayout = () => {
<title>{t['Trash']()} - AFFiNE</title> <title>{t['Trash']()} - AFFiNE</title>
</Head> </Head>
<Header <Header
currentWorkspace={currentWorkspace} currentWorkspaceId={currentWorkspace.id}
currentEntry={{ currentEntry={{
subPath: WorkspaceSubPath.TRASH, subPath: WorkspaceSubPath.TRASH,
}} }}
+12 -14
View File
@@ -5,7 +5,7 @@ import {
} from '@affine/workspace/atom'; } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { arrayMove } from '@dnd-kit/sortable'; import { arrayMove } from '@dnd-kit/sortable';
import { useAtom, useSetAtom } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { FC, ReactElement } from 'react'; import type { FC, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useTransition } from 'react'; import { lazy, Suspense, useCallback, useTransition } from 'react';
@@ -20,8 +20,6 @@ import {
} from '../atoms'; } from '../atoms';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useRouterHelper } from '../hooks/use-router-helper'; import { useRouterHelper } from '../hooks/use-router-helper';
import { useWorkspaces } from '../hooks/use-workspaces';
import type { AllWorkspace } from '../shared';
const SettingModal = lazy(() => const SettingModal = lazy(() =>
import('../components/affine/setting-modal').then(module => ({ import('../components/affine/setting-modal').then(module => ({
@@ -57,16 +55,16 @@ const OnboardingModal = lazy(() =>
export const Setting: FC = () => { export const Setting: FC = () => {
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const [{ open, workspace, activeTab }, setOpenSettingModalAtom] = const [{ open, workspaceId, activeTab }, setOpenSettingModalAtom] =
useAtom(openSettingModalAtom); useAtom(openSettingModalAtom);
assertExists(currentWorkspace); assertExists(currentWorkspace);
const onSettingClick = useCallback( const onSettingClick = useCallback(
({ ({
activeTab, activeTab,
workspace, workspaceId,
}: Pick<SettingAtom, 'activeTab' | 'workspace'>) => { }: Pick<SettingAtom, 'activeTab' | 'workspaceId'>) => {
setOpenSettingModalAtom(prev => ({ ...prev, activeTab, workspace })); setOpenSettingModalAtom(prev => ({ ...prev, activeTab, workspaceId }));
}, },
[setOpenSettingModalAtom] [setOpenSettingModalAtom]
); );
@@ -75,7 +73,7 @@ export const Setting: FC = () => {
<SettingModal <SettingModal
open={open} open={open}
activeTab={activeTab} activeTab={activeTab}
workspace={workspace} workspaceId={workspaceId}
onSettingClick={onSettingClick} onSettingClick={onSettingClick}
setOpen={useCallback( setOpen={useCallback(
open => { open => {
@@ -132,7 +130,7 @@ export const AllWorkspaceModals = (): ReactElement => {
const router = useRouter(); const router = useRouter();
const { jumpToSubPath } = useRouterHelper(router); const { jumpToSubPath } = useRouterHelper(router);
const workspaces = useWorkspaces(); const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom); const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom( const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom rootCurrentWorkspaceIdAtom
@@ -141,13 +139,13 @@ export const AllWorkspaceModals = (): ReactElement => {
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom); const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
const handleOpenSettingModal = useCallback( const handleOpenSettingModal = useCallback(
(workspace: AllWorkspace) => { (workspaceId: string) => {
setOpenWorkspacesModal(false); setOpenWorkspacesModal(false);
setOpenSettingModalAtom({ setOpenSettingModalAtom({
open: true, open: true,
activeTab: 'workspace', activeTab: 'workspace',
workspace, workspaceId,
}); });
}, },
[setOpenSettingModalAtom, setOpenWorkspacesModal] [setOpenSettingModalAtom, setOpenWorkspacesModal]
@@ -179,10 +177,10 @@ export const AllWorkspaceModals = (): ReactElement => {
[setWorkspaces, workspaces] [setWorkspaces, workspaces]
)} )}
onClickWorkspace={useCallback( onClickWorkspace={useCallback(
workspace => { workspaceId => {
setOpenWorkspacesModal(false); setOpenWorkspacesModal(false);
setCurrentWorkspaceId(workspace.id); setCurrentWorkspaceId(workspaceId);
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL).catch(error => { jumpToSubPath(workspaceId, WorkspaceSubPath.ALL).catch(error => {
console.error(error); console.error(error);
}); });
}, },
+3 -7
View File
@@ -26,14 +26,12 @@ export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
export enum WorkspaceSubPath { export enum WorkspaceSubPath {
ALL = 'all', ALL = 'all',
SETTING = 'setting',
TRASH = 'trash', TRASH = 'trash',
SHARED = 'shared', SHARED = 'shared',
} }
export const WorkspaceSubPathName = { export const WorkspaceSubPathName = {
[WorkspaceSubPath.ALL]: 'All Pages', [WorkspaceSubPath.ALL]: 'All Pages',
[WorkspaceSubPath.SETTING]: 'Settings',
[WorkspaceSubPath.TRASH]: 'Trash', [WorkspaceSubPath.TRASH]: 'Trash',
[WorkspaceSubPath.SHARED]: 'Shared', [WorkspaceSubPath.SHARED]: 'Shared',
} satisfies { } satisfies {
@@ -43,17 +41,15 @@ export const WorkspaceSubPathName = {
export const pathGenerator = { export const pathGenerator = {
all: workspaceId => `/workspace/${workspaceId}/all`, all: workspaceId => `/workspace/${workspaceId}/all`,
trash: workspaceId => `/workspace/${workspaceId}/trash`, trash: workspaceId => `/workspace/${workspaceId}/trash`,
setting: workspaceId => `/workspace/${workspaceId}/setting`,
shared: workspaceId => `/workspace/${workspaceId}/shared`, shared: workspaceId => `/workspace/${workspaceId}/shared`,
} satisfies { } satisfies {
[Path in WorkspaceSubPath]: (workspaceId: string) => string; [Path in WorkspaceSubPath]: (workspaceId: string) => string;
}; };
export const publicPathGenerator = { export const publicPathGenerator = {
all: workspaceId => `/public-workspace/${workspaceId}/all`, all: workspaceId => `/share/${workspaceId}/all`,
trash: workspaceId => `/public-workspace/${workspaceId}/trash`, trash: workspaceId => `/share/${workspaceId}/trash`,
setting: workspaceId => `/public-workspace/${workspaceId}/setting`, shared: workspaceId => `/share/${workspaceId}/shared`,
shared: workspaceId => `/public-workspace/${workspaceId}/shared`,
} satisfies { } satisfies {
[Path in WorkspaceSubPath]: (workspaceId: string) => string; [Path in WorkspaceSubPath]: (workspaceId: string) => string;
}; };
@@ -1,4 +1,4 @@
import { Skeleton } from '@mui/material'; import { NoSsr, Skeleton } from '@mui/material';
import { assignInlineVars } from '@vanilla-extract/dynamic'; import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx'; import clsx from 'clsx';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
@@ -107,7 +107,9 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
data-enable-animation={enableAnimation && !isResizing} data-enable-animation={enableAnimation && !isResizing}
> >
<nav className={navStyle} ref={navRef} data-testid="app-sidebar"> <nav className={navStyle} ref={navRef} data-testid="app-sidebar">
<SidebarHeader router={props.router} /> <NoSsr>
<SidebarHeader router={props.router} />
</NoSsr>
<div className={navBodyStyle} data-testid="sliderBar-inner"> <div className={navBodyStyle} data-testid="sliderBar-inner">
{props.children} {props.children}
</div> </div>
@@ -1,10 +1,14 @@
import type {
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { useStaticBlockSuiteWorkspace } from '@affine/workspace/utils';
import { SettingsIcon } from '@blocksuite/icons'; import { SettingsIcon } from '@blocksuite/icons';
import {
CloudWorkspaceIcon as DefaultCloudWorkspaceIcon,
CollaborationIcon as DefaultJoinedWorkspaceIcon,
LocalDataIcon as DefaultLocalDataIcon,
LocalWorkspaceIcon as DefaultLocalWorkspaceIcon,
} from '@blocksuite/icons';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import type { FC } from 'react'; import type { FC } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
@@ -18,16 +22,8 @@ import {
} from './styles'; } from './styles';
export type WorkspaceTypeProps = { export type WorkspaceTypeProps = {
workspace: AffineCloudWorkspace | LocalWorkspace; flavour: WorkspaceFlavour;
}; };
import {
CloudWorkspaceIcon as DefaultCloudWorkspaceIcon,
CollaborationIcon as DefaultJoinedWorkspaceIcon,
LocalDataIcon as DefaultLocalDataIcon,
LocalWorkspaceIcon as DefaultLocalWorkspaceIcon,
} from '@blocksuite/icons';
const JoinedWorkspaceIcon = () => { const JoinedWorkspaceIcon = () => {
return <DefaultJoinedWorkspaceIcon style={{ color: '#FF646B' }} />; return <DefaultJoinedWorkspaceIcon style={{ color: '#FF646B' }} />;
}; };
@@ -43,12 +39,12 @@ const LocalDataIcon = () => {
return <DefaultLocalDataIcon style={{ color: '#62CD80' }} />; return <DefaultLocalDataIcon style={{ color: '#62CD80' }} />;
}; };
const WorkspaceType: FC<WorkspaceTypeProps> = ({ workspace }) => { const WorkspaceType: FC<WorkspaceTypeProps> = ({ flavour }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
// fixme: cloud regression // fixme: cloud regression
const isOwner = true; const isOwner = true;
if (workspace.flavour === WorkspaceFlavour.LOCAL) { if (flavour === WorkspaceFlavour.LOCAL) {
return ( return (
<p title={t['Local Workspace']()}> <p title={t['Local Workspace']()}>
<LocalWorkspaceIcon /> <LocalWorkspaceIcon />
@@ -72,51 +68,46 @@ const WorkspaceType: FC<WorkspaceTypeProps> = ({ workspace }) => {
export type WorkspaceCardProps = { export type WorkspaceCardProps = {
currentWorkspaceId: string | null; currentWorkspaceId: string | null;
workspace: AffineCloudWorkspace | LocalWorkspace; meta: RootWorkspaceMetadata;
onClick: (workspace: AffineCloudWorkspace | LocalWorkspace) => void; onClick: (workspaceId: string) => void;
onSettingClick: (workspace: AffineCloudWorkspace | LocalWorkspace) => void; onSettingClick: (workspaceId: string) => void;
}; };
export const WorkspaceCard: FC<WorkspaceCardProps> = ({ export const WorkspaceCard: FC<WorkspaceCardProps> = ({
workspace,
onClick, onClick,
onSettingClick, onSettingClick,
currentWorkspaceId, currentWorkspaceId,
meta,
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace); const workspace = useStaticBlockSuiteWorkspace(meta.id);
const [name] = useBlockSuiteWorkspaceName(workspace);
return ( return (
<StyledCard <StyledCard
data-testid="workspace-card" data-testid="workspace-card"
onClick={useCallback(() => { onClick={useCallback(() => {
onClick(workspace); onClick(meta.id);
}, [onClick, workspace])} }, [onClick, meta.id])}
active={workspace.id === currentWorkspaceId} active={workspace.id === currentWorkspaceId}
> >
<WorkspaceAvatar size={58} workspace={workspace} /> <WorkspaceAvatar size={58} workspace={workspace} />
<StyleWorkspaceInfo> <StyleWorkspaceInfo>
<StyleWorkspaceTitle>{name}</StyleWorkspaceTitle> <StyleWorkspaceTitle>{name}</StyleWorkspaceTitle>
<WorkspaceType workspace={workspace} /> <WorkspaceType flavour={meta.flavour} />
{workspace.flavour === WorkspaceFlavour.LOCAL && ( {meta.flavour === WorkspaceFlavour.LOCAL && (
<p title={t['Available Offline']()}> <p title={t['Available Offline']()}>
<LocalDataIcon /> <LocalDataIcon />
<span>{t['Available Offline']()}</span> <span>{t['Available Offline']()}</span>
</p> </p>
)} )}
{/* {workspace.flavour === WorkspaceFlavour.AFFINE && workspace.public && (
<p title={t['Published to Web']()}>
<PublishIcon />
<span>{t['Published to Web']()}</span>
</p>
)} */}
</StyleWorkspaceInfo> </StyleWorkspaceInfo>
<StyledSettingLink <StyledSettingLink
className="setting-entry" className="setting-entry"
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
onSettingClick(workspace); onSettingClick(meta.id);
}} }}
> >
<SettingsIcon /> <SettingsIcon />
@@ -1,8 +1,3 @@
import type {
AffineCloudWorkspace,
AffinePublicWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import type { Workspace } from '@blocksuite/store'; import type { Workspace } from '@blocksuite/store';
import * as RadixAvatar from '@radix-ui/react-avatar'; import * as RadixAvatar from '@radix-ui/react-avatar';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
@@ -15,11 +10,7 @@ import { avatarImageStyle, avatarStyle } from './index.css';
export type WorkspaceAvatarProps = { export type WorkspaceAvatarProps = {
size?: number; size?: number;
workspace: workspace: Workspace | null;
| AffineCloudWorkspace
| LocalWorkspace
| AffinePublicWorkspace
| null;
className?: string; className?: string;
}; };
@@ -60,13 +51,9 @@ export const WorkspaceAvatar: React.FC<WorkspaceAvatarProps> = ({
workspace, workspace,
...props ...props
}) => { }) => {
if (workspace && 'blockSuiteWorkspace' in workspace) { if (workspace) {
return ( return (
<BlockSuiteWorkspaceAvatar <BlockSuiteWorkspaceAvatar {...props} size={size} workspace={workspace} />
{...props}
size={size}
workspace={workspace.blockSuiteWorkspace}
/>
); );
} }
return ( return (
@@ -2,6 +2,7 @@ import type {
AffineCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { import {
DndContext, DndContext,
@@ -10,7 +11,8 @@ import {
useSensors, useSensors,
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { SortableContext, useSortable } from '@dnd-kit/sortable'; import { SortableContext, useSortable } from '@dnd-kit/sortable';
import type { FC } from 'react'; import type { CSSProperties, FC } from 'react';
import { useMemo } from 'react';
import { WorkspaceCard } from '../../components/card/workspace-card'; import { WorkspaceCard } from '../../components/card/workspace-card';
import { workspaceItemStyle } from './index.css'; import { workspaceItemStyle } from './index.css';
@@ -19,26 +21,29 @@ export type WorkspaceListProps = {
disabled?: boolean; disabled?: boolean;
currentWorkspaceId: string | null; currentWorkspaceId: string | null;
items: (AffineCloudWorkspace | LocalWorkspace)[]; items: (AffineCloudWorkspace | LocalWorkspace)[];
onClick: (workspace: AffineCloudWorkspace | LocalWorkspace) => void; onClick: (workspaceId: string) => void;
onSettingClick: (workspace: AffineCloudWorkspace | LocalWorkspace) => void; onSettingClick: (workspaceId: string) => void;
onDragEnd: (event: DragEndEvent) => void; onDragEnd: (event: DragEndEvent) => void;
}; };
const SortableWorkspaceItem: FC< const SortableWorkspaceItem: FC<
Omit<WorkspaceListProps, 'items'> & { Omit<WorkspaceListProps, 'items'> & {
item: AffineCloudWorkspace | LocalWorkspace; item: RootWorkspaceMetadata;
} }
> = props => { > = props => {
const { setNodeRef, attributes, listeners, transform } = useSortable({ const { setNodeRef, attributes, listeners, transform } = useSortable({
id: props.item.id, id: props.item.id,
}); });
const style: React.CSSProperties = { const style: CSSProperties = useMemo(
transform: transform () => ({
? `translate3d(${transform.x}px, ${transform.y}px, 0)` transform: transform
: undefined, ? `translate3d(${transform.x}px, ${transform.y}px, 0)`
pointerEvents: props.disabled ? 'none' : undefined, : undefined,
opacity: props.disabled ? 0.6 : undefined, pointerEvents: props.disabled ? 'none' : undefined,
}; opacity: props.disabled ? 0.6 : undefined,
}),
[props.disabled, transform]
);
return ( return (
<div <div
className={workspaceItemStyle} className={workspaceItemStyle}
@@ -50,7 +55,7 @@ const SortableWorkspaceItem: FC<
> >
<WorkspaceCard <WorkspaceCard
currentWorkspaceId={props.currentWorkspaceId} currentWorkspaceId={props.currentWorkspaceId}
workspace={props.item} meta={props.item}
onClick={props.onClick} onClick={props.onClick}
onSettingClick={props.onSettingClick} onSettingClick={props.onSettingClick}
/> />
+10 -6
View File
@@ -1,8 +1,10 @@
import type { TooltipProps } from '@mui/material'; import type { TooltipProps } from '@mui/material';
import { NoSsr } from '@mui/material';
import { styled } from '../../styles'; import { styled } from '../../styles';
import { Popper, type PopperProps } from '../popper'; import { Popper, type PopperProps } from '../popper';
import StyledPopperContainer from '../shared/container'; import StyledPopperContainer from '../shared/container';
const StyledTooltip = styled(StyledPopperContainer)(() => { const StyledTooltip = styled(StyledPopperContainer)(() => {
return { return {
maxWidth: '320px', maxWidth: '320px',
@@ -19,11 +21,13 @@ const StyledTooltip = styled(StyledPopperContainer)(() => {
export const Tooltip = (props: PopperProps & Omit<TooltipProps, 'title'>) => { export const Tooltip = (props: PopperProps & Omit<TooltipProps, 'title'>) => {
const { content, placement = 'top-start', children } = props; const { content, placement = 'top-start', children } = props;
return ( return (
<Popper <NoSsr>
{...props} <Popper
content={<StyledTooltip placement={placement}>{content}</StyledTooltip>} {...props}
> content={<StyledTooltip placement={placement}>{content}</StyledTooltip>}
{children} >
</Popper> {children}
</Popper>
</NoSsr>
); );
}; };
+15 -8
View File
@@ -50,18 +50,25 @@ export interface SQLiteDBDownloadProvider extends ActiveDocProvider {
flavour: 'sqlite-download'; flavour: 'sqlite-download';
} }
// todo: update type with nest.js type BaseWorkspace = {
export type AffineCloudWorkspace = Omit<LocalWorkspace, 'flavour'> & { flavour: string;
flavour: WorkspaceFlavour.AFFINE_CLOUD; id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
}; };
export interface LocalWorkspace { export interface AffineCloudWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.AFFINE_CLOUD;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
}
export interface LocalWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.LOCAL; flavour: WorkspaceFlavour.LOCAL;
id: string; id: string;
blockSuiteWorkspace: BlockSuiteWorkspace; blockSuiteWorkspace: BlockSuiteWorkspace;
} }
export interface AffinePublicWorkspace { export interface AffinePublicWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.PUBLIC; flavour: WorkspaceFlavour.PUBLIC;
id: string; id: string;
blockSuiteWorkspace: BlockSuiteWorkspace; blockSuiteWorkspace: BlockSuiteWorkspace;
@@ -107,15 +114,15 @@ export interface WorkspaceRegistry {
export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> { export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> {
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>; create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
delete: (workspace: WorkspaceRegistry[Flavour]) => Promise<void>; delete: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<void>;
get: (workspaceId: string) => Promise<WorkspaceRegistry[Flavour] | null>; get: (workspaceId: string) => Promise<WorkspaceRegistry[Flavour] | null>;
// not supported yet // not supported yet
// update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>; // update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
list: () => Promise<WorkspaceRegistry[Flavour][]>; list: () => Promise<WorkspaceRegistry[Flavour][]>;
} }
type UIBaseProps<Flavour extends keyof WorkspaceRegistry> = { type UIBaseProps<_Flavour extends keyof WorkspaceRegistry> = {
currentWorkspace: WorkspaceRegistry[Flavour]; currentWorkspaceId: string;
}; };
export type WorkspaceHeaderProps<Flavour extends keyof WorkspaceRegistry> = export type WorkspaceHeaderProps<Flavour extends keyof WorkspaceRegistry> =
@@ -3,9 +3,10 @@
*/ */
import 'fake-indexeddb/auto'; import 'fake-indexeddb/auto';
import type { LocalWorkspace, WorkspaceCRUD } from '@affine/env/workspace'; import type { WorkspaceCRUD } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { assertExists } from '@blocksuite/global/utils';
import { Workspace } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store';
import { afterEach, assertType, describe, expect, test } from 'vitest'; import { afterEach, assertType, describe, expect, test } from 'vitest';
@@ -29,11 +30,7 @@ describe('crud', () => {
test('delete not exist', async () => { test('delete not exist', async () => {
await expect(async () => await expect(async () =>
CRUD.delete({ CRUD.delete(new Workspace({ id: 'test' }))
id: 'not_exist',
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: new Workspace({ id: 'test' }),
})
).rejects.toThrowError(); ).rejects.toThrowError();
}); });
@@ -54,7 +51,8 @@ describe('crud', () => {
const list = await CRUD.list(); const list = await CRUD.list();
expect(list.length).toBe(1); expect(list.length).toBe(1);
expect(list[0].id).toBe(id); expect(list[0].id).toBe(id);
const localWorkspace = list.at(0) as LocalWorkspace; const localWorkspace = list.at(0);
assertExists(localWorkspace);
expect(localWorkspace.id).toBe(id); expect(localWorkspace.id).toBe(id);
expect(localWorkspace.flavour).toBe(WorkspaceFlavour.LOCAL); expect(localWorkspace.flavour).toBe(WorkspaceFlavour.LOCAL);
expect(localWorkspace.blockSuiteWorkspace.doc.toJSON()).toEqual({ expect(localWorkspace.blockSuiteWorkspace.doc.toJSON()).toEqual({
@@ -64,7 +62,7 @@ describe('crud', () => {
}), }),
}); });
await CRUD.delete(localWorkspace); await CRUD.delete(localWorkspace.blockSuiteWorkspace);
expect(await CRUD.get(id)).toBeNull(); expect(await CRUD.get(id)).toBeNull();
expect(await CRUD.list()).toEqual([]); expect(await CRUD.list()).toEqual([]);
}); });
+53 -48
View File
@@ -7,25 +7,19 @@ import {
} from '@affine/workspace/providers'; } from '@affine/workspace/providers';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import type { import type {
ActiveDocProvider,
DocProviderCreator, DocProviderCreator,
Generator, Generator,
StoreOptions, StoreOptions,
} from '@blocksuite/store'; } from '@blocksuite/store';
import { createIndexeddbStorage, Workspace } from '@blocksuite/store'; import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
import { rootStore } from '@toeverything/plugin-infra/manager'; import { useAtomValue } from 'jotai/react';
import type { Atom } from 'jotai/vanilla';
import { atom } from 'jotai/vanilla';
import { rootWorkspacesMetadataAtom } from './atom';
import { createStaticStorage } from './blob/local-static-storage'; import { createStaticStorage } from './blob/local-static-storage';
import { createSQLiteStorage } from './blob/sqlite-blob-storage'; import { createSQLiteStorage } from './blob/sqlite-blob-storage';
export function cleanupWorkspace(flavour: WorkspaceFlavour) {
rootStore
.set(rootWorkspacesMetadataAtom, metas =>
metas.filter(meta => meta.flavour !== flavour)
)
.catch(console.error);
}
function setEditorFlags(workspace: Workspace) { function setEditorFlags(workspace: Workspace) {
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => { Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
workspace.awarenessStore.setFlag( workspace.awarenessStore.setFlag(
@@ -39,12 +33,53 @@ function setEditorFlags(workspace: Workspace) {
); );
} }
const hashMap = new Map<string, Workspace>(); // guid -> Workspace
export const workspaceHashMap = new Map<string, Workspace>();
/** const workspacePassiveAtomWeakMap = new WeakMap<
* @internal test only Workspace,
*/ Atom<Promise<Workspace>>
export const _cleanupBlockSuiteWorkspaceCache = () => hashMap.clear(); >();
const workspaceActiveWeakMap = new WeakMap<Workspace, boolean>();
export function getWorkspace(id: string) {
if (!workspaceHashMap.has(id)) {
throw new Error('Workspace not found');
}
return workspaceHashMap.get(id) as Workspace;
}
export function getPassiveBlockSuiteWorkspaceAtom(
id: string
): Atom<Promise<Workspace>> {
if (!workspaceHashMap.has(id)) {
throw new Error('Workspace not found');
}
const workspace = workspaceHashMap.get(id) as Workspace;
if (!workspacePassiveAtomWeakMap.has(workspace)) {
const baseAtom = atom(async () => {
if (workspaceActiveWeakMap.get(workspace) !== true) {
const providers = workspace.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;
}
workspaceActiveWeakMap.set(workspace, true);
}
return workspace;
});
workspacePassiveAtomWeakMap.set(workspace, baseAtom);
}
return workspacePassiveAtomWeakMap.get(workspace) as Atom<Promise<Workspace>>;
}
export function useStaticBlockSuiteWorkspace(id: string): Workspace {
return useAtomValue(getPassiveBlockSuiteWorkspaceAtom(id));
}
export function createEmptyBlockSuiteWorkspace( export function createEmptyBlockSuiteWorkspace(
id: string, id: string,
@@ -73,8 +108,8 @@ export function createEmptyBlockSuiteWorkspace(
const providerCreators: DocProviderCreator[] = []; const providerCreators: DocProviderCreator[] = [];
const prefix: string = config?.cachePrefix ?? ''; const prefix: string = config?.cachePrefix ?? '';
const cacheKey = `${prefix}${id}`; const cacheKey = `${prefix}${id}`;
if (hashMap.has(cacheKey)) { if (workspaceHashMap.has(cacheKey)) {
return hashMap.get(cacheKey) as Workspace; return workspaceHashMap.get(cacheKey) as Workspace;
} }
const idGenerator = config?.idGenerator; const idGenerator = config?.idGenerator;
@@ -111,36 +146,6 @@ export function createEmptyBlockSuiteWorkspace(
.register(AffineSchemas) .register(AffineSchemas)
.register(__unstableSchemas); .register(__unstableSchemas);
setEditorFlags(workspace); setEditorFlags(workspace);
hashMap.set(cacheKey, workspace); workspaceHashMap.set(cacheKey, workspace);
return workspace; return workspace;
} }
export class CallbackSet extends Set<() => void> {
#ready = false;
get ready(): boolean {
return this.#ready;
}
set ready(v: boolean) {
this.#ready = v;
}
override add(cb: () => void) {
if (this.ready) {
cb();
return this;
}
if (this.has(cb)) {
return this;
}
return super.add(cb);
}
override delete(cb: () => void) {
if (this.has(cb)) {
return super.delete(cb);
}
return false;
}
}
+8 -1
View File
@@ -19,6 +19,7 @@ test('database is useable', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitEditorLoad(page); await waitEditorLoad(page);
await newPage(page); await newPage(page);
await waitEditorLoad(page);
await page.keyboard.insertText('test title'); await page.keyboard.insertText('test title');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
const title = page.locator('.affine-default-page-block-title'); const title = page.locator('.affine-default-page-block-title');
@@ -29,9 +30,11 @@ test('database is useable', async ({ page }) => {
await page.reload(); await page.reload();
await waitEditorLoad(page); await waitEditorLoad(page);
await newPage(page); await newPage(page);
await waitEditorLoad(page);
await page.keyboard.insertText('test title2'); await page.keyboard.insertText('test title2');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
const title2 = page.locator('.affine-default-page-block-title'); const title2 = page.locator('.affine-default-page-block-title');
await page.waitForTimeout(500);
expect(await title2.innerText()).toBe('test title2'); expect(await title2.innerText()).toBe('test title2');
await addDatabase(page); await addDatabase(page);
const database2 = page.locator('.affine-database-table'); const database2 = page.locator('.affine-database-table');
@@ -42,11 +45,13 @@ test('link page is useable', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitEditorLoad(page); await waitEditorLoad(page);
await newPage(page); await newPage(page);
await waitEditorLoad(page);
await page.keyboard.insertText('page1'); await page.keyboard.insertText('page1');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
const title = page.locator('.affine-default-page-block-title'); const title = page.locator('.affine-default-page-block-title');
expect(await title.innerText()).toBe('page1'); expect(await title.innerText()).toBe('page1');
await newPage(page); await newPage(page);
await waitEditorLoad(page);
await page.keyboard.insertText('page2'); await page.keyboard.insertText('page2');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
const title2 = page.locator('.affine-default-page-block-title'); const title2 = page.locator('.affine-default-page-block-title');
@@ -59,7 +64,9 @@ test('link page is useable', async ({ page }) => {
await page.keyboard.press('1'); await page.keyboard.press('1');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
const link = page.locator('.affine-reference'); const link = page.locator('.affine-reference');
expect(link).toBeVisible(); await page.waitForTimeout(500);
await expect(link).toBeVisible();
await page.click('.affine-reference'); await page.click('.affine-reference');
await page.waitForTimeout(500);
expect(await title.innerText()).toBe('page1'); expect(await title.innerText()).toBe('page1');
}); });
@@ -13,6 +13,7 @@ test('drag a page from "All pages" list onto the "Trash" folder in the sidebar t
await openHomePage(page); await openHomePage(page);
await waitEditorLoad(page); await waitEditorLoad(page);
await page.getByText('All Pages').click(); await page.getByText('All Pages').click();
await page.waitForTimeout(500);
} }
const title = 'AFFiNE - not just a note taking app'; const title = 'AFFiNE - not just a note taking app';
@@ -103,6 +103,7 @@ test('create multi workspace in the workspace list', async ({ page }) => {
await page.reload(); await page.reload();
await openWorkspaceListModal(page); await openWorkspaceListModal(page);
await page.waitForTimeout(1000);
// check workspace list length // check workspace list length
{ {
const workspaceCards1 = await page.$$('data-testid=workspace-card'); const workspaceCards1 = await page.$$('data-testid=workspace-card');
+2 -2
View File
@@ -8,9 +8,9 @@ test('goto not found page', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitEditorLoad(page); await waitEditorLoad(page);
const currentUrl = page.url(); const currentUrl = page.url();
const invalidUrl = currentUrl.replace(/\/$/, '') + '/invalid'; const invalidUrl = currentUrl.replace('hello-world', 'invalid');
await page.goto(invalidUrl); await page.goto(invalidUrl);
await expect(page.getByTestId('notFound').isVisible()).toBeTruthy(); await expect(page.getByTestId('notFound')).toBeVisible();
}); });
test('goto not found workspace', async ({ page }) => { test('goto not found workspace', async ({ page }) => {