refactor: workspace loading logic (#1966)

This commit is contained in:
Himself65
2023-04-16 16:02:41 -05:00
committed by GitHub
parent caa292e097
commit 7bbe67af43
88 changed files with 2684 additions and 2268 deletions

View File

@@ -78,6 +78,7 @@ const nextConfig = {
}, },
reactStrictMode: true, reactStrictMode: true,
transpilePackages: [ transpilePackages: [
'jotai-devtools',
'@affine/component', '@affine/component',
'@affine/i18n', '@affine/i18n',
'@affine/debug', '@affine/debug',

View File

@@ -36,6 +36,7 @@
"css-spring": "^4.1.0", "css-spring": "^4.1.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"jotai": "^2.0.4", "jotai": "^2.0.4",
"jotai-devtools": "^0.4.0",
"lit": "^2.7.2", "lit": "^2.7.2",
"lottie-web": "^5.11.0", "lottie-web": "^5.11.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",

View File

@@ -1,40 +1,68 @@
import { DebugLogger } from '@affine/debug';
import { atomWithSyncStorage } from '@affine/jotai'; import { atomWithSyncStorage } from '@affine/jotai';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor'; import {
rootCurrentEditorAtom,
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins'; import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms');
// workspace necessary atoms // workspace necessary atoms
export const currentWorkspaceIdAtom = atom<string | null>(null); /**
export const currentPageIdAtom = atom<string | null>(null); * @deprecated Use `rootCurrentWorkspaceIdAtom` directly instead.
export const currentEditorAtom = atom<Readonly<EditorContainer> | null>(null); */
export const currentWorkspaceIdAtom = rootCurrentWorkspaceIdAtom;
// todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadata[] {
const Plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
} satisfies RootWorkspaceMetadata)
);
}).filter((ids): ids is RootWorkspaceMetadata => !!ids);
}
setAtom(metadata => {
if (metadata.length === 0) {
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
};
/**
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
*/
export const currentPageIdAtom = rootCurrentPageIdAtom;
/**
* @deprecated Use `rootCurrentEditorAtom` directly instead.
*/
export const currentEditorAtom = rootCurrentEditorAtom;
// modal atoms // modal atoms
export const openWorkspacesModalAtom = atom(false); export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom(false); export const openCreateWorkspaceModalAtom = atom(false);
export const openQuickSearchModalAtom = atom(false); export const openQuickSearchModalAtom = atom(false);
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => { export { workspacesAtom } from './root';
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(jotaiWorkspacesAtom).filter(workspace =>
flavours.includes(workspace.flavour)
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
type View = { id: string; mode: 'page' | 'edgeless' }; type View = { id: string; mode: 'page' | 'edgeless' };

View File

@@ -0,0 +1,78 @@
//#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
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 => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom).filter(workspace =>
flavours.includes(workspace.flavour)
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
logger.info('workspaces', workspaces);
workspaces.forEach(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 workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
/**
* 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 => {
const metadata = 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}.`);
}
const workspace = await WorkspacePlugins[targetWorkspace.flavour].CRUD.get(
targetWorkspace.id
);
if (!workspace) {
throw new Error(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
);
}
return workspace;
}
);
// Do not add `rootCurrentWorkspacePageAtom`, this is not needed.
// It can be derived from `rootCurrentWorkspaceAtom` and `rootCurrentPageIdAtom`
//#endregion

View File

@@ -3,6 +3,7 @@
*/ */
import 'fake-indexeddb/auto'; import 'fake-indexeddb/auto';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import matchers from '@testing-library/jest-dom/matchers'; import matchers from '@testing-library/jest-dom/matchers';
import type { RenderResult } from '@testing-library/react'; import type { RenderResult } from '@testing-library/react';
@@ -12,11 +13,9 @@ import type { FC, PropsWithChildren } from 'react';
import { beforeEach, describe, expect, test } from 'vitest'; import { beforeEach, describe, expect, test } from 'vitest';
import { workspacesAtom } from '../../atoms'; import { workspacesAtom } from '../../atoms';
import { import { rootCurrentWorkspaceAtom } from '../../atoms/root';
currentWorkspaceAtom, import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
useCurrentWorkspace, import { useAppHelper } from '../../hooks/use-workspaces';
} from '../../hooks/current/use-current-workspace';
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider'; import { ThemeProvider } from '../../providers/ThemeProvider';
import type { BlockSuiteWorkspace } from '../../shared'; import type { BlockSuiteWorkspace } from '../../shared';
import type { PinboardProps } from '../pure/workspace-slider-bar/Pinboard'; import type { PinboardProps } from '../pure/workspace-slider-bar/Pinboard';
@@ -42,24 +41,26 @@ const initPinBoard = async () => {
// - pinboard2 // - pinboard2
// - noPinboardPage // - noPinboardPage
const mutationHook = renderHook(() => useWorkspacesHelper(), { const mutationHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
const rootPageIds = ['hasPinboardPage', 'noPinboardPage']; const rootPageIds = ['hasPinboardPage', 'noPinboardPage'];
const pinboardPageIds = ['pinboard1', 'pinboard2']; const pinboardPageIds = ['pinboard1', 'pinboard2'];
const id = await mutationHook.result.current.createLocalWorkspace('test0'); const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom);
mutationHook.rerender();
await store.get(currentWorkspaceAtom); store.set(rootCurrentWorkspaceIdAtom, id);
await store.get(workspacesAtom);
await store.get(rootCurrentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), { const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
currentWorkspaceHook.result.current[1](id); currentWorkspaceHook.result.current[1](id);
const currentWorkspace = await store.get(currentWorkspaceAtom); const currentWorkspace = await store.get(rootCurrentWorkspaceAtom);
const blockSuiteWorkspace = const blockSuiteWorkspace =
currentWorkspace?.blockSuiteWorkspace as BlockSuiteWorkspace; currentWorkspace?.blockSuiteWorkspace as BlockSuiteWorkspace;
mutationHook.rerender();
// create root pinboard // create root pinboard
mutationHook.result.current.createWorkspacePage(id, 'rootPinboard'); mutationHook.result.current.createWorkspacePage(id, 'rootPinboard');
blockSuiteWorkspace.meta.setPageMeta('rootPinboard', { blockSuiteWorkspace.meta.setPageMeta('rootPinboard', {
@@ -73,7 +74,7 @@ const initPinBoard = async () => {
subpageIds: rootPageId === rootPageIds[0] ? pinboardPageIds : [], subpageIds: rootPageId === rootPageIds[0] ? pinboardPageIds : [],
}); });
}); });
// create children to firs parent // create children to first parent
pinboardPageIds.forEach(pinboardId => { pinboardPageIds.forEach(pinboardId => {
mutationHook.result.current.createWorkspacePage(id, pinboardId); mutationHook.result.current.createWorkspacePage(id, pinboardId);
blockSuiteWorkspace.meta.setPageMeta(pinboardId, { blockSuiteWorkspace.meta.setPageMeta(pinboardId, {

View File

@@ -3,23 +3,27 @@
*/ */
import 'fake-indexeddb/auto'; import 'fake-indexeddb/auto';
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
} from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import { render, renderHook } from '@testing-library/react'; import { render, renderHook } from '@testing-library/react';
import { createStore, getDefaultStore, Provider } from 'jotai'; import { createStore, getDefaultStore, Provider, useAtomValue } from 'jotai';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type React from 'react'; import type React from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { beforeEach, describe, expect, test, vi } from 'vitest'; import { beforeEach, describe, expect, test, vi } from 'vitest';
import { workspacesAtom } from '../../atoms'; import { workspacesAtom } from '../../atoms';
import { useCurrentPageId } from '../../hooks/current/use-current-page-id'; import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import { import {
currentWorkspaceAtom, currentWorkspaceAtom,
useCurrentWorkspace, useCurrentWorkspace,
} from '../../hooks/current/use-current-workspace'; } from '../../hooks/current/use-current-workspace';
import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper'; import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper';
import { useWorkspacesHelper } from '../../hooks/use-workspaces'; import { useAppHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider'; import { ThemeProvider } from '../../providers/ThemeProvider';
import { pathGenerator } from '../../shared'; import { pathGenerator } from '../../shared';
import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar'; import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar';
@@ -45,21 +49,22 @@ describe('WorkSpaceSliderBar', () => {
const onOpenWorkspaceListModalFn = vi.fn(); const onOpenWorkspaceListModalFn = vi.fn();
const onOpenQuickSearchModalFn = vi.fn(); const onOpenQuickSearchModalFn = vi.fn();
const mutationHook = renderHook(() => useWorkspacesHelper(), { const mutationHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
const id = await mutationHook.result.current.createLocalWorkspace('test0'); const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom); await store.get(workspacesAtom);
mutationHook.rerender(); mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'test1'); mutationHook.result.current.createWorkspacePage(id, 'test1');
await store.get(currentWorkspaceAtom); store.set(rootCurrentWorkspaceIdAtom, id);
await store.get(rootCurrentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), { const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
let i = 0; let i = 0;
const Component = () => { const Component = () => {
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId(); const currentPageId = useAtomValue(rootCurrentPageIdAtom);
assertExists(currentWorkspace); assertExists(currentWorkspace);
const helper = useBlockSuiteWorkspaceHelper( const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace

View File

@@ -1,3 +1,4 @@
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import type { PageBlockModel } from '@blocksuite/blocks'; import type { PageBlockModel } from '@blocksuite/blocks';
import { PlusIcon } from '@blocksuite/icons'; import { PlusIcon } from '@blocksuite/icons';
@@ -5,6 +6,7 @@ import { assertEquals, nanoid } from '@blocksuite/store';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import type { NextRouter } from 'next/router'; import type { NextRouter } from 'next/router';
import type React from 'react'; import type React from 'react';
import { useCallback } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper'; import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper';
import { useRouterHelper } from '../../../hooks/use-router-helper'; import { useRouterHelper } from '../../../hooks/use-router-helper';
@@ -35,25 +37,25 @@ export const Footer: React.FC<FooterProps> = ({
return ( return (
<Command.Item <Command.Item
data-testid="quick-search-add-new-page" data-testid="quick-search-add-new-page"
onSelect={async () => { onSelect={useCallback(() => {
onClose();
const id = nanoid(); const id = nanoid();
const page = await createPage(id); const page = createPage(id);
assertEquals(page.id, id); assertEquals(page.id, id);
await jumpToPage(blockSuiteWorkspace.id, page.id); initPage(page);
if (!query) { const block = page.getBlockByFlavour(
return;
}
const newPage = blockSuiteWorkspace.getPage(page.id);
if (newPage) {
const block = newPage.getBlockByFlavour(
'affine:page' 'affine:page'
)[0] as PageBlockModel; )[0] as PageBlockModel;
if (block) { if (block) {
block.title.insert(query, 0); block.title.insert(query, 0);
} else {
console.warn('No page block found');
} }
} blockSuiteWorkspace.setPageMeta(page.id, {
}} title: query,
});
onClose();
void jumpToPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
> >
<StyledModalFooterContent> <StyledModalFooterContent>
<PlusIcon /> <PlusIcon />

View File

@@ -5,7 +5,10 @@ import 'fake-indexeddb/auto';
import assert from 'node:assert'; import assert from 'node:assert';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalWorkspace } from '@affine/workspace/type'; import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
import type { PageBlockModel } from '@blocksuite/blocks'; import type { PageBlockModel } from '@blocksuite/blocks';
@@ -38,11 +41,7 @@ import {
useRecentlyViewed, useRecentlyViewed,
useSyncRecentViewsWithRouter, useSyncRecentViewsWithRouter,
} from '../use-recent-views'; } from '../use-recent-views';
import { import { useAppHelper, useWorkspaces } from '../use-workspaces';
REDIRECT_TIMEOUT,
useSyncRouterWithCurrentWorkspaceAndPage,
} from '../use-sync-router-with-current-workspace-and-page';
import { useWorkspaces, useWorkspacesHelper } from '../use-workspaces';
vi.mock( vi.mock(
'../../components/blocksuite/header/editor-mode-switch/CustomLottie', '../../components/blocksuite/header/editor-mode-switch/CustomLottie',
@@ -167,23 +166,24 @@ describe('usePageMetas', async () => {
describe('useWorkspacesHelper', () => { describe('useWorkspacesHelper', () => {
test('basic', async () => { test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext(); const { ProviderWrapper, store } = await getJotaiContext();
const workspaceHelperHook = renderHook(() => useWorkspacesHelper(), { const workspaceHelperHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
const id = await workspaceHelperHook.result.current.createLocalWorkspace( const id = await workspaceHelperHook.result.current.createLocalWorkspace(
'test' 'test'
); );
const workspaces = await store.get(workspacesAtom); const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toBe(1); expect(workspaces.length).toBe(2);
expect(workspaces[0].id).toBe(id); expect(workspaces[1].id).toBe(id);
const workspacesHook = renderHook(() => useWorkspaces(), { const workspacesHook = renderHook(() => useWorkspaces(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
store.set(rootCurrentWorkspaceIdAtom, workspacesHook.result.current[1].id);
await store.get(currentWorkspaceAtom); await store.get(currentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), { const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id); currentWorkspaceHook.result.current[1](workspacesHook.result.current[1].id);
}); });
}); });
@@ -198,115 +198,35 @@ describe('useWorkspaces', () => {
test('mutation', async () => { test('mutation', async () => {
const { ProviderWrapper, store } = await getJotaiContext(); const { ProviderWrapper, store } = await getJotaiContext();
const { result } = renderHook(() => useWorkspacesHelper(), { const { result } = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
await result.current.createLocalWorkspace('test'); {
const workspaces = await store.get(workspacesAtom); const workspaces = await store.get(workspacesAtom);
console.log(workspaces);
expect(workspaces.length).toEqual(1); 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(), { const { result: result2 } = renderHook(() => useWorkspaces(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
expect(result2.current.length).toEqual(1); expect(result2.current.length).toEqual(2);
const firstWorkspace = result2.current[0]; const firstWorkspace = result2.current[1];
expect(firstWorkspace.flavour).toBe('local'); expect(firstWorkspace.flavour).toBe('local');
assert(firstWorkspace.flavour === WorkspaceFlavour.LOCAL); assert(firstWorkspace.flavour === WorkspaceFlavour.LOCAL);
expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test'); expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test');
}); });
}); });
describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
test('from "/"', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(currentWorkspaceAtom);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'page0');
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push('/');
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe('/');
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
});
test('from empty workspace', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
mutationHook.rerender();
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/not_exist`);
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT + 50));
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/all`);
});
test('from incorrect "/workspace/[workspaceId]/[pageId]"', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'page0');
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/not_exist`);
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT + 50));
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
});
});
describe('useRecentlyViewed', () => { describe('useRecentlyViewed', () => {
test('basic', async () => { test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext(); const { ProviderWrapper, store } = await getJotaiContext();
const workspaceId = blockSuiteWorkspace.id; const workspaceId = blockSuiteWorkspace.id;
const pageId = 'page0'; const pageId = 'page0';
store.set(jotaiWorkspacesAtom, [ store.set(rootWorkspacesMetadataAtom, [
{ {
id: workspaceId, id: workspaceId,
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,

View File

@@ -1,4 +1,4 @@
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type'; import type { AffineWorkspace } from '@affine/workspace/type';
import { useCallback } from 'react'; import { useCallback } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -16,7 +16,7 @@ export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
}); });
await mutate(QueryKey.getWorkspaces); await mutate(QueryKey.getWorkspaces);
// fixme: remove force update // fixme: remove force update
jotaiStore.set(jotaiWorkspacesAtom, ws => [...ws]); rootStore.set(rootWorkspacesMetadataAtom, ws => [...ws]);
}, },
[mutate, workspace.id] [mutate, workspace.id]
); );

View File

@@ -1,43 +1,12 @@
import type { Page } from '@blocksuite/store'; import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { atom, useAtom, useAtomValue } from 'jotai'; import { useAtom } from 'jotai';
import { currentPageIdAtom } from '../../atoms';
import { currentWorkspaceAtom } from './use-current-workspace';
export const currentPageAtom = atom<Promise<Page | null>>(async get => {
const id = get(currentPageIdAtom);
const workspace = await get(currentWorkspaceAtom);
if (!workspace || !id) {
return Promise.resolve(null);
}
const page = workspace.blockSuiteWorkspace.getPage(id);
if (page) {
return page;
} else {
return new Promise(resolve => {
const dispose = workspace.blockSuiteWorkspace.slots.pageAdded.on(
pageId => {
if (pageId === id) {
resolve(page);
dispose.dispose();
}
}
);
});
}
});
export function useCurrentPage(): Page | null {
return useAtomValue(currentPageAtom);
}
/** /**
* @deprecated * @deprecated Use `rootCurrentPageIdAtom` directly instead.
*/ */
export function useCurrentPageId(): [ export function useCurrentPageId(): [
string | null, string | null,
(newId: string | null) => void (newId: string | null) => void
] { ] {
return useAtom(currentPageIdAtom); return useAtom(rootCurrentPageIdAtom);
} }

View File

@@ -1,21 +1,15 @@
import { atomWithSyncStorage } from '@affine/jotai'; import { atomWithSyncStorage } from '@affine/jotai';
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
currentPageIdAtom, import { rootCurrentWorkspaceAtom } from '../../atoms/root';
currentWorkspaceIdAtom,
workspacesAtom,
} from '../../atoms';
import type { AllWorkspace } from '../../shared'; import type { AllWorkspace } from '../../shared';
export const currentWorkspaceAtom = atom<Promise<AllWorkspace | null>>( /**
async get => { * @deprecated use `rootCurrentWorkspaceAtom` instead
const id = get(currentWorkspaceIdAtom); */
const workspaces = await get(workspacesAtom); export const currentWorkspaceAtom = rootCurrentWorkspaceAtom;
return workspaces.find(workspace => workspace.id === id) ?? null;
}
);
export const lastWorkspaceIdAtom = atomWithSyncStorage<string | null>( export const lastWorkspaceIdAtom = atomWithSyncStorage<string | null>(
'last_workspace_id', 'last_workspace_id',
@@ -26,7 +20,7 @@ export function useCurrentWorkspace(): [
AllWorkspace | null, AllWorkspace | null,
(id: string | null) => void (id: string | null) => void
] { ] {
const currentWorkspace = useAtomValue(currentWorkspaceAtom); const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const [, setId] = useAtom(currentWorkspaceIdAtom); const [, setId] = useAtom(currentWorkspaceIdAtom);
const [, setPageId] = useAtom(currentPageIdAtom); const [, setPageId] = useAtom(currentPageIdAtom);
const setLast = useSetAtom(lastWorkspaceIdAtom); const setLast = useSetAtom(lastWorkspaceIdAtom);

View File

@@ -1,30 +0,0 @@
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { useEffect } from 'react';
import { WorkspacePlugins } from '../plugins';
export function useCreateFirstWorkspace() {
// may not need use effect at all, right?
useEffect(() => {
return jotaiStore.sub(jotaiWorkspacesAtom, () => {
const workspaces = jotaiStore.get(jotaiWorkspacesAtom);
if (workspaces.length === 0) {
createFirst();
}
/**
* Create a first workspace, only just once for a browser
*/
async function createFirst() {
const Plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
for (const Plugin of Plugins) {
await Plugin.Events['app:first-init']?.();
}
}
});
}, []);
}

View File

@@ -0,0 +1,84 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect, useRef } from 'react';
import { rootCurrentWorkspaceAtom } from '../atoms/root';
export const HALT_PROBLEM_TIMEOUT = 1000;
export function useRouterAndWorkspaceWithPageIdDefense(router: NextRouter) {
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const [currentPageId, setCurrentPageId] = useAtom(rootCurrentPageIdAtom);
const fallbackModeRef = useRef(false);
useEffect(() => {
if (!router.isReady) {
return;
}
const { workspaceId, pageId } = router.query;
if (typeof pageId !== 'string') {
console.warn('pageId is not a string', pageId);
return;
}
if (typeof workspaceId !== 'string') {
console.warn('workspaceId is not a string', workspaceId);
return;
}
if (currentWorkspace?.id !== workspaceId) {
console.warn('workspaceId is not currentWorkspace', workspaceId);
return;
}
if (currentPageId !== pageId && !fallbackModeRef.current) {
console.log('set current page id', pageId);
setCurrentPageId(pageId);
void router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
...router.query,
workspaceId,
pageId,
},
});
}
}, [currentPageId, currentWorkspace.id, router, setCurrentPageId]);
useEffect(() => {
if (fallbackModeRef.current) {
return;
}
const id = setTimeout(() => {
if (currentPageId) {
const page =
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
const firstOne =
currentWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0);
if (firstOne) {
console.warn(
'cannot find page',
currentPageId,
'so redirect to',
firstOne.id
);
setCurrentPageId(firstOne.id);
void router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: firstOne.id,
},
});
fallbackModeRef.current = true;
}
}
}
}, HALT_PROBLEM_TIMEOUT);
return () => {
clearTimeout(id);
};
}, [
currentPageId,
currentWorkspace.blockSuiteWorkspace,
currentWorkspace.id,
router,
setCurrentPageId,
]);
}

View File

@@ -0,0 +1,48 @@
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useRouterWithWorkspaceIdDefense(router: NextRouter) {
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspaceId) {
return;
}
const exist = metadata.find(m => m.id === currentWorkspaceId);
if (!exist) {
// clean up
setCurrentWorkspaceId(null);
setCurrentPageId(null);
const firstOne = metadata.at(0);
if (!firstOne) {
throw new Error('no workspace');
}
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
...router.query,
workspaceId: firstOne.id,
},
});
}
}, [
currentWorkspaceId,
metadata,
router,
router.isReady,
setCurrentPageId,
setCurrentWorkspaceId,
]);
}

View File

@@ -0,0 +1,18 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useSyncRouterWithCurrentPageId(router: NextRouter) {
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
const pageId = router.query.pageId;
if (typeof pageId === 'string') {
console.log('set page id', pageId);
setCurrentPageId(pageId);
}
}, [router.isReady, router.query.pageId, setCurrentPageId]);
}

View File

@@ -1,215 +0,0 @@
import { jotaiStore } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
import { currentPageIdAtom } from '../atoms';
import type { AllWorkspace } from '../shared';
import { WorkspaceSubPath } from '../shared';
import { useCurrentPageId } from './current/use-current-page-id';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { RouteLogic, useRouterHelper } from './use-router-helper';
import { useWorkspaces } from './use-workspaces';
export function findSuitablePageId(
workspace: AllWorkspace,
targetId: string
): string | null {
switch (workspace.flavour) {
case WorkspaceFlavour.AFFINE: {
return (
workspace.blockSuiteWorkspace.meta.pageMetas.find(
page => page.id === targetId
)?.id ??
workspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id ??
null
);
}
case WorkspaceFlavour.LOCAL: {
return (
workspace.blockSuiteWorkspace.meta.pageMetas.find(
page => page.id === targetId
)?.id ??
workspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id ??
null
);
}
case WorkspaceFlavour.PUBLIC: {
return null;
}
}
}
export const REDIRECT_TIMEOUT = 1000;
export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
const [currentPageId, setCurrentPageId] = useCurrentPageId();
const workspaces = useWorkspaces();
const { jumpToSubPath } = useRouterHelper(router);
useEffect(() => {
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
if (url.startsWith('/')) {
const path = url.split('/');
if (path.length === 4 && path[1] === 'workspace') {
if (
path[3] === 'all' ||
path[3] === 'setting' ||
path[3] === 'trash' ||
path[3] === 'favorite' ||
path[3] === 'shared'
) {
return;
}
setCurrentWorkspaceId(path[2]);
if (currentWorkspace && 'blockSuiteWorkspace' in currentWorkspace) {
if (currentWorkspace.blockSuiteWorkspace.getPage(path[3])) {
setCurrentPageId(path[3]);
}
}
}
}
};
router.events.on('routeChangeStart', listener);
return () => {
router.events.off('routeChangeStart', listener);
};
}, [currentWorkspace, router, setCurrentPageId, setCurrentWorkspaceId]);
useEffect(() => {
if (!router.isReady) {
return;
}
if (
router.pathname === '/workspace/[workspaceId]/[pageId]' ||
router.pathname === '/'
) {
const targetPageId = router.query.pageId;
const targetWorkspaceId = router.query.workspaceId;
if (currentWorkspace && currentPageId) {
if (
currentWorkspace.id === targetWorkspaceId &&
currentPageId === targetPageId
) {
return;
}
}
if (
typeof targetPageId !== 'string' ||
typeof targetWorkspaceId !== 'string'
) {
if (router.asPath === '/') {
const first = workspaces.at(0);
if (first && 'blockSuiteWorkspace' in first) {
const targetWorkspaceId = first.id;
const targetPageId =
first.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (targetPageId) {
setCurrentWorkspaceId(targetWorkspaceId);
setCurrentPageId(targetPageId);
router.push(`/workspace/${targetWorkspaceId}/${targetPageId}`);
}
}
}
return;
}
if (!currentWorkspace) {
const targetWorkspace = workspaces.find(
workspace => workspace.id === targetPageId
);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
router.push({
query: {
...router.query,
workspaceId: targetWorkspace.id,
},
});
return;
} else {
const first = workspaces.at(0);
if (first) {
setCurrentWorkspaceId(first.id);
router.push({
query: {
...router.query,
workspaceId: first.id,
},
});
return;
}
}
} else {
if (!currentPageId && currentWorkspace) {
const targetId = findSuitablePageId(currentWorkspace, targetPageId);
if (targetId) {
setCurrentPageId(targetId);
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: targetId,
},
});
return;
} else {
const dispose =
currentWorkspace.blockSuiteWorkspace.slots.pageAdded.on(
pageId => {
if (pageId === targetPageId) {
dispose.dispose();
setCurrentPageId(pageId);
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: targetPageId,
},
});
}
}
);
const clearId = setTimeout(() => {
const pageId = jotaiStore.get(currentPageIdAtom);
if (pageId === null) {
const id =
currentWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (id) {
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: id,
},
});
setCurrentPageId(id);
dispose.dispose();
return;
}
}
jumpToSubPath(
currentWorkspace.blockSuiteWorkspace.id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
);
dispose.dispose();
}, REDIRECT_TIMEOUT);
return () => {
clearTimeout(clearId);
dispose.dispose();
};
}
}
}
}
}, [
currentPageId,
currentWorkspace,
router.query.workspaceId,
router.query.pageId,
setCurrentPageId,
setCurrentWorkspaceId,
workspaces,
router,
jumpToSubPath,
]);
}

View File

@@ -0,0 +1,51 @@
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom
);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
const workspaceId = router.query.workspaceId;
if (typeof workspaceId !== 'string') {
return;
}
if (currentWorkspaceId) {
return;
}
const targetWorkspace = metadata.find(
workspace => workspace.id === workspaceId
);
if (targetWorkspace) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
} else {
const targetWorkspace = metadata.at(0);
if (targetWorkspace) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
}
}
}, [currentWorkspaceId, metadata, router, setCurrentWorkspaceId]);
}

View File

@@ -1,53 +0,0 @@
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { useWorkspaces } from './use-workspaces';
export function useSyncRouterWithCurrentWorkspace(router: NextRouter) {
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
const workspaces = useWorkspaces();
useEffect(() => {
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
if (url.startsWith('/')) {
const path = url.split('/');
if (path.length === 4 && path[1] === 'workspace') {
setCurrentWorkspaceId(path[2]);
}
}
};
router.events.on('routeChangeStart', listener);
return () => {
router.events.off('routeChangeStart', listener);
};
}, [currentWorkspace, router, setCurrentWorkspaceId]);
useEffect(() => {
if (!router.isReady) {
return;
}
const workspaceId = router.query.workspaceId;
if (typeof workspaceId !== 'string') {
return;
}
if (!currentWorkspace) {
const targetWorkspace = workspaces.find(
workspace => workspace.id === workspaceId
);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
} else {
const targetWorkspace = workspaces.at(0);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
}
}
}
}, [currentWorkspace, router, setCurrentWorkspaceId, workspaces]);
}

View File

@@ -1,12 +1,13 @@
import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { WorkspaceFlavour } from '@affine/workspace/type'; import type { WorkspaceFlavour } from '@affine/workspace/type';
import type { WorkspaceRegistry } from '@affine/workspace/type'; import type { WorkspaceRegistry } from '@affine/workspace/type';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { WorkspacePlugins } from '../plugins'; import { WorkspacePlugins } from '../plugins';
import { useRouterHelper } from './use-router-helper';
/** /**
* Transform workspace from one flavour to another * Transform workspace from one flavour to another
@@ -14,9 +15,8 @@ import { useRouterHelper } from './use-router-helper';
* The logic here is to delete the old workspace and create a new one. * The logic here is to delete the old workspace and create a new one.
*/ */
export function useTransformWorkspace() { export function useTransformWorkspace() {
const set = useSetAtom(jotaiWorkspacesAtom); const setCurrentWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
const router = useRouter(); const set = useSetAtom(rootWorkspacesMetadataAtom);
const helper = useRouterHelper(router);
return useCallback( return useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>( async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From, from: From,
@@ -35,9 +35,9 @@ export function useTransformWorkspace() {
}); });
return [...workspaces]; return [...workspaces];
}); });
await helper.jumpToWorkspace(newId); setCurrentWorkspaceId(newId);
return newId; return newId;
}, },
[helper, set] [set, setCurrentWorkspaceId]
); );
} }

View File

@@ -1,5 +1,5 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { LocalWorkspace } from '@affine/workspace/type'; import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -18,10 +18,13 @@ export function useWorkspaces(): AllWorkspace[] {
const logger = new DebugLogger('use-workspaces'); const logger = new DebugLogger('use-workspaces');
export function useWorkspacesHelper() { /**
* This hook has the permission to all workspaces. Be careful when using it.
*/
export function useAppHelper() {
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom); const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const set = useSetAtom(jotaiWorkspacesAtom); const set = useSetAtom(rootWorkspacesMetadataAtom);
return { return {
createWorkspacePage: useCallback( createWorkspacePage: useCallback(
(workspaceId: string, pageId: string) => { (workspaceId: string, pageId: string) => {

View File

@@ -1,21 +1,24 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env'; import { config, DEFAULT_HELLO_WORLD_PAGE_ID } from '@affine/env';
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
import { setUpLanguage, useTranslation } from '@affine/i18n'; import { setUpLanguage, useTranslation } from '@affine/i18n';
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync'; import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootStore,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists, nanoid } from '@blocksuite/store'; import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { FC, PropsWithChildren } from 'react'; import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect } from 'react'; import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import { import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
currentWorkspaceIdAtom,
openQuickSearchModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { import {
publicWorkspaceAtom, publicWorkspaceAtom,
publicWorkspaceIdAtom, publicWorkspaceIdAtom,
@@ -23,18 +26,19 @@ import {
import { HelpIsland } from '../components/pure/help-island'; import { HelpIsland } from '../components/pure/help-island';
import { PageLoading } from '../components/pure/loading'; import { PageLoading } from '../components/pure/loading';
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar'; import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
import { useCurrentPageId } from '../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper'; import { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper';
import { useCreateFirstWorkspace } from '../hooks/use-create-first-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 { useRouterWithWorkspaceIdDefense } from '../hooks/use-router-with-workspace-id-defense';
import { import {
useSidebarFloating, useSidebarFloating,
useSidebarResizing, useSidebarResizing,
useSidebarStatus, useSidebarStatus,
useSidebarWidth, useSidebarWidth,
} from '../hooks/use-sidebar-status'; } from '../hooks/use-sidebar-status';
import { useSyncRouterWithCurrentPageId } from '../hooks/use-sync-router-with-current-page-id';
import { useSyncRouterWithCurrentWorkspaceId } from '../hooks/use-sync-router-with-current-workspace-id';
import { useWorkspaces } from '../hooks/use-workspaces'; import { useWorkspaces } from '../hooks/use-workspaces';
import { WorkspacePlugins } from '../plugins'; import { WorkspacePlugins } from '../plugins';
import { ModalProvider } from '../providers/ModalProvider'; import { ModalProvider } from '../providers/ModalProvider';
@@ -114,6 +118,53 @@ const logger = new DebugLogger('workspace-layout');
const affineGlobalChannel = createAffineGlobalChannel( const affineGlobalChannel = createAffineGlobalChannel(
WorkspacePlugins[WorkspaceFlavour.AFFINE].CRUD WorkspacePlugins[WorkspaceFlavour.AFFINE].CRUD
); );
export const AllWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const workspaces = useWorkspaces();
useEffect(() => {
const providers = workspaces
// ignore current workspace
.filter(workspace => workspace.id !== currentWorkspaceId)
.flatMap(workspace =>
workspace.providers.filter(provider => provider.background)
);
providers.forEach(provider => {
provider.connect();
});
return () => {
providers.forEach(provider => {
provider.disconnect();
});
};
}, [currentWorkspaceId, workspaces]);
return <>{children}</>;
};
export const CurrentWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
const router = useRouter();
const workspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
useSyncRouterWithCurrentWorkspaceId(router);
useSyncRouterWithCurrentPageId(router);
useRouterWithWorkspaceIdDefense(router);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
if (!router.isReady) {
return <PageLoading text="Router is loading" />;
}
if (!workspaceId) {
return <PageLoading text="Finding workspace id" />;
}
if (!exist) {
return <PageLoading text="Workspace not found" />;
}
return <>{children}</>;
};
export const WorkspaceLayout: FC<PropsWithChildren> = export const WorkspaceLayout: FC<PropsWithChildren> =
function WorkspacesSuspense({ children }) { function WorkspacesSuspense({ children }) {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
@@ -122,10 +173,9 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
// todo(himself65): this is a hack, we should use a better way to set the language // todo(himself65): this is a hack, we should use a better way to set the language
setUpLanguage(i18n); setUpLanguage(i18n);
}, [i18n]); }, [i18n]);
useCreateFirstWorkspace(); const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom); const set = useSetAtom(rootWorkspacesMetadataAtom);
const set = useSetAtom(jotaiWorkspacesAtom);
useEffect(() => { useEffect(() => {
logger.info('mount'); logger.info('mount');
const controller = new AbortController(); const controller = new AbortController();
@@ -134,7 +184,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
.map(({ CRUD }) => CRUD.list); .map(({ CRUD }) => CRUD.list);
async function fetch() { async function fetch() {
const jotaiWorkspaces = jotaiStore.get(jotaiWorkspacesAtom); const jotaiWorkspaces = rootStore.get(rootWorkspacesMetadataAtom);
const items = []; const items = [];
for (const list of lists) { for (const list of lists) {
try { try {
@@ -180,22 +230,31 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
return ( return (
<> <>
{/* fixme(himself65): don't re-render whole modals */} {/* fixme(himself65): don't re-render whole modals */}
<AllWorkspaceContext>
<CurrentWorkspaceContext>
<ModalProvider key={currentWorkspaceId} /> <ModalProvider key={currentWorkspaceId} />
<Suspense fallback={<PageLoading />}> </CurrentWorkspaceContext>
</AllWorkspaceContext>
<CurrentWorkspaceContext>
<Suspense fallback={<PageLoading text="Finding current workspace" />}>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner> <WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</Suspense> </Suspense>
</CurrentWorkspaceContext>
</> </>
); );
}; };
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => { export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId(); const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
const workspaces = useWorkspaces(); const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
logger.info('workspaces: ', workspaces); logger.info('currentWorkspace: ', currentWorkspace);
}, [workspaces]); }, [currentWorkspace]);
useEffect(() => { useEffect(() => {
if (currentWorkspace) { if (currentWorkspace) {
@@ -203,38 +262,82 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
} }
}, [currentWorkspace]); }, [currentWorkspace]);
useEffect(() => {
const providers = workspaces.flatMap(workspace =>
workspace.providers.filter(provider => provider.background)
);
providers.forEach(provider => {
provider.connect();
});
return () => {
providers.forEach(provider => {
provider.disconnect();
});
};
}, [workspaces]);
useEffect(() => { useEffect(() => {
if (currentWorkspace) { if (currentWorkspace) {
currentWorkspace.providers.forEach(provider => { currentWorkspace.providers.forEach(provider => {
if (provider.background) {
return;
}
provider.connect(); provider.connect();
}); });
return () => { return () => {
currentWorkspace.providers.forEach(provider => { currentWorkspace.providers.forEach(provider => {
if (provider.background) {
return;
}
provider.disconnect(); provider.disconnect();
}); });
}; };
} }
}, [currentWorkspace]); }, [currentWorkspace]);
const router = useRouter();
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspace) {
return;
}
const localProvider = currentWorkspace.providers.find(
provider => provider.flavour === 'local-indexeddb'
);
if (localProvider && localProvider.flavour === 'local-indexeddb') {
const provider = localProvider as LocalIndexedDBProvider;
const callback = () => {
setIsLoading(false);
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
// this is a new workspace, so we should redirect to the new page
const pageId = nanoid();
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
assertEquals(page.id, pageId);
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
initPage(page);
if (!router.query.pageId) {
setCurrentPageId(pageId);
void jumpToPage(currentWorkspace.id, pageId);
}
}
// no matter the workspace is empty, ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
};
provider.callbacks.add(callback);
return () => {
provider.callbacks.delete(callback);
};
}
}, [currentWorkspace, jumpToPage, router, setCurrentPageId]);
useEffect(() => {
if (!currentWorkspace) {
return;
}
const page = currentWorkspace.blockSuiteWorkspace.getPage(
DEFAULT_HELLO_WORLD_PAGE_ID
);
if (page && page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(
DEFAULT_HELLO_WORLD_PAGE_ID,
{
jumpOnce: false,
}
);
setCurrentPageId(currentPageId);
void jumpToPage(currentWorkspace.id, page.id);
}
}, [
currentPageId,
currentWorkspace,
jumpToPage,
router.query.pageId,
setCurrentPageId,
]);
const { openPage } = useRouterHelper(router); const { openPage } = useRouterHelper(router);
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom); const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = useBlockSuiteWorkspaceHelper( const helper = useBlockSuiteWorkspaceHelper(
@@ -339,7 +442,9 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
</StyledSpacer> </StyledSpacer>
<MainContainerWrapper resizing={resizing} style={{ width: mainWidth }}> <MainContainerWrapper resizing={resizing} style={{ width: mainWidth }}>
<MainContainer className="main-container"> <MainContainer className="main-container">
{children} <Suspense fallback={<PageLoading text="Page is Loading" />}>
{isLoading ? <PageLoading text="Page is Loading" /> : children}
</Suspense>
<StyledToolWrapper> <StyledToolWrapper>
{/* fixme(himself65): remove this */} {/* fixme(himself65): remove this */}
<div id="toolWrapper" style={{ marginBottom: '12px' }}> <div id="toolWrapper" style={{ marginBottom: '12px' }}>

View File

@@ -2,14 +2,15 @@ import '@affine/component/theme/global.css';
import { config, setupGlobal } from '@affine/env'; import { config, setupGlobal } from '@affine/env';
import { createI18n, I18nextProvider } from '@affine/i18n'; import { createI18n, I18nextProvider } from '@affine/i18n';
import { jotaiStore } from '@affine/workspace/atom'; import { rootStore } from '@affine/workspace/atom';
import type { EmotionCache } from '@emotion/cache'; import type { EmotionCache } from '@emotion/cache';
import { CacheProvider } from '@emotion/react'; import { CacheProvider } from '@emotion/react';
import { Provider } from 'jotai'; import { Provider } from 'jotai';
import { DevTools } from 'jotai-devtools';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { ReactElement } from 'react'; import type { PropsWithChildren, ReactElement } from 'react';
import React, { Suspense, useEffect, useMemo } from 'react'; import React, { Suspense, useEffect, useMemo } from 'react';
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary'; import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
@@ -30,6 +31,15 @@ const EmptyLayout = (page: ReactElement) => page;
const clientSideEmotionCache = createEmotionCache(); const clientSideEmotionCache = createEmotionCache();
const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
return (
<>
{process.env.DEBUG_JOTAI === 'true' && <DevTools />}
{children}
</>
);
};
const App = function App({ const App = function App({
Component, Component,
pageProps, pageProps,
@@ -55,10 +65,12 @@ const App = function App({
<Suspense fallback={<PageLoading key="RootPageLoading" />}> <Suspense fallback={<PageLoading key="RootPageLoading" />}>
<ProviderComposer <ProviderComposer
contexts={useMemo( contexts={useMemo(
() => [ () =>
<Provider key="JotaiProvider" store={jotaiStore} />, [
<Provider key="JotaiProvider" store={rootStore} />,
<DebugProvider key="DebugProvider" />,
<ThemeProvider key="ThemeProvider" />, <ThemeProvider key="ThemeProvider" />,
], ].filter(Boolean),
[] []
)} )}
> >

View File

@@ -1,9 +1,9 @@
import { initPage } from '@affine/env/blocksuite';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { StyledPage, StyledWrapper } from '../../layouts/styles'; import { StyledPage, StyledWrapper } from '../../layouts/styles';
import type { NextPageWithLayout } from '../../shared'; import type { NextPageWithLayout } from '../../shared';
import { initPage } from '../../utils';
const Editor = lazy(() => const Editor = lazy(() =>
import('../../components/__debug__/client/Editor').then(module => ({ import('../../components/__debug__/client/Editor').then(module => ({

View File

@@ -5,18 +5,18 @@ import React, { Suspense, useEffect } from 'react';
import { PageLoading } from '../components/pure/loading'; import { PageLoading } from '../components/pure/loading';
import { useLastWorkspaceId } from '../hooks/affine/use-last-leave-workspace-id'; import { useLastWorkspaceId } from '../hooks/affine/use-last-leave-workspace-id';
import { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace';
import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper'; import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper';
import { useWorkspaces } from '../hooks/use-workspaces'; import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
import { WorkspaceSubPath } from '../shared'; import { WorkspaceSubPath } from '../shared';
const logger = new DebugLogger('IndexPage'); const logger = new DebugLogger('index-page');
const IndexPageInner = () => { const IndexPageInner = () => {
const router = useRouter(); const router = useRouter();
const { jumpToPage, jumpToSubPath } = useRouterHelper(router); const { jumpToPage, jumpToSubPath } = useRouterHelper(router);
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const lastWorkspaceId = useLastWorkspaceId(); const lastWorkspaceId = useLastWorkspaceId();
const helper = useAppHelper();
useEffect(() => { useEffect(() => {
if (!router.isReady) { if (!router.isReady) {
@@ -32,13 +32,12 @@ const IndexPageInner = () => {
targetWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id; targetWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (pageId) { if (pageId) {
logger.debug('Found target workspace. Jump to page', pageId); logger.debug('Found target workspace. Jump to page', pageId);
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE); void jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
return;
} else { } else {
const clearId = setTimeout(() => { const clearId = setTimeout(() => {
dispose.dispose(); dispose.dispose();
logger.debug('Found target workspace. Jump to all pages'); logger.debug('Found target workspace. Jump to all pages');
jumpToSubPath( void jumpToSubPath(
targetWorkspace.id, targetWorkspace.id,
WorkspaceSubPath.ALL, WorkspaceSubPath.ALL,
RouteLogic.REPLACE RouteLogic.REPLACE
@@ -47,7 +46,7 @@ const IndexPageInner = () => {
const dispose = const dispose =
targetWorkspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => { targetWorkspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => {
clearTimeout(clearId); clearTimeout(clearId);
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE); void jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
}); });
return () => { return () => {
clearTimeout(clearId); clearTimeout(clearId);
@@ -55,19 +54,16 @@ const IndexPageInner = () => {
}; };
} }
} else { } else {
logger.debug('No target workspace. jump to all pages'); console.warn('No target workspace. This should not happen in production');
// fixme: should create new workspace
jumpToSubPath('ERROR', WorkspaceSubPath.ALL, RouteLogic.REPLACE);
} }
}, [jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]); }, [helper, jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]);
return <PageLoading key="IndexPageInfinitePageLoading" />; return <PageLoading key="IndexPageInfinitePageLoading" />;
}; };
const IndexPage: NextPage = () => { const IndexPage: NextPage = () => {
useCreateFirstWorkspace();
return ( return (
<Suspense fallback={<PageLoading />}> <Suspense fallback={<PageLoading text="Loading all workspaces" />}>
<IndexPageInner /> <IndexPageInner />
</Suspense> </Suspense>
); );

View File

@@ -1,4 +1,5 @@
import { Breadcrumbs, displayFlex, styled } from '@affine/component'; import { Breadcrumbs, displayFlex, styled } from '@affine/component';
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { PageIcon } from '@blocksuite/icons'; import { PageIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
@@ -25,7 +26,6 @@ import {
PublicWorkspaceLayout, PublicWorkspaceLayout,
} from '../../../layouts/public-workspace-layout'; } from '../../../layouts/public-workspace-layout';
import type { NextPageWithLayout } from '../../../shared'; import type { NextPageWithLayout } from '../../../shared';
import { initPage } from '../../../utils';
export const NavContainer = styled('div')(({ theme }) => { export const NavContainer = styled('div')(({ theme }) => {
return { return {

View File

@@ -1,20 +1,23 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-blocksuite-workspace-page';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type React from 'react'; import type React from 'react';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { rootCurrentWorkspaceAtom } from '../../../atoms/root';
import { Unreachable } from '../../../components/affine/affine-error-eoundary'; import { Unreachable } from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading'; import { PageLoading } from '../../../components/pure/loading';
import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect'; import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect';
import { useCurrentPageId } from '../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { usePageMeta, usePageMetaHelper } from '../../../hooks/use-page-meta'; import { usePageMeta, usePageMetaHelper } from '../../../hooks/use-page-meta';
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler'; import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
import { useSyncRecentViewsWithRouter } from '../../../hooks/use-recent-views'; import { useSyncRecentViewsWithRouter } from '../../../hooks/use-recent-views';
import { useRouterAndWorkspaceWithPageIdDefense } from '../../../hooks/use-router-and-workspace-with-page-id-defense';
import { useRouterHelper } from '../../../hooks/use-router-helper'; import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspaceAndPage } from '../../../hooks/use-sync-router-with-current-workspace-and-page'; import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspaceLayout } from '../../../layouts';
import { WorkspacePlugins } from '../../../plugins'; import { WorkspacePlugins } from '../../../plugins';
import type { BlockSuiteWorkspace, NextPageWithLayout } from '../../../shared'; import type { BlockSuiteWorkspace, NextPageWithLayout } from '../../../shared';
@@ -32,7 +35,7 @@ function enableFullFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
const WorkspaceDetail: React.FC = () => { const WorkspaceDetail: React.FC = () => {
const router = useRouter(); const router = useRouter();
const { openPage } = useRouterHelper(router); const { openPage } = useRouterHelper(router);
const [currentPageId] = useCurrentPageId(); const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace ?? null; const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace ?? null;
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
@@ -80,7 +83,7 @@ const WorkspaceDetail: React.FC = () => {
return <PageLoading />; return <PageLoading />;
} }
if (!currentPageId) { if (!currentPageId) {
return <PageLoading />; return <PageLoading text="Loading page." />;
} }
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) { if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].UI.PageDetail; const PageDetail = WorkspacePlugins[currentWorkspace.flavour].UI.PageDetail;
@@ -104,14 +107,17 @@ const WorkspaceDetail: React.FC = () => {
const WorkspaceDetailPage: NextPageWithLayout = () => { const WorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
useSyncRouterWithCurrentWorkspaceAndPage(router); const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
useRouterAndWorkspaceWithPageIdDefense(router);
const page = useBlockSuiteWorkspacePage(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
if (!router.isReady) { if (!router.isReady) {
return <PageLoading />; return <PageLoading text="Router is loading" />;
} else if ( } else if (!currentPageId || !page) {
typeof router.query.pageId !== 'string' || return <PageLoading text="Page is loading" />;
typeof router.query.workspaceId !== 'string'
) {
throw new Error('Invalid router query');
} }
return <WorkspaceDetail />; return <WorkspaceDetail />;
}; };

View File

@@ -1,11 +1,10 @@
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
import { FolderIcon } from '@blocksuite/icons'; import { FolderIcon } from '@blocksuite/icons';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback } from 'react';
import { import {
QueryParamError, QueryParamError,
@@ -15,56 +14,17 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title'; import { WorkspaceTitle } from '../../../components/pure/workspace-title';
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 { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace'; import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts'; import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins'; import { WorkspacePlugins } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared'; import type { NextPageWithLayout } from '../../../shared';
import { ensureRootPinboard } from '../../../utils';
const AllPage: NextPageWithLayout = () => { const AllPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { jumpToPage } = useRouterHelper(router); const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation(); const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router); useSyncRouterWithCurrentWorkspaceId(router);
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspace) {
return;
}
if (currentWorkspace.flavour !== WorkspaceFlavour.LOCAL) {
// only create a new page for local workspace
// just ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
return;
}
const localProvider = currentWorkspace.providers.find(
provider => provider.flavour === 'local-indexeddb'
);
if (localProvider && localProvider.flavour === 'local-indexeddb') {
const provider = localProvider as LocalIndexedDBProvider;
const callback = () => {
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
// this is a new workspace, so we should redirect to the new page
const pageId = nanoid();
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
assertEquals(page.id, pageId);
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
jumpToPage(currentWorkspace.id, pageId);
}
// no matter workspace is empty, ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
};
provider.callbacks.add(callback);
return () => {
provider.callbacks.delete(callback);
};
}
}, [currentWorkspace, jumpToPage, router]);
const onClickPage = useCallback( const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => { (pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace); assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title'; import { WorkspaceTitle } from '../../../components/pure/workspace-title';
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 { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace'; import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts'; import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared'; import type { NextPageWithLayout } from '../../../shared';
const FavouritePage: NextPageWithLayout = () => { const FavouritePage: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const FavouritePage: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router); const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation(); const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router); useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback( const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => { (pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace); assertExists(currentWorkspace);

View File

@@ -18,9 +18,9 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title'; import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; import { useCurrentWorkspace } 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 { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace'; import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { useWorkspacesHelper } from '../../../hooks/use-workspaces'; import { useAppHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts'; import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins'; import { WorkspacePlugins } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared'; import type { NextPageWithLayout } from '../../../shared';
@@ -33,7 +33,7 @@ const SettingPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation(); const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router); useSyncRouterWithCurrentWorkspaceId(router);
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom); const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
useEffect(() => {}); useEffect(() => {});
const onChangeTab = useCallback( const onChangeTab = useCallback(
@@ -92,7 +92,7 @@ const SettingPage: NextPageWithLayout = () => {
} }
}, [currentTab, router, setCurrentTab]); }, [currentTab, router, setCurrentTab]);
const helper = useWorkspacesHelper(); const helper = useAppHelper();
const onDeleteWorkspace = useCallback(() => { const onDeleteWorkspace = useCallback(() => {
assertExists(currentWorkspace); assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title'; import { WorkspaceTitle } from '../../../components/pure/workspace-title';
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 { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace'; import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts'; import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared'; import type { NextPageWithLayout } from '../../../shared';
const SharedPages: NextPageWithLayout = () => { const SharedPages: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const SharedPages: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router); const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation(); const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router); useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback( const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => { (pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace); assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title'; import { WorkspaceTitle } from '../../../components/pure/workspace-title';
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 { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace'; import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts'; import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared'; import type { NextPageWithLayout } from '../../../shared';
const TrashPage: NextPageWithLayout = () => { const TrashPage: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const TrashPage: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router); const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation(); const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router); useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback( const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => { (pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace); assertExists(currentWorkspace);

View File

@@ -1,5 +1,5 @@
import { getLoginStorage } from '@affine/workspace/affine/login'; import { getLoginStorage } from '@affine/workspace/affine/login';
import { jotaiStore } from '@affine/workspace/atom'; import { rootStore } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type'; import type { AffineWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -43,7 +43,7 @@ export const fetcher = async (
if (typeof key !== 'string') { if (typeof key !== 'string') {
throw new TypeError('key must be a string'); throw new TypeError('key must be a string');
} }
const workspaces = await jotaiStore.get(workspacesAtom); const workspaces = await rootStore.get(workspacesAtom);
const workspace = workspaces.find(({ id }) => id === workspaceId); const workspace = workspaces.find(({ id }) => id === workspaceId);
assertExists(workspace); assertExists(workspace);
const storage = await workspace.blockSuiteWorkspace.blobs; const storage = await workspace.blockSuiteWorkspace.blobs;

View File

@@ -1,4 +1,5 @@
import { prefixUrl } from '@affine/env'; import { prefixUrl } from '@affine/env';
import { initPage } from '@affine/env/blocksuite';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import { import {
clearLoginStorage, clearLoginStorage,
@@ -8,7 +9,7 @@ import {
setLoginStorage, setLoginStorage,
SignMethod, SignMethod,
} from '@affine/workspace/affine/login'; } from '@affine/workspace/affine/login';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type'; import type { AffineWorkspace } from '@affine/workspace/type';
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type'; import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -26,7 +27,7 @@ import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh
import { AffineSWRConfigProvider } from '../../providers/AffineSWRConfigProvider'; import { AffineSWRConfigProvider } from '../../providers/AffineSWRConfigProvider';
import { BlockSuiteWorkspace } from '../../shared'; import { BlockSuiteWorkspace } from '../../shared';
import { affineApis } from '../../shared/apis'; import { affineApis } from '../../shared/apis';
import { initPage, toast } from '../../utils'; import { toast } from '../../utils';
import type { WorkspacePlugin } from '..'; import type { WorkspacePlugin } from '..';
import { QueryKey } from './fetcher'; import { QueryKey } from './fetcher';
@@ -81,20 +82,20 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
if (response) { if (response) {
setLoginStorage(response); setLoginStorage(response);
const user = parseIdToken(response.token); const user = parseIdToken(response.token);
jotaiStore.set(currentAffineUserAtom, user); rootStore.set(currentAffineUserAtom, user);
} else { } else {
toast('Login failed'); toast('Login failed');
} }
}, },
'workspace:revoke': async () => { 'workspace:revoke': async () => {
jotaiStore.set(jotaiWorkspacesAtom, workspaces => rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
workspaces.filter( workspaces.filter(
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
) )
); );
storage.removeItem(kAffineLocal); storage.removeItem(kAffineLocal);
clearLoginStorage(); clearLoginStorage();
jotaiStore.set(currentAffineUserAtom, null); rootStore.set(currentAffineUserAtom, null);
}, },
}, },
CRUD: { CRUD: {

View File

@@ -27,9 +27,7 @@ export const WorkspacePlugins = {
[WorkspaceFlavour.PUBLIC]: { [WorkspaceFlavour.PUBLIC]: {
flavour: WorkspaceFlavour.PUBLIC, flavour: WorkspaceFlavour.PUBLIC,
loadPriority: LoadPriority.LOW, loadPriority: LoadPriority.LOW,
Events: { Events: {} as Partial<AppEvents>,
'app:first-init': async () => {},
},
// todo: implement this // todo: implement this
CRUD: { CRUD: {
get: unimplemented, get: unimplemented,

View File

@@ -1,17 +1,23 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { DEFAULT_WORKSPACE_NAME } from '@affine/env'; import {
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; DEFAULT_HELLO_WORLD_PAGE_ID,
import { CRUD } from '@affine/workspace/local/crud'; DEFAULT_WORKSPACE_NAME,
} from '@affine/env';
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
import {
CRUD,
saveWorkspaceToLocalStorage,
} from '@affine/workspace/local/crud';
import { createIndexedDBProvider } from '@affine/workspace/providers';
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type'; import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import React from 'react'; import React from 'react';
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary'; import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail'; import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list'; import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { PageDetailEditor } from '../../components/page-detail-editor'; import { PageDetailEditor } from '../../components/page-detail-editor';
import { initPage } from '../../utils';
import type { WorkspacePlugin } from '..'; import type { WorkspacePlugin } from '..';
const logger = new DebugLogger('use-create-first-workspace'); const logger = new DebugLogger('use-create-first-workspace');
@@ -20,25 +26,29 @@ export const LocalPlugin: WorkspacePlugin<WorkspaceFlavour.LOCAL> = {
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
loadPriority: LoadPriority.LOW, loadPriority: LoadPriority.LOW,
Events: { Events: {
'app:first-init': async () => { 'app:init': () => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
nanoid(), nanoid(),
(_: string) => undefined (_: string) => undefined
); );
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME); blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace); const page = blockSuiteWorkspace.createPage(DEFAULT_HELLO_WORLD_PAGE_ID);
const workspace = await LocalPlugin.CRUD.get(id); blockSuiteWorkspace.setPageMeta(page.id, {
assertExists(workspace); init: true,
assertEquals(workspace.id, id); });
// todo: use a better way to set initial workspace initPage(page);
jotaiStore.set(jotaiWorkspacesAtom, ws => [ blockSuiteWorkspace.setPageMeta(page.id, {
...ws, jumpOnce: true,
{ });
id: workspace.id, const provider = createIndexedDBProvider(blockSuiteWorkspace);
flavour: WorkspaceFlavour.LOCAL, provider.connect();
}, provider.callbacks.add(() => {
]); provider.disconnect();
logger.debug('create first workspace', workspace); });
ensureRootPinboard(blockSuiteWorkspace);
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
logger.debug('create first workspace');
return [blockSuiteWorkspace.id];
}, },
}, },
CRUD, CRUD,

View File

@@ -1,8 +1,8 @@
import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { arrayMove } from '@dnd-kit/sortable'; import { arrayMove } from '@dnd-kit/sortable';
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type React from 'react'; import type { ReactElement } from 'react';
import { lazy, Suspense, useCallback, useTransition } from 'react'; import { lazy, Suspense, useCallback, useTransition } from 'react';
import { import {
@@ -15,7 +15,7 @@ import { useAffineLogOut } from '../hooks/affine/use-affine-log-out';
import { useCurrentUser } from '../hooks/current/use-current-user'; import { useCurrentUser } from '../hooks/current/use-current-user';
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, useWorkspacesHelper } from '../hooks/use-workspaces'; import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
import { WorkspaceSubPath } from '../shared'; import { WorkspaceSubPath } from '../shared';
const WorkspaceListModal = lazy(() => const WorkspaceListModal = lazy(() =>
@@ -41,10 +41,10 @@ export function Modals() {
const { jumpToSubPath } = useRouterHelper(router); const { jumpToSubPath } = useRouterHelper(router);
const user = useCurrentUser(); const user = useCurrentUser();
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const setWorkspaces = useSetAtom(jotaiWorkspacesAtom); const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const [, setCurrentWorkspace] = useCurrentWorkspace(); const [, setCurrentWorkspace] = useCurrentWorkspace();
const { createLocalWorkspace } = useWorkspacesHelper(); const { createLocalWorkspace } = useAppHelper();
const [transitioning, transition] = useTransition(); const [transitioning, transition] = useTransition();
return ( return (
@@ -122,13 +122,10 @@ export function Modals() {
); );
} }
export const ModalProvider: React.FC<React.PropsWithChildren> = ({ export const ModalProvider = (): ReactElement => {
children,
}) => {
return ( return (
<> <>
<Modals /> <Modals />
{children}
</> </>
); );
}; };

View File

@@ -7,7 +7,7 @@ import {
import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import type { LoginResponse } from '@affine/workspace/affine/login'; import type { LoginResponse } from '@affine/workspace/affine/login';
import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login'; import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login';
import { jotaiStore } from '@affine/workspace/atom'; import { rootStore } from '@affine/workspace/atom';
const affineApis = {} as ReturnType<typeof createUserApis> & const affineApis = {} as ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>; ReturnType<typeof createWorkspaceApis>;
@@ -19,7 +19,7 @@ const debugLogger = new DebugLogger('affine-debug-apis');
if (!globalThis.AFFINE_APIS) { if (!globalThis.AFFINE_APIS) {
globalThis.AFFINE_APIS = affineApis; globalThis.AFFINE_APIS = affineApis;
globalThis.setLogin = (response: LoginResponse) => { globalThis.setLogin = (response: LoginResponse) => {
jotaiStore.set(currentAffineUserAtom, parseIdToken(response.token)); rootStore.set(currentAffineUserAtom, parseIdToken(response.token));
setLoginStorage(response); setLoginStorage(response);
}; };
const loginMockUser1 = async () => { const loginMockUser1 = async () => {

View File

@@ -1,5 +1,6 @@
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type'; import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import type { AffinePublicWorkspace } from '@affine/workspace/type'; import type { AffinePublicWorkspace } from '@affine/workspace/type';
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import type { ReactElement, ReactNode } from 'react'; import type { ReactElement, ReactNode } from 'react';
@@ -11,7 +12,7 @@ export type AffineOfficialWorkspace =
| LocalWorkspace | LocalWorkspace
| AffinePublicWorkspace; | AffinePublicWorkspace;
export type AllWorkspace = AffineOfficialWorkspace; export type AllWorkspace = WorkspaceRegistry[keyof WorkspaceRegistry];
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage< export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
P, P,

View File

@@ -1,4 +1,3 @@
export * from './blocksuite';
export * from './create-emotion-cache'; export * from './create-emotion-cache';
export * from './string2color'; export * from './string2color';
export * from './toast'; export * from './toast';

View File

@@ -229,3 +229,28 @@ graph TB
Notice that we do not assume the Workspace UI has to be written in React.js(for now, it has to be), Notice that we do not assume the Workspace UI has to be written in React.js(for now, it has to be),
In the future, we can support other UI frameworks instead, like Vue and Svelte. In the future, we can support other UI frameworks instead, like Vue and Svelte.
### Workspace Loading Details
```mermaid
flowchart TD
subgraph JavaScript Runtime
subgraph Next.js
Start((entry point)) -->|setup environment| OnMount{On mount}
OnMount -->|empty data| Init[Init Workspaces]
Init --> LoadData
OnMount -->|already have data| LoadData>Load data]
LoadData --> CurrentWorkspace[Current workspace]
LoadData --> Workspaces[Workspaces]
Workspaces --> Providers[Providers]
subgraph React
Router([Router]) -->|sync `query.workspaceId`| CurrentWorkspace
CurrentWorkspace -->|sync `currentWorkspaceId`| Router
CurrentWorkspace -->|render| WorkspaceUI[Workspace UI]
end
end
Providers -->|push new update| Persistence[(Persistence)]
Persistence -->|patch workspace| Providers
end
```

View File

@@ -12,7 +12,8 @@
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./constant": "./src/constant.ts" "./constant": "./src/constant.ts",
"./blocksuite": "./src/blocksuite.ts"
}, },
"peerDependencies": { "peerDependencies": {
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly" "@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly"

View File

@@ -1,10 +1,14 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import markdown from '@affine/templates/Welcome-to-AFFiNE.md'; import markdown from '@affine/templates/Welcome-to-AFFiNE.md';
import { ContentParser } from '@blocksuite/blocks/content-parser'; import { ContentParser } from '@blocksuite/blocks/content-parser';
import type { Page } from '@blocksuite/store'; import type { Page, Workspace } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import type { BlockSuiteWorkspace } from '../shared'; declare global {
interface Window {
lastImportedMarkdown: string;
}
}
const demoTitle = markdown const demoTitle = markdown
.split('\n') .split('\n')
@@ -50,20 +54,11 @@ export function _initPageWithDemoMarkdown(page: Page): void {
const frameId = page.addBlock('affine:frame', {}, pageBlockId); const frameId = page.addBlock('affine:frame', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId); page.addBlock('affine:paragraph', {}, frameId);
const contentParser = new ContentParser(page); const contentParser = new ContentParser(page);
contentParser.importMarkdown(demoText, frameId).then(() => { contentParser.importMarkdown(demoText, frameId);
document.dispatchEvent(
new CustomEvent('markdown:imported', {
detail: {
workspaceId: page.workspace.id,
pageId: page.id,
},
})
);
});
page.workspace.setPageMeta(page.id, { demoTitle }); page.workspace.setPageMeta(page.id, { demoTitle });
} }
export function ensureRootPinboard(blockSuiteWorkspace: BlockSuiteWorkspace) { export function ensureRootPinboard(blockSuiteWorkspace: Workspace) {
const metas = blockSuiteWorkspace.meta.pageMetas; const metas = blockSuiteWorkspace.meta.pageMetas;
const rootMeta = metas.find(m => m.isRootPinboard); const rootMeta = metas.find(m => m.isRootPinboard);

View File

@@ -1,5 +1,6 @@
export const DEFAULT_WORKSPACE_NAME = 'Demo Workspace'; export const DEFAULT_WORKSPACE_NAME = 'Demo Workspace';
export const UNTITLED_WORKSPACE_NAME = 'Untitled'; export const UNTITLED_WORKSPACE_NAME = 'Untitled';
export const DEFAULT_HELLO_WORLD_PAGE_ID = 'hello-world';
export const enum MessageCode { export const enum MessageCode {
loginError, loginError,

10
packages/env/src/types.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/// <reference types="@webpack/env"" />
// not using import because it will break the declare module line below
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path='../../../electron/layers/preload/preload.d.ts' />
declare module '*.md' {
const text: string;
export default text;
}

View File

@@ -10,9 +10,12 @@ declare module '@blocksuite/store' {
export function useBlockSuiteWorkspacePageIsPublic(page: Page) { export function useBlockSuiteWorkspacePageIsPublic(page: Page) {
const [isPublic, set] = useState<boolean>(() => page.meta.isPublic ?? false); const [isPublic, set] = useState<boolean>(() => page.meta.isPublic ?? false);
useEffect(() => { useEffect(() => {
page.workspace.meta.pageMetasUpdated.on(() => { const disposable = page.workspace.meta.pageMetasUpdated.on(() => {
set(page.meta.isPublic ?? false); set(page.meta.isPublic ?? false);
}); });
return () => {
disposable.dispose();
};
}, [page]); }, [page]);
const setIsPublic = useCallback( const setIsPublic = useCallback(
(isPublic: boolean) => { (isPublic: boolean) => {

View File

@@ -0,0 +1,30 @@
import type { Page, Workspace } from '@blocksuite/store';
import { useEffect, useState } from 'react';
export function useBlockSuiteWorkspacePage(
blockSuiteWorkspace: Workspace,
pageId: string | null
): Page | null {
const [page, setPage] = useState(() => {
if (pageId === null) {
return null;
}
return blockSuiteWorkspace.getPage(pageId);
});
useEffect(() => {
if (pageId) {
setPage(blockSuiteWorkspace.getPage(pageId));
}
}, [blockSuiteWorkspace, pageId]);
useEffect(() => {
const disposable = blockSuiteWorkspace.slots.pageAdded.on(id => {
if (pageId === id) {
setPage(blockSuiteWorkspace.getPage(id));
}
});
return () => {
disposable.dispose();
};
}, [blockSuiteWorkspace, pageId]);
return page;
}

View File

@@ -6,7 +6,7 @@ import {
} from '@affine/workspace/affine/api'; } from '@affine/workspace/affine/api';
import { WebsocketClient } from '@affine/workspace/affine/channel'; import { WebsocketClient } from '@affine/workspace/affine/channel';
import { storageChangeSlot } from '@affine/workspace/affine/login'; import { storageChangeSlot } from '@affine/workspace/affine/login';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { WorkspaceCRUD } from '@affine/workspace/type'; import type { WorkspaceCRUD } from '@affine/workspace/type';
import type { WorkspaceFlavour } from '@affine/workspace/type'; import type { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
@@ -51,7 +51,7 @@ export function createAffineGlobalChannel(
// If the workspace is not in the current workspace list, remove it // If the workspace is not in the current workspace list, remove it
if (workspaceIndex === -1) { if (workspaceIndex === -1) {
jotaiStore.set(jotaiWorkspacesAtom, workspaces => { rootStore.set(rootWorkspacesMetadataAtom, workspaces => {
const idx = workspaces.findIndex(workspace => workspace.id === id); const idx = workspaces.findIndex(workspace => workspace.id === id);
workspaces.splice(idx, 1); workspaces.splice(idx, 1);
return [...workspaces]; return [...workspaces];

View File

@@ -1,18 +1,49 @@
import { atomWithSyncStorage } from '@affine/jotai'; import { atomWithSyncStorage } from '@affine/jotai';
import type { WorkspaceFlavour } from '@affine/workspace/type'; import type { WorkspaceFlavour } from '@affine/workspace/type';
import { createStore } from 'jotai/index'; import type { EditorContainer } from '@blocksuite/editor';
import { atom, createStore } from 'jotai';
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
export type JotaiWorkspace = { export type RootWorkspaceMetadata = {
id: string; id: string;
flavour: WorkspaceFlavour; flavour: WorkspaceFlavour;
}; };
// #region root atoms
// root primitive atom that stores the necessary data for the whole app
// be careful when you use this atom,
// it should be used only in the root component
// root primitive atom that stores the list of workspaces which could be used in the app /**
// if a workspace is not in this list, it should not be used in the app * root workspaces atom
export const jotaiWorkspacesAtom = atomWithSyncStorage<JotaiWorkspace[]>( * this atom stores the metadata of all workspaces,
* which is `id` and `flavour`, that is enough to load the real workspace data
*/
export const rootWorkspacesMetadataAtom = atomWithSyncStorage<
RootWorkspaceMetadata[]
>(
// don't change this key,
// otherwise it will cause the data loss in the production
'jotai-workspaces', 'jotai-workspaces',
[] []
); );
// global jotai store, which is used to store all the atoms // two more atoms to store the current workspace and page
export const jotaiStore = createStore(); export const rootCurrentWorkspaceIdAtom = atomWithStorage<string | null>(
'root-current-workspace-id',
null,
createJSONStorage(() => sessionStorage)
);
export const rootCurrentPageIdAtom = atomWithStorage<string | null>(
'root-current-page-id',
null,
createJSONStorage(() => sessionStorage)
);
// current editor atom, each app should have only one editor in the same time
export const rootCurrentEditorAtom = atom<Readonly<EditorContainer> | null>(
null
);
//#endregion
// global store
export const rootStore = createStore();

View File

@@ -1,3 +1,4 @@
import { DebugLogger } from '@affine/debug';
import { nanoid, Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import { nanoid, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { createIndexedDBProvider } from '@toeverything/y-indexeddb'; import { createIndexedDBProvider } from '@toeverything/y-indexeddb';
import { createJSONStorage } from 'jotai/utils'; import { createJSONStorage } from 'jotai/utils';
@@ -13,8 +14,25 @@ const getStorage = () => createJSONStorage(() => localStorage);
const kStoreKey = 'affine-local-workspace'; const kStoreKey = 'affine-local-workspace';
const schema = z.array(z.string()); const schema = z.array(z.string());
const logger = new DebugLogger('affine:workspace:local:crud');
/**
* @internal
*/
export function saveWorkspaceToLocalStorage(workspaceId: string) {
const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) && storage.setItem(kStoreKey, []);
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
const id = data.find(id => id === workspaceId);
if (!id) {
logger.debug('saveWorkspaceToLocalStorage', workspaceId);
storage.setItem(kStoreKey, [...data, workspaceId]);
}
}
export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = { export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
get: async workspaceId => { get: async workspaceId => {
logger.debug('get', workspaceId);
const storage = getStorage(); const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) && !Array.isArray(storage.getItem(kStoreKey)) &&
storage.setItem(kStoreKey, []); storage.setItem(kStoreKey, []);
@@ -36,10 +54,10 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
return workspace; return workspace;
}, },
create: async ({ doc }) => { create: async ({ doc }) => {
logger.debug('create', doc);
const storage = getStorage(); const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) && !Array.isArray(storage.getItem(kStoreKey)) &&
storage.setItem(kStoreKey, []); storage.setItem(kStoreKey, []);
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdateV2(doc); const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdateV2(doc);
const id = nanoid(); const id = nanoid();
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
@@ -52,11 +70,11 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
await persistence.whenSynced.then(() => { await persistence.whenSynced.then(() => {
persistence.disconnect(); persistence.disconnect();
}); });
storage.setItem(kStoreKey, [...data, id]); saveWorkspaceToLocalStorage(id);
console.log('create', id, storage.getItem(kStoreKey));
return id; return id;
}, },
delete: async workspace => { delete: async workspace => {
logger.debug('delete', workspace);
const storage = getStorage(); const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) && !Array.isArray(storage.getItem(kStoreKey)) &&
storage.setItem(kStoreKey, []); storage.setItem(kStoreKey, []);
@@ -69,6 +87,7 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
storage.setItem(kStoreKey, [...data]); storage.setItem(kStoreKey, [...data]);
}, },
list: async () => { list: async () => {
logger.debug('list');
const storage = getStorage(); const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) && !Array.isArray(storage.getItem(kStoreKey)) &&
storage.setItem(kStoreKey, []); storage.setItem(kStoreKey, []);

View File

@@ -69,6 +69,36 @@ const createAffineWebSocketProvider = (
return apis; return apis;
}; };
class CallbackSet extends Set<() => void> {
#ready = false;
get ready(): boolean {
return this.#ready;
}
set ready(v: boolean) {
this.#ready = v;
}
add(cb: () => void) {
if (this.ready) {
cb();
return this;
}
if (this.has(cb)) {
return this;
}
return super.add(cb);
}
delete(cb: () => void) {
if (this.has(cb)) {
return super.delete(cb);
}
return false;
}
}
const createIndexedDBProvider = ( const createIndexedDBProvider = (
blockSuiteWorkspace: BlockSuiteWorkspace blockSuiteWorkspace: BlockSuiteWorkspace
): LocalIndexedDBProvider => { ): LocalIndexedDBProvider => {
@@ -76,7 +106,7 @@ const createIndexedDBProvider = (
blockSuiteWorkspace.id, blockSuiteWorkspace.id,
blockSuiteWorkspace.doc blockSuiteWorkspace.doc
); );
const callbacks = new Set<() => void>(); const callbacks = new CallbackSet();
return { return {
flavour: 'local-indexeddb', flavour: 'local-indexeddb',
callbacks, callbacks,
@@ -93,6 +123,7 @@ const createIndexedDBProvider = (
indexeddbProvider.connect(); indexeddbProvider.connect();
indexeddbProvider.whenSynced indexeddbProvider.whenSynced
.then(() => { .then(() => {
callbacks.ready = true;
callbacks.forEach(cb => cb()); callbacks.forEach(cb => cb());
}) })
.catch(error => { .catch(error => {
@@ -110,6 +141,7 @@ const createIndexedDBProvider = (
blockSuiteWorkspace.id blockSuiteWorkspace.id
); );
indexeddbProvider.disconnect(); indexeddbProvider.disconnect();
callbacks.ready = false;
}, },
}; };
}; };

View File

@@ -2,8 +2,11 @@
/// <reference path='../../../apps/electron/layers/preload/preload.d.ts' /> /// <reference path='../../../apps/electron/layers/preload/preload.d.ts' />
import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api'; import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api';
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import type { createStore } from 'jotai';
import type { FC, PropsWithChildren } from 'react'; import type { FC, PropsWithChildren } from 'react';
export type JotaiStore = ReturnType<typeof createStore>;
export type BaseProvider = { export type BaseProvider = {
flavour: string; flavour: string;
// if this is true, we will connect the provider on the background // if this is true, we will connect the provider on the background
@@ -141,9 +144,9 @@ export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
} }
export interface AppEvents { export interface AppEvents {
// event when app is first initialized // event there is no workspace
// usually used to initialize workspace plugin // usually used to initialize workspace plugin
'app:first-init': () => Promise<void>; 'app:init': () => string[];
// request to gain access to workspace plugin // request to gain access to workspace plugin
'workspace:access': () => Promise<void>; 'workspace:access': () => Promise<void>;
// request to revoke access to workspace plugin // request to revoke access to workspace plugin

View File

@@ -37,7 +37,7 @@ const config: PlaywrightTestConfig = {
}, },
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
workers: 4, workers: 4,
retries: 3, retries: 1,
// 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot' // 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'
// default 'list' when running locally // default 'list' when running locally
// See https://playwright.dev/docs/test-reporters#github-actions-annotations // See https://playwright.dev/docs/test-reporters#github-actions-annotations

View File

@@ -4,7 +4,6 @@ import { getMetas } from './utils';
export async function openHomePage(page: Page) { export async function openHomePage(page: Page) {
await page.goto('http://localhost:8080'); await page.goto('http://localhost:8080');
await page.waitForSelector('#__next');
} }
export async function initHomePageWithPinboard(page: Page) { export async function initHomePageWithPinboard(page: Page) {

View File

@@ -1,12 +1,7 @@
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
export async function waitMarkdownImported(page: Page) { export async function waitMarkdownImported(page: Page) {
return page.evaluate( await page.waitForSelector('v-line');
() =>
new Promise(resolve => {
document.addEventListener('markdown:imported', resolve);
})
);
} }
export async function newPage(page: Page) { export async function newPage(page: Page) {

View File

@@ -41,11 +41,6 @@ export const test = baseTest.extend({
); );
} }
for (const page of context.pages()) {
await page.evaluate(() => window.localStorage.clear());
await page.evaluate(() => window.sessionStorage.clear());
}
await use(context); await use(context);
if (enableCoverage) { if (enableCoverage) {

View File

@@ -8,7 +8,6 @@ import {
} from '../../libs/sidebar'; } from '../../libs/sidebar';
import { getBuiltInUser, loginUser, openHomePage } from '../../libs/utils'; import { getBuiltInUser, loginUser, openHomePage } from '../../libs/utils';
test.describe('affine built in workspace', () => {
test('collaborative', async ({ page, browser }) => { test('collaborative', async ({ page, browser }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -38,4 +37,3 @@ test.describe('affine built in workspace', () => {
.textContent()) as string; .textContent()) as string;
expect(title.trim()).toBe('hello'); expect(title.trim()).toBe('hello');
}); });
});

View File

@@ -6,10 +6,7 @@ import { clickNewPageButton } from '../../libs/sidebar';
import { createFakeUser, loginUser, openHomePage } from '../../libs/utils'; import { createFakeUser, loginUser, openHomePage } from '../../libs/utils';
import { createWorkspace } from '../../libs/workspace'; import { createWorkspace } from '../../libs/workspace';
test.describe('affine single page', () => {
test('public single page', async ({ page, browser }) => { test('public single page', async ({ page, browser }) => {
// when enable single page, most time the page will crash
test.fail();
await openHomePage(page); await openHomePage(page);
const [a] = await createFakeUser(); const [a] = await createFakeUser();
await loginUser(page, a); await loginUser(page, a);
@@ -51,9 +48,7 @@ test.describe('affine single page', () => {
await page.getByTestId('affine-share-create-link').click(); await page.getByTestId('affine-share-create-link').click();
await page.getByTestId('affine-share-copy-link').click(); await page.getByTestId('affine-share-copy-link').click();
const url = await page.evaluate(() => navigator.clipboard.readText()); const url = await page.evaluate(() => navigator.clipboard.readText());
expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe( expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe(true);
true
);
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
const context2 = await browser.newContext(); const context2 = await browser.newContext();
const page2 = await context2.newPage(); const page2 = await context2.newPage();
@@ -64,4 +59,3 @@ test.describe('affine single page', () => {
.textContent(); .textContent();
expect(currentTitle2).toBe(title); expect(currentTitle2).toBe(title);
}); });
});

View File

@@ -10,7 +10,6 @@ import {
import { createFakeUser, loginUser, openHomePage } from '../../libs/utils'; import { createFakeUser, loginUser, openHomePage } from '../../libs/utils';
import { createWorkspace } from '../../libs/workspace'; import { createWorkspace } from '../../libs/workspace';
test.describe('affine public workspace', () => {
test('enable public workspace', async ({ page, context }) => { test('enable public workspace', async ({ page, context }) => {
await openHomePage(page); await openHomePage(page);
const [a] = await createFakeUser(); const [a] = await createFakeUser();
@@ -32,9 +31,7 @@ test.describe('affine public workspace', () => {
timeout: 10000, timeout: 10000,
}); });
const url = await page.getByTestId('share-url').inputValue(); const url = await page.getByTestId('share-url').inputValue();
expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe( expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe(true);
true
);
const page2 = await context.newPage(); const page2 = await context.newPage();
await page2.goto(url); await page2.goto(url);
await page2.waitForSelector('thead', { await page2.waitForSelector('thead', {
@@ -71,4 +68,3 @@ test.describe('affine public workspace', () => {
await page2.goto(url.replace('workspace', 'public-workspace')); await page2.goto(url.replace('workspace', 'public-workspace'));
await page2.waitForSelector('v-line'); await page2.waitForSelector('v-line');
}); });
});

View File

@@ -21,7 +21,6 @@ import {
openWorkspaceListModal, openWorkspaceListModal,
} from '../../libs/workspace'; } from '../../libs/workspace';
test.describe('affine workspace', () => {
test('should login with user A', async ({ page }) => { test('should login with user A', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -61,4 +60,3 @@ test.describe('affine workspace', () => {
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -4,7 +4,6 @@ import { openHomePage } from '../libs/load-page';
import { clickPageMoreActions, waitMarkdownImported } from '../libs/page-logic'; import { clickPageMoreActions, waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('Change page mode(Page or Edgeless)', () => {
test('Switch to edgeless by switch edgeless item', async ({ page }) => { test('Switch to edgeless by switch edgeless item', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -25,4 +24,3 @@ test.describe('Change page mode(Page or Edgeless)', () => {
const edgeless = page.locator('affine-edgeless-page'); const edgeless = page.locator('affine-edgeless-page');
expect(await edgeless.isVisible()).toBe(true); expect(await edgeless.isVisible()).toBe(true);
}); });
});

View File

@@ -4,7 +4,6 @@ import { openHomePage } from '../libs/load-page';
import { waitMarkdownImported } from '../libs/page-logic'; import { waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('Open contact us', () => {
test('Click right-bottom corner contact icon', async ({ page }) => { test('Click right-bottom corner contact icon', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -15,9 +14,6 @@ test.describe('Open contact us', () => {
expect(await rightBottomContactUs.isVisible()).toEqual(true); expect(await rightBottomContactUs.isVisible()).toEqual(true);
await rightBottomContactUs.click(); await rightBottomContactUs.click();
const contactUsModal = page.locator( const contactUsModal = page.locator('[data-testid=contact-us-modal-content]');
'[data-testid=contact-us-modal-content]'
);
await expect(contactUsModal).toContainText('Check Our Docs'); await expect(contactUsModal).toContainText('Check Our Docs');
}); });
});

View File

@@ -2,11 +2,8 @@ import { expect } from '@playwright/test';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('Debug page broadcast', () => {
test('should have page0', async ({ page }) => { test('should have page0', async ({ page }) => {
await page.goto( await page.goto('http://localhost:8080/_debug/init-page?type=importMarkdown');
'http://localhost:8080/_debug/init-page?type=importMarkdown'
);
await page.waitForSelector('v-line'); await page.waitForSelector('v-line');
const pageId = await page.evaluate(async () => { const pageId = await page.evaluate(async () => {
// @ts-ignore // @ts-ignore
@@ -14,4 +11,3 @@ test.describe('Debug page broadcast', () => {
}); });
expect(pageId).toBe('page0'); expect(pageId).toBe('page0');
}); });
});

View File

@@ -2,7 +2,6 @@ import { expect } from '@playwright/test';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('Debug page broadcast', () => {
test('should broadcast a message to all debug pages', async ({ test('should broadcast a message to all debug pages', async ({
page, page,
context, context,
@@ -19,4 +18,3 @@ test.describe('Debug page broadcast', () => {
expect(await page.locator('tr').count()).toBe(3); expect(await page.locator('tr').count()).toBe(3);
expect(await page2.locator('tr').count()).toBe(3); expect(await page2.locator('tr').count()).toBe(3);
}); });
});

View File

@@ -2,10 +2,8 @@ import { expect } from '@playwright/test';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('exception page', () => {
test('visit 404 page', async ({ page }) => { test('visit 404 page', async ({ page }) => {
await page.goto('http://localhost:8080/404'); await page.goto('http://localhost:8080/404');
const notFoundTip = page.locator('[data-testid=notFound]'); const notFoundTip = page.locator('[data-testid=notFound]');
await expect(notFoundTip).toBeVisible(); await expect(notFoundTip).toBeVisible();
}); });
});

View File

@@ -2,10 +2,8 @@ import { expect } from '@playwright/test';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('invite code page', () => {
test('the link has expired', async ({ page }) => { test('the link has expired', async ({ page }) => {
await page.goto('http://localhost:8080//invite/abc'); await page.goto('http://localhost:8080//invite/abc');
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
expect(page.getByText('The link has expired')).not.toBeUndefined(); expect(page.getByText('The link has expired')).not.toBeUndefined();
}); });
});

View File

@@ -4,7 +4,6 @@ import { openHomePage } from '../libs/load-page';
import { waitMarkdownImported } from '../libs/page-logic'; import { waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('Layout ui', () => {
test('Collapse Sidebar', async ({ page }) => { test('Collapse Sidebar', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -74,4 +73,3 @@ test.describe('Layout ui', () => {
}); });
await expect(sliderBarArea).not.toBeInViewport(); await expect(sliderBarArea).not.toBeInViewport();
}); });
});

View File

@@ -5,7 +5,6 @@ import { newPage, waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first create page', () => {
test('should create a page with a local first avatar', async ({ page }) => { test('should create a page with a local first avatar', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -38,4 +37,3 @@ test.describe('Local first create page', () => {
expect(blobUrl).toContain('blob:'); expect(blobUrl).toContain('blob:');
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -9,7 +9,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first delete page', () => {
test('New a page , then delete it in all pages, permanently delete it', async ({ test('New a page , then delete it in all pages, permanently delete it', async ({
page, page,
}) => { }) => {
@@ -54,4 +53,3 @@ test.describe('Local first delete page', () => {
).not.toBeUndefined(); ).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -6,7 +6,6 @@ import { test } from '../libs/playwright';
import { clickSideBarSettingButton } from '../libs/sidebar'; import { clickSideBarSettingButton } from '../libs/sidebar';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first delete workspace', () => {
test('New a workspace , then delete it in all workspaces, permanently delete it', async ({ test('New a workspace , then delete it in all workspaces, permanently delete it', async ({
page, page,
}) => { }) => {
@@ -28,4 +27,3 @@ test.describe('Local first delete workspace', () => {
expect(await page.getByTestId('workspace-card').count()).toBe(0); expect(await page.getByTestId('workspace-card').count()).toBe(0);
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -10,7 +10,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first export page', () => {
test.skip('New a page ,then open it and export html', async ({ page }) => { test.skip('New a page ,then open it and export html', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -41,9 +40,7 @@ test.describe('Local first export page', () => {
); );
}); });
test.skip('New a page ,then open it and export markdown', async ({ test.skip('New a page ,then open it and export markdown', async ({ page }) => {
page,
}) => {
await newPage(page); await newPage(page);
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).click();
await page await page
@@ -70,4 +67,3 @@ test.describe('Local first export page', () => {
); );
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -10,7 +10,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first favorite and cancel favorite page', () => {
test('New a page and open it ,then favorite it', async ({ page }) => { test('New a page and open it ,then favorite it', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -95,4 +94,3 @@ test.describe('Local first favorite and cancel favorite page', () => {
).not.toBeUndefined(); ).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -10,7 +10,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first favorite items ui', () => {
test('Show favorite items in sidebar', async ({ page }) => { test('Show favorite items in sidebar', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -66,4 +65,3 @@ test.describe('Local first favorite items ui', () => {
).toBe(true); ).toBe(true);
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -9,7 +9,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('local first new page', () => {
test('click btn new page', async ({ page }) => { test('click btn new page', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -31,4 +30,3 @@ test.describe('local first new page', () => {
expect(cell).not.toBeUndefined(); expect(cell).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -9,7 +9,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('local first new page', () => {
test('click btn bew page and open in tab', async ({ page }) => { test('click btn bew page and open in tab', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -34,4 +33,3 @@ test.describe('local first new page', () => {
expect(newTabPage.url()).toBe(newPageUrl); expect(newTabPage.url()).toBe(newPageUrl);
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -9,7 +9,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first delete page', () => {
test('New a page , then delete it in all pages, restore it', async ({ test('New a page , then delete it in all pages, restore it', async ({
page, page,
}) => { }) => {
@@ -56,4 +55,3 @@ test.describe('Local first delete page', () => {
expect(restoreCell).not.toBeUndefined(); expect(restoreCell).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -8,15 +8,12 @@ import { test } from '../libs/playwright';
import { clickSideBarSettingButton } from '../libs/sidebar'; import { clickSideBarSettingButton } from '../libs/sidebar';
import { testResultDir } from '../libs/utils'; import { testResultDir } from '../libs/utils';
test.describe('Local first setting page', () => {
test('Should highlight the setting page menu when selected', async ({ test('Should highlight the setting page menu when selected', async ({
page, page,
}) => { }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
const element = await page.getByTestId( const element = await page.getByTestId('slider-bar-workspace-setting-button');
'slider-bar-workspace-setting-button'
);
const prev = await element.screenshot({ const prev = await element.screenshot({
path: resolve( path: resolve(
testResultDir, testResultDir,
@@ -33,4 +30,3 @@ test.describe('Local first setting page', () => {
}); });
expect(prev).not.toEqual(after); expect(prev).not.toEqual(after);
}); });
});

View File

@@ -10,7 +10,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first delete page', () => {
test('New a page ,then open it and show delete modal', async ({ page }) => { test('New a page ,then open it and show delete modal', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -58,4 +57,3 @@ test.describe('Local first delete page', () => {
expect(confirmTip).not.toBeUndefined(); expect(confirmTip).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -9,7 +9,6 @@ import {
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first trash page', () => {
test('New a page , then delete it in all pages, finally find it in trash', async ({ test('New a page , then delete it in all pages, finally find it in trash', async ({
page, page,
}) => { }) => {
@@ -43,4 +42,3 @@ test.describe('Local first trash page', () => {
).not.toBeUndefined(); ).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -6,7 +6,6 @@ import { test } from '../libs/playwright';
import { clickSideBarAllPageButton } from '../libs/sidebar'; import { clickSideBarAllPageButton } from '../libs/sidebar';
import { createWorkspace, openWorkspaceListModal } from '../libs/workspace'; import { createWorkspace, openWorkspaceListModal } from '../libs/workspace';
test.describe('Local first workspace list', () => {
test('just one item in the workspace list at first', async ({ page }) => { test('just one item in the workspace list at first', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -116,4 +115,3 @@ test.describe('Local first workspace list', () => {
const nextId: string = await page.evaluate(() => currentWorkspace.id); const nextId: string = await page.evaluate(() => currentWorkspace.id);
expect(currentId).toBe(nextId); expect(currentId).toBe(nextId);
}); });
});

View File

@@ -6,7 +6,6 @@ import { test } from '../libs/playwright';
import { clickSideBarCurrentWorkspaceBanner } from '../libs/sidebar'; import { clickSideBarCurrentWorkspaceBanner } from '../libs/sidebar';
import { assertCurrentWorkspaceFlavour } from '../libs/workspace'; import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
test.describe('Local first default workspace', () => {
test('preset workspace name', async ({ page }) => { test('preset workspace name', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -22,8 +21,6 @@ test.describe('Local first default workspace', () => {
// await workspaceAvatar.locator('img').getAttribute('src') // await workspaceAvatar.locator('img').getAttribute('src')
// ).not.toBeNull(); // ).not.toBeNull();
// }); // });
});
test.describe('Language switch', () => {
test('Open language switch menu', async ({ page }) => { test('Open language switch menu', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -34,4 +31,3 @@ test.describe('Language switch', () => {
expect(actual).toEqual('English'); expect(actual).toEqual('English');
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
}); });
});

View File

@@ -5,7 +5,6 @@ import { waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { createWorkspace } from '../libs/workspace'; import { createWorkspace } from '../libs/workspace';
test.describe('Open AFFiNE', () => {
test('Open last workspace when back to affine', async ({ page }) => { test('Open last workspace when back to affine', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -28,9 +27,7 @@ test.describe('Open AFFiNE', () => {
); );
expect(currentWorkspaceName).toEqual('New Workspace 2'); expect(currentWorkspaceName).toEqual('New Workspace 2');
}); });
});
test.describe('AFFiNE change log', () => {
test('Open affine in first time after updated', async ({ page }) => { test('Open affine in first time after updated', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
const changeLogItem = page.locator('[data-testid=change-log]'); const changeLogItem = page.locator('[data-testid=change-log]');
@@ -57,4 +54,3 @@ test.describe('AFFiNE change log', () => {
); );
expect(await normalRightBottomChangeLog.isVisible()).toEqual(true); expect(await normalRightBottomChangeLog.isVisible()).toEqual(true);
}); });
});

View File

@@ -24,7 +24,6 @@ async function checkIsChildInsertToParentInEditor(page: Page, pageId: string) {
expect(referenceLink).not.toBeNull(); expect(referenceLink).not.toBeNull();
} }
test.describe('PinBoard interaction', () => {
test('Have initial root pinboard page when first in', async ({ page }) => { test('Have initial root pinboard page when first in', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page); const rootPinboardMeta = await initHomePageWithPinboard(page);
expect(rootPinboardMeta).not.toBeUndefined(); expect(rootPinboardMeta).not.toBeUndefined();
@@ -83,7 +82,6 @@ test.describe('PinBoard interaction', () => {
.getByTestId('sidebar-pinboard-container') .getByTestId('sidebar-pinboard-container')
.getByTestId(`pinboard-${newPageMeta?.id}`) .getByTestId(`pinboard-${newPageMeta?.id}`)
).not.toBeNull(); ).not.toBeNull();
console.log('rootPinboardMeta', rootPinboardMeta);
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? ''); await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
}); });
@@ -202,16 +200,12 @@ test.describe('PinBoard interaction', () => {
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
await createPinboardPage(page, childMeta?.id ?? '', 'test2'); await createPinboardPage(page, childMeta?.id ?? '', 'test2');
const grandChildMeta = (await getMetas(page)).find( const grandChildMeta = (await getMetas(page)).find(m => m.title === 'test2');
m => m.title === 'test2'
);
await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
await page.getByTestId('pinboard-operation-move-to-trash').click(); await page.getByTestId('pinboard-operation-move-to-trash').click();
( (await page.waitForSelector('[data-testid="move-to-trash-confirm"]')).click();
await page.waitForSelector('[data-testid="move-to-trash-confirm"]')
).click();
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
expect( expect(
@@ -252,4 +246,3 @@ test.describe('PinBoard interaction', () => {
.evaluate((input: HTMLInputElement) => input.value); .evaluate((input: HTMLInputElement) => input.value);
expect(copiedValue).toEqual(page.url()); expect(copiedValue).toEqual(page.url());
}); });
});

View File

@@ -20,10 +20,12 @@ async function assertTitle(page: Page, text: string) {
expect(actual).toBe(text); expect(actual).toBe(text);
} }
} }
async function assertResultList(page: Page, texts: string[]) { async function assertResultList(page: Page, texts: string[]) {
const actual = await page.locator('[cmdk-item]').allInnerTexts(); const actual = await page.locator('[cmdk-item]').allInnerTexts();
expect(actual).toEqual(texts); expect(actual).toEqual(texts);
} }
async function titleIsFocused(page: Page) { async function titleIsFocused(page: Page) {
const edgeless = page.locator('affine-edgeless-page'); const edgeless = page.locator('affine-edgeless-page');
if (!edgeless) { if (!edgeless) {
@@ -33,7 +35,6 @@ async function titleIsFocused(page: Page) {
} }
} }
test.describe('Open quick search', () => {
test('Click slider bar button', async ({ page }) => { test('Click slider bar button', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -66,9 +67,7 @@ test.describe('Open quick search', () => {
const quickSearch = page.locator('[data-testid=quickSearch]'); const quickSearch = page.locator('[data-testid=quickSearch]');
await expect(quickSearch).toBeVisible(); await expect(quickSearch).toBeVisible();
}); });
});
test.describe('Add new page in quick search', () => {
test('Create a new page without keyword', async ({ page }) => { test('Create a new page without keyword', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -91,9 +90,7 @@ test.describe('Add new page in quick search', () => {
await page.waitForTimeout(300); await page.waitForTimeout(300);
await assertTitle(page, 'test123456'); await assertTitle(page, 'test123456');
}); });
});
test.describe('Search and select', () => {
test('Enter a keyword to search for', async ({ page }) => { test('Enter a keyword to search for', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -103,26 +100,28 @@ test.describe('Search and select', () => {
const actual = await page.locator('[cmdk-input]').inputValue(); const actual = await page.locator('[cmdk-input]').inputValue();
expect(actual).toBe('test123456'); expect(actual).toBe('test123456');
}); });
test('Create a new page and search this page', async ({ page }) => { test('Create a new page and search this page', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
await newPage(page); await newPage(page);
await openQuickSearchByShortcut(page); await openQuickSearchByShortcut(page);
// input title and create new page
await page.keyboard.insertText('test123456'); await page.keyboard.insertText('test123456');
await page.waitForTimeout(300);
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]'); const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
await addNewPage.click(); await addNewPage.click();
await page.waitForTimeout(300); await page.waitForTimeout(300);
await assertTitle(page, 'test123456'); await assertTitle(page, 'test123456');
await openQuickSearchByShortcut(page); await openQuickSearchByShortcut(page);
await page.keyboard.insertText('test123456'); await page.keyboard.insertText('test123456');
await page.waitForTimeout(50); await page.waitForTimeout(300);
await assertResultList(page, ['test123456']); await assertResultList(page, ['test123456']);
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await page.waitForTimeout(300); await page.waitForTimeout(300);
await assertTitle(page, 'test123456'); await assertTitle(page, 'test123456');
}); });
});
test.describe('Disable search on 404 page', () => {
test('Navigate to the 404 page and try to open quick search', async ({ test('Navigate to the 404 page and try to open quick search', async ({
page, page,
}) => { }) => {
@@ -133,8 +132,7 @@ test.describe('Disable search on 404 page', () => {
const quickSearch = page.locator('[data-testid=quickSearch]'); const quickSearch = page.locator('[data-testid=quickSearch]');
await expect(quickSearch).toBeVisible({ visible: false }); await expect(quickSearch).toBeVisible({ visible: false });
}); });
});
test.describe('Open quick search on the published page', () => {
test('Open quick search on local page', async ({ page }) => { test('Open quick search on local page', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -143,9 +141,7 @@ test.describe('Open quick search on the published page', () => {
const publishedSearchResults = page.locator('[publishedSearchResults]'); const publishedSearchResults = page.locator('[publishedSearchResults]');
await expect(publishedSearchResults).toBeVisible({ visible: false }); await expect(publishedSearchResults).toBeVisible({ visible: false });
}); });
});
test.describe('Focus event for quick search', () => {
test('Autofocus input after opening quick search', async ({ page }) => { test('Autofocus input after opening quick search', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -174,8 +170,7 @@ test.describe('Focus event for quick search', () => {
await addNewPage.click(); await addNewPage.click();
await titleIsFocused(page); await titleIsFocused(page);
}); });
});
test.describe('Novice guidance for quick search', () => {
test('When opening the website for the first time, the first folding sidebar will appear novice guide', async ({ test('When opening the website for the first time, the first folding sidebar will appear novice guide', async ({
page, page,
}) => { }) => {
@@ -258,4 +253,3 @@ test.describe('Novice guidance for quick search', () => {
}); });
expect(expandTop).toBe('0px'); expect(expandTop).toBe('0px');
}); });
});

View File

@@ -4,7 +4,6 @@ import { openHomePage } from '../libs/load-page';
import { waitMarkdownImported } from '../libs/page-logic'; import { waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('Shortcuts Modal', () => {
test('Open shortcuts modal', async ({ page }) => { test('Open shortcuts modal', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -19,4 +18,3 @@ test.describe('Shortcuts Modal', () => {
const shortcutsModal = page.locator('[data-testid=shortcuts-modal]'); const shortcutsModal = page.locator('[data-testid=shortcuts-modal]');
await expect(shortcutsModal).toContainText('Keyboard Shortcuts'); await expect(shortcutsModal).toContainText('Keyboard Shortcuts');
}); });
});

View File

@@ -7,16 +7,13 @@ async function openStorybook(page: Page, storyName?: string) {
return page.goto(`http://localhost:6006`); return page.goto(`http://localhost:6006`);
} }
test.describe('storybook - Button', () => {
test('Basic', async ({ page }) => { test('Basic', async ({ page }) => {
await openStorybook(page); await openStorybook(page);
await page.click('#storybook-explorer-tree >> #affine-button'); await page.click('#storybook-explorer-tree >> #affine-button');
await page.click('#affine-button--test'); await page.click('#affine-button--test');
const iframe = page.frameLocator('iframe'); const iframe = page.frameLocator('iframe');
await iframe await iframe.locator('input[data-testid="test-input"]').type('Hello World!');
.locator('input[data-testid="test-input"]')
.type('Hello World!');
expect( expect(
await iframe.locator('input[data-testid="test-input"]').inputValue() await iframe.locator('input[data-testid="test-input"]').inputValue()
@@ -26,4 +23,3 @@ test.describe('storybook - Button', () => {
await iframe.locator('input[data-testid="test-input"]').textContent() await iframe.locator('input[data-testid="test-input"]').textContent()
).toBe(''); ).toBe('');
}); });
});

View File

@@ -4,7 +4,6 @@ import { openHomePage } from '../libs/load-page';
import { waitMarkdownImported } from '../libs/page-logic'; import { waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
test.describe('subpage', () => {
test('Create subpage', async ({ page }) => { test('Create subpage', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
@@ -12,4 +11,3 @@ test.describe('subpage', () => {
const sliderBarArea = page.getByTestId('sliderBar-inner'); const sliderBarArea = page.getByTestId('sliderBar-inner');
await expect(sliderBarArea).not.toBeInViewport(); await expect(sliderBarArea).not.toBeInViewport();
}); });
});

View File

@@ -1,10 +1,12 @@
import { resolve } from 'node:path';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
import { openHomePage } from '../libs/load-page'; import { openHomePage } from '../libs/load-page';
import { waitMarkdownImported } from '../libs/page-logic'; import { waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright'; import { test } from '../libs/playwright';
import { testResultDir } from '../libs/utils';
test.describe('Change Theme', () => {
// default could be anything according to the system // default could be anything according to the system
test('default white', async ({ browser }) => { test('default white', async ({ browser }) => {
const context = await browser.newContext({ const context = await browser.newContext({
@@ -13,27 +15,20 @@ test.describe('Change Theme', () => {
const page = await context.newPage(); const page = await context.newPage();
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
await page.waitForSelector('html');
const root = page.locator('html'); const root = page.locator('html');
const themeMode = await root.evaluate(element => const themeMode = await root.evaluate(element =>
element.getAttribute('data-theme') element.getAttribute('data-theme')
); );
expect(themeMode).toBe('light'); expect(themeMode).toBe('light');
const prev = await page.screenshot({
path: resolve(testResultDir, 'affine-light-theme.png'),
});
await page.getByTestId('change-theme-dark').click();
await page.waitForTimeout(50); await page.waitForTimeout(50);
const rightMenu = page.getByTestId('editor-option-menu'); const after = await page.screenshot({
const rightMenuBox = await rightMenu.boundingBox(); path: resolve(testResultDir, 'affine-dark-theme.png'),
const lightButton = page.getByTestId('change-theme-light'); });
const lightButtonBox = await lightButton.boundingBox(); expect(prev).not.toEqual(after);
const darkButton = page.getByTestId('change-theme-dark');
const darkButtonBox = await darkButton.boundingBox();
if (!rightMenuBox || !lightButtonBox || !darkButtonBox) {
throw new Error('rightMenuBox or lightButtonBox or darkButtonBox is nil');
}
expect(darkButtonBox.x).toBeLessThan(rightMenuBox.x);
expect(darkButtonBox.y).toBeGreaterThan(rightMenuBox.y);
expect(lightButtonBox.y).toBeCloseTo(rightMenuBox.y);
expect(lightButtonBox.x).toBeCloseTo(darkButtonBox.x);
}); });
// test('change theme to dark', async ({ page }) => { // test('change theme to dark', async ({ page }) => {
@@ -58,4 +53,3 @@ test.describe('Change Theme', () => {
// ); // );
// expect(themeMode).toBe('dark'); // expect(themeMode).toBe('dark');
// }); // });
});

388
yarn.lock
View File

@@ -224,6 +224,7 @@ __metadata:
eslint: ^8.38.0 eslint: ^8.38.0
eslint-config-next: ^13.3.0 eslint-config-next: ^13.3.0
jotai: ^2.0.4 jotai: ^2.0.4
jotai-devtools: ^0.4.0
lit: ^2.7.2 lit: ^2.7.2
lottie-web: ^5.11.0 lottie-web: ^5.11.0
next: =13.2.3 next: =13.2.3
@@ -1703,7 +1704,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": "@babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.21.0 version: 7.21.0
resolution: "@babel/runtime@npm:7.21.0" resolution: "@babel/runtime@npm:7.21.0"
dependencies: dependencies:
@@ -3216,6 +3217,48 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@floating-ui/core@npm:^1.2.6":
version: 1.2.6
resolution: "@floating-ui/core@npm:1.2.6"
checksum: e4aa96c435277f1720d4bc939e17a79b1e1eebd589c20b622d3c646a5273590ff889b8c6e126f7be61873cf8c4d7db7d418895986ea19b8b0d0530de32504c3a
languageName: node
linkType: hard
"@floating-ui/dom@npm:^1.2.1":
version: 1.2.6
resolution: "@floating-ui/dom@npm:1.2.6"
dependencies:
"@floating-ui/core": ^1.2.6
checksum: 2226c6c244b96ae75ab14cc35bb119c8d7b83a85e2ff04e9d9800cffdb17faf4a7cf82db741dd045242ced56e31c8a08e33c8c512c972309a934d83b1f410441
languageName: node
linkType: hard
"@floating-ui/react-dom@npm:^1.3.0":
version: 1.3.0
resolution: "@floating-ui/react-dom@npm:1.3.0"
dependencies:
"@floating-ui/dom": ^1.2.1
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: ce0ad3e3bbe43cfd15a6a0d5cccede02175c845862bfab52027995ab99c6b29630180dc7d146f76ebb34730f90a6ab9bf193c8984fe8d7f56062308e4ca98f77
languageName: node
linkType: hard
"@floating-ui/react@npm:^0.19.1":
version: 0.19.2
resolution: "@floating-ui/react@npm:0.19.2"
dependencies:
"@floating-ui/react-dom": ^1.3.0
aria-hidden: ^1.1.3
tabbable: ^6.0.1
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 00fd827c2dcf879fec221d89ef5b90836bbecacc236ce2acc787db32ae7311d490cd136b13a8d0b6ab12842554a2ee1110605aa832af71a45c0a7297e342072c
languageName: node
linkType: hard
"@gar/promisify@npm:^1.1.3": "@gar/promisify@npm:^1.1.3":
version: 1.1.3 version: 1.1.3
resolution: "@gar/promisify@npm:1.1.3" resolution: "@gar/promisify@npm:1.1.3"
@@ -3782,6 +3825,71 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/core@npm:^6.0.4":
version: 6.0.7
resolution: "@mantine/core@npm:6.0.7"
dependencies:
"@floating-ui/react": ^0.19.1
"@mantine/styles": 6.0.7
"@mantine/utils": 6.0.7
"@radix-ui/react-scroll-area": 1.0.2
react-remove-scroll: ^2.5.5
react-textarea-autosize: 8.3.4
peerDependencies:
"@mantine/hooks": 6.0.7
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 70f17785e128827ef834f98465709916f2298fe3b3d23364151684f7ab7602afac09e3b94804085c2b06877c1ea754a477b417268f052665be86eeadb835aa01
languageName: node
linkType: hard
"@mantine/hooks@npm:^6.0.4":
version: 6.0.7
resolution: "@mantine/hooks@npm:6.0.7"
peerDependencies:
react: ">=16.8.0"
checksum: 1c2f080690dc0c0621c732d96287578b5fd9742ab4ffea6aeb0217ccf2bec53e1f5da3e1a611e640ca87edb491d5109519ed18ed411cf5cf4f8fc95b19fdee8d
languageName: node
linkType: hard
"@mantine/prism@npm:^6.0.4":
version: 6.0.7
resolution: "@mantine/prism@npm:6.0.7"
dependencies:
"@mantine/utils": 6.0.7
prism-react-renderer: ^1.2.1
peerDependencies:
"@mantine/core": 6.0.7
"@mantine/hooks": 6.0.7
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: ad1ce40e4b50d57cbd5775fc0a1ba1b222f53c7c6f00f4fa4b613958ca579f14c1c4371c0dc4166ae694311476fcf07af1a4f8ea66466bb52ba11f2846c00f0f
languageName: node
linkType: hard
"@mantine/styles@npm:6.0.7":
version: 6.0.7
resolution: "@mantine/styles@npm:6.0.7"
dependencies:
clsx: 1.1.1
csstype: 3.0.9
peerDependencies:
"@emotion/react": ">=11.9.0"
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 7770b928925ec27bf8596cf39be7780b77759c567d9c1183b0cdbc1878ec5fe175591b900f4ad536c429497bd9bdace9e84bad34b6c92834d7851081789f5a78
languageName: node
linkType: hard
"@mantine/utils@npm:6.0.7":
version: 6.0.7
resolution: "@mantine/utils@npm:6.0.7"
peerDependencies:
react: ">=16.8.0"
checksum: dbc4749e2ef908b5a2b1be5898e460541abdfa1de949237e45abaf2091ed3c6ca512b83f03360216d87af85d8b8d1deade5db426a197866b2be376b3d1509245
languageName: node
linkType: hard
"@mdx-js/react@npm:^2.1.5": "@mdx-js/react@npm:^2.1.5":
version: 2.3.0 version: 2.3.0
resolution: "@mdx-js/react@npm:2.3.0" resolution: "@mdx-js/react@npm:2.3.0"
@@ -4422,6 +4530,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/number@npm:1.0.0":
version: 1.0.0
resolution: "@radix-ui/number@npm:1.0.0"
dependencies:
"@babel/runtime": ^7.13.10
checksum: 517ac0790e05cceb41401154d1bc55d4738accd51095e2a918ef9bcedac6a455cd7179201e88e76121bedec19cd93a37b2c20288b084fb224b69c74e67935457
languageName: node
linkType: hard
"@radix-ui/primitive@npm:1.0.0": "@radix-ui/primitive@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/primitive@npm:1.0.0" resolution: "@radix-ui/primitive@npm:1.0.0"
@@ -4495,6 +4612,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-direction@npm:1.0.0":
version: 1.0.0
resolution: "@radix-ui/react-direction@npm:1.0.0"
dependencies:
"@babel/runtime": ^7.13.10
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
checksum: 92a40de4087b161a56957872daf204a7735bd21f2fccbd42deff322d759977d085ad3dcdae05af437b7e64e628e939e0d67e5bc468a3027e1b02e0a7dc90c485
languageName: node
linkType: hard
"@radix-ui/react-dismissable-layer@npm:1.0.0": "@radix-ui/react-dismissable-layer@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.0" resolution: "@radix-ui/react-dismissable-layer@npm:1.0.0"
@@ -4590,6 +4718,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-primitive@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-primitive@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-slot": 1.0.1
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
checksum: 1cc86b72f926be4a42122e7e456e965de0906f16b0dc244b8448bac05905f208598c984a0dd40026f654b4a71d0235335d48a18e377b07b0ec6c6917576a8080
languageName: node
linkType: hard
"@radix-ui/react-primitive@npm:1.0.2": "@radix-ui/react-primitive@npm:1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "@radix-ui/react-primitive@npm:1.0.2" resolution: "@radix-ui/react-primitive@npm:1.0.2"
@@ -4603,6 +4744,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-scroll-area@npm:1.0.2":
version: 1.0.2
resolution: "@radix-ui/react-scroll-area@npm:1.0.2"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/number": 1.0.0
"@radix-ui/primitive": 1.0.0
"@radix-ui/react-compose-refs": 1.0.0
"@radix-ui/react-context": 1.0.0
"@radix-ui/react-direction": 1.0.0
"@radix-ui/react-presence": 1.0.0
"@radix-ui/react-primitive": 1.0.1
"@radix-ui/react-use-callback-ref": 1.0.0
"@radix-ui/react-use-layout-effect": 1.0.0
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
checksum: c59062c3321fb92b526f2b63be2636569c4a33fa81bd5c5109b13516932cebaf680a5cd318263d37e7e6e4ca62aa45521c34447478fd2acde3b2639ae53252d7
languageName: node
linkType: hard
"@radix-ui/react-slot@npm:1.0.0": "@radix-ui/react-slot@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-slot@npm:1.0.0" resolution: "@radix-ui/react-slot@npm:1.0.0"
@@ -6165,6 +6327,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tabler/icons@npm:^1.119.0":
version: 1.119.0
resolution: "@tabler/icons@npm:1.119.0"
peerDependencies:
react: ^16.x || 17.x || 18.x
react-dom: ^16.x || 17.x || 18.x
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
checksum: ef1ac50c1a47b2205cb86ca43c28d69c7b8f547ad9c2c5545190fc4a455e9767f49cb511a6f9e8dc45b046ee7d2dab3d2c87af4fd5bbb1694832a15698158753
languageName: node
linkType: hard
"@testing-library/dom@npm:^8.3.0": "@testing-library/dom@npm:^8.3.0":
version: 8.20.0 version: 8.20.0
resolution: "@testing-library/dom@npm:8.20.0" resolution: "@testing-library/dom@npm:8.20.0"
@@ -7831,7 +8008,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"aria-hidden@npm:^1.1.1": "aria-hidden@npm:^1.1.1, aria-hidden@npm:^1.1.3":
version: 1.2.3 version: 1.2.3
resolution: "aria-hidden@npm:1.2.3" resolution: "aria-hidden@npm:1.2.3"
dependencies: dependencies:
@@ -7937,6 +8114,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"as-table@npm:^1.0.36":
version: 1.0.55
resolution: "as-table@npm:1.0.55"
dependencies:
printable-characters: ^1.0.42
checksum: 341c99d9e99a702c315b3f0744d49b4764b26ef7ddd32bafb9e1706626560c0e599100521fc1b17f640e804bd0503ce70b2ba519c023da6edf06bdd9086dccd9
languageName: node
linkType: hard
"assert@npm:^2.0.0": "assert@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "assert@npm:2.0.0" resolution: "assert@npm:2.0.0"
@@ -8937,6 +9123,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"clsx@npm:1.1.1":
version: 1.1.1
resolution: "clsx@npm:1.1.1"
checksum: ff052650329773b9b245177305fc4c4dc3129f7b2be84af4f58dc5defa99538c61d4207be7419405a5f8f3d92007c954f4daba5a7b74e563d5de71c28c830063
languageName: node
linkType: hard
"clsx@npm:^1.2.1": "clsx@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "clsx@npm:1.2.1" resolution: "clsx@npm:1.2.1"
@@ -9296,6 +9489,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"copy-anything@npm:^3.0.2":
version: 3.0.3
resolution: "copy-anything@npm:3.0.3"
dependencies:
is-what: ^4.1.8
checksum: d456dc5ec98dee7c7cf87d809eac30dc2ac942acd4cf970fab394e280ceb6dd7a8a7a5a44fcbcc50e0206658de3cc20b92863562f5797930bb2619f164f4c182
languageName: node
linkType: hard
"copy-to-clipboard@npm:^3.3.3": "copy-to-clipboard@npm:^3.3.3":
version: 3.3.3 version: 3.3.3
resolution: "copy-to-clipboard@npm:3.3.3" resolution: "copy-to-clipboard@npm:3.3.3"
@@ -9424,6 +9626,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"csstype@npm:3.0.9":
version: 3.0.9
resolution: "csstype@npm:3.0.9"
checksum: 199f9af7e673f9f188525c3102a329d637ff46c52f6385a4427ff5cb17adcb736189150170a7af7c5701d18d7704bdad130273f4aa7e44c6c4f9967e6115dc93
languageName: node
linkType: hard
"csstype@npm:^3.0.2, csstype@npm:^3.0.7, csstype@npm:^3.1.2": "csstype@npm:^3.0.2, csstype@npm:^3.0.7, csstype@npm:^3.1.2":
version: 3.1.2 version: 3.1.2
resolution: "csstype@npm:3.1.2" resolution: "csstype@npm:3.1.2"
@@ -9455,6 +9664,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"data-uri-to-buffer@npm:^2.0.0":
version: 2.0.2
resolution: "data-uri-to-buffer@npm:2.0.2"
checksum: 152bec5e77513ee253a7c686700a1723246f582dad8b614e8eaaaba7fa45a15c8671ae4b8f4843f4f3a002dae1d3e7a20f852f7d7bdc8b4c15cfe7adfdfb07f8
languageName: node
linkType: hard
"date-fns@npm:^2.29.3": "date-fns@npm:^2.29.3":
version: 2.29.3 version: 2.29.3
resolution: "date-fns@npm:2.29.3" resolution: "date-fns@npm:2.29.3"
@@ -11680,6 +11896,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"get-source@npm:^2.0.12":
version: 2.0.12
resolution: "get-source@npm:2.0.12"
dependencies:
data-uri-to-buffer: ^2.0.0
source-map: ^0.6.1
checksum: c73368fee709594ba38682ec1a96872aac6f7d766396019611d3d2358b68186a7847765a773ea0db088c42781126cc6bc09e4b87f263951c74dae5dcea50ad42
languageName: node
linkType: hard
"get-stream@npm:^6.0.0, get-stream@npm:^6.0.1": "get-stream@npm:^6.0.0, get-stream@npm:^6.0.1":
version: 6.0.1 version: 6.0.1
resolution: "get-stream@npm:6.0.1" resolution: "get-stream@npm:6.0.1"
@@ -13004,6 +13230,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-what@npm:^4.1.8":
version: 4.1.8
resolution: "is-what@npm:4.1.8"
checksum: b9bec3acff102d14ad467f4c74c9886af310fa160e07a63292c8c181e6768c7c4c1054644e13d67185b963644e4a513bce8c6b8ce3d3ca6f9488a69fccad5f97
languageName: node
linkType: hard
"is-windows@npm:^0.2.0": "is-windows@npm:^0.2.0":
version: 0.2.0 version: 0.2.0
resolution: "is-windows@npm:0.2.0" resolution: "is-windows@npm:0.2.0"
@@ -13880,6 +14113,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jotai-devtools@npm:^0.4.0":
version: 0.4.0
resolution: "jotai-devtools@npm:0.4.0"
dependencies:
"@mantine/core": ^6.0.4
"@mantine/hooks": ^6.0.4
"@mantine/prism": ^6.0.4
"@redux-devtools/extension": ^3.2.5
"@tabler/icons": ^1.119.0
react-error-boundary: ^3.1.4
react-resizable-panels: ^0.0.37
stacktracey: ^2.1.8
superjson: ^1.12.2
peerDependencies:
"@emotion/react": ">=11.0.0"
jotai: ">=1.11.0"
react: ">=17.0.0"
checksum: 6f08ad73788e63f20e82874bf79a4e0d78c63a39db4488fe976a218586d407e16166506c87f474bd202ba824368b4074714f8da63b8b514ac01d117362c82eec
languageName: node
linkType: hard
"jotai@npm:^2.0.4": "jotai@npm:^2.0.4":
version: 2.0.4 version: 2.0.4
resolution: "jotai@npm:2.0.4" resolution: "jotai@npm:2.0.4"
@@ -16259,6 +16513,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"printable-characters@npm:^1.0.42":
version: 1.0.42
resolution: "printable-characters@npm:1.0.42"
checksum: 2724aa02919d7085933af0f8f904bd0de67a6b53834f2e5b98fc7aa3650e20755c805e8c85bcf96c09f678cb16a58b55640dd3a2da843195fce06b1ccb0c8ce4
languageName: node
linkType: hard
"prism-react-renderer@npm:^1.2.1":
version: 1.3.5
resolution: "prism-react-renderer@npm:1.3.5"
peerDependencies:
react: ">=0.14.9"
checksum: c18806dcbc4c0b4fd6fd15bd06b4f7c0a6da98d93af235c3e970854994eb9b59e23315abb6cfc29e69da26d36709a47e25da85ab27fed81b6812f0a52caf6dfa
languageName: node
linkType: hard
"prismjs@npm:^1.27.0": "prismjs@npm:^1.27.0":
version: 1.29.0 version: 1.29.0
resolution: "prismjs@npm:1.29.0" resolution: "prismjs@npm:1.29.0"
@@ -16699,6 +16969,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-error-boundary@npm:^3.1.4":
version: 3.1.4
resolution: "react-error-boundary@npm:3.1.4"
dependencies:
"@babel/runtime": ^7.12.5
peerDependencies:
react: ">=16.13.1"
checksum: f36270a5d775a25c8920f854c0d91649ceea417b15b5bc51e270a959b0476647bb79abb4da3be7dd9a4597b029214e8fe43ea914a7f16fa7543c91f784977f1b
languageName: node
linkType: hard
"react-error-boundary@npm:^4.0.3": "react-error-boundary@npm:^4.0.3":
version: 4.0.3 version: 4.0.3
resolution: "react-error-boundary@npm:4.0.3" resolution: "react-error-boundary@npm:4.0.3"
@@ -16807,6 +17088,35 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-remove-scroll@npm:^2.5.5":
version: 2.5.5
resolution: "react-remove-scroll@npm:2.5.5"
dependencies:
react-remove-scroll-bar: ^2.3.3
react-style-singleton: ^2.2.1
tslib: ^2.1.0
use-callback-ref: ^1.3.0
use-sidecar: ^1.1.2
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 2c7fe9cbd766f5e54beb4bec2e2efb2de3583037b23fef8fa511ab426ed7f1ae992382db5acd8ab5bfb030a4b93a06a2ebca41377d6eeaf0e6791bb0a59616a4
languageName: node
linkType: hard
"react-resizable-panels@npm:^0.0.37":
version: 0.0.37
resolution: "react-resizable-panels@npm:0.0.37"
peerDependencies:
react: ^16.14.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0
checksum: adf1559eda9a4d7634462969ac0226d1aeb3fa21c772cd2f742e1af56408d7517323fe21705a2972a8ed3616b0d15829f5ee4d796f564d3f1a285972c97703ef
languageName: node
linkType: hard
"react-style-singleton@npm:^2.2.1": "react-style-singleton@npm:^2.2.1":
version: 2.2.1 version: 2.2.1
resolution: "react-style-singleton@npm:2.2.1" resolution: "react-style-singleton@npm:2.2.1"
@@ -16839,6 +17149,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-textarea-autosize@npm:8.3.4":
version: 8.3.4
resolution: "react-textarea-autosize@npm:8.3.4"
dependencies:
"@babel/runtime": ^7.10.2
use-composed-ref: ^1.3.0
use-latest: ^1.2.1
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 87360d4392276d4e87511a73be9b0634b8bcce8f4f648cf659334d993f25ad3d4062f468f1e1944fc614123acae4299580aad00b760c6a96cec190e076f847f5
languageName: node
linkType: hard
"react-transition-group@npm:^4.4.5": "react-transition-group@npm:^4.4.5":
version: 4.4.5 version: 4.4.5
resolution: "react-transition-group@npm:4.4.5" resolution: "react-transition-group@npm:4.4.5"
@@ -18035,6 +18358,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"stacktracey@npm:^2.1.8":
version: 2.1.8
resolution: "stacktracey@npm:2.1.8"
dependencies:
as-table: ^1.0.36
get-source: ^2.0.12
checksum: abd8316b4e120884108f5a47b2f61abdcaeaa118afd95f3c48317cb057fff43d697450ba00de3f9fe7fee61ee72644ccda4db990a8e4553706644f7c17522eab
languageName: node
linkType: hard
"statuses@npm:2.0.1": "statuses@npm:2.0.1":
version: 2.0.1 version: 2.0.1
resolution: "statuses@npm:2.0.1" resolution: "statuses@npm:2.0.1"
@@ -18355,6 +18688,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"superjson@npm:^1.12.2":
version: 1.12.2
resolution: "superjson@npm:1.12.2"
dependencies:
copy-anything: ^3.0.2
checksum: cf7735e172811ed87476a7c2f1bb0e83725a0e3c2d7a50a71303a973060b3c710288767fb767a7a7eee8e5625d3ccaee1176a93e27f43841627512c15c4cdf84
languageName: node
linkType: hard
"supports-color@npm:^5.3.0": "supports-color@npm:^5.3.0":
version: 5.5.0 version: 5.5.0
resolution: "supports-color@npm:5.5.0" resolution: "supports-color@npm:5.5.0"
@@ -18434,6 +18776,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tabbable@npm:^6.0.1":
version: 6.1.1
resolution: "tabbable@npm:6.1.1"
checksum: 348639497262241ce8e0ccb0664ea582a386183107299ee8f27cf7b56bc84f36e09eaf667d3cb4201e789634012a91f7129bcbd49760abe874fbace35b4cf429
languageName: node
linkType: hard
"table@npm:^6.8.0": "table@npm:^6.8.0":
version: 6.8.1 version: 6.8.1
resolution: "table@npm:6.8.1" resolution: "table@npm:6.8.1"
@@ -19275,6 +19624,41 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"use-composed-ref@npm:^1.3.0":
version: 1.3.0
resolution: "use-composed-ref@npm:1.3.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: f771cbadfdc91e03b7ab9eb32d0fc0cc647755711801bf507e891ad38c4bbc5f02b2509acadf9c965ec9c5f2f642fd33bdfdfb17b0873c4ad0a9b1f5e5e724bf
languageName: node
linkType: hard
"use-isomorphic-layout-effect@npm:^1.1.1":
version: 1.1.2
resolution: "use-isomorphic-layout-effect@npm:1.1.2"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: a6532f7fc9ae222c3725ff0308aaf1f1ddbd3c00d685ef9eee6714fd0684de5cb9741b432fbf51e61a784e2955424864f7ea9f99734a02f237b17ad3e18ea5cb
languageName: node
linkType: hard
"use-latest@npm:^1.2.1":
version: 1.2.1
resolution: "use-latest@npm:1.2.1"
dependencies:
use-isomorphic-layout-effect: ^1.1.1
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: ed3f2ddddf6f21825e2ede4c2e0f0db8dcce5129802b69d1f0575fc1b42380436e8c76a6cd885d4e9aa8e292e60fb8b959c955f33c6a9123b83814a1a1875367
languageName: node
linkType: hard
"use-resize-observer@npm:^9.1.0": "use-resize-observer@npm:^9.1.0":
version: 9.1.0 version: 9.1.0
resolution: "use-resize-observer@npm:9.1.0" resolution: "use-resize-observer@npm:9.1.0"