mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 03:49:56 +08:00
refactor: lazy load workspaces (#3091)
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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])),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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']()}
|
||||||
|
|||||||
@@ -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,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+15
-8
@@ -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([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user