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; 'affine:page'
)[0] as PageBlockModel;
if (block) {
block.title.insert(query, 0);
} else {
console.warn('No page block found');
} }
const newPage = blockSuiteWorkspace.getPage(page.id); blockSuiteWorkspace.setPageMeta(page.id, {
if (newPage) { title: query,
const block = newPage.getBlockByFlavour( });
'affine:page' onClose();
)[0] as PageBlockModel; void jumpToPage(blockSuiteWorkspace.id, page.id);
if (block) { }, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
block.title.insert(query, 0);
}
}
}}
> >
<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,
}); });
{
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
}
await result.current.createLocalWorkspace('test'); await result.current.createLocalWorkspace('test');
const workspaces = await store.get(workspacesAtom); {
console.log(workspaces); const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1); 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 */}
<ModalProvider key={currentWorkspaceId} /> <AllWorkspaceContext>
<Suspense fallback={<PageLoading />}> <CurrentWorkspaceContext>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner> <ModalProvider key={currentWorkspaceId} />
</Suspense> </CurrentWorkspaceContext>
</AllWorkspaceContext>
<CurrentWorkspaceContext>
<Suspense fallback={<PageLoading text="Finding current workspace" />}>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</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} />, [
<ThemeProvider key="ThemeProvider" />, <Provider key="JotaiProvider" store={rootStore} />,
], <DebugProvider key="DebugProvider" />,
<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

@@ -6,7 +6,7 @@ This document delves into the design and architecture of the AFFiNE platform, pr
## Addressing the Challenge ## Addressing the Challenge
AFFiNE is a platform designed to be the next-generation collaborative knowledge base for professionals. It is local-first, yet collaborative; It is robust as a foundational platform, yet friendly to extend. We believe that a knowledge base that truly meets the needs of professionals in different scenarios should be open-source and open to the community. By using AFFiNE, people can take full control of their data and workflow, thus achieving data sovereignty. AFFiNE is a platform designed to be the next-generation collaborative knowledge base for professionals. It is local-first, yet collaborative; It is robust as a foundational platform, yet friendly to extend. We believe that a knowledge base that truly meets the needs of professionals in different scenarios should be open-source and open to the community. By using AFFiNE, people can take full control of their data and workflow, thus achieving data sovereignty.
To do so, we should have a stable plugin system that is easy to use by the community and a well-modularized editor for customizability. Let's list the challenges from the perspective of data modeling, UI and feature plugins, and cross-platform support. To do so, we should have a stable plugin system that is easy to use by the community and a well-modularized editor for customizability. Let's list the challenges from the perspective of data modeling, UI and feature plugins, and cross-platform support.
### Data might come from anywhere and go anywhere, in spite of the cloud ### Data might come from anywhere and go anywhere, in spite of the cloud
@@ -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,34 +8,32 @@ 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); const [a, b] = await getBuiltInUser();
const [a, b] = await getBuiltInUser(); await loginUser(page, a);
await loginUser(page, a); await page.reload();
await page.reload(); await page.waitForTimeout(50);
await page.waitForTimeout(50); await clickSideBarCurrentWorkspaceBanner(page);
await clickSideBarCurrentWorkspaceBanner(page); await page.getByText('Cloud Workspace').click();
await page.getByText('Cloud Workspace').click(); const context2 = await browser.newContext();
const context2 = await browser.newContext(); const page2 = await context2.newPage();
const page2 = await context2.newPage(); await openHomePage(page2);
await openHomePage(page2); await loginUser(page2, b);
await loginUser(page2, b); await page2.reload();
await page2.reload(); await clickSideBarCurrentWorkspaceBanner(page2);
await clickSideBarCurrentWorkspaceBanner(page2); await page2.getByText('Joined Workspace').click();
await page2.getByText('Joined Workspace').click(); await clickNewPageButton(page);
await clickNewPageButton(page); const url = page.url();
const url = page.url(); await page2.goto(url);
await page2.goto(url); await page.focus('.affine-default-page-block-title');
await page.focus('.affine-default-page-block-title'); await page.type('.affine-default-page-block-title', 'hello', {
await page.type('.affine-default-page-block-title', 'hello', { delay: 100,
delay: 100,
});
await page.waitForTimeout(100);
const title = (await page
.locator('.affine-default-page-block-title')
.textContent()) as string;
expect(title.trim()).toBe('hello');
}); });
await page.waitForTimeout(100);
const title = (await page
.locator('.affine-default-page-block-title')
.textContent()) as string;
expect(title.trim()).toBe('hello');
}); });

View File

@@ -6,62 +6,56 @@ 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 }) => { await openHomePage(page);
// when enable single page, most time the page will crash const [a] = await createFakeUser();
test.fail(); await loginUser(page, a);
await openHomePage(page); await waitMarkdownImported(page);
const [a] = await createFakeUser(); const name = `test-${Date.now()}`;
await loginUser(page, a); await createWorkspace({ name }, page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
const name = `test-${Date.now()}`; await clickNewPageButton(page);
await createWorkspace({ name }, page); const page1Id = page.url().split('/').at(-1);
await waitMarkdownImported(page); await clickNewPageButton(page);
await clickNewPageButton(page); const page2Id = page.url().split('/').at(-1);
const page1Id = page.url().split('/').at(-1); expect(typeof page2Id).toBe('string');
await clickNewPageButton(page); expect(page1Id).not.toBe(page2Id);
const page2Id = page.url().split('/').at(-1); const title = 'This is page 2';
expect(typeof page2Id).toBe('string'); await page.locator('[data-block-is-title="true"]').type(title, {
expect(page1Id).not.toBe(page2Id); delay: 50,
const title = 'This is page 2';
await page.locator('[data-block-is-title="true"]').type(title, {
delay: 50,
});
await page.getByTestId('share-menu-button').click();
await page.getByTestId('share-menu-enable-affine-cloud-button').click();
const promise = page.evaluate(
async () =>
new Promise(resolve =>
window.addEventListener('affine-workspace:transform', resolve, {
once: true,
})
)
);
await page.getByTestId('confirm-enable-cloud-button').click();
await promise;
const newPage2Url = page.url().split('/');
newPage2Url[newPage2Url.length - 1] = page2Id as string;
await page.goto(newPage2Url.join('/'));
await page.waitForSelector('v-line');
const currentTitle = await page
.locator('[data-block-is-title="true"]')
.textContent();
expect(currentTitle).toBe(title);
await page.getByTestId('share-menu-button').click();
await page.getByTestId('affine-share-create-link').click();
await page.getByTestId('affine-share-copy-link').click();
const url = await page.evaluate(() => navigator.clipboard.readText());
expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe(
true
);
await page.waitForTimeout(1000);
const context2 = await browser.newContext();
const page2 = await context2.newPage();
await page2.goto(url);
await page2.waitForSelector('v-line');
const currentTitle2 = await page2
.locator('[data-block-is-title="true"]')
.textContent();
expect(currentTitle2).toBe(title);
}); });
await page.getByTestId('share-menu-button').click();
await page.getByTestId('share-menu-enable-affine-cloud-button').click();
const promise = page.evaluate(
async () =>
new Promise(resolve =>
window.addEventListener('affine-workspace:transform', resolve, {
once: true,
})
)
);
await page.getByTestId('confirm-enable-cloud-button').click();
await promise;
const newPage2Url = page.url().split('/');
newPage2Url[newPage2Url.length - 1] = page2Id as string;
await page.goto(newPage2Url.join('/'));
await page.waitForSelector('v-line');
const currentTitle = await page
.locator('[data-block-is-title="true"]')
.textContent();
expect(currentTitle).toBe(title);
await page.getByTestId('share-menu-button').click();
await page.getByTestId('affine-share-create-link').click();
await page.getByTestId('affine-share-copy-link').click();
const url = await page.evaluate(() => navigator.clipboard.readText());
expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe(true);
await page.waitForTimeout(1000);
const context2 = await browser.newContext();
const page2 = await context2.newPage();
await page2.goto(url);
await page2.waitForSelector('v-line');
const currentTitle2 = await page2
.locator('[data-block-is-title="true"]')
.textContent();
expect(currentTitle2).toBe(title);
}); });

View File

@@ -10,65 +10,61 @@ 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(); await loginUser(page, a);
await loginUser(page, a); await waitMarkdownImported(page);
await waitMarkdownImported(page); const name = `test-${Date.now()}`;
const name = `test-${Date.now()}`; await createWorkspace({ name }, page);
await createWorkspace({ name }, page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await clickSideBarSettingButton(page);
await clickSideBarSettingButton(page); await page.waitForTimeout(50);
await page.waitForTimeout(50); await clickPublishPanel(page);
await clickPublishPanel(page); await page.getByTestId('publish-enable-affine-cloud-button').click();
await page.getByTestId('publish-enable-affine-cloud-button').click(); await page.getByTestId('confirm-enable-affine-cloud-button').click();
await page.getByTestId('confirm-enable-affine-cloud-button').click(); await page.getByTestId('publish-to-web-button').waitFor({
await page.getByTestId('publish-to-web-button').waitFor({ timeout: 10000,
timeout: 10000,
});
await page.getByTestId('publish-to-web-button').click();
await page.getByTestId('share-url').waitFor({
timeout: 10000,
});
const url = await page.getByTestId('share-url').inputValue();
expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe(
true
);
const page2 = await context.newPage();
await page2.goto(url);
await page2.waitForSelector('thead', {
timeout: 10000,
});
await page2.getByText('Welcome to AFFiNE').click();
}); });
await page.getByTestId('publish-to-web-button').click();
test('access public workspace page', async ({ page, browser }) => { await page.getByTestId('share-url').waitFor({
await openHomePage(page); timeout: 10000,
const [a] = await createFakeUser();
await loginUser(page, a);
await waitMarkdownImported(page);
const name = `test-${Date.now()}`;
await createWorkspace({ name }, page);
await waitMarkdownImported(page);
await clickSideBarSettingButton(page);
await page.waitForTimeout(50);
await clickPublishPanel(page);
await page.getByTestId('publish-enable-affine-cloud-button').click();
await page.getByTestId('confirm-enable-affine-cloud-button').click();
await page.getByTestId('publish-to-web-button').waitFor({
timeout: 10000,
});
await page.getByTestId('publish-to-web-button').click();
await page.getByTestId('share-url').waitFor({
timeout: 10000,
});
await clickSideBarAllPageButton(page);
await page.locator('tr').nth(1).click();
const url = page.url();
const context = await browser.newContext();
const page2 = await context.newPage();
await page2.goto(url.replace('workspace', 'public-workspace'));
await page2.waitForSelector('v-line');
}); });
const url = await page.getByTestId('share-url').inputValue();
expect(url.startsWith('http://localhost:8080/public-workspace/')).toBe(true);
const page2 = await context.newPage();
await page2.goto(url);
await page2.waitForSelector('thead', {
timeout: 10000,
});
await page2.getByText('Welcome to AFFiNE').click();
});
test('access public workspace page', async ({ page, browser }) => {
await openHomePage(page);
const [a] = await createFakeUser();
await loginUser(page, a);
await waitMarkdownImported(page);
const name = `test-${Date.now()}`;
await createWorkspace({ name }, page);
await waitMarkdownImported(page);
await clickSideBarSettingButton(page);
await page.waitForTimeout(50);
await clickPublishPanel(page);
await page.getByTestId('publish-enable-affine-cloud-button').click();
await page.getByTestId('confirm-enable-affine-cloud-button').click();
await page.getByTestId('publish-to-web-button').waitFor({
timeout: 10000,
});
await page.getByTestId('publish-to-web-button').click();
await page.getByTestId('share-url').waitFor({
timeout: 10000,
});
await clickSideBarAllPageButton(page);
await page.locator('tr').nth(1).click();
const url = page.url();
const context = await browser.newContext();
const page2 = await context.newPage();
await page2.goto(url.replace('workspace', 'public-workspace'));
await page2.waitForSelector('v-line');
}); });

View File

@@ -21,44 +21,42 @@ 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); const [a] = await createFakeUser(userA, userB);
const [a] = await createFakeUser(userA, userB); await loginUser(page, a);
await loginUser(page, a); await clickSideBarCurrentWorkspaceBanner(page);
await clickSideBarCurrentWorkspaceBanner(page); const footer = page.locator('[data-testid="workspace-list-modal-footer"]');
const footer = page.locator('[data-testid="workspace-list-modal-footer"]'); expect(await footer.getByText(userA.name).isVisible()).toBe(true);
expect(await footer.getByText(userA.name).isVisible()).toBe(true); expect(await footer.getByText(userA.email).isVisible()).toBe(true);
expect(await footer.getByText(userA.email).isVisible()).toBe(true); await assertCurrentWorkspaceFlavour('local', page);
await assertCurrentWorkspaceFlavour('local', page); });
});
test('should enable affine workspace successfully', async ({ page }) => {
test('should enable affine workspace successfully', async ({ page }) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); const [a] = await createFakeUser();
const [a] = await createFakeUser(); await loginUser(page, a);
await loginUser(page, a); const name = `test-${Date.now()}`;
const name = `test-${Date.now()}`; await createWorkspace({ name }, page);
await createWorkspace({ name }, page); await page.waitForTimeout(50);
await page.waitForTimeout(50); await clickSideBarSettingButton(page);
await clickSideBarSettingButton(page); await page.waitForTimeout(50);
await page.waitForTimeout(50); await clickCollaborationPanel(page);
await clickCollaborationPanel(page); await page.getByTestId('local-workspace-enable-cloud-button').click();
await page.getByTestId('local-workspace-enable-cloud-button').click(); await page.getByTestId('confirm-enable-cloud-button').click();
await page.getByTestId('confirm-enable-cloud-button').click(); await page.waitForSelector("[data-testid='member-length']", {
await page.waitForSelector("[data-testid='member-length']", { timeout: 20000,
timeout: 20000, });
}); await clickSideBarAllPageButton(page);
await clickSideBarAllPageButton(page); await clickNewPageButton(page);
await clickNewPageButton(page); await page.locator('[data-block-is-title="true"]').type('Hello, world!', {
await page.locator('[data-block-is-title="true"]').type('Hello, world!', { delay: 50,
delay: 50, });
}); await assertCurrentWorkspaceFlavour('affine', page);
await assertCurrentWorkspaceFlavour('affine', page); await openWorkspaceListModal(page);
await openWorkspaceListModal(page); await page.getByTestId('workspace-list-modal-sign-out').click();
await page.getByTestId('workspace-list-modal-sign-out').click(); await page.waitForTimeout(1000);
await page.waitForTimeout(1000); await assertCurrentWorkspaceFlavour('local', page);
await assertCurrentWorkspaceFlavour('local', page);
});
}); });

View File

@@ -4,25 +4,23 @@ 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); const btn = await page.getByTestId('switch-edgeless-mode-button');
const btn = await page.getByTestId('switch-edgeless-mode-button'); await btn.click();
await btn.click();
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);
}); });
test('Convert to edgeless by editor header items', async ({ page }) => { test('Convert to edgeless by editor header items', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
await clickPageMoreActions(page); await clickPageMoreActions(page);
const menusEdgelessItem = page.getByTestId('editor-option-menu-edgeless'); const menusEdgelessItem = page.getByTestId('editor-option-menu-edgeless');
await menusEdgelessItem.click({ delay: 100 }); await menusEdgelessItem.click({ delay: 100 });
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,20 +4,16 @@ 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); await page.locator('[data-testid=help-island]').click();
await page.locator('[data-testid=help-island]').click(); const rightBottomContactUs = page.locator(
const rightBottomContactUs = page.locator( '[data-testid=right-bottom-contact-us-icon]'
'[data-testid=right-bottom-contact-us-icon]' );
); 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,16 +2,12 @@ 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('http://localhost:8080/_debug/init-page?type=importMarkdown');
await page.goto( await page.waitForSelector('v-line');
'http://localhost:8080/_debug/init-page?type=importMarkdown' const pageId = await page.evaluate(async () => {
); // @ts-ignore
await page.waitForSelector('v-line'); return globalThis.page.id;
const pageId = await page.evaluate(async () => {
// @ts-ignore
return globalThis.page.id;
});
expect(pageId).toBe('page0');
}); });
expect(pageId).toBe('page0');
}); });

View File

@@ -2,21 +2,19 @@ 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, }) => {
}) => { await page.goto('http://localhost:8080/_debug/broadcast');
await page.goto('http://localhost:8080/_debug/broadcast'); const page2 = await context.newPage();
const page2 = await context.newPage(); await page2.goto('http://localhost:8080/_debug/broadcast');
await page2.goto('http://localhost:8080/_debug/broadcast'); await page.waitForSelector('#__next');
await page.waitForSelector('#__next'); await page2.waitForSelector('#__next');
await page2.waitForSelector('#__next'); await page.click('[data-testid="create-page"]');
await page.click('[data-testid="create-page"]'); expect(await page.locator('tr').count()).toBe(2);
expect(await page.locator('tr').count()).toBe(2); expect(await page2.locator('tr').count()).toBe(2);
expect(await page2.locator('tr').count()).toBe(2); await page2.click('[data-testid="create-page"]');
await page2.click('[data-testid="create-page"]'); 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,74 +4,72 @@ 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); await page.getByTestId('sliderBar-arrowButton-collapse').click();
await page.getByTestId('sliderBar-arrowButton-collapse').click(); const sliderBarArea = page.getByTestId('sliderBar-root');
const sliderBarArea = page.getByTestId('sliderBar-root'); await expect(sliderBarArea).not.toBeInViewport();
await expect(sliderBarArea).not.toBeInViewport(); });
});
test('Expand Sidebar', async ({ page }) => {
test('Expand Sidebar', async ({ page }) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await page.getByTestId('sliderBar-arrowButton-collapse').click();
await page.getByTestId('sliderBar-arrowButton-collapse').click(); const sliderBarArea = page.getByTestId('sliderBar-inner');
const sliderBarArea = page.getByTestId('sliderBar-inner'); await expect(sliderBarArea).not.toBeInViewport();
await expect(sliderBarArea).not.toBeInViewport();
await page.getByTestId('sliderBar-arrowButton-expand').click();
await page.getByTestId('sliderBar-arrowButton-expand').click(); await expect(sliderBarArea).toBeInViewport();
await expect(sliderBarArea).toBeInViewport(); });
});
test('Click resizer can close sidebar', async ({ page }) => {
test('Click resizer can close sidebar', async ({ page }) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); const sliderBarArea = page.getByTestId('sliderBar-inner');
const sliderBarArea = page.getByTestId('sliderBar-inner'); await expect(sliderBarArea).toBeVisible();
await expect(sliderBarArea).toBeVisible();
await page.getByTestId('sliderBar-resizer').click();
await page.getByTestId('sliderBar-resizer').click(); await expect(sliderBarArea).not.toBeInViewport();
await expect(sliderBarArea).not.toBeInViewport(); });
});
test('Drag resizer can resize sidebar', async ({ page }) => {
test('Drag resizer can resize sidebar', async ({ page }) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); const sliderBarArea = page.getByTestId('sliderBar-inner');
const sliderBarArea = page.getByTestId('sliderBar-inner'); await expect(sliderBarArea).toBeVisible();
await expect(sliderBarArea).toBeVisible();
const sliderResizer = page.getByTestId('sliderBar-resizer');
const sliderResizer = page.getByTestId('sliderBar-resizer'); await sliderResizer.hover();
await sliderResizer.hover(); await page.mouse.down();
await page.mouse.down(); await page.mouse.move(400, 300, {
await page.mouse.move(400, 300, { steps: 10,
steps: 10, });
}); await page.mouse.up();
await page.mouse.up(); const boundingBox = await page.getByTestId('sliderBar-root').boundingBox();
const boundingBox = await page.getByTestId('sliderBar-root').boundingBox(); expect(boundingBox?.width).toBe(400);
expect(boundingBox?.width).toBe(400); });
});
test('Sidebar in between sm & md breakpoint', async ({ page }) => {
test('Sidebar in between sm & md breakpoint', async ({ page }) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); const sliderBarArea = page.getByTestId('sliderBar-inner');
const sliderBarArea = page.getByTestId('sliderBar-inner'); const sliderBarModalBackground = page.getByTestId(
const sliderBarModalBackground = page.getByTestId( 'sliderBar-modalBackground'
'sliderBar-modalBackground' );
); await expect(sliderBarArea).toBeInViewport();
await expect(sliderBarArea).toBeInViewport(); await expect(sliderBarModalBackground).not.toBeVisible();
await expect(sliderBarModalBackground).not.toBeVisible();
await page.setViewportSize({
await page.setViewportSize({ width: 768,
width: 768, height: 1024,
height: 1024, });
}); await expect(sliderBarModalBackground).toBeVisible();
await expect(sliderBarModalBackground).toBeVisible();
// click modal background can close sidebar
// click modal background can close sidebar await sliderBarModalBackground.click({
await sliderBarModalBackground.click({ force: true,
force: true, position: { x: 600, y: 150 },
position: { x: 600, y: 150 }, });
}); await expect(sliderBarArea).not.toBeInViewport();
await expect(sliderBarArea).not.toBeInViewport();
});
}); });

View File

@@ -5,37 +5,35 @@ 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); await newPage(page);
await newPage(page); await page.getByTestId('workspace-name').click();
await page.getByTestId('workspace-name').click(); await page.getByTestId('new-workspace').click({ delay: 50 });
await page.getByTestId('new-workspace').click({ delay: 50 }); await page
await page .getByTestId('create-workspace-input')
.getByTestId('create-workspace-input') .type('Test Workspace 1', { delay: 50 });
.type('Test Workspace 1', { delay: 50 }); await page.getByTestId('create-workspace-button').click();
await page.getByTestId('create-workspace-button').click(); await page.getByTestId('workspace-name').click();
await page.getByTestId('workspace-name').click(); await page.getByTestId('workspace-card').nth(1).click();
await page.getByTestId('workspace-card').nth(1).click(); await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('slider-bar-workspace-setting-button').click(); await page
await page .getByTestId('upload-avatar')
.getByTestId('upload-avatar') .setInputFiles('./tests/fixtures/smile.png');
.setInputFiles('./tests/fixtures/smile.png'); await page.getByTestId('workspace-name').click();
await page.getByTestId('workspace-name').click(); await page.getByTestId('workspace-card').nth(0).click();
await page.getByTestId('workspace-card').nth(0).click(); await page.waitForTimeout(1000);
await page.waitForTimeout(1000); const text = await page.getByTestId('workspace-avatar').textContent();
const text = await page.getByTestId('workspace-avatar').textContent(); // default avatar for default workspace
// default avatar for default workspace expect(text).toBe('D');
expect(text).toBe('D'); await page.getByTestId('workspace-name').click();
await page.getByTestId('workspace-name').click(); await page.getByTestId('workspace-card').nth(1).click();
await page.getByTestId('workspace-card').nth(1).click(); const blobUrl = await page
const blobUrl = await page .getByTestId('workspace-avatar')
.getByTestId('workspace-avatar') .locator('img')
.locator('img') .getAttribute('src');
.getAttribute('src'); // out user uploaded avatar
// out user uploaded avatar expect(blobUrl).toContain('blob:');
expect(blobUrl).toContain('blob:'); await assertCurrentWorkspaceFlavour('local', page);
await assertCurrentWorkspaceFlavour('local', page);
});
}); });

View File

@@ -9,49 +9,47 @@ 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, }) => {
}) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore'); const newPageId = page.url().split('/').reverse()[0];
const newPageId = page.url().split('/').reverse()[0]; await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click(); const cell = page.getByRole('cell', {
const cell = page.getByRole('cell', { name: 'this is a new page to restore',
name: 'this is a new page to restore',
});
expect(cell).not.toBeUndefined();
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
const deleteBtn = page.getByTestId('move-to-trash');
await deleteBtn.click();
const confirmTip = page.getByText('Delete page?');
expect(confirmTip).not.toBeUndefined();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
// permanently delete it
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.nth(1)
.click();
await page.getByText('Delete permanently?').dblclick();
// show empty tip
expect(
page.getByText(
'Tips: Click Add to Favorites/Trash and the page will appear here.'
)
).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });
expect(cell).not.toBeUndefined();
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
const deleteBtn = page.getByTestId('move-to-trash');
await deleteBtn.click();
const confirmTip = page.getByText('Delete page?');
expect(confirmTip).not.toBeUndefined();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
// permanently delete it
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.nth(1)
.click();
await page.getByText('Delete permanently?').dblclick();
// show empty tip
expect(
page.getByText(
'Tips: Click Add to Favorites/Trash and the page will appear here.'
)
).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });

View File

@@ -6,26 +6,24 @@ 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, }) => {
}) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await clickSideBarSettingButton(page);
await clickSideBarSettingButton(page); await page.getByTestId('delete-workspace-button').click();
await page.getByTestId('delete-workspace-button').click(); const workspaceNameDom = await page.getByTestId('workspace-name');
const workspaceNameDom = await page.getByTestId('workspace-name'); const currentWorkspaceName = await workspaceNameDom.evaluate(
const currentWorkspaceName = await workspaceNameDom.evaluate( node => node.textContent
node => node.textContent );
); await page
await page .getByTestId('delete-workspace-input')
.getByTestId('delete-workspace-input') .type(currentWorkspaceName as string);
.type(currentWorkspaceName as string); await page.getByTestId('delete-workspace-confirm-button').click();
await page.getByTestId('delete-workspace-confirm-button').click(); await page.getByTestId('affine-toast').waitFor({ state: 'attached' });
await page.getByTestId('affine-toast').waitFor({ state: 'attached' }); expect(await page.getByTestId('workspace-card').count()).toBe(0);
expect(await page.getByTestId('workspace-card').count()).toBe(0); await page.mouse.click(1, 1);
await page.mouse.click(1, 1); 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,64 +10,60 @@ 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); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await page
await page .getByPlaceholder('Title')
.getByPlaceholder('Title') .fill('this is a new page to export html content');
.fill('this is a new page to export html content'); await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click();
const cell = page.getByRole('cell', { const cell = page.getByRole('cell', {
name: 'this is a new page to export html content', name: 'this is a new page to export html content',
});
expect(cell).not.toBeUndefined();
await cell.click();
await clickPageMoreActions(page);
const exportParentBtn = page.getByRole('tooltip', {
name: 'Add to favorites Convert to Edgeless Export Delete',
});
await exportParentBtn.click();
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export to HTML' }).click(),
]);
expect(download.suggestedFilename()).toBe(
'this is a new page to export html content.html'
);
}); });
expect(cell).not.toBeUndefined();
test.skip('New a page ,then open it and export markdown', async ({ await cell.click();
page, await clickPageMoreActions(page);
}) => { const exportParentBtn = page.getByRole('tooltip', {
await newPage(page); name: 'Add to favorites Convert to Edgeless Export Delete',
await getBlockSuiteEditorTitle(page).click();
await page
.getByPlaceholder('Title')
.fill('this is a new page to export markdown content');
await page.getByRole('link', { name: 'All pages' }).click();
const cell = page.getByRole('cell', {
name: 'this is a new page to export markdown content',
});
expect(cell).not.toBeUndefined();
await cell.click();
await clickPageMoreActions(page);
const exportParentBtn = page.getByRole('tooltip', {
name: 'Add to favorites Convert to Edgeless Export Delete',
});
await exportParentBtn.click();
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export to Markdown' }).click(),
]);
expect(download.suggestedFilename()).toBe(
'this is a new page to export markdown content.md'
);
await assertCurrentWorkspaceFlavour('local', page);
}); });
await exportParentBtn.click();
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export to HTML' }).click(),
]);
expect(download.suggestedFilename()).toBe(
'this is a new page to export html content.html'
);
});
test.skip('New a page ,then open it and export markdown', async ({ page }) => {
await newPage(page);
await getBlockSuiteEditorTitle(page).click();
await page
.getByPlaceholder('Title')
.fill('this is a new page to export markdown content');
await page.getByRole('link', { name: 'All pages' }).click();
const cell = page.getByRole('cell', {
name: 'this is a new page to export markdown content',
});
expect(cell).not.toBeUndefined();
await cell.click();
await clickPageMoreActions(page);
const exportParentBtn = page.getByRole('tooltip', {
name: 'Add to favorites Convert to Edgeless Export Delete',
});
await exportParentBtn.click();
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export to Markdown' }).click(),
]);
expect(download.suggestedFilename()).toBe(
'this is a new page to export markdown content.md'
);
await assertCurrentWorkspaceFlavour('local', page);
}); });

View File

@@ -10,89 +10,87 @@ 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); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite'); await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click(); const cell = page.getByRole('cell', {
const cell = page.getByRole('cell', { name: 'this is a new page to favorite',
name: 'this is a new page to favorite',
});
expect(cell).not.toBeUndefined();
await cell.click();
await clickPageMoreActions(page);
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
await assertCurrentWorkspaceFlavour('local', page);
}); });
expect(cell).not.toBeUndefined();
test('Export to html and markdown', async ({ page }) => { await cell.click();
await openHomePage(page); await clickPageMoreActions(page);
await waitMarkdownImported(page); const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
{ await favoriteBtn.click();
await clickPageMoreActions(page); await assertCurrentWorkspaceFlavour('local', page);
await page.getByTestId('export-menu').click(); });
const downloadPromise = page.waitForEvent('download');
await page.getByTestId('export-to-markdown').click(); test('Export to html and markdown', async ({ page }) => {
await downloadPromise; await openHomePage(page);
} await waitMarkdownImported(page);
await page.waitForTimeout(50); {
{ await clickPageMoreActions(page);
await clickPageMoreActions(page); await page.getByTestId('export-menu').click();
await page.getByTestId('export-menu').click(); const downloadPromise = page.waitForEvent('download');
const downloadPromise = page.waitForEvent('download'); await page.getByTestId('export-to-markdown').click();
await page.getByTestId('export-to-html').click(); await downloadPromise;
await downloadPromise; }
} await page.waitForTimeout(50);
}); {
await clickPageMoreActions(page);
test('Cancel favorite', async ({ page }) => { await page.getByTestId('export-menu').click();
await openHomePage(page); const downloadPromise = page.waitForEvent('download');
await waitMarkdownImported(page); await page.getByTestId('export-to-html').click();
await newPage(page); await downloadPromise;
await getBlockSuiteEditorTitle(page).click(); }
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite'); });
await page.getByRole('link', { name: 'All pages' }).click();
const cell = page.getByRole('cell', { test('Cancel favorite', async ({ page }) => {
name: 'this is a new page to favorite', await openHomePage(page);
}); await waitMarkdownImported(page);
expect(cell).not.toBeUndefined(); await newPage(page);
await getBlockSuiteEditorTitle(page).click();
await cell.click(); await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await clickPageMoreActions(page); await page.getByRole('link', { name: 'All pages' }).click();
const cell = page.getByRole('cell', {
const favoriteBtn = page.getByTestId('editor-option-menu-favorite'); name: 'this is a new page to favorite',
await favoriteBtn.click(); });
expect(cell).not.toBeUndefined();
// expect it in favorite list
await page.getByRole('link', { name: 'Favorites' }).click(); await cell.click();
expect( await clickPageMoreActions(page);
page.getByRole('cell', { name: 'this is a new page to favorite' })
).not.toBeUndefined(); const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
// cancel favorite
// expect it in favorite list
await page.getByRole('link', { name: 'All pages' }).click(); await page.getByRole('link', { name: 'Favorites' }).click();
expect(
const box = await page page.getByRole('cell', { name: 'this is a new page to favorite' })
.getByRole('cell', { name: 'this is a new page to favorite' }) ).not.toBeUndefined();
.boundingBox();
//hover table record // cancel favorite
await page.mouse.move((box?.x ?? 0) + 10, (box?.y ?? 0) + 10);
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByTestId('favorited-icon').click();
const box = await page
// expect it not in favorite list .getByRole('cell', { name: 'this is a new page to favorite' })
await page.getByRole('link', { name: 'Favorites' }).click(); .boundingBox();
expect( //hover table record
page.getByText( await page.mouse.move((box?.x ?? 0) + 10, (box?.y ?? 0) + 10);
'Tips: Click Add to Favorites/Trash and the page will appear here.'
) await page.getByTestId('favorited-icon').click();
).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page); // expect it not in favorite list
}); await page.getByRole('link', { name: 'Favorites' }).click();
expect(
page.getByText(
'Tips: Click Add to Favorites/Trash and the page will appear here.'
)
).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });

View File

@@ -10,60 +10,58 @@ 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); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite'); const newPageId = page.url().split('/').reverse()[0];
const newPageId = page.url().split('/').reverse()[0]; await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click(); const cell = page.getByRole('cell', {
const cell = page.getByRole('cell', { name: 'this is a new page to favorite',
name: 'this is a new page to favorite',
});
await expect(cell).toBeVisible();
await cell.click();
await clickPageMoreActions(page);
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
const favoriteListItemInSidebar = page.getByTestId(
'favorite-list-item-' + newPageId
);
expect(await favoriteListItemInSidebar.textContent()).toBe(
'this is a new page to favorite'
);
}); });
await expect(cell).toBeVisible();
await cell.click();
await clickPageMoreActions(page);
test('Show favorite items in favorite list', async ({ page }) => { const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await openHomePage(page); await favoriteBtn.click();
await waitMarkdownImported(page); const favoriteListItemInSidebar = page.getByTestId(
await newPage(page); 'favorite-list-item-' + newPageId
await getBlockSuiteEditorTitle(page).click(); );
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite'); expect(await favoriteListItemInSidebar.textContent()).toBe(
await page.getByRole('link', { name: 'All pages' }).click(); 'this is a new page to favorite'
const cell = page.getByRole('cell', { );
name: 'this is a new page to favorite', });
});
expect(cell).not.toBeUndefined(); test('Show favorite items in favorite list', async ({ page }) => {
await cell.click(); await openHomePage(page);
await clickPageMoreActions(page); await waitMarkdownImported(page);
await newPage(page);
const favoriteBtn = page.getByTestId('editor-option-menu-favorite'); await getBlockSuiteEditorTitle(page).click();
await favoriteBtn.click(); await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'Favorites' }).click(); const cell = page.getByRole('cell', {
expect( name: 'this is a new page to favorite',
page.getByRole('cell', { name: 'this is a new page to favorite' }) });
).not.toBeUndefined(); expect(cell).not.toBeUndefined();
await cell.click();
await page.getByRole('cell').getByRole('button').nth(0).click(); await clickPageMoreActions(page);
expect(
await page const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
.getByText('Click Add to Favorites and the page will appear here.') await favoriteBtn.click();
.isVisible()
).toBe(true); await page.getByRole('link', { name: 'Favorites' }).click();
await assertCurrentWorkspaceFlavour('local', page); expect(
}); page.getByRole('cell', { name: 'this is a new page to favorite' })
).not.toBeUndefined();
await page.getByRole('cell').getByRole('button').nth(0).click();
expect(
await page
.getByText('Click Add to Favorites and the page will appear here.')
.isVisible()
).toBe(true);
await assertCurrentWorkspaceFlavour('local', page);
}); });

View File

@@ -9,26 +9,24 @@ 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); const originPageId = page.url().split('/').reverse()[0];
const originPageId = page.url().split('/').reverse()[0]; await newPage(page);
await newPage(page); const newPageId = page.url().split('/').reverse()[0];
const newPageId = page.url().split('/').reverse()[0]; expect(newPageId).not.toBe(originPageId);
expect(newPageId).not.toBe(originPageId); await assertCurrentWorkspaceFlavour('local', page);
await assertCurrentWorkspaceFlavour('local', page); });
});
test('click btn bew page and find it in all pages', async ({ page }) => {
test('click btn bew page and find it in all pages', async ({ page }) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).fill('this is a new page');
await getBlockSuiteEditorTitle(page).fill('this is a new page'); await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click(); const cell = page.getByRole('cell', { name: 'this is a new page' });
const cell = page.getByRole('cell', { name: 'this is a new page' }); expect(cell).not.toBeUndefined();
expect(cell).not.toBeUndefined(); await assertCurrentWorkspaceFlavour('local', page);
await assertCurrentWorkspaceFlavour('local', page);
});
}); });

View File

@@ -9,29 +9,27 @@ 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); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).fill('this is a new page');
await getBlockSuiteEditorTitle(page).fill('this is a new page'); const newPageUrl = page.url();
const newPageUrl = page.url(); const newPageId = page.url().split('/').reverse()[0];
const newPageId = page.url().split('/').reverse()[0];
await page.getByRole('link', { name: 'All pages' }).click(); await page.getByRole('link', { name: 'All pages' }).click();
await page await page
.getByTestId('more-actions-' + newPageId) .getByTestId('more-actions-' + newPageId)
.getByRole('button') .getByRole('button')
.first() .first()
.click(); .click();
const [newTabPage] = await Promise.all([ const [newTabPage] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.getByRole('button', { name: 'Open in new tab' }).click(), page.getByRole('button', { name: 'Open in new tab' }).click(),
]); ]);
expect(newTabPage.url()).toBe(newPageUrl); expect(newTabPage.url()).toBe(newPageUrl);
await assertCurrentWorkspaceFlavour('local', page); await assertCurrentWorkspaceFlavour('local', page);
});
}); });

View File

@@ -9,51 +9,49 @@ 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, }) => {
}) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore'); const newPageId = page.url().split('/').reverse()[0];
const newPageId = page.url().split('/').reverse()[0]; await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click(); const cell = page.getByRole('cell', {
const cell = page.getByRole('cell', { name: 'this is a new page to restore',
name: 'this is a new page to restore',
});
expect(cell).not.toBeUndefined();
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
const deleteBtn = page.getByTestId('move-to-trash');
await deleteBtn.click();
const confirmTip = page.getByText('Delete page?');
expect(confirmTip).not.toBeUndefined();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
await page.waitForTimeout(50);
const trashPage = page.url();
// restore it
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
// stay in trash page
expect(page.url()).toBe(trashPage);
await page.getByRole('link', { name: 'All pages' }).click();
const restoreCell = page.getByRole('cell', {
name: 'this is a new page to restore',
});
expect(restoreCell).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });
expect(cell).not.toBeUndefined();
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
const deleteBtn = page.getByTestId('move-to-trash');
await deleteBtn.click();
const confirmTip = page.getByText('Delete page?');
expect(confirmTip).not.toBeUndefined();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
await page.waitForTimeout(50);
const trashPage = page.url();
// restore it
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
// stay in trash page
expect(page.url()).toBe(trashPage);
await page.getByRole('link', { name: 'All pages' }).click();
const restoreCell = page.getByRole('cell', {
name: 'this is a new page to restore',
});
expect(restoreCell).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });

View File

@@ -8,29 +8,25 @@ 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('slider-bar-workspace-setting-button');
const element = await page.getByTestId( const prev = await element.screenshot({
'slider-bar-workspace-setting-button' path: resolve(
); testResultDir,
const prev = await element.screenshot({ 'slider-bar-workspace-setting-button-prev.png'
path: resolve( ),
testResultDir,
'slider-bar-workspace-setting-button-prev.png'
),
});
await clickSideBarSettingButton(page);
await page.waitForTimeout(50);
const after = await element.screenshot({
path: resolve(
testResultDir,
'slider-bar-workspace-setting-button-after.png'
),
});
expect(prev).not.toEqual(after);
}); });
await clickSideBarSettingButton(page);
await page.waitForTimeout(50);
const after = await element.screenshot({
path: resolve(
testResultDir,
'slider-bar-workspace-setting-button-after.png'
),
});
expect(prev).not.toEqual(after);
}); });

View File

@@ -10,52 +10,50 @@ 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); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete'); await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click(); const cell = page.getByRole('cell', {
const cell = page.getByRole('cell', { name: 'this is a new page to delete',
name: 'this is a new page to delete',
});
expect(cell).not.toBeUndefined();
await cell.click();
await clickPageMoreActions(page);
const deleteBtn = page.getByTestId('editor-option-menu-delete');
await deleteBtn.click();
const confirmTip = page.getByText('Delete page?');
expect(confirmTip).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });
expect(cell).not.toBeUndefined();
test('New a page ,then go to all pages and show delete modal', async ({ await cell.click();
page, await clickPageMoreActions(page);
}) => { const deleteBtn = page.getByTestId('editor-option-menu-delete');
await openHomePage(page); await deleteBtn.click();
await waitMarkdownImported(page); const confirmTip = page.getByText('Delete page?');
await newPage(page); expect(confirmTip).not.toBeUndefined();
await getBlockSuiteEditorTitle(page).click(); await assertCurrentWorkspaceFlavour('local', page);
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete'); });
const newPageId = page.url().split('/').reverse()[0];
await page.getByRole('link', { name: 'All pages' }).click(); test('New a page ,then go to all pages and show delete modal', async ({
const cell = page.getByRole('cell', { page,
name: 'this is a new page to delete', }) => {
}); await openHomePage(page);
expect(cell).not.toBeUndefined(); await waitMarkdownImported(page);
await newPage(page);
await page await getBlockSuiteEditorTitle(page).click();
.getByTestId('more-actions-' + newPageId) await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
.getByRole('button') const newPageId = page.url().split('/').reverse()[0];
.first() await page.getByRole('link', { name: 'All pages' }).click();
.click(); const cell = page.getByRole('cell', {
const deleteBtn = page.getByTestId('move-to-trash'); name: 'this is a new page to delete',
await deleteBtn.click(); });
const confirmTip = page.getByText('Delete page?'); expect(cell).not.toBeUndefined();
expect(confirmTip).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page); await page
}); .getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
const deleteBtn = page.getByTestId('move-to-trash');
await deleteBtn.click();
const confirmTip = page.getByText('Delete page?');
expect(confirmTip).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });

View File

@@ -9,38 +9,36 @@ 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, }) => {
}) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await newPage(page);
await newPage(page); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete'); const newPageId = page.url().split('/').reverse()[0];
const newPageId = page.url().split('/').reverse()[0]; await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click(); const cell = page.getByRole('cell', {
const cell = page.getByRole('cell', { name: 'this is a new page to delete',
name: 'this is a new page to delete',
});
expect(cell).not.toBeUndefined();
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
const deleteBtn = page.getByTestId('move-to-trash');
await deleteBtn.click();
const confirmTip = page.getByText('Delete page?');
expect(confirmTip).not.toBeUndefined();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
expect(
page.getByRole('cell', { name: 'this is a new page to delete' })
).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });
expect(cell).not.toBeUndefined();
await page
.getByTestId('more-actions-' + newPageId)
.getByRole('button')
.first()
.click();
const deleteBtn = page.getByTestId('move-to-trash');
await deleteBtn.click();
const confirmTip = page.getByText('Delete page?');
expect(confirmTip).not.toBeUndefined();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('link', { name: 'Trash' }).click();
expect(
page.getByRole('cell', { name: 'this is a new page to delete' })
).not.toBeUndefined();
await assertCurrentWorkspaceFlavour('local', page);
}); });

View File

@@ -6,114 +6,112 @@ 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); const workspaceName = page.getByTestId('workspace-name');
const workspaceName = page.getByTestId('workspace-name'); await workspaceName.click();
await workspaceName.click(); expect(
expect( page
page .locator('div')
.locator('div') .filter({ hasText: 'AFFiNE TestLocal WorkspaceAvailable Offline' })
.filter({ hasText: 'AFFiNE TestLocal WorkspaceAvailable Offline' }) .nth(3)
.nth(3) ).not.toBeNull();
).not.toBeNull(); });
});
test('create one workspace in the workspace list', async ({ page }) => { test('create one workspace in the workspace list', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
const newWorkspaceNameStr = 'New Workspace'; const newWorkspaceNameStr = 'New Workspace';
await createWorkspace({ name: newWorkspaceNameStr }, page); await createWorkspace({ name: newWorkspaceNameStr }, page);
// check new workspace name // check new workspace name
const newWorkspaceName = page.getByTestId('workspace-name'); const newWorkspaceName = page.getByTestId('workspace-name');
await newWorkspaceName.click(); await newWorkspaceName.click();
//check workspace list length
const workspaceCards = await page.$$('data-testid=workspace-card');
expect(workspaceCards.length).toBe(2);
//check page list length
const closeWorkspaceModal = page.getByTestId('close-workspace-modal');
await closeWorkspaceModal.click();
await clickSideBarAllPageButton(page);
await page.waitForTimeout(1000);
const pageList = page.locator('[data-testid=page-list-item]');
const result = await pageList.count();
expect(result).toBe(0);
await page.reload();
await page.waitForTimeout(1000);
const pageList1 = page.locator('[data-testid=page-list-item]');
const result1 = await pageList1.count();
expect(result1).toBe(0);
});
test('create multi workspace in the workspace list', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await createWorkspace({ name: 'New Workspace 2' }, page);
await createWorkspace({ name: 'New Workspace 3' }, page);
// show workspace list
const workspaceName = page.getByTestId('workspace-name');
await workspaceName.click();
{
//check workspace list length //check workspace list length
const workspaceCards = await page.$$('data-testid=workspace-card'); const workspaceCards = await page.$$('data-testid=workspace-card');
expect(workspaceCards.length).toBe(2); expect(workspaceCards.length).toBe(3);
}
//check page list length await page.reload();
const closeWorkspaceModal = page.getByTestId('close-workspace-modal'); await openWorkspaceListModal(page);
await closeWorkspaceModal.click(); await page.getByTestId('draggable-item').nth(1).click();
await clickSideBarAllPageButton(page); await page.waitForTimeout(50);
await page.waitForTimeout(1000);
const pageList = page.locator('[data-testid=page-list-item]');
const result = await pageList.count();
expect(result).toBe(0);
await page.reload();
await page.waitForTimeout(1000);
const pageList1 = page.locator('[data-testid=page-list-item]');
const result1 = await pageList1.count();
expect(result1).toBe(0);
});
test('create multi workspace in the workspace list', async ({ page }) => { // @ts-expect-error
await openHomePage(page); const currentId: string = await page.evaluate(() => currentWorkspace.id);
await waitMarkdownImported(page);
await createWorkspace({ name: 'New Workspace 2' }, page);
await createWorkspace({ name: 'New Workspace 3' }, page);
// show workspace list await openWorkspaceListModal(page);
const workspaceName = page.getByTestId('workspace-name'); const sourceElement = await page.getByTestId('draggable-item').nth(2);
await workspaceName.click(); const targetElement = await page.getByTestId('draggable-item').nth(1);
const sourceBox = await sourceElement.boundingBox();
const targetBox = await targetElement.boundingBox();
if (!sourceBox || !targetBox) {
throw new Error('sourceBox or targetBox is null');
}
await page.mouse.move(
sourceBox.x + sourceBox.width / 2,
sourceBox.y + sourceBox.height / 2,
{ {
//check workspace list length steps: 5,
const workspaceCards = await page.$$('data-testid=workspace-card');
expect(workspaceCards.length).toBe(3);
} }
);
await page.reload(); await page.mouse.down();
await openWorkspaceListModal(page); await page.mouse.move(
await page.getByTestId('draggable-item').nth(1).click(); targetBox.x + targetBox.width / 2,
await page.waitForTimeout(50); targetBox.y + targetBox.height / 2,
// @ts-expect-error
const currentId: string = await page.evaluate(() => currentWorkspace.id);
await openWorkspaceListModal(page);
const sourceElement = await page.getByTestId('draggable-item').nth(2);
const targetElement = await page.getByTestId('draggable-item').nth(1);
const sourceBox = await sourceElement.boundingBox();
const targetBox = await targetElement.boundingBox();
if (!sourceBox || !targetBox) {
throw new Error('sourceBox or targetBox is null');
}
await page.mouse.move(
sourceBox.x + sourceBox.width / 2,
sourceBox.y + sourceBox.height / 2,
{
steps: 5,
}
);
await page.mouse.down();
await page.mouse.move(
targetBox.x + targetBox.width / 2,
targetBox.y + targetBox.height / 2,
{
steps: 5,
}
);
await page.mouse.up();
await page.waitForTimeout(50);
await page.reload();
await openWorkspaceListModal(page);
//check workspace list length
{ {
const workspaceCards1 = await page.$$('data-testid=workspace-card'); steps: 5,
expect(workspaceCards1.length).toBe(3);
} }
);
await page.mouse.up();
await page.waitForTimeout(50);
await page.reload();
await openWorkspaceListModal(page);
await page.getByTestId('draggable-item').nth(2).click(); //check workspace list length
{
const workspaceCards1 = await page.$$('data-testid=workspace-card');
expect(workspaceCards1.length).toBe(3);
}
// @ts-expect-error await page.getByTestId('draggable-item').nth(2).click();
const nextId: string = await page.evaluate(() => currentWorkspace.id);
expect(currentId).toBe(nextId); // @ts-expect-error
}); const nextId: string = await page.evaluate(() => currentWorkspace.id);
expect(currentId).toBe(nextId);
}); });

View File

@@ -6,32 +6,28 @@ 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); const workspaceName = page.getByTestId('workspace-name');
const workspaceName = page.getByTestId('workspace-name'); await page.waitForTimeout(1000);
await page.waitForTimeout(1000); expect(await workspaceName.textContent()).toBe('Demo Workspace');
expect(await workspaceName.textContent()).toBe('Demo Workspace'); await assertCurrentWorkspaceFlavour('local', page);
await assertCurrentWorkspaceFlavour('local', page); });
});
// test('default workspace avatar', async ({ page }) => { // test('default workspace avatar', async ({ page }) => {
// const workspaceAvatar = page.getByTestId('workspace-avatar'); // const workspaceAvatar = page.getByTestId('workspace-avatar');
// expect( // expect(
// await workspaceAvatar.locator('img').getAttribute('src') // await workspaceAvatar.locator('img').getAttribute('src')
// ).not.toBeNull(); // ).not.toBeNull();
// }); // });
}); test('Open language switch menu', async ({ page }) => {
test.describe('Language switch', () => { await openHomePage(page);
test('Open language switch menu', async ({ page }) => { await waitMarkdownImported(page);
await openHomePage(page); await clickSideBarCurrentWorkspaceBanner(page);
await waitMarkdownImported(page); const languageMenuButton = page.getByTestId('language-menu-button');
await clickSideBarCurrentWorkspaceBanner(page); await expect(languageMenuButton).toBeVisible();
const languageMenuButton = page.getByTestId('language-menu-button'); const actual = await languageMenuButton.innerText();
await expect(languageMenuButton).toBeVisible(); expect(actual).toEqual('English');
const actual = await languageMenuButton.innerText(); await assertCurrentWorkspaceFlavour('local', page);
expect(actual).toEqual('English');
await assertCurrentWorkspaceFlavour('local', page);
});
}); });

View File

@@ -5,56 +5,52 @@ 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); await createWorkspace({ name: 'New Workspace 2' }, page);
await createWorkspace({ name: 'New Workspace 2' }, page); // FIXME: can not get when the new workspace is surely created, hack a timeout to wait
// FIXME: can not get when the new workspace is surely created, hack a timeout to wait // waiting for page loading end
// waiting for page loading end await page.waitForTimeout(3000);
await page.waitForTimeout(3000); // show workspace list
// show workspace list await page.getByTestId('workspace-name').click();
await page.getByTestId('workspace-name').click();
//check workspace list length //check workspace list length
const workspaceCards = await page.$$('data-testid=workspace-card'); const workspaceCards = await page.$$('data-testid=workspace-card');
expect(workspaceCards.length).toBe(2); expect(workspaceCards.length).toBe(2);
await workspaceCards[1].click(); await workspaceCards[1].click();
await openHomePage(page); await openHomePage(page);
const workspaceNameDom = await page.getByTestId('workspace-name'); const workspaceNameDom = await page.getByTestId('workspace-name');
const currentWorkspaceName = await workspaceNameDom.evaluate( const currentWorkspaceName = await workspaceNameDom.evaluate(
node => node.textContent node => node.textContent
); );
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]'); await expect(changeLogItem).toBeVisible();
await expect(changeLogItem).toBeVisible(); const closeButton = page.locator('[data-testid=change-log-close-button]');
const closeButton = page.locator('[data-testid=change-log-close-button]'); await closeButton.click();
await closeButton.click(); await expect(changeLogItem).not.toBeVisible();
await expect(changeLogItem).not.toBeVisible(); await page.goto('http://localhost:8080');
await page.goto('http://localhost:8080'); const currentChangeLogItem = page.locator('[data-testid=change-log]');
const currentChangeLogItem = page.locator('[data-testid=change-log]'); await expect(currentChangeLogItem).not.toBeVisible();
await expect(currentChangeLogItem).not.toBeVisible(); });
}); test('Click right-bottom corner change log icon', async ({ page }) => {
test('Click right-bottom corner change log icon', async ({ page }) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await page.locator('[data-testid=help-island]').click();
await page.locator('[data-testid=help-island]').click(); const editorRightBottomChangeLog = page.locator(
const editorRightBottomChangeLog = page.locator( '[data-testid=right-bottom-change-log-icon]'
'[data-testid=right-bottom-change-log-icon]' );
); await page.waitForTimeout(50);
await page.waitForTimeout(50); expect(await editorRightBottomChangeLog.isVisible()).toEqual(true);
expect(await editorRightBottomChangeLog.isVisible()).toEqual(true); await page.getByRole('link', { name: 'All pages' }).click();
await page.getByRole('link', { name: 'All pages' }).click(); const normalRightBottomChangeLog = page.locator(
const normalRightBottomChangeLog = page.locator( '[data-testid=right-bottom-change-log-icon]'
'[data-testid=right-bottom-change-log-icon]' );
); expect(await normalRightBottomChangeLog.isVisible()).toEqual(true);
expect(await normalRightBottomChangeLog.isVisible()).toEqual(true);
});
}); });

View File

@@ -24,232 +24,225 @@ 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(); });
});
test('Root pinboard page have no operation of "trash" ,"rename" and "move to"', async ({
test('Root pinboard page have no operation of "trash" ,"rename" and "move to"', async ({ page,
page, }) => {
}) => { const rootPinboardMeta = await initHomePageWithPinboard(page);
const rootPinboardMeta = await initHomePageWithPinboard(page); await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? ''); expect(
expect( await page
await page .locator(`[data-testid="pinboard-operation-move-to-trash"]`)
.locator(`[data-testid="pinboard-operation-move-to-trash"]`) .count()
.count() ).toEqual(0);
).toEqual(0); expect(
expect( await page.locator(`[data-testid="pinboard-operation-rename"]`).count()
await page.locator(`[data-testid="pinboard-operation-rename"]`).count() ).toEqual(0);
).toEqual(0); expect(
expect( await page.locator(`[data-testid="pinboard-operation-move-to"]`).count()
await page.locator(`[data-testid="pinboard-operation-move-to"]`).count() ).toEqual(0);
).toEqual(0); });
});
test('Click Pinboard in sidebar should open root pinboard page', async ({
test('Click Pinboard in sidebar should open root pinboard page', async ({ page,
page, }) => {
}) => { const rootPinboardMeta = await initHomePageWithPinboard(page);
const rootPinboardMeta = await initHomePageWithPinboard(page); await page.getByTestId(`pinboard-${rootPinboardMeta?.id}`).click();
await page.getByTestId(`pinboard-${rootPinboardMeta?.id}`).click(); await page.waitForTimeout(200);
await page.waitForTimeout(200); expect(await page.locator('affine-editor-container')).not.toBeNull();
expect(await page.locator('affine-editor-container')).not.toBeNull(); });
});
test('Add pinboard by header operation menu', async ({ page }) => {
test('Add pinboard by header operation menu', async ({ page }) => { const rootPinboardMeta = await initHomePageWithPinboard(page);
const rootPinboardMeta = await initHomePageWithPinboard(page); await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); const meta = (await getMetas(page)).find(m => m.title === 'test1');
const meta = (await getMetas(page)).find(m => m.title === 'test1'); expect(meta).not.toBeUndefined();
expect(meta).not.toBeUndefined(); expect(
expect( await page
await page .getByTestId('[data-testid="sidebar-pinboard-container"]')
.getByTestId('[data-testid="sidebar-pinboard-container"]') .getByTestId(`pinboard-${meta?.id}`)
.getByTestId(`pinboard-${meta?.id}`) ).not.toBeNull();
).not.toBeNull(); await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? ''); });
});
test('Add pinboard by sidebar operation menu', async ({ page }) => {
test('Add pinboard by sidebar operation menu', async ({ page }) => { const rootPinboardMeta = await initHomePageWithPinboard(page);
const rootPinboardMeta = await initHomePageWithPinboard(page);
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? ''); await page.getByTestId('pinboard-operation-add').click();
await page.getByTestId('pinboard-operation-add').click(); const newPageMeta = (await getMetas(page)).find(
const newPageMeta = (await getMetas(page)).find( m => m.id !== rootPinboardMeta?.id
m => m.id !== rootPinboardMeta?.id );
); expect(
expect( await page
await page .getByTestId('sidebar-pinboard-container')
.getByTestId('sidebar-pinboard-container') .getByTestId(`pinboard-${newPageMeta?.id}`)
.getByTestId(`pinboard-${newPageMeta?.id}`) ).not.toBeNull();
).not.toBeNull(); await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
console.log('rootPinboardMeta', rootPinboardMeta); });
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
}); test('Move pinboard to another in sidebar', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
test('Move pinboard to another in sidebar', async ({ page }) => { await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const rootPinboardMeta = await initHomePageWithPinboard(page); await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2'); const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2'); await page.getByTestId('pinboard-operation-move-to').click();
await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); await page
await page.getByTestId('pinboard-operation-move-to').click(); .getByTestId('pinboard-menu')
await page .getByTestId(`pinboard-${childMeta2?.id}`)
.getByTestId('pinboard-menu') .click();
.getByTestId(`pinboard-${childMeta2?.id}`) expect(
.click(); (await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length
expect( ).toBe(1);
(await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length });
).toBe(1);
}); test('Should no show pinboard self in move to menu', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
test('Should no show pinboard self in move to menu', async ({ page }) => { await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const rootPinboardMeta = await initHomePageWithPinboard(page); await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); await page.getByTestId('all-pages').click();
await page
await page.getByTestId('all-pages').click(); .getByTestId(`page-list-item-${childMeta?.id}`)
await page .getByTestId('page-list-operation-button')
.getByTestId(`page-list-item-${childMeta?.id}`) .click();
.getByTestId('page-list-operation-button') await page.getByTestId('move-to-menu-item').click();
.click();
await page.getByTestId('move-to-menu-item').click(); expect(
await page
expect( .getByTestId('pinboard-menu')
await page .locator(`[data-testid="pinboard-${childMeta?.id}"]`)
.getByTestId('pinboard-menu') .count()
.locator(`[data-testid="pinboard-${childMeta?.id}"]`) ).toEqual(0);
.count() });
).toEqual(0); test('Move pinboard to another in page list', async ({ page }) => {
}); const rootPinboardMeta = await initHomePageWithPinboard(page);
test('Move pinboard to another in page list', async ({ page }) => { await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const rootPinboardMeta = await initHomePageWithPinboard(page); await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2'); const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2'); await page.getByTestId('all-pages').click();
await page
await page.getByTestId('all-pages').click(); .getByTestId(`page-list-item-${childMeta?.id}`)
await page .getByTestId('page-list-operation-button')
.getByTestId(`page-list-item-${childMeta?.id}`) .click();
.getByTestId('page-list-operation-button') await page.getByTestId('move-to-menu-item').click();
.click(); await page
await page.getByTestId('move-to-menu-item').click(); .getByTestId('pinboard-menu')
await page .getByTestId(`pinboard-${childMeta2?.id}`)
.getByTestId('pinboard-menu') .click();
.getByTestId(`pinboard-${childMeta2?.id}`) expect(
.click(); (await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length
expect( ).toBe(1);
(await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length });
).toBe(1);
}); test('Remove from pinboard', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
test('Remove from pinboard', async ({ page }) => { await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const rootPinboardMeta = await initHomePageWithPinboard(page); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); await page.getByTestId('pinboard-operation-move-to').click();
await page.getByTestId('remove-from-pinboard-button').click();
await page.getByTestId('pinboard-operation-move-to').click(); await page.waitForTimeout(1000);
await page.getByTestId('remove-from-pinboard-button').click(); expect(
await page.waitForTimeout(1000); await page.locator(`[data-testid="pinboard-${childMeta?.id}"]`).count()
expect( ).toEqual(0);
await page.locator(`[data-testid="pinboard-${childMeta?.id}"]`).count() });
).toEqual(0);
}); test('search pinboard', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
test('search pinboard', async ({ page }) => { await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const rootPinboardMeta = await initHomePageWithPinboard(page); await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); await page.getByTestId('pinboard-operation-move-to').click();
await page.getByTestId('pinboard-operation-move-to').click(); await page.fill('[data-testid="pinboard-menu-search"]', '111');
expect(await page.locator('[alt="no result"]').count()).toEqual(1);
await page.fill('[data-testid="pinboard-menu-search"]', '111');
expect(await page.locator('[alt="no result"]').count()).toEqual(1); await page.fill('[data-testid="pinboard-menu-search"]', 'test2');
expect(
await page.fill('[data-testid="pinboard-menu-search"]', 'test2'); await page.locator('[data-testid="pinboard-search-result"]').count()
expect( ).toEqual(1);
await page.locator('[data-testid="pinboard-search-result"]').count() });
).toEqual(1);
}); test('Rename pinboard', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
test('Rename pinboard', async ({ page }) => { await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const rootPinboardMeta = await initHomePageWithPinboard(page); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); await page.getByTestId('pinboard-operation-rename').click();
await page.fill(`[data-testid="pinboard-input-${childMeta?.id}"]`, 'test2');
await page.getByTestId('pinboard-operation-rename').click();
await page.fill(`[data-testid="pinboard-input-${childMeta?.id}"]`, 'test2'); const title = (await page
.locator('.affine-default-page-block-title')
const title = (await page .textContent()) as string;
.locator('.affine-default-page-block-title')
.textContent()) as string; expect(title).toEqual('test2');
});
expect(title).toEqual('test2');
}); test('Move pinboard to trash', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
test('Move pinboard to trash', async ({ page }) => { await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const rootPinboardMeta = await initHomePageWithPinboard(page); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); await createPinboardPage(page, childMeta?.id ?? '', 'test2');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); const grandChildMeta = (await getMetas(page)).find(m => m.title === 'test2');
await createPinboardPage(page, childMeta?.id ?? '', 'test2');
const grandChildMeta = (await getMetas(page)).find( await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
m => m.title === 'test2'
); await page.getByTestId('pinboard-operation-move-to-trash').click();
(await page.waitForSelector('[data-testid="move-to-trash-confirm"]')).click();
await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); await page.waitForTimeout(1000);
await page.getByTestId('pinboard-operation-move-to-trash').click(); expect(
( await page
await page.waitForSelector('[data-testid="move-to-trash-confirm"]') .getByTestId('sidebar-pinboard-container')
).click(); .locator(`[data-testid="pinboard-${childMeta?.id}"]`)
await page.waitForTimeout(1000); .count()
).toEqual(0);
expect(
await page expect(
.getByTestId('sidebar-pinboard-container') await page
.locator(`[data-testid="pinboard-${childMeta?.id}"]`) .getByTestId('sidebar-pinboard-container')
.count() .locator(`[data-testid="pinboard-${grandChildMeta?.id}"]`)
).toEqual(0); .count()
).toEqual(0);
expect( });
await page
.getByTestId('sidebar-pinboard-container') // FIXME
.locator(`[data-testid="pinboard-${grandChildMeta?.id}"]`) test.skip('Copy link', async ({ page }) => {
.count() const rootPinboardMeta = await initHomePageWithPinboard(page);
).toEqual(0); await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
}); const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
// FIXME await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
test.skip('Copy link', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page); await page.getByTestId('copy-link').click();
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); await page.evaluate(() => {
const input = document.createElement('input');
await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); input.id = 'paste-input';
document.body.appendChild(input);
await page.getByTestId('copy-link').click(); input.focus();
});
await page.evaluate(() => { await page.keyboard.press(`Meta+v`, { delay: 50 });
const input = document.createElement('input'); await page.keyboard.press(`Control+v`, { delay: 50 });
input.id = 'paste-input'; const copiedValue = await page
document.body.appendChild(input); .locator('#paste-input')
input.focus(); .evaluate((input: HTMLInputElement) => input.value);
}); expect(copiedValue).toEqual(page.url());
await page.keyboard.press(`Meta+v`, { delay: 50 });
await page.keyboard.press(`Control+v`, { delay: 50 });
const copiedValue = await page
.locator('#paste-input')
.evaluate((input: HTMLInputElement) => input.value);
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,229 +35,221 @@ 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); await newPage(page);
await newPage(page); const quickSearchButton = page.locator(
const quickSearchButton = page.locator( '[data-testid=slider-bar-quick-search-button]'
'[data-testid=slider-bar-quick-search-button]' );
); await quickSearchButton.click();
await quickSearchButton.click(); const quickSearch = page.locator('[data-testid=quickSearch]');
const quickSearch = page.locator('[data-testid=quickSearch]'); await expect(quickSearch).toBeVisible();
await expect(quickSearch).toBeVisible();
});
test('Click arrowDown icon after title', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
const quickSearchButton = page.locator(
'[data-testid=slider-bar-quick-search-button]'
);
await quickSearchButton.click();
const quickSearch = page.locator('[data-testid=quickSearch]');
await expect(quickSearch).toBeVisible();
});
test('Press the shortcut key cmd+k', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
const quickSearch = page.locator('[data-testid=quickSearch]');
await expect(quickSearch).toBeVisible();
});
}); });
test.describe('Add new page in quick search', () => { test('Click arrowDown icon after title', async ({ page }) => {
test('Create a new page without keyword', async ({ page }) => { await openHomePage(page);
await openHomePage(page); await waitMarkdownImported(page);
await waitMarkdownImported(page); await newPage(page);
await newPage(page); const quickSearchButton = page.locator(
await openQuickSearchByShortcut(page); '[data-testid=slider-bar-quick-search-button]'
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]'); );
await addNewPage.click(); await quickSearchButton.click();
await page.waitForTimeout(300); const quickSearch = page.locator('[data-testid=quickSearch]');
await assertTitle(page, ''); await expect(quickSearch).toBeVisible();
});
test('Create a new page with keyword', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
await page.keyboard.insertText('test123456');
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
await addNewPage.click();
await page.waitForTimeout(300);
await assertTitle(page, 'test123456');
});
}); });
test.describe('Search and select', () => { test('Press the shortcut key cmd+k', async ({ page }) => {
test('Enter a keyword to search for', 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); const quickSearch = page.locator('[data-testid=quickSearch]');
await page.keyboard.insertText('test123456'); await expect(quickSearch).toBeVisible();
const actual = await page.locator('[cmdk-input]').inputValue();
expect(actual).toBe('test123456');
});
test('Create a new page and search this page', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
await page.keyboard.insertText('test123456');
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
await addNewPage.click();
await page.waitForTimeout(300);
await assertTitle(page, 'test123456');
await openQuickSearchByShortcut(page);
await page.keyboard.insertText('test123456');
await page.waitForTimeout(50);
await assertResultList(page, ['test123456']);
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
await assertTitle(page, 'test123456');
});
});
test.describe('Disable search on 404 page', () => {
test('Navigate to the 404 page and try to open quick search', async ({
page,
}) => {
await page.goto('http://localhost:8080/404');
const notFoundTip = page.locator('[data-testid=notFound]');
await expect(notFoundTip).toBeVisible();
await openQuickSearchByShortcut(page);
const quickSearch = page.locator('[data-testid=quickSearch]');
await expect(quickSearch).toBeVisible({ visible: false });
});
});
test.describe('Open quick search on the published page', () => {
test('Open quick search on local page', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
const publishedSearchResults = page.locator('[publishedSearchResults]');
await expect(publishedSearchResults).toBeVisible({ visible: false });
});
}); });
test.describe('Focus event for quick search', () => { test('Create a new page without keyword', async ({ page }) => {
test('Autofocus input after opening quick search', 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); const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
const locator = page.locator('[cmdk-input]'); await addNewPage.click();
await expect(locator).toBeVisible(); await page.waitForTimeout(300);
await expect(locator).toBeFocused(); await assertTitle(page, '');
});
test('Autofocus input after select', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
await page.keyboard.press('ArrowUp');
const locator = page.locator('[cmdk-input]');
await expect(locator).toBeVisible();
await expect(locator).toBeFocused();
});
test('Focus title after creating a new page', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
await addNewPage.click();
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 ({
page,
}) => {
await openHomePage(page);
await waitMarkdownImported(page);
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
await expect(quickSearchTips).not.toBeVisible();
await page.getByTestId('sliderBar-arrowButton-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar-inner');
await expect(sliderBarArea).not.toBeInViewport();
await expect(quickSearchTips).toBeVisible();
await page.locator('[data-testid=quick-search-got-it]').click();
await expect(quickSearchTips).not.toBeVisible();
});
test('After appearing once, it will not appear a second time', async ({
page,
}) => {
await openHomePage(page);
await waitMarkdownImported(page);
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
await expect(quickSearchTips).not.toBeVisible();
await page.getByTestId('sliderBar-arrowButton-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar');
await expect(sliderBarArea).not.toBeVisible();
await expect(quickSearchTips).toBeVisible();
await page.locator('[data-testid=quick-search-got-it]').click();
await expect(quickSearchTips).not.toBeVisible();
await page.reload();
await page.locator('[data-testid=sliderBar-arrowButton-expand]').click();
await page.getByTestId('sliderBar-arrowButton-collapse').click();
await expect(quickSearchTips).not.toBeVisible();
});
test('Show navigation path if page is a subpage', async ({ page }) => { test('Create a new page with keyword', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page); await openHomePage(page);
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); await waitMarkdownImported(page);
await openQuickSearchByShortcut(page); await newPage(page);
expect(await page.getByTestId('navigation-path').count()).toBe(1); await openQuickSearchByShortcut(page);
}); await page.keyboard.insertText('test123456');
test('Not show navigation path if page is not a subpage or current page is not in editor', async ({ const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
page, await addNewPage.click();
}) => { await page.waitForTimeout(300);
await openHomePage(page); await assertTitle(page, 'test123456');
await waitMarkdownImported(page); });
await openQuickSearchByShortcut(page);
expect(await page.getByTestId('navigation-path').count()).toBe(0); test('Enter a keyword to search for', async ({ page }) => {
}); await openHomePage(page);
test('Navigation path item click will jump to page, but not current active item', async ({ await waitMarkdownImported(page);
page, await newPage(page);
}) => { await openQuickSearchByShortcut(page);
const rootPinboardMeta = await initHomePageWithPinboard(page); await page.keyboard.insertText('test123456');
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); const actual = await page.locator('[cmdk-input]').inputValue();
await openQuickSearchByShortcut(page); expect(actual).toBe('test123456');
const oldUrl = page.url(); });
expect(
await page.locator('[data-testid="navigation-path-link"]').count() test('Create a new page and search this page', async ({ page }) => {
).toBe(2); await openHomePage(page);
await page.locator('[data-testid="navigation-path-link"]').nth(1).click(); await waitMarkdownImported(page);
expect(page.url()).toBe(oldUrl); await newPage(page);
await page.locator('[data-testid="navigation-path-link"]').nth(0).click(); await openQuickSearchByShortcut(page);
expect(page.url()).not.toBe(oldUrl); // input title and create new page
}); await page.keyboard.insertText('test123456');
test('Navigation path expand', async ({ page }) => { await page.waitForTimeout(300);
// const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
const rootPinboardMeta = await initHomePageWithPinboard(page); await addNewPage.click();
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
await openQuickSearchByShortcut(page); await page.waitForTimeout(300);
const top = await page await assertTitle(page, 'test123456');
.getByTestId('navigation-path-expand-panel') await openQuickSearchByShortcut(page);
.evaluate(el => { await page.keyboard.insertText('test123456');
return window.getComputedStyle(el).getPropertyValue('top'); await page.waitForTimeout(300);
}); await assertResultList(page, ['test123456']);
expect(parseInt(top)).toBeLessThan(0); await page.keyboard.press('Enter');
await page.getByTestId('navigation-path-expand-btn').click(); await page.waitForTimeout(300);
await page.waitForTimeout(500); await assertTitle(page, 'test123456');
const expandTop = await page });
.getByTestId('navigation-path-expand-panel') test('Navigate to the 404 page and try to open quick search', async ({
.evaluate(el => { page,
return window.getComputedStyle(el).getPropertyValue('top'); }) => {
}); await page.goto('http://localhost:8080/404');
expect(expandTop).toBe('0px'); const notFoundTip = page.locator('[data-testid=notFound]');
}); await expect(notFoundTip).toBeVisible();
await openQuickSearchByShortcut(page);
const quickSearch = page.locator('[data-testid=quickSearch]');
await expect(quickSearch).toBeVisible({ visible: false });
});
test('Open quick search on local page', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
const publishedSearchResults = page.locator('[publishedSearchResults]');
await expect(publishedSearchResults).toBeVisible({ visible: false });
});
test('Autofocus input after opening quick search', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
const locator = page.locator('[cmdk-input]');
await expect(locator).toBeVisible();
await expect(locator).toBeFocused();
});
test('Autofocus input after select', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
await page.keyboard.press('ArrowUp');
const locator = page.locator('[cmdk-input]');
await expect(locator).toBeVisible();
await expect(locator).toBeFocused();
});
test('Focus title after creating a new page', async ({ page }) => {
await openHomePage(page);
await waitMarkdownImported(page);
await newPage(page);
await openQuickSearchByShortcut(page);
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
await addNewPage.click();
await titleIsFocused(page);
});
test('When opening the website for the first time, the first folding sidebar will appear novice guide', async ({
page,
}) => {
await openHomePage(page);
await waitMarkdownImported(page);
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
await expect(quickSearchTips).not.toBeVisible();
await page.getByTestId('sliderBar-arrowButton-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar-inner');
await expect(sliderBarArea).not.toBeInViewport();
await expect(quickSearchTips).toBeVisible();
await page.locator('[data-testid=quick-search-got-it]').click();
await expect(quickSearchTips).not.toBeVisible();
});
test('After appearing once, it will not appear a second time', async ({
page,
}) => {
await openHomePage(page);
await waitMarkdownImported(page);
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
await expect(quickSearchTips).not.toBeVisible();
await page.getByTestId('sliderBar-arrowButton-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar');
await expect(sliderBarArea).not.toBeVisible();
await expect(quickSearchTips).toBeVisible();
await page.locator('[data-testid=quick-search-got-it]').click();
await expect(quickSearchTips).not.toBeVisible();
await page.reload();
await page.locator('[data-testid=sliderBar-arrowButton-expand]').click();
await page.getByTestId('sliderBar-arrowButton-collapse').click();
await expect(quickSearchTips).not.toBeVisible();
});
test('Show navigation path if page is a subpage', async ({ page }) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
await openQuickSearchByShortcut(page);
expect(await page.getByTestId('navigation-path').count()).toBe(1);
});
test('Not show navigation path if page is not a subpage or current page is not in editor', async ({
page,
}) => {
await openHomePage(page);
await waitMarkdownImported(page);
await openQuickSearchByShortcut(page);
expect(await page.getByTestId('navigation-path').count()).toBe(0);
});
test('Navigation path item click will jump to page, but not current active item', async ({
page,
}) => {
const rootPinboardMeta = await initHomePageWithPinboard(page);
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
await openQuickSearchByShortcut(page);
const oldUrl = page.url();
expect(
await page.locator('[data-testid="navigation-path-link"]').count()
).toBe(2);
await page.locator('[data-testid="navigation-path-link"]').nth(1).click();
expect(page.url()).toBe(oldUrl);
await page.locator('[data-testid="navigation-path-link"]').nth(0).click();
expect(page.url()).not.toBe(oldUrl);
});
test('Navigation path expand', async ({ page }) => {
//
const rootPinboardMeta = await initHomePageWithPinboard(page);
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
await openQuickSearchByShortcut(page);
const top = await page
.getByTestId('navigation-path-expand-panel')
.evaluate(el => {
return window.getComputedStyle(el).getPropertyValue('top');
});
expect(parseInt(top)).toBeLessThan(0);
await page.getByTestId('navigation-path-expand-btn').click();
await page.waitForTimeout(500);
const expandTop = await page
.getByTestId('navigation-path-expand-panel')
.evaluate(el => {
return window.getComputedStyle(el).getPropertyValue('top');
});
expect(expandTop).toBe('0px');
}); });

View File

@@ -4,19 +4,17 @@ 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); await page.locator('[data-testid=help-island]').click();
await page.locator('[data-testid=help-island]').click();
const shortcutsIcon = page.locator('[data-testid=shortcuts-icon]'); const shortcutsIcon = page.locator('[data-testid=shortcuts-icon]');
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
expect(await shortcutsIcon.isVisible()).toEqual(true); expect(await shortcutsIcon.isVisible()).toEqual(true);
await shortcutsIcon.click(); await shortcutsIcon.click();
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
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,23 +7,19 @@ 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()
).toBe('Hello World!'); ).toBe('Hello World!');
await iframe.locator('[data-testid="clear-button"]').click(); await iframe.locator('[data-testid="clear-button"]').click();
expect( expect(
await iframe.locator('input[data-testid="test-input"]').textContent() await iframe.locator('input[data-testid="test-input"]').textContent()
).toBe(''); ).toBe('');
});
}); });

View File

@@ -4,12 +4,10 @@ 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); await page.getByTestId('sliderBar-arrowButton-collapse').click();
await page.getByTestId('sliderBar-arrowButton-collapse').click(); 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,61 +1,55 @@
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({ colorScheme: 'light',
colorScheme: 'light',
});
const page = await context.newPage();
await openHomePage(page);
await waitMarkdownImported(page);
await page.waitForSelector('html');
const root = page.locator('html');
const themeMode = await root.evaluate(element =>
element.getAttribute('data-theme')
);
expect(themeMode).toBe('light');
await page.waitForTimeout(50);
const rightMenu = page.getByTestId('editor-option-menu');
const rightMenuBox = await rightMenu.boundingBox();
const lightButton = page.getByTestId('change-theme-light');
const lightButtonBox = await lightButton.boundingBox();
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);
}); });
const page = await context.newPage();
// test('change theme to dark', async ({ page }) => { await openHomePage(page);
// const changeThemeContainer = page.locator( await waitMarkdownImported(page);
// '[data-testid=change-theme-container]' const root = page.locator('html');
// ); const themeMode = await root.evaluate(element =>
// const box = await changeThemeContainer.boundingBox(); element.getAttribute('data-theme')
// expect(box?.x).not.toBeUndefined(); );
// expect(themeMode).toBe('light');
// await page.mouse.move((box?.x ?? 0) + 5, (box?.y ?? 0) + 5); const prev = await page.screenshot({
// await page.waitForTimeout(1000); path: resolve(testResultDir, 'affine-light-theme.png'),
// const darkButton = page.locator('[data-testid=change-theme-dark]'); });
// const darkButtonPositionTop = await darkButton.evaluate( await page.getByTestId('change-theme-dark').click();
// element => element.getBoundingClientRect().y await page.waitForTimeout(50);
// ); const after = await page.screenshot({
// expect(darkButtonPositionTop).toBe(box?.y); path: resolve(testResultDir, 'affine-dark-theme.png'),
// });
// await page.mouse.click((box?.x ?? 0) + 5, (box?.y ?? 0) + 5); expect(prev).not.toEqual(after);
// const root = page.locator('html');
// const themeMode = await root.evaluate(element =>
// element.getAttribute('data-theme')
// );
// expect(themeMode).toBe('dark');
// });
}); });
// test('change theme to dark', async ({ page }) => {
// const changeThemeContainer = page.locator(
// '[data-testid=change-theme-container]'
// );
// const box = await changeThemeContainer.boundingBox();
// expect(box?.x).not.toBeUndefined();
//
// await page.mouse.move((box?.x ?? 0) + 5, (box?.y ?? 0) + 5);
// await page.waitForTimeout(1000);
// const darkButton = page.locator('[data-testid=change-theme-dark]');
// const darkButtonPositionTop = await darkButton.evaluate(
// element => element.getBoundingClientRect().y
// );
// expect(darkButtonPositionTop).toBe(box?.y);
//
// await page.mouse.click((box?.x ?? 0) + 5, (box?.y ?? 0) + 5);
// const root = page.locator('html');
// const themeMode = await root.evaluate(element =>
// element.getAttribute('data-theme')
// );
// 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"