mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 19:11:32 +03:00
refactor: workspace loading logic (#1966)
This commit is contained in:
parent
caa292e097
commit
7bbe67af43
@ -78,6 +78,7 @@ const nextConfig = {
|
||||
},
|
||||
reactStrictMode: true,
|
||||
transpilePackages: [
|
||||
'jotai-devtools',
|
||||
'@affine/component',
|
||||
'@affine/i18n',
|
||||
'@affine/debug',
|
||||
|
@ -36,6 +36,7 @@
|
||||
"css-spring": "^4.1.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"jotai": "^2.0.4",
|
||||
"jotai-devtools": "^0.4.0",
|
||||
"lit": "^2.7.2",
|
||||
"lottie-web": "^5.11.0",
|
||||
"next-themes": "^0.2.1",
|
||||
|
@ -1,40 +1,68 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { atomWithSyncStorage } from '@affine/jotai';
|
||||
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import {
|
||||
rootCurrentEditorAtom,
|
||||
rootCurrentPageIdAtom,
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
rootWorkspacesMetadataAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { WorkspacePlugins } from '../plugins';
|
||||
import type { AllWorkspace } from '../shared';
|
||||
|
||||
const logger = new DebugLogger('web:atoms');
|
||||
|
||||
// workspace necessary atoms
|
||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
||||
export const currentPageIdAtom = atom<string | null>(null);
|
||||
export const currentEditorAtom = atom<Readonly<EditorContainer> | null>(null);
|
||||
/**
|
||||
* @deprecated Use `rootCurrentWorkspaceIdAtom` directly instead.
|
||||
*/
|
||||
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
|
||||
export const openWorkspacesModalAtom = atom(false);
|
||||
export const openCreateWorkspaceModalAtom = atom(false);
|
||||
export const openQuickSearchModalAtom = atom(false);
|
||||
|
||||
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
|
||||
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[];
|
||||
});
|
||||
export { workspacesAtom } from './root';
|
||||
|
||||
type View = { id: string; mode: 'page' | 'edgeless' };
|
||||
|
||||
|
78
apps/web/src/atoms/root.ts
Normal file
78
apps/web/src/atoms/root.ts
Normal 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
|
@ -3,6 +3,7 @@
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import matchers from '@testing-library/jest-dom/matchers';
|
||||
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 { workspacesAtom } from '../../atoms';
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
useCurrentWorkspace,
|
||||
} from '../../hooks/current/use-current-workspace';
|
||||
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
|
||||
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
import { useAppHelper } from '../../hooks/use-workspaces';
|
||||
import { ThemeProvider } from '../../providers/ThemeProvider';
|
||||
import type { BlockSuiteWorkspace } from '../../shared';
|
||||
import type { PinboardProps } from '../pure/workspace-slider-bar/Pinboard';
|
||||
@ -42,24 +41,26 @@ const initPinBoard = async () => {
|
||||
// - pinboard2
|
||||
// - noPinboardPage
|
||||
|
||||
const mutationHook = renderHook(() => useWorkspacesHelper(), {
|
||||
const mutationHook = renderHook(() => useAppHelper(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
const rootPageIds = ['hasPinboardPage', 'noPinboardPage'];
|
||||
const pinboardPageIds = ['pinboard1', 'pinboard2'];
|
||||
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(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
currentWorkspaceHook.result.current[1](id);
|
||||
const currentWorkspace = await store.get(currentWorkspaceAtom);
|
||||
const currentWorkspace = await store.get(rootCurrentWorkspaceAtom);
|
||||
const blockSuiteWorkspace =
|
||||
currentWorkspace?.blockSuiteWorkspace as BlockSuiteWorkspace;
|
||||
|
||||
mutationHook.rerender();
|
||||
// create root pinboard
|
||||
mutationHook.result.current.createWorkspacePage(id, 'rootPinboard');
|
||||
blockSuiteWorkspace.meta.setPageMeta('rootPinboard', {
|
||||
@ -73,7 +74,7 @@ const initPinBoard = async () => {
|
||||
subpageIds: rootPageId === rootPageIds[0] ? pinboardPageIds : [],
|
||||
});
|
||||
});
|
||||
// create children to firs parent
|
||||
// create children to first parent
|
||||
pinboardPageIds.forEach(pinboardId => {
|
||||
mutationHook.result.current.createWorkspacePage(id, pinboardId);
|
||||
blockSuiteWorkspace.meta.setPageMeta(pinboardId, {
|
||||
|
@ -3,23 +3,27 @@
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import {
|
||||
rootCurrentPageIdAtom,
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
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 type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { workspacesAtom } from '../../atoms';
|
||||
import { useCurrentPageId } from '../../hooks/current/use-current-page-id';
|
||||
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
useCurrentWorkspace,
|
||||
} from '../../hooks/current/use-current-workspace';
|
||||
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 { pathGenerator } from '../../shared';
|
||||
import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar';
|
||||
@ -45,21 +49,22 @@ describe('WorkSpaceSliderBar', () => {
|
||||
|
||||
const onOpenWorkspaceListModalFn = vi.fn();
|
||||
const onOpenQuickSearchModalFn = vi.fn();
|
||||
const mutationHook = renderHook(() => useWorkspacesHelper(), {
|
||||
const mutationHook = renderHook(() => useAppHelper(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
const id = await mutationHook.result.current.createLocalWorkspace('test0');
|
||||
await store.get(workspacesAtom);
|
||||
mutationHook.rerender();
|
||||
mutationHook.result.current.createWorkspacePage(id, 'test1');
|
||||
await store.get(currentWorkspaceAtom);
|
||||
store.set(rootCurrentWorkspaceIdAtom, id);
|
||||
await store.get(rootCurrentWorkspaceAtom);
|
||||
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
let i = 0;
|
||||
const Component = () => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const [currentPageId] = useCurrentPageId();
|
||||
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
|
||||
assertExists(currentWorkspace);
|
||||
const helper = useBlockSuiteWorkspaceHelper(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { initPage } from '@affine/env/blocksuite';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import type { PageBlockModel } from '@blocksuite/blocks';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
@ -5,6 +6,7 @@ import { assertEquals, nanoid } from '@blocksuite/store';
|
||||
import { Command } from 'cmdk';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
@ -35,25 +37,25 @@ export const Footer: React.FC<FooterProps> = ({
|
||||
return (
|
||||
<Command.Item
|
||||
data-testid="quick-search-add-new-page"
|
||||
onSelect={async () => {
|
||||
onClose();
|
||||
onSelect={useCallback(() => {
|
||||
const id = nanoid();
|
||||
const page = await createPage(id);
|
||||
const page = createPage(id);
|
||||
assertEquals(page.id, id);
|
||||
await jumpToPage(blockSuiteWorkspace.id, page.id);
|
||||
if (!query) {
|
||||
return;
|
||||
initPage(page);
|
||||
const block = page.getBlockByFlavour(
|
||||
'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);
|
||||
if (newPage) {
|
||||
const block = newPage.getBlockByFlavour(
|
||||
'affine:page'
|
||||
)[0] as PageBlockModel;
|
||||
if (block) {
|
||||
block.title.insert(query, 0);
|
||||
}
|
||||
}
|
||||
}}
|
||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
||||
title: query,
|
||||
});
|
||||
onClose();
|
||||
void jumpToPage(blockSuiteWorkspace.id, page.id);
|
||||
}, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
|
||||
>
|
||||
<StyledModalFooterContent>
|
||||
<PlusIcon />
|
||||
|
@ -5,7 +5,10 @@ import 'fake-indexeddb/auto';
|
||||
|
||||
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 { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import type { PageBlockModel } from '@blocksuite/blocks';
|
||||
@ -38,11 +41,7 @@ import {
|
||||
useRecentlyViewed,
|
||||
useSyncRecentViewsWithRouter,
|
||||
} from '../use-recent-views';
|
||||
import {
|
||||
REDIRECT_TIMEOUT,
|
||||
useSyncRouterWithCurrentWorkspaceAndPage,
|
||||
} from '../use-sync-router-with-current-workspace-and-page';
|
||||
import { useWorkspaces, useWorkspacesHelper } from '../use-workspaces';
|
||||
import { useAppHelper, useWorkspaces } from '../use-workspaces';
|
||||
|
||||
vi.mock(
|
||||
'../../components/blocksuite/header/editor-mode-switch/CustomLottie',
|
||||
@ -167,23 +166,24 @@ describe('usePageMetas', async () => {
|
||||
describe('useWorkspacesHelper', () => {
|
||||
test('basic', async () => {
|
||||
const { ProviderWrapper, store } = await getJotaiContext();
|
||||
const workspaceHelperHook = renderHook(() => useWorkspacesHelper(), {
|
||||
const workspaceHelperHook = renderHook(() => useAppHelper(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
const id = await workspaceHelperHook.result.current.createLocalWorkspace(
|
||||
'test'
|
||||
);
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
expect(workspaces.length).toBe(1);
|
||||
expect(workspaces[0].id).toBe(id);
|
||||
expect(workspaces.length).toBe(2);
|
||||
expect(workspaces[1].id).toBe(id);
|
||||
const workspacesHook = renderHook(() => useWorkspaces(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
store.set(rootCurrentWorkspaceIdAtom, workspacesHook.result.current[1].id);
|
||||
await store.get(currentWorkspaceAtom);
|
||||
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
|
||||
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 () => {
|
||||
const { ProviderWrapper, store } = await getJotaiContext();
|
||||
const { result } = renderHook(() => useWorkspacesHelper(), {
|
||||
const { result } = renderHook(() => useAppHelper(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
{
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
expect(workspaces.length).toEqual(1);
|
||||
}
|
||||
await result.current.createLocalWorkspace('test');
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
console.log(workspaces);
|
||||
expect(workspaces.length).toEqual(1);
|
||||
{
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
expect(workspaces.length).toEqual(2);
|
||||
}
|
||||
const { result: result2 } = renderHook(() => useWorkspaces(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
expect(result2.current.length).toEqual(1);
|
||||
const firstWorkspace = result2.current[0];
|
||||
expect(result2.current.length).toEqual(2);
|
||||
const firstWorkspace = result2.current[1];
|
||||
expect(firstWorkspace.flavour).toBe('local');
|
||||
assert(firstWorkspace.flavour === WorkspaceFlavour.LOCAL);
|
||||
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', () => {
|
||||
test('basic', async () => {
|
||||
const { ProviderWrapper, store } = await getJotaiContext();
|
||||
const workspaceId = blockSuiteWorkspace.id;
|
||||
const pageId = 'page0';
|
||||
store.set(jotaiWorkspacesAtom, [
|
||||
store.set(rootWorkspacesMetadataAtom, [
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
|
@ -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 { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
@ -16,7 +16,7 @@ export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
||||
});
|
||||
await mutate(QueryKey.getWorkspaces);
|
||||
// fixme: remove force update
|
||||
jotaiStore.set(jotaiWorkspacesAtom, ws => [...ws]);
|
||||
rootStore.set(rootWorkspacesMetadataAtom, ws => [...ws]);
|
||||
},
|
||||
[mutate, workspace.id]
|
||||
);
|
||||
|
@ -1,43 +1,12 @@
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { atom, useAtom, useAtomValue } 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);
|
||||
}
|
||||
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
|
||||
*/
|
||||
export function useCurrentPageId(): [
|
||||
string | null,
|
||||
(newId: string | null) => void
|
||||
] {
|
||||
return useAtom(currentPageIdAtom);
|
||||
return useAtom(rootCurrentPageIdAtom);
|
||||
}
|
||||
|
@ -1,21 +1,15 @@
|
||||
import { atomWithSyncStorage } from '@affine/jotai';
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
workspacesAtom,
|
||||
} from '../../atoms';
|
||||
import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
|
||||
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
|
||||
import type { AllWorkspace } from '../../shared';
|
||||
|
||||
export const currentWorkspaceAtom = atom<Promise<AllWorkspace | null>>(
|
||||
async get => {
|
||||
const id = get(currentWorkspaceIdAtom);
|
||||
const workspaces = await get(workspacesAtom);
|
||||
return workspaces.find(workspace => workspace.id === id) ?? null;
|
||||
}
|
||||
);
|
||||
/**
|
||||
* @deprecated use `rootCurrentWorkspaceAtom` instead
|
||||
*/
|
||||
export const currentWorkspaceAtom = rootCurrentWorkspaceAtom;
|
||||
|
||||
export const lastWorkspaceIdAtom = atomWithSyncStorage<string | null>(
|
||||
'last_workspace_id',
|
||||
@ -26,7 +20,7 @@ export function useCurrentWorkspace(): [
|
||||
AllWorkspace | null,
|
||||
(id: string | null) => void
|
||||
] {
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
|
||||
const [, setId] = useAtom(currentWorkspaceIdAtom);
|
||||
const [, setPageId] = useAtom(currentPageIdAtom);
|
||||
const setLast = useSetAtom(lastWorkspaceIdAtom);
|
||||
|
@ -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']?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
48
apps/web/src/hooks/use-router-with-workspace-id-defense.ts
Normal file
48
apps/web/src/hooks/use-router-with-workspace-id-defense.ts
Normal 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,
|
||||
]);
|
||||
}
|
18
apps/web/src/hooks/use-sync-router-with-current-page-id.ts
Normal file
18
apps/web/src/hooks/use-sync-router-with-current-page-id.ts
Normal 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]);
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
@ -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]);
|
||||
}
|
@ -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]);
|
||||
}
|
@ -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 { WorkspaceRegistry } from '@affine/workspace/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspacePlugins } from '../plugins';
|
||||
import { useRouterHelper } from './use-router-helper';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function useTransformWorkspace() {
|
||||
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||
const router = useRouter();
|
||||
const helper = useRouterHelper(router);
|
||||
const setCurrentWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
|
||||
const set = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
@ -35,9 +35,9 @@ export function useTransformWorkspace() {
|
||||
});
|
||||
return [...workspaces];
|
||||
});
|
||||
await helper.jumpToWorkspace(newId);
|
||||
setCurrentWorkspaceId(newId);
|
||||
return newId;
|
||||
},
|
||||
[helper, set]
|
||||
[set, setCurrentWorkspaceId]
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
@ -18,10 +18,13 @@ export function useWorkspaces(): AllWorkspace[] {
|
||||
|
||||
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 jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
|
||||
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const set = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
return {
|
||||
createWorkspacePage: useCallback(
|
||||
(workspaceId: string, pageId: string) => {
|
||||
|
@ -1,21 +1,24 @@
|
||||
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 { 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 { assertExists, nanoid } from '@blocksuite/store';
|
||||
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
import type { FC, PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
currentWorkspaceIdAtom,
|
||||
openQuickSearchModalAtom,
|
||||
openWorkspacesModalAtom,
|
||||
} from '../atoms';
|
||||
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
|
||||
import {
|
||||
publicWorkspaceAtom,
|
||||
publicWorkspaceIdAtom,
|
||||
@ -23,18 +26,19 @@ import {
|
||||
import { HelpIsland } from '../components/pure/help-island';
|
||||
import { PageLoading } from '../components/pure/loading';
|
||||
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 { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper';
|
||||
import { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace';
|
||||
import { useRouterHelper } from '../hooks/use-router-helper';
|
||||
import { useRouterTitle } from '../hooks/use-router-title';
|
||||
import { useRouterWithWorkspaceIdDefense } from '../hooks/use-router-with-workspace-id-defense';
|
||||
import {
|
||||
useSidebarFloating,
|
||||
useSidebarResizing,
|
||||
useSidebarStatus,
|
||||
useSidebarWidth,
|
||||
} 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 { WorkspacePlugins } from '../plugins';
|
||||
import { ModalProvider } from '../providers/ModalProvider';
|
||||
@ -114,6 +118,53 @@ const logger = new DebugLogger('workspace-layout');
|
||||
const affineGlobalChannel = createAffineGlobalChannel(
|
||||
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> =
|
||||
function WorkspacesSuspense({ children }) {
|
||||
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
|
||||
setUpLanguage(i18n);
|
||||
}, [i18n]);
|
||||
useCreateFirstWorkspace();
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
|
||||
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
|
||||
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const set = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
useEffect(() => {
|
||||
logger.info('mount');
|
||||
const controller = new AbortController();
|
||||
@ -134,7 +184,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
|
||||
.map(({ CRUD }) => CRUD.list);
|
||||
|
||||
async function fetch() {
|
||||
const jotaiWorkspaces = jotaiStore.get(jotaiWorkspacesAtom);
|
||||
const jotaiWorkspaces = rootStore.get(rootWorkspacesMetadataAtom);
|
||||
const items = [];
|
||||
for (const list of lists) {
|
||||
try {
|
||||
@ -180,22 +230,31 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
|
||||
return (
|
||||
<>
|
||||
{/* fixme(himself65): don't re-render whole modals */}
|
||||
<ModalProvider key={currentWorkspaceId} />
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||
</Suspense>
|
||||
<AllWorkspaceContext>
|
||||
<CurrentWorkspaceContext>
|
||||
<ModalProvider key={currentWorkspaceId} />
|
||||
</CurrentWorkspaceContext>
|
||||
</AllWorkspaceContext>
|
||||
<CurrentWorkspaceContext>
|
||||
<Suspense fallback={<PageLoading text="Finding current workspace" />}>
|
||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||
</Suspense>
|
||||
</CurrentWorkspaceContext>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const [currentPageId] = useCurrentPageId();
|
||||
const workspaces = useWorkspaces();
|
||||
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
|
||||
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
|
||||
const router = useRouter();
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
logger.info('workspaces: ', workspaces);
|
||||
}, [workspaces]);
|
||||
logger.info('currentWorkspace: ', currentWorkspace);
|
||||
}, [currentWorkspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
@ -203,38 +262,82 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
}
|
||||
}, [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(() => {
|
||||
if (currentWorkspace) {
|
||||
currentWorkspace.providers.forEach(provider => {
|
||||
if (provider.background) {
|
||||
return;
|
||||
}
|
||||
provider.connect();
|
||||
});
|
||||
return () => {
|
||||
currentWorkspace.providers.forEach(provider => {
|
||||
if (provider.background) {
|
||||
return;
|
||||
}
|
||||
provider.disconnect();
|
||||
});
|
||||
};
|
||||
}
|
||||
}, [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 [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
|
||||
const helper = useBlockSuiteWorkspaceHelper(
|
||||
@ -339,7 +442,9 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
</StyledSpacer>
|
||||
<MainContainerWrapper resizing={resizing} style={{ width: mainWidth }}>
|
||||
<MainContainer className="main-container">
|
||||
{children}
|
||||
<Suspense fallback={<PageLoading text="Page is Loading" />}>
|
||||
{isLoading ? <PageLoading text="Page is Loading" /> : children}
|
||||
</Suspense>
|
||||
<StyledToolWrapper>
|
||||
{/* fixme(himself65): remove this */}
|
||||
<div id="toolWrapper" style={{ marginBottom: '12px' }}>
|
@ -2,14 +2,15 @@ import '@affine/component/theme/global.css';
|
||||
|
||||
import { config, setupGlobal } from '@affine/env';
|
||||
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 { CacheProvider } from '@emotion/react';
|
||||
import { Provider } from 'jotai';
|
||||
import { DevTools } from 'jotai-devtools';
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactElement } from 'react';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import React, { Suspense, useEffect, useMemo } from 'react';
|
||||
|
||||
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
|
||||
@ -30,6 +31,15 @@ const EmptyLayout = (page: ReactElement) => page;
|
||||
|
||||
const clientSideEmotionCache = createEmotionCache();
|
||||
|
||||
const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
{process.env.DEBUG_JOTAI === 'true' && <DevTools />}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const App = function App({
|
||||
Component,
|
||||
pageProps,
|
||||
@ -55,10 +65,12 @@ const App = function App({
|
||||
<Suspense fallback={<PageLoading key="RootPageLoading" />}>
|
||||
<ProviderComposer
|
||||
contexts={useMemo(
|
||||
() => [
|
||||
<Provider key="JotaiProvider" store={jotaiStore} />,
|
||||
<ThemeProvider key="ThemeProvider" />,
|
||||
],
|
||||
() =>
|
||||
[
|
||||
<Provider key="JotaiProvider" store={rootStore} />,
|
||||
<DebugProvider key="DebugProvider" />,
|
||||
<ThemeProvider key="ThemeProvider" />,
|
||||
].filter(Boolean),
|
||||
[]
|
||||
)}
|
||||
>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { initPage } from '@affine/env/blocksuite';
|
||||
import { useRouter } from 'next/router';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { StyledPage, StyledWrapper } from '../../layouts/styles';
|
||||
import type { NextPageWithLayout } from '../../shared';
|
||||
import { initPage } from '../../utils';
|
||||
|
||||
const Editor = lazy(() =>
|
||||
import('../../components/__debug__/client/Editor').then(module => ({
|
||||
|
@ -5,18 +5,18 @@ import React, { Suspense, useEffect } from 'react';
|
||||
|
||||
import { PageLoading } from '../components/pure/loading';
|
||||
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 { useWorkspaces } from '../hooks/use-workspaces';
|
||||
import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
|
||||
const logger = new DebugLogger('IndexPage');
|
||||
const logger = new DebugLogger('index-page');
|
||||
|
||||
const IndexPageInner = () => {
|
||||
const router = useRouter();
|
||||
const { jumpToPage, jumpToSubPath } = useRouterHelper(router);
|
||||
const workspaces = useWorkspaces();
|
||||
const lastWorkspaceId = useLastWorkspaceId();
|
||||
const helper = useAppHelper();
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) {
|
||||
@ -32,13 +32,12 @@ const IndexPageInner = () => {
|
||||
targetWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
|
||||
if (pageId) {
|
||||
logger.debug('Found target workspace. Jump to page', pageId);
|
||||
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
|
||||
return;
|
||||
void jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
|
||||
} else {
|
||||
const clearId = setTimeout(() => {
|
||||
dispose.dispose();
|
||||
logger.debug('Found target workspace. Jump to all pages');
|
||||
jumpToSubPath(
|
||||
void jumpToSubPath(
|
||||
targetWorkspace.id,
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
@ -47,7 +46,7 @@ const IndexPageInner = () => {
|
||||
const dispose =
|
||||
targetWorkspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => {
|
||||
clearTimeout(clearId);
|
||||
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
|
||||
void jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
|
||||
});
|
||||
return () => {
|
||||
clearTimeout(clearId);
|
||||
@ -55,19 +54,16 @@ const IndexPageInner = () => {
|
||||
};
|
||||
}
|
||||
} else {
|
||||
logger.debug('No target workspace. jump to all pages');
|
||||
// fixme: should create new workspace
|
||||
jumpToSubPath('ERROR', WorkspaceSubPath.ALL, RouteLogic.REPLACE);
|
||||
console.warn('No target workspace. This should not happen in production');
|
||||
}
|
||||
}, [jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]);
|
||||
}, [helper, jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]);
|
||||
|
||||
return <PageLoading key="IndexPageInfinitePageLoading" />;
|
||||
};
|
||||
|
||||
const IndexPage: NextPage = () => {
|
||||
useCreateFirstWorkspace();
|
||||
return (
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<Suspense fallback={<PageLoading text="Loading all workspaces" />}>
|
||||
<IndexPageInner />
|
||||
</Suspense>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Breadcrumbs, displayFlex, styled } from '@affine/component';
|
||||
import { initPage } from '@affine/env/blocksuite';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { PageIcon } from '@blocksuite/icons';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
@ -25,7 +26,6 @@ import {
|
||||
PublicWorkspaceLayout,
|
||||
} from '../../../layouts/public-workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
import { initPage } from '../../../utils';
|
||||
|
||||
export const NavContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
|
@ -1,20 +1,23 @@
|
||||
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-blocksuite-workspace-page';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { rootCurrentWorkspaceAtom } from '../../../atoms/root';
|
||||
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
|
||||
import { PageLoading } from '../../../components/pure/loading';
|
||||
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 { usePageMeta, usePageMetaHelper } from '../../../hooks/use-page-meta';
|
||||
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
|
||||
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 { useSyncRouterWithCurrentWorkspaceAndPage } from '../../../hooks/use-sync-router-with-current-workspace-and-page';
|
||||
import { WorkspaceLayout } from '../../../layouts';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import { WorkspacePlugins } from '../../../plugins';
|
||||
import type { BlockSuiteWorkspace, NextPageWithLayout } from '../../../shared';
|
||||
|
||||
@ -32,7 +35,7 @@ function enableFullFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
|
||||
const WorkspaceDetail: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { openPage } = useRouterHelper(router);
|
||||
const [currentPageId] = useCurrentPageId();
|
||||
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace ?? null;
|
||||
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
@ -80,7 +83,7 @@ const WorkspaceDetail: React.FC = () => {
|
||||
return <PageLoading />;
|
||||
}
|
||||
if (!currentPageId) {
|
||||
return <PageLoading />;
|
||||
return <PageLoading text="Loading page." />;
|
||||
}
|
||||
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].UI.PageDetail;
|
||||
@ -104,14 +107,17 @@ const WorkspaceDetail: React.FC = () => {
|
||||
|
||||
const WorkspaceDetailPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
useSyncRouterWithCurrentWorkspaceAndPage(router);
|
||||
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
|
||||
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
|
||||
useRouterAndWorkspaceWithPageIdDefense(router);
|
||||
const page = useBlockSuiteWorkspacePage(
|
||||
currentWorkspace.blockSuiteWorkspace,
|
||||
currentPageId
|
||||
);
|
||||
if (!router.isReady) {
|
||||
return <PageLoading />;
|
||||
} else if (
|
||||
typeof router.query.pageId !== 'string' ||
|
||||
typeof router.query.workspaceId !== 'string'
|
||||
) {
|
||||
throw new Error('Invalid router query');
|
||||
return <PageLoading text="Router is loading" />;
|
||||
} else if (!currentPageId || !page) {
|
||||
return <PageLoading text="Page is loading" />;
|
||||
}
|
||||
return <WorkspaceDetail />;
|
||||
};
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { FolderIcon } from '@blocksuite/icons';
|
||||
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
QueryParamError,
|
||||
@ -15,56 +14,17 @@ import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||
import { WorkspaceLayout } from '../../../layouts';
|
||||
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import { WorkspacePlugins } from '../../../plugins';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
import { ensureRootPinboard } from '../../../utils';
|
||||
|
||||
const AllPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const { t } = useTranslation();
|
||||
useSyncRouterWithCurrentWorkspace(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]);
|
||||
useSyncRouterWithCurrentWorkspaceId(router);
|
||||
const onClickPage = useCallback(
|
||||
(pageId: string, newTab?: boolean) => {
|
||||
assertExists(currentWorkspace);
|
||||
|
@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||
import { WorkspaceLayout } from '../../../layouts';
|
||||
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
|
||||
const FavouritePage: NextPageWithLayout = () => {
|
||||
@ -19,7 +19,7 @@ const FavouritePage: NextPageWithLayout = () => {
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const { t } = useTranslation();
|
||||
useSyncRouterWithCurrentWorkspace(router);
|
||||
useSyncRouterWithCurrentWorkspaceId(router);
|
||||
const onClickPage = useCallback(
|
||||
(pageId: string, newTab?: boolean) => {
|
||||
assertExists(currentWorkspace);
|
||||
|
@ -18,9 +18,9 @@ import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
||||
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||
import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
|
||||
import { WorkspaceLayout } from '../../../layouts';
|
||||
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
|
||||
import { useAppHelper } from '../../../hooks/use-workspaces';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import { WorkspacePlugins } from '../../../plugins';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
|
||||
@ -33,7 +33,7 @@ const SettingPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const { t } = useTranslation();
|
||||
useSyncRouterWithCurrentWorkspace(router);
|
||||
useSyncRouterWithCurrentWorkspaceId(router);
|
||||
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
|
||||
useEffect(() => {});
|
||||
const onChangeTab = useCallback(
|
||||
@ -92,7 +92,7 @@ const SettingPage: NextPageWithLayout = () => {
|
||||
}
|
||||
}, [currentTab, router, setCurrentTab]);
|
||||
|
||||
const helper = useWorkspacesHelper();
|
||||
const helper = useAppHelper();
|
||||
|
||||
const onDeleteWorkspace = useCallback(() => {
|
||||
assertExists(currentWorkspace);
|
||||
|
@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||
import { WorkspaceLayout } from '../../../layouts';
|
||||
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
|
||||
const SharedPages: NextPageWithLayout = () => {
|
||||
@ -19,7 +19,7 @@ const SharedPages: NextPageWithLayout = () => {
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const { t } = useTranslation();
|
||||
useSyncRouterWithCurrentWorkspace(router);
|
||||
useSyncRouterWithCurrentWorkspaceId(router);
|
||||
const onClickPage = useCallback(
|
||||
(pageId: string, newTab?: boolean) => {
|
||||
assertExists(currentWorkspace);
|
||||
|
@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||
import { WorkspaceLayout } from '../../../layouts';
|
||||
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
|
||||
const TrashPage: NextPageWithLayout = () => {
|
||||
@ -19,7 +19,7 @@ const TrashPage: NextPageWithLayout = () => {
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const { t } = useTranslation();
|
||||
useSyncRouterWithCurrentWorkspace(router);
|
||||
useSyncRouterWithCurrentWorkspaceId(router);
|
||||
const onClickPage = useCallback(
|
||||
(pageId: string, newTab?: boolean) => {
|
||||
assertExists(currentWorkspace);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
@ -43,7 +43,7 @@ export const fetcher = async (
|
||||
if (typeof key !== '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);
|
||||
assertExists(workspace);
|
||||
const storage = await workspace.blockSuiteWorkspace.blobs;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { prefixUrl } from '@affine/env';
|
||||
import { initPage } from '@affine/env/blocksuite';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import {
|
||||
clearLoginStorage,
|
||||
@ -8,7 +9,7 @@ import {
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} 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 { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
@ -26,7 +27,7 @@ import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh
|
||||
import { AffineSWRConfigProvider } from '../../providers/AffineSWRConfigProvider';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
import { initPage, toast } from '../../utils';
|
||||
import { toast } from '../../utils';
|
||||
import type { WorkspacePlugin } from '..';
|
||||
import { QueryKey } from './fetcher';
|
||||
|
||||
@ -81,20 +82,20 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
const user = parseIdToken(response.token);
|
||||
jotaiStore.set(currentAffineUserAtom, user);
|
||||
rootStore.set(currentAffineUserAtom, user);
|
||||
} else {
|
||||
toast('Login failed');
|
||||
}
|
||||
},
|
||||
'workspace:revoke': async () => {
|
||||
jotaiStore.set(jotaiWorkspacesAtom, workspaces =>
|
||||
rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
|
||||
workspaces.filter(
|
||||
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
|
||||
)
|
||||
);
|
||||
storage.removeItem(kAffineLocal);
|
||||
clearLoginStorage();
|
||||
jotaiStore.set(currentAffineUserAtom, null);
|
||||
rootStore.set(currentAffineUserAtom, null);
|
||||
},
|
||||
},
|
||||
CRUD: {
|
||||
|
@ -27,9 +27,7 @@ export const WorkspacePlugins = {
|
||||
[WorkspaceFlavour.PUBLIC]: {
|
||||
flavour: WorkspaceFlavour.PUBLIC,
|
||||
loadPriority: LoadPriority.LOW,
|
||||
Events: {
|
||||
'app:first-init': async () => {},
|
||||
},
|
||||
Events: {} as Partial<AppEvents>,
|
||||
// todo: implement this
|
||||
CRUD: {
|
||||
get: unimplemented,
|
||||
|
@ -1,17 +1,23 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
||||
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||
import { CRUD } from '@affine/workspace/local/crud';
|
||||
import {
|
||||
DEFAULT_HELLO_WORLD_PAGE_ID,
|
||||
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 { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import React from 'react';
|
||||
|
||||
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
|
||||
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
|
||||
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
||||
import { PageDetailEditor } from '../../components/page-detail-editor';
|
||||
import { initPage } from '../../utils';
|
||||
import type { WorkspacePlugin } from '..';
|
||||
|
||||
const logger = new DebugLogger('use-create-first-workspace');
|
||||
@ -20,25 +26,29 @@ export const LocalPlugin: WorkspacePlugin<WorkspaceFlavour.LOCAL> = {
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
loadPriority: LoadPriority.LOW,
|
||||
Events: {
|
||||
'app:first-init': async () => {
|
||||
'app:init': () => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
nanoid(),
|
||||
(_: string) => undefined
|
||||
);
|
||||
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
||||
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace);
|
||||
const workspace = await LocalPlugin.CRUD.get(id);
|
||||
assertExists(workspace);
|
||||
assertEquals(workspace.id, id);
|
||||
// todo: use a better way to set initial workspace
|
||||
jotaiStore.set(jotaiWorkspacesAtom, ws => [
|
||||
...ws,
|
||||
{
|
||||
id: workspace.id,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
},
|
||||
]);
|
||||
logger.debug('create first workspace', workspace);
|
||||
const page = blockSuiteWorkspace.createPage(DEFAULT_HELLO_WORLD_PAGE_ID);
|
||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
||||
init: true,
|
||||
});
|
||||
initPage(page);
|
||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
||||
jumpOnce: true,
|
||||
});
|
||||
const provider = createIndexedDBProvider(blockSuiteWorkspace);
|
||||
provider.connect();
|
||||
provider.callbacks.add(() => {
|
||||
provider.disconnect();
|
||||
});
|
||||
ensureRootPinboard(blockSuiteWorkspace);
|
||||
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
|
||||
logger.debug('create first workspace');
|
||||
return [blockSuiteWorkspace.id];
|
||||
},
|
||||
},
|
||||
CRUD,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useTransition } from 'react';
|
||||
|
||||
import {
|
||||
@ -15,7 +15,7 @@ import { useAffineLogOut } from '../hooks/affine/use-affine-log-out';
|
||||
import { useCurrentUser } from '../hooks/current/use-current-user';
|
||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
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';
|
||||
|
||||
const WorkspaceListModal = lazy(() =>
|
||||
@ -41,10 +41,10 @@ export function Modals() {
|
||||
const { jumpToSubPath } = useRouterHelper(router);
|
||||
const user = useCurrentUser();
|
||||
const workspaces = useWorkspaces();
|
||||
const setWorkspaces = useSetAtom(jotaiWorkspacesAtom);
|
||||
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||
const [, setCurrentWorkspace] = useCurrentWorkspace();
|
||||
const { createLocalWorkspace } = useWorkspacesHelper();
|
||||
const { createLocalWorkspace } = useAppHelper();
|
||||
const [transitioning, transition] = useTransition();
|
||||
|
||||
return (
|
||||
@ -122,13 +122,10 @@ export function Modals() {
|
||||
);
|
||||
}
|
||||
|
||||
export const ModalProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
export const ModalProvider = (): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<Modals />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import type { LoginResponse } 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> &
|
||||
ReturnType<typeof createWorkspaceApis>;
|
||||
@ -19,7 +19,7 @@ const debugLogger = new DebugLogger('affine-debug-apis');
|
||||
if (!globalThis.AFFINE_APIS) {
|
||||
globalThis.AFFINE_APIS = affineApis;
|
||||
globalThis.setLogin = (response: LoginResponse) => {
|
||||
jotaiStore.set(currentAffineUserAtom, parseIdToken(response.token));
|
||||
rootStore.set(currentAffineUserAtom, parseIdToken(response.token));
|
||||
setLoginStorage(response);
|
||||
};
|
||||
const loginMockUser1 = async () => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { AffineWorkspace, LocalWorkspace } 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 type { NextPage } from 'next';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
@ -11,7 +12,7 @@ export type AffineOfficialWorkspace =
|
||||
| LocalWorkspace
|
||||
| AffinePublicWorkspace;
|
||||
|
||||
export type AllWorkspace = AffineOfficialWorkspace;
|
||||
export type AllWorkspace = WorkspaceRegistry[keyof WorkspaceRegistry];
|
||||
|
||||
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
|
||||
P,
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './blocksuite';
|
||||
export * from './create-emotion-cache';
|
||||
export * from './string2color';
|
||||
export * from './toast';
|
||||
|
@ -6,7 +6,7 @@ This document delves into the design and architecture of the AFFiNE platform, pr
|
||||
|
||||
## 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.
|
||||
|
||||
### 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),
|
||||
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
|
||||
```
|
||||
|
3
packages/env/package.json
vendored
3
packages/env/package.json
vendored
@ -12,7 +12,8 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./constant": "./src/constant.ts"
|
||||
"./constant": "./src/constant.ts",
|
||||
"./blocksuite": "./src/blocksuite.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly"
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import markdown from '@affine/templates/Welcome-to-AFFiNE.md';
|
||||
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 type { BlockSuiteWorkspace } from '../shared';
|
||||
declare global {
|
||||
interface Window {
|
||||
lastImportedMarkdown: string;
|
||||
}
|
||||
}
|
||||
|
||||
const demoTitle = markdown
|
||||
.split('\n')
|
||||
@ -50,20 +54,11 @@ export function _initPageWithDemoMarkdown(page: Page): void {
|
||||
const frameId = page.addBlock('affine:frame', {}, pageBlockId);
|
||||
page.addBlock('affine:paragraph', {}, frameId);
|
||||
const contentParser = new ContentParser(page);
|
||||
contentParser.importMarkdown(demoText, frameId).then(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('markdown:imported', {
|
||||
detail: {
|
||||
workspaceId: page.workspace.id,
|
||||
pageId: page.id,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
contentParser.importMarkdown(demoText, frameId);
|
||||
page.workspace.setPageMeta(page.id, { demoTitle });
|
||||
}
|
||||
|
||||
export function ensureRootPinboard(blockSuiteWorkspace: BlockSuiteWorkspace) {
|
||||
export function ensureRootPinboard(blockSuiteWorkspace: Workspace) {
|
||||
const metas = blockSuiteWorkspace.meta.pageMetas;
|
||||
const rootMeta = metas.find(m => m.isRootPinboard);
|
||||
|
1
packages/env/src/constant.ts
vendored
1
packages/env/src/constant.ts
vendored
@ -1,5 +1,6 @@
|
||||
export const DEFAULT_WORKSPACE_NAME = 'Demo Workspace';
|
||||
export const UNTITLED_WORKSPACE_NAME = 'Untitled';
|
||||
export const DEFAULT_HELLO_WORLD_PAGE_ID = 'hello-world';
|
||||
|
||||
export const enum MessageCode {
|
||||
loginError,
|
||||
|
10
packages/env/src/types.d.ts
vendored
Normal file
10
packages/env/src/types.d.ts
vendored
Normal 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;
|
||||
}
|
@ -10,9 +10,12 @@ declare module '@blocksuite/store' {
|
||||
export function useBlockSuiteWorkspacePageIsPublic(page: Page) {
|
||||
const [isPublic, set] = useState<boolean>(() => page.meta.isPublic ?? false);
|
||||
useEffect(() => {
|
||||
page.workspace.meta.pageMetasUpdated.on(() => {
|
||||
const disposable = page.workspace.meta.pageMetasUpdated.on(() => {
|
||||
set(page.meta.isPublic ?? false);
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [page]);
|
||||
const setIsPublic = useCallback(
|
||||
(isPublic: boolean) => {
|
||||
|
30
packages/hooks/src/use-blocksuite-workspace-page.ts
Normal file
30
packages/hooks/src/use-blocksuite-workspace-page.ts
Normal 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;
|
||||
}
|
@ -6,7 +6,7 @@ import {
|
||||
} from '@affine/workspace/affine/api';
|
||||
import { WebsocketClient } from '@affine/workspace/affine/channel';
|
||||
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 { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
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 (workspaceIndex === -1) {
|
||||
jotaiStore.set(jotaiWorkspacesAtom, workspaces => {
|
||||
rootStore.set(rootWorkspacesMetadataAtom, workspaces => {
|
||||
const idx = workspaces.findIndex(workspace => workspace.id === id);
|
||||
workspaces.splice(idx, 1);
|
||||
return [...workspaces];
|
||||
|
@ -1,18 +1,49 @@
|
||||
import { atomWithSyncStorage } from '@affine/jotai';
|
||||
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;
|
||||
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
|
||||
export const jotaiWorkspacesAtom = atomWithSyncStorage<JotaiWorkspace[]>(
|
||||
/**
|
||||
* root workspaces atom
|
||||
* 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',
|
||||
[]
|
||||
);
|
||||
|
||||
// global jotai store, which is used to store all the atoms
|
||||
export const jotaiStore = createStore();
|
||||
// two more atoms to store the current workspace and page
|
||||
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();
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { nanoid, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import { createIndexedDBProvider } from '@toeverything/y-indexeddb';
|
||||
import { createJSONStorage } from 'jotai/utils';
|
||||
@ -13,8 +14,25 @@ const getStorage = () => createJSONStorage(() => localStorage);
|
||||
const kStoreKey = 'affine-local-workspace';
|
||||
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> = {
|
||||
get: async workspaceId => {
|
||||
logger.debug('get', workspaceId);
|
||||
const storage = getStorage();
|
||||
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||
storage.setItem(kStoreKey, []);
|
||||
@ -36,10 +54,10 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
|
||||
return workspace;
|
||||
},
|
||||
create: async ({ doc }) => {
|
||||
logger.debug('create', doc);
|
||||
const storage = getStorage();
|
||||
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||
storage.setItem(kStoreKey, []);
|
||||
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
|
||||
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdateV2(doc);
|
||||
const id = nanoid();
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
@ -52,11 +70,11 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
|
||||
await persistence.whenSynced.then(() => {
|
||||
persistence.disconnect();
|
||||
});
|
||||
storage.setItem(kStoreKey, [...data, id]);
|
||||
console.log('create', id, storage.getItem(kStoreKey));
|
||||
saveWorkspaceToLocalStorage(id);
|
||||
return id;
|
||||
},
|
||||
delete: async workspace => {
|
||||
logger.debug('delete', workspace);
|
||||
const storage = getStorage();
|
||||
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||
storage.setItem(kStoreKey, []);
|
||||
@ -69,6 +87,7 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
|
||||
storage.setItem(kStoreKey, [...data]);
|
||||
},
|
||||
list: async () => {
|
||||
logger.debug('list');
|
||||
const storage = getStorage();
|
||||
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||
storage.setItem(kStoreKey, []);
|
||||
|
@ -69,6 +69,36 @@ const createAffineWebSocketProvider = (
|
||||
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 = (
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
): LocalIndexedDBProvider => {
|
||||
@ -76,7 +106,7 @@ const createIndexedDBProvider = (
|
||||
blockSuiteWorkspace.id,
|
||||
blockSuiteWorkspace.doc
|
||||
);
|
||||
const callbacks = new Set<() => void>();
|
||||
const callbacks = new CallbackSet();
|
||||
return {
|
||||
flavour: 'local-indexeddb',
|
||||
callbacks,
|
||||
@ -93,6 +123,7 @@ const createIndexedDBProvider = (
|
||||
indexeddbProvider.connect();
|
||||
indexeddbProvider.whenSynced
|
||||
.then(() => {
|
||||
callbacks.ready = true;
|
||||
callbacks.forEach(cb => cb());
|
||||
})
|
||||
.catch(error => {
|
||||
@ -110,6 +141,7 @@ const createIndexedDBProvider = (
|
||||
blockSuiteWorkspace.id
|
||||
);
|
||||
indexeddbProvider.disconnect();
|
||||
callbacks.ready = false;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -2,8 +2,11 @@
|
||||
/// <reference path='../../../apps/electron/layers/preload/preload.d.ts' />
|
||||
import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api';
|
||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import type { createStore } from 'jotai';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
export type JotaiStore = ReturnType<typeof createStore>;
|
||||
|
||||
export type BaseProvider = {
|
||||
flavour: string;
|
||||
// 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 {
|
||||
// event when app is first initialized
|
||||
// event there is no workspace
|
||||
// usually used to initialize workspace plugin
|
||||
'app:first-init': () => Promise<void>;
|
||||
'app:init': () => string[];
|
||||
// request to gain access to workspace plugin
|
||||
'workspace:access': () => Promise<void>;
|
||||
// request to revoke access to workspace plugin
|
||||
|
@ -37,7 +37,7 @@ const config: PlaywrightTestConfig = {
|
||||
},
|
||||
forbidOnly: !!process.env.CI,
|
||||
workers: 4,
|
||||
retries: 3,
|
||||
retries: 1,
|
||||
// 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'
|
||||
// default 'list' when running locally
|
||||
// See https://playwright.dev/docs/test-reporters#github-actions-annotations
|
||||
|
@ -4,7 +4,6 @@ import { getMetas } from './utils';
|
||||
|
||||
export async function openHomePage(page: Page) {
|
||||
await page.goto('http://localhost:8080');
|
||||
await page.waitForSelector('#__next');
|
||||
}
|
||||
|
||||
export async function initHomePageWithPinboard(page: Page) {
|
||||
|
@ -1,12 +1,7 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export async function waitMarkdownImported(page: Page) {
|
||||
return page.evaluate(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
document.addEventListener('markdown:imported', resolve);
|
||||
})
|
||||
);
|
||||
await page.waitForSelector('v-line');
|
||||
}
|
||||
|
||||
export async function newPage(page: Page) {
|
||||
|
@ -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);
|
||||
|
||||
if (enableCoverage) {
|
||||
|
@ -8,34 +8,32 @@ import {
|
||||
} from '../../libs/sidebar';
|
||||
import { getBuiltInUser, loginUser, openHomePage } from '../../libs/utils';
|
||||
|
||||
test.describe('affine built in workspace', () => {
|
||||
test('collaborative', async ({ page, browser }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const [a, b] = await getBuiltInUser();
|
||||
await loginUser(page, a);
|
||||
await page.reload();
|
||||
await page.waitForTimeout(50);
|
||||
await clickSideBarCurrentWorkspaceBanner(page);
|
||||
await page.getByText('Cloud Workspace').click();
|
||||
const context2 = await browser.newContext();
|
||||
const page2 = await context2.newPage();
|
||||
await openHomePage(page2);
|
||||
await loginUser(page2, b);
|
||||
await page2.reload();
|
||||
await clickSideBarCurrentWorkspaceBanner(page2);
|
||||
await page2.getByText('Joined Workspace').click();
|
||||
await clickNewPageButton(page);
|
||||
const url = page.url();
|
||||
await page2.goto(url);
|
||||
await page.focus('.affine-default-page-block-title');
|
||||
await page.type('.affine-default-page-block-title', 'hello', {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForTimeout(100);
|
||||
const title = (await page
|
||||
.locator('.affine-default-page-block-title')
|
||||
.textContent()) as string;
|
||||
expect(title.trim()).toBe('hello');
|
||||
test('collaborative', async ({ page, browser }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const [a, b] = await getBuiltInUser();
|
||||
await loginUser(page, a);
|
||||
await page.reload();
|
||||
await page.waitForTimeout(50);
|
||||
await clickSideBarCurrentWorkspaceBanner(page);
|
||||
await page.getByText('Cloud Workspace').click();
|
||||
const context2 = await browser.newContext();
|
||||
const page2 = await context2.newPage();
|
||||
await openHomePage(page2);
|
||||
await loginUser(page2, b);
|
||||
await page2.reload();
|
||||
await clickSideBarCurrentWorkspaceBanner(page2);
|
||||
await page2.getByText('Joined Workspace').click();
|
||||
await clickNewPageButton(page);
|
||||
const url = page.url();
|
||||
await page2.goto(url);
|
||||
await page.focus('.affine-default-page-block-title');
|
||||
await page.type('.affine-default-page-block-title', 'hello', {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForTimeout(100);
|
||||
const title = (await page
|
||||
.locator('.affine-default-page-block-title')
|
||||
.textContent()) as string;
|
||||
expect(title.trim()).toBe('hello');
|
||||
});
|
||||
|
@ -6,62 +6,56 @@ import { clickNewPageButton } from '../../libs/sidebar';
|
||||
import { createFakeUser, loginUser, openHomePage } from '../../libs/utils';
|
||||
import { createWorkspace } from '../../libs/workspace';
|
||||
|
||||
test.describe('affine single page', () => {
|
||||
test('public single page', async ({ page, browser }) => {
|
||||
// when enable single page, most time the page will crash
|
||||
test.fail();
|
||||
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 clickNewPageButton(page);
|
||||
const page1Id = page.url().split('/').at(-1);
|
||||
await clickNewPageButton(page);
|
||||
const page2Id = page.url().split('/').at(-1);
|
||||
expect(typeof page2Id).toBe('string');
|
||||
expect(page1Id).not.toBe(page2Id);
|
||||
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);
|
||||
test('public single 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 clickNewPageButton(page);
|
||||
const page1Id = page.url().split('/').at(-1);
|
||||
await clickNewPageButton(page);
|
||||
const page2Id = page.url().split('/').at(-1);
|
||||
expect(typeof page2Id).toBe('string');
|
||||
expect(page1Id).not.toBe(page2Id);
|
||||
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);
|
||||
});
|
||||
|
@ -10,65 +10,61 @@ import {
|
||||
import { createFakeUser, loginUser, openHomePage } from '../../libs/utils';
|
||||
import { createWorkspace } from '../../libs/workspace';
|
||||
|
||||
test.describe('affine public workspace', () => {
|
||||
test('enable public workspace', async ({ page, context }) => {
|
||||
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,
|
||||
});
|
||||
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('enable public workspace', async ({ page, context }) => {
|
||||
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,
|
||||
});
|
||||
|
||||
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');
|
||||
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();
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
@ -21,44 +21,42 @@ import {
|
||||
openWorkspaceListModal,
|
||||
} from '../../libs/workspace';
|
||||
|
||||
test.describe('affine workspace', () => {
|
||||
test('should login with user A', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const [a] = await createFakeUser(userA, userB);
|
||||
await loginUser(page, a);
|
||||
await clickSideBarCurrentWorkspaceBanner(page);
|
||||
const footer = page.locator('[data-testid="workspace-list-modal-footer"]');
|
||||
expect(await footer.getByText(userA.name).isVisible()).toBe(true);
|
||||
expect(await footer.getByText(userA.email).isVisible()).toBe(true);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
||||
test('should enable affine workspace successfully', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const [a] = await createFakeUser();
|
||||
await loginUser(page, a);
|
||||
const name = `test-${Date.now()}`;
|
||||
await createWorkspace({ name }, page);
|
||||
await page.waitForTimeout(50);
|
||||
await clickSideBarSettingButton(page);
|
||||
await page.waitForTimeout(50);
|
||||
await clickCollaborationPanel(page);
|
||||
await page.getByTestId('local-workspace-enable-cloud-button').click();
|
||||
await page.getByTestId('confirm-enable-cloud-button').click();
|
||||
await page.waitForSelector("[data-testid='member-length']", {
|
||||
timeout: 20000,
|
||||
});
|
||||
await clickSideBarAllPageButton(page);
|
||||
await clickNewPageButton(page);
|
||||
await page.locator('[data-block-is-title="true"]').type('Hello, world!', {
|
||||
delay: 50,
|
||||
});
|
||||
await assertCurrentWorkspaceFlavour('affine', page);
|
||||
await openWorkspaceListModal(page);
|
||||
await page.getByTestId('workspace-list-modal-sign-out').click();
|
||||
await page.waitForTimeout(1000);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
test('should login with user A', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const [a] = await createFakeUser(userA, userB);
|
||||
await loginUser(page, a);
|
||||
await clickSideBarCurrentWorkspaceBanner(page);
|
||||
const footer = page.locator('[data-testid="workspace-list-modal-footer"]');
|
||||
expect(await footer.getByText(userA.name).isVisible()).toBe(true);
|
||||
expect(await footer.getByText(userA.email).isVisible()).toBe(true);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
||||
test('should enable affine workspace successfully', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const [a] = await createFakeUser();
|
||||
await loginUser(page, a);
|
||||
const name = `test-${Date.now()}`;
|
||||
await createWorkspace({ name }, page);
|
||||
await page.waitForTimeout(50);
|
||||
await clickSideBarSettingButton(page);
|
||||
await page.waitForTimeout(50);
|
||||
await clickCollaborationPanel(page);
|
||||
await page.getByTestId('local-workspace-enable-cloud-button').click();
|
||||
await page.getByTestId('confirm-enable-cloud-button').click();
|
||||
await page.waitForSelector("[data-testid='member-length']", {
|
||||
timeout: 20000,
|
||||
});
|
||||
await clickSideBarAllPageButton(page);
|
||||
await clickNewPageButton(page);
|
||||
await page.locator('[data-block-is-title="true"]').type('Hello, world!', {
|
||||
delay: 50,
|
||||
});
|
||||
await assertCurrentWorkspaceFlavour('affine', page);
|
||||
await openWorkspaceListModal(page);
|
||||
await page.getByTestId('workspace-list-modal-sign-out').click();
|
||||
await page.waitForTimeout(1000);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
@ -4,25 +4,23 @@ import { openHomePage } from '../libs/load-page';
|
||||
import { clickPageMoreActions, waitMarkdownImported } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('Change page mode(Page or Edgeless)', () => {
|
||||
test('Switch to edgeless by switch edgeless item', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const btn = await page.getByTestId('switch-edgeless-mode-button');
|
||||
await btn.click();
|
||||
test('Switch to edgeless by switch edgeless item', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const btn = await page.getByTestId('switch-edgeless-mode-button');
|
||||
await btn.click();
|
||||
|
||||
const edgeless = page.locator('affine-edgeless-page');
|
||||
expect(await edgeless.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('Convert to edgeless by editor header items', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await clickPageMoreActions(page);
|
||||
const menusEdgelessItem = page.getByTestId('editor-option-menu-edgeless');
|
||||
await menusEdgelessItem.click({ delay: 100 });
|
||||
|
||||
const edgeless = page.locator('affine-edgeless-page');
|
||||
expect(await edgeless.isVisible()).toBe(true);
|
||||
});
|
||||
const edgeless = page.locator('affine-edgeless-page');
|
||||
expect(await edgeless.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('Convert to edgeless by editor header items', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await clickPageMoreActions(page);
|
||||
const menusEdgelessItem = page.getByTestId('editor-option-menu-edgeless');
|
||||
await menusEdgelessItem.click({ delay: 100 });
|
||||
|
||||
const edgeless = page.locator('affine-edgeless-page');
|
||||
expect(await edgeless.isVisible()).toBe(true);
|
||||
});
|
||||
|
@ -4,20 +4,16 @@ import { openHomePage } from '../libs/load-page';
|
||||
import { waitMarkdownImported } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('Open contact us', () => {
|
||||
test('Click right-bottom corner contact icon', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.locator('[data-testid=help-island]').click();
|
||||
const rightBottomContactUs = page.locator(
|
||||
'[data-testid=right-bottom-contact-us-icon]'
|
||||
);
|
||||
expect(await rightBottomContactUs.isVisible()).toEqual(true);
|
||||
test('Click right-bottom corner contact icon', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.locator('[data-testid=help-island]').click();
|
||||
const rightBottomContactUs = page.locator(
|
||||
'[data-testid=right-bottom-contact-us-icon]'
|
||||
);
|
||||
expect(await rightBottomContactUs.isVisible()).toEqual(true);
|
||||
|
||||
await rightBottomContactUs.click();
|
||||
const contactUsModal = page.locator(
|
||||
'[data-testid=contact-us-modal-content]'
|
||||
);
|
||||
await expect(contactUsModal).toContainText('Check Our Docs');
|
||||
});
|
||||
await rightBottomContactUs.click();
|
||||
const contactUsModal = page.locator('[data-testid=contact-us-modal-content]');
|
||||
await expect(contactUsModal).toContainText('Check Our Docs');
|
||||
});
|
||||
|
@ -2,16 +2,12 @@ import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('Debug page broadcast', () => {
|
||||
test('should have page0', async ({ page }) => {
|
||||
await page.goto(
|
||||
'http://localhost:8080/_debug/init-page?type=importMarkdown'
|
||||
);
|
||||
await page.waitForSelector('v-line');
|
||||
const pageId = await page.evaluate(async () => {
|
||||
// @ts-ignore
|
||||
return globalThis.page.id;
|
||||
});
|
||||
expect(pageId).toBe('page0');
|
||||
test('should have page0', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/_debug/init-page?type=importMarkdown');
|
||||
await page.waitForSelector('v-line');
|
||||
const pageId = await page.evaluate(async () => {
|
||||
// @ts-ignore
|
||||
return globalThis.page.id;
|
||||
});
|
||||
expect(pageId).toBe('page0');
|
||||
});
|
||||
|
@ -2,21 +2,19 @@ import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('Debug page broadcast', () => {
|
||||
test('should broadcast a message to all debug pages', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await page.goto('http://localhost:8080/_debug/broadcast');
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto('http://localhost:8080/_debug/broadcast');
|
||||
await page.waitForSelector('#__next');
|
||||
await page2.waitForSelector('#__next');
|
||||
await page.click('[data-testid="create-page"]');
|
||||
expect(await page.locator('tr').count()).toBe(2);
|
||||
expect(await page2.locator('tr').count()).toBe(2);
|
||||
await page2.click('[data-testid="create-page"]');
|
||||
expect(await page.locator('tr').count()).toBe(3);
|
||||
expect(await page2.locator('tr').count()).toBe(3);
|
||||
});
|
||||
test('should broadcast a message to all debug pages', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await page.goto('http://localhost:8080/_debug/broadcast');
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto('http://localhost:8080/_debug/broadcast');
|
||||
await page.waitForSelector('#__next');
|
||||
await page2.waitForSelector('#__next');
|
||||
await page.click('[data-testid="create-page"]');
|
||||
expect(await page.locator('tr').count()).toBe(2);
|
||||
expect(await page2.locator('tr').count()).toBe(2);
|
||||
await page2.click('[data-testid="create-page"]');
|
||||
expect(await page.locator('tr').count()).toBe(3);
|
||||
expect(await page2.locator('tr').count()).toBe(3);
|
||||
});
|
||||
|
@ -2,10 +2,8 @@ import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('exception page', () => {
|
||||
test('visit 404 page', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/404');
|
||||
const notFoundTip = page.locator('[data-testid=notFound]');
|
||||
await expect(notFoundTip).toBeVisible();
|
||||
});
|
||||
test('visit 404 page', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/404');
|
||||
const notFoundTip = page.locator('[data-testid=notFound]');
|
||||
await expect(notFoundTip).toBeVisible();
|
||||
});
|
||||
|
@ -2,10 +2,8 @@ import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('invite code page', () => {
|
||||
test('the link has expired', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080//invite/abc');
|
||||
await page.waitForTimeout(1000);
|
||||
expect(page.getByText('The link has expired')).not.toBeUndefined();
|
||||
});
|
||||
test('the link has expired', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080//invite/abc');
|
||||
await page.waitForTimeout(1000);
|
||||
expect(page.getByText('The link has expired')).not.toBeUndefined();
|
||||
});
|
||||
|
@ -4,74 +4,72 @@ import { openHomePage } from '../libs/load-page';
|
||||
import { waitMarkdownImported } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('Layout ui', () => {
|
||||
test('Collapse Sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
||||
const sliderBarArea = page.getByTestId('sliderBar-root');
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
|
||||
test('Expand Sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
|
||||
await page.getByTestId('sliderBar-arrowButton-expand').click();
|
||||
await expect(sliderBarArea).toBeInViewport();
|
||||
});
|
||||
|
||||
test('Click resizer can close sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await expect(sliderBarArea).toBeVisible();
|
||||
|
||||
await page.getByTestId('sliderBar-resizer').click();
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
|
||||
test('Drag resizer can resize sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await expect(sliderBarArea).toBeVisible();
|
||||
|
||||
const sliderResizer = page.getByTestId('sliderBar-resizer');
|
||||
await sliderResizer.hover();
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(400, 300, {
|
||||
steps: 10,
|
||||
});
|
||||
await page.mouse.up();
|
||||
const boundingBox = await page.getByTestId('sliderBar-root').boundingBox();
|
||||
expect(boundingBox?.width).toBe(400);
|
||||
});
|
||||
|
||||
test('Sidebar in between sm & md breakpoint', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
const sliderBarModalBackground = page.getByTestId(
|
||||
'sliderBar-modalBackground'
|
||||
);
|
||||
await expect(sliderBarArea).toBeInViewport();
|
||||
await expect(sliderBarModalBackground).not.toBeVisible();
|
||||
|
||||
await page.setViewportSize({
|
||||
width: 768,
|
||||
height: 1024,
|
||||
});
|
||||
await expect(sliderBarModalBackground).toBeVisible();
|
||||
|
||||
// click modal background can close sidebar
|
||||
await sliderBarModalBackground.click({
|
||||
force: true,
|
||||
position: { x: 600, y: 150 },
|
||||
});
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
test('Collapse Sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
||||
const sliderBarArea = page.getByTestId('sliderBar-root');
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
|
||||
test('Expand Sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
|
||||
await page.getByTestId('sliderBar-arrowButton-expand').click();
|
||||
await expect(sliderBarArea).toBeInViewport();
|
||||
});
|
||||
|
||||
test('Click resizer can close sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await expect(sliderBarArea).toBeVisible();
|
||||
|
||||
await page.getByTestId('sliderBar-resizer').click();
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
|
||||
test('Drag resizer can resize sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await expect(sliderBarArea).toBeVisible();
|
||||
|
||||
const sliderResizer = page.getByTestId('sliderBar-resizer');
|
||||
await sliderResizer.hover();
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(400, 300, {
|
||||
steps: 10,
|
||||
});
|
||||
await page.mouse.up();
|
||||
const boundingBox = await page.getByTestId('sliderBar-root').boundingBox();
|
||||
expect(boundingBox?.width).toBe(400);
|
||||
});
|
||||
|
||||
test('Sidebar in between sm & md breakpoint', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
const sliderBarModalBackground = page.getByTestId(
|
||||
'sliderBar-modalBackground'
|
||||
);
|
||||
await expect(sliderBarArea).toBeInViewport();
|
||||
await expect(sliderBarModalBackground).not.toBeVisible();
|
||||
|
||||
await page.setViewportSize({
|
||||
width: 768,
|
||||
height: 1024,
|
||||
});
|
||||
await expect(sliderBarModalBackground).toBeVisible();
|
||||
|
||||
// click modal background can close sidebar
|
||||
await sliderBarModalBackground.click({
|
||||
force: true,
|
||||
position: { x: 600, y: 150 },
|
||||
});
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
|
@ -5,37 +5,35 @@ import { newPage, waitMarkdownImported } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
|
||||
|
||||
test.describe('Local first create page', () => {
|
||||
test('should create a page with a local first avatar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await page.getByTestId('workspace-name').click();
|
||||
await page.getByTestId('new-workspace').click({ delay: 50 });
|
||||
await page
|
||||
.getByTestId('create-workspace-input')
|
||||
.type('Test Workspace 1', { delay: 50 });
|
||||
await page.getByTestId('create-workspace-button').click();
|
||||
await page.getByTestId('workspace-name').click();
|
||||
await page.getByTestId('workspace-card').nth(1).click();
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await page
|
||||
.getByTestId('upload-avatar')
|
||||
.setInputFiles('./tests/fixtures/smile.png');
|
||||
await page.getByTestId('workspace-name').click();
|
||||
await page.getByTestId('workspace-card').nth(0).click();
|
||||
await page.waitForTimeout(1000);
|
||||
const text = await page.getByTestId('workspace-avatar').textContent();
|
||||
// default avatar for default workspace
|
||||
expect(text).toBe('D');
|
||||
await page.getByTestId('workspace-name').click();
|
||||
await page.getByTestId('workspace-card').nth(1).click();
|
||||
const blobUrl = await page
|
||||
.getByTestId('workspace-avatar')
|
||||
.locator('img')
|
||||
.getAttribute('src');
|
||||
// out user uploaded avatar
|
||||
expect(blobUrl).toContain('blob:');
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
test('should create a page with a local first avatar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await page.getByTestId('workspace-name').click();
|
||||
await page.getByTestId('new-workspace').click({ delay: 50 });
|
||||
await page
|
||||
.getByTestId('create-workspace-input')
|
||||
.type('Test Workspace 1', { delay: 50 });
|
||||
await page.getByTestId('create-workspace-button').click();
|
||||
await page.getByTestId('workspace-name').click();
|
||||
await page.getByTestId('workspace-card').nth(1).click();
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await page
|
||||
.getByTestId('upload-avatar')
|
||||
.setInputFiles('./tests/fixtures/smile.png');
|
||||
await page.getByTestId('workspace-name').click();
|
||||
await page.getByTestId('workspace-card').nth(0).click();
|
||||
await page.waitForTimeout(1000);
|
||||
const text = await page.getByTestId('workspace-avatar').textContent();
|
||||
// default avatar for default workspace
|
||||
expect(text).toBe('D');
|
||||
await page.getByTestId('workspace-name').click();
|
||||
await page.getByTestId('workspace-card').nth(1).click();
|
||||
const blobUrl = await page
|
||||
.getByTestId('workspace-avatar')
|
||||
.locator('img')
|
||||
.getAttribute('src');
|
||||
// out user uploaded avatar
|
||||
expect(blobUrl).toContain('blob:');
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
@ -9,49 +9,47 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
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 ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', {
|
||||
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);
|
||||
test('New a page , then delete it in all pages, permanently delete it', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', {
|
||||
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);
|
||||
});
|
||||
|
@ -6,26 +6,24 @@ import { test } from '../libs/playwright';
|
||||
import { clickSideBarSettingButton } from '../libs/sidebar';
|
||||
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 ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await clickSideBarSettingButton(page);
|
||||
await page.getByTestId('delete-workspace-button').click();
|
||||
const workspaceNameDom = await page.getByTestId('workspace-name');
|
||||
const currentWorkspaceName = await workspaceNameDom.evaluate(
|
||||
node => node.textContent
|
||||
);
|
||||
await page
|
||||
.getByTestId('delete-workspace-input')
|
||||
.type(currentWorkspaceName as string);
|
||||
await page.getByTestId('delete-workspace-confirm-button').click();
|
||||
await page.getByTestId('affine-toast').waitFor({ state: 'attached' });
|
||||
expect(await page.getByTestId('workspace-card').count()).toBe(0);
|
||||
await page.mouse.click(1, 1);
|
||||
expect(await page.getByTestId('workspace-card').count()).toBe(0);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
test('New a workspace , then delete it in all workspaces, permanently delete it', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await clickSideBarSettingButton(page);
|
||||
await page.getByTestId('delete-workspace-button').click();
|
||||
const workspaceNameDom = await page.getByTestId('workspace-name');
|
||||
const currentWorkspaceName = await workspaceNameDom.evaluate(
|
||||
node => node.textContent
|
||||
);
|
||||
await page
|
||||
.getByTestId('delete-workspace-input')
|
||||
.type(currentWorkspaceName as string);
|
||||
await page.getByTestId('delete-workspace-confirm-button').click();
|
||||
await page.getByTestId('affine-toast').waitFor({ state: 'attached' });
|
||||
expect(await page.getByTestId('workspace-card').count()).toBe(0);
|
||||
await page.mouse.click(1, 1);
|
||||
expect(await page.getByTestId('workspace-card').count()).toBe(0);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
@ -10,64 +10,60 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
|
||||
|
||||
test.describe('Local first export page', () => {
|
||||
test.skip('New a page ,then open it and export html', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await page
|
||||
.getByPlaceholder('Title')
|
||||
.fill('this is a new page to export html content');
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
test.skip('New a page ,then open it and export html', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await page
|
||||
.getByPlaceholder('Title')
|
||||
.fill('this is a new page to export html content');
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
|
||||
const cell = page.getByRole('cell', {
|
||||
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'
|
||||
);
|
||||
const cell = page.getByRole('cell', {
|
||||
name: 'this is a new page to export html content',
|
||||
});
|
||||
expect(cell).not.toBeUndefined();
|
||||
|
||||
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);
|
||||
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'
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
@ -10,89 +10,87 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
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 }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
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', {
|
||||
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);
|
||||
test('New a page and open it ,then favorite it', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
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', {
|
||||
name: 'this is a new page to favorite',
|
||||
});
|
||||
expect(cell).not.toBeUndefined();
|
||||
|
||||
test('Export to html and markdown', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
{
|
||||
await clickPageMoreActions(page);
|
||||
await page.getByTestId('export-menu').click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByTestId('export-to-markdown').click();
|
||||
await downloadPromise;
|
||||
}
|
||||
await page.waitForTimeout(50);
|
||||
{
|
||||
await clickPageMoreActions(page);
|
||||
await page.getByTestId('export-menu').click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByTestId('export-to-html').click();
|
||||
await downloadPromise;
|
||||
}
|
||||
});
|
||||
|
||||
test('Cancel favorite', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
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', {
|
||||
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();
|
||||
|
||||
// expect it in favorite list
|
||||
await page.getByRole('link', { name: 'Favorites' }).click();
|
||||
expect(
|
||||
page.getByRole('cell', { name: 'this is a new page to favorite' })
|
||||
).not.toBeUndefined();
|
||||
|
||||
// cancel favorite
|
||||
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
|
||||
const box = await page
|
||||
.getByRole('cell', { name: 'this is a new page to favorite' })
|
||||
.boundingBox();
|
||||
//hover table record
|
||||
await page.mouse.move((box?.x ?? 0) + 10, (box?.y ?? 0) + 10);
|
||||
|
||||
await page.getByTestId('favorited-icon').click();
|
||||
|
||||
// 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);
|
||||
});
|
||||
await cell.click();
|
||||
await clickPageMoreActions(page);
|
||||
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
|
||||
await favoriteBtn.click();
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
||||
test('Export to html and markdown', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
{
|
||||
await clickPageMoreActions(page);
|
||||
await page.getByTestId('export-menu').click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByTestId('export-to-markdown').click();
|
||||
await downloadPromise;
|
||||
}
|
||||
await page.waitForTimeout(50);
|
||||
{
|
||||
await clickPageMoreActions(page);
|
||||
await page.getByTestId('export-menu').click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByTestId('export-to-html').click();
|
||||
await downloadPromise;
|
||||
}
|
||||
});
|
||||
|
||||
test('Cancel favorite', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
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', {
|
||||
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();
|
||||
|
||||
// expect it in favorite list
|
||||
await page.getByRole('link', { name: 'Favorites' }).click();
|
||||
expect(
|
||||
page.getByRole('cell', { name: 'this is a new page to favorite' })
|
||||
).not.toBeUndefined();
|
||||
|
||||
// cancel favorite
|
||||
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
|
||||
const box = await page
|
||||
.getByRole('cell', { name: 'this is a new page to favorite' })
|
||||
.boundingBox();
|
||||
//hover table record
|
||||
await page.mouse.move((box?.x ?? 0) + 10, (box?.y ?? 0) + 10);
|
||||
|
||||
await page.getByTestId('favorited-icon').click();
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
@ -10,60 +10,58 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
|
||||
|
||||
test.describe('Local first favorite items ui', () => {
|
||||
test('Show favorite items in sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', {
|
||||
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'
|
||||
);
|
||||
test('Show favorite items in sidebar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', {
|
||||
name: '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 }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
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', {
|
||||
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 page.getByRole('link', { name: 'Favorites' }).click();
|
||||
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);
|
||||
});
|
||||
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'
|
||||
);
|
||||
});
|
||||
|
||||
test('Show favorite items in favorite list', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
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', {
|
||||
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 page.getByRole('link', { name: 'Favorites' }).click();
|
||||
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);
|
||||
});
|
||||
|
@ -9,26 +9,24 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
|
||||
|
||||
test.describe('local first new page', () => {
|
||||
test('click btn new page', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const originPageId = page.url().split('/').reverse()[0];
|
||||
await newPage(page);
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
expect(newPageId).not.toBe(originPageId);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
||||
test('click btn bew page and find it in all pages', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page');
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', { name: 'this is a new page' });
|
||||
expect(cell).not.toBeUndefined();
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
test('click btn new page', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const originPageId = page.url().split('/').reverse()[0];
|
||||
await newPage(page);
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
expect(newPageId).not.toBe(originPageId);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
||||
test('click btn bew page and find it in all pages', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page');
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', { name: 'this is a new page' });
|
||||
expect(cell).not.toBeUndefined();
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
@ -9,29 +9,27 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
|
||||
|
||||
test.describe('local first new page', () => {
|
||||
test('click btn bew page and open in tab', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page');
|
||||
const newPageUrl = page.url();
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
test('click btn bew page and open in tab', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page');
|
||||
const newPageUrl = page.url();
|
||||
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
|
||||
.getByTestId('more-actions-' + newPageId)
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click();
|
||||
const [newTabPage] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.getByRole('button', { name: 'Open in new tab' }).click(),
|
||||
]);
|
||||
await page
|
||||
.getByTestId('more-actions-' + newPageId)
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click();
|
||||
const [newTabPage] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.getByRole('button', { name: 'Open in new tab' }).click(),
|
||||
]);
|
||||
|
||||
expect(newTabPage.url()).toBe(newPageUrl);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
expect(newTabPage.url()).toBe(newPageUrl);
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
@ -9,51 +9,49 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
|
||||
|
||||
test.describe('Local first delete page', () => {
|
||||
test('New a page , then delete it in all pages, restore it', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', {
|
||||
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);
|
||||
test('New a page , then delete it in all pages, restore it', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
|
||||
const newPageId = page.url().split('/').reverse()[0];
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', {
|
||||
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);
|
||||
});
|
||||
|
@ -8,29 +8,25 @@ import { test } from '../libs/playwright';
|
||||
import { clickSideBarSettingButton } from '../libs/sidebar';
|
||||
import { testResultDir } from '../libs/utils';
|
||||
|
||||
test.describe('Local first setting page', () => {
|
||||
test('Should highlight the setting page menu when selected', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const element = await page.getByTestId(
|
||||
'slider-bar-workspace-setting-button'
|
||||
);
|
||||
const prev = await element.screenshot({
|
||||
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);
|
||||
test('Should highlight the setting page menu when selected', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const element = await page.getByTestId('slider-bar-workspace-setting-button');
|
||||
const prev = await element.screenshot({
|
||||
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);
|
||||
});
|
||||
|
@ -10,52 +10,50 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
|
||||
|
||||
test.describe('Local first delete page', () => {
|
||||
test('New a page ,then open it and show delete modal', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', {
|
||||
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);
|
||||
test('New a page ,then open it and show delete modal', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const cell = page.getByRole('cell', {
|
||||
name: 'this is a new page to delete',
|
||||
});
|
||||
expect(cell).not.toBeUndefined();
|
||||
|
||||
test('New a page ,then go to all pages and show delete modal', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
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();
|
||||
const cell = page.getByRole('cell', {
|
||||
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 assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
test('New a page ,then go to all pages and show delete modal', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
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();
|
||||
const cell = page.getByRole('cell', {
|
||||
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 assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
@ -9,38 +9,36 @@ import {
|
||||
import { test } from '../libs/playwright';
|
||||
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 ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
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();
|
||||
const cell = page.getByRole('cell', {
|
||||
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);
|
||||
test('New a page , then delete it in all pages, finally find it in trash', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
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();
|
||||
const cell = page.getByRole('cell', {
|
||||
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);
|
||||
});
|
||||
|
@ -6,114 +6,112 @@ import { test } from '../libs/playwright';
|
||||
import { clickSideBarAllPageButton } from '../libs/sidebar';
|
||||
import { createWorkspace, openWorkspaceListModal } from '../libs/workspace';
|
||||
|
||||
test.describe('Local first workspace list', () => {
|
||||
test('just one item in the workspace list at first', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const workspaceName = page.getByTestId('workspace-name');
|
||||
await workspaceName.click();
|
||||
expect(
|
||||
page
|
||||
.locator('div')
|
||||
.filter({ hasText: 'AFFiNE TestLocal WorkspaceAvailable Offline' })
|
||||
.nth(3)
|
||||
).not.toBeNull();
|
||||
});
|
||||
test('just one item in the workspace list at first', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const workspaceName = page.getByTestId('workspace-name');
|
||||
await workspaceName.click();
|
||||
expect(
|
||||
page
|
||||
.locator('div')
|
||||
.filter({ hasText: 'AFFiNE TestLocal WorkspaceAvailable Offline' })
|
||||
.nth(3)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
test('create one workspace in the workspace list', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const newWorkspaceNameStr = 'New Workspace';
|
||||
await createWorkspace({ name: newWorkspaceNameStr }, page);
|
||||
test('create one workspace in the workspace list', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const newWorkspaceNameStr = 'New Workspace';
|
||||
await createWorkspace({ name: newWorkspaceNameStr }, page);
|
||||
|
||||
// check new workspace name
|
||||
const newWorkspaceName = page.getByTestId('workspace-name');
|
||||
await newWorkspaceName.click();
|
||||
// check new workspace name
|
||||
const newWorkspaceName = page.getByTestId('workspace-name');
|
||||
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
|
||||
const workspaceCards = await page.$$('data-testid=workspace-card');
|
||||
expect(workspaceCards.length).toBe(2);
|
||||
expect(workspaceCards.length).toBe(3);
|
||||
}
|
||||
|
||||
//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);
|
||||
});
|
||||
await page.reload();
|
||||
await openWorkspaceListModal(page);
|
||||
await page.getByTestId('draggable-item').nth(1).click();
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
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);
|
||||
// @ts-expect-error
|
||||
const currentId: string = await page.evaluate(() => currentWorkspace.id);
|
||||
|
||||
// show workspace list
|
||||
const workspaceName = page.getByTestId('workspace-name');
|
||||
await workspaceName.click();
|
||||
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,
|
||||
{
|
||||
//check workspace list length
|
||||
const workspaceCards = await page.$$('data-testid=workspace-card');
|
||||
expect(workspaceCards.length).toBe(3);
|
||||
steps: 5,
|
||||
}
|
||||
|
||||
await page.reload();
|
||||
await openWorkspaceListModal(page);
|
||||
await page.getByTestId('draggable-item').nth(1).click();
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
// @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
|
||||
);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(
|
||||
targetBox.x + targetBox.width / 2,
|
||||
targetBox.y + targetBox.height / 2,
|
||||
{
|
||||
const workspaceCards1 = await page.$$('data-testid=workspace-card');
|
||||
expect(workspaceCards1.length).toBe(3);
|
||||
steps: 5,
|
||||
}
|
||||
);
|
||||
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
|
||||
const nextId: string = await page.evaluate(() => currentWorkspace.id);
|
||||
expect(currentId).toBe(nextId);
|
||||
});
|
||||
await page.getByTestId('draggable-item').nth(2).click();
|
||||
|
||||
// @ts-expect-error
|
||||
const nextId: string = await page.evaluate(() => currentWorkspace.id);
|
||||
expect(currentId).toBe(nextId);
|
||||
});
|
||||
|
@ -6,32 +6,28 @@ import { test } from '../libs/playwright';
|
||||
import { clickSideBarCurrentWorkspaceBanner } from '../libs/sidebar';
|
||||
import { assertCurrentWorkspaceFlavour } from '../libs/workspace';
|
||||
|
||||
test.describe('Local first default workspace', () => {
|
||||
test('preset workspace name', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const workspaceName = page.getByTestId('workspace-name');
|
||||
await page.waitForTimeout(1000);
|
||||
expect(await workspaceName.textContent()).toBe('Demo Workspace');
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
test('preset workspace name', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const workspaceName = page.getByTestId('workspace-name');
|
||||
await page.waitForTimeout(1000);
|
||||
expect(await workspaceName.textContent()).toBe('Demo Workspace');
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
||||
// test('default workspace avatar', async ({ page }) => {
|
||||
// const workspaceAvatar = page.getByTestId('workspace-avatar');
|
||||
// expect(
|
||||
// await workspaceAvatar.locator('img').getAttribute('src')
|
||||
// ).not.toBeNull();
|
||||
// });
|
||||
});
|
||||
test.describe('Language switch', () => {
|
||||
test('Open language switch menu', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await clickSideBarCurrentWorkspaceBanner(page);
|
||||
const languageMenuButton = page.getByTestId('language-menu-button');
|
||||
await expect(languageMenuButton).toBeVisible();
|
||||
const actual = await languageMenuButton.innerText();
|
||||
expect(actual).toEqual('English');
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
// test('default workspace avatar', async ({ page }) => {
|
||||
// const workspaceAvatar = page.getByTestId('workspace-avatar');
|
||||
// expect(
|
||||
// await workspaceAvatar.locator('img').getAttribute('src')
|
||||
// ).not.toBeNull();
|
||||
// });
|
||||
test('Open language switch menu', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await clickSideBarCurrentWorkspaceBanner(page);
|
||||
const languageMenuButton = page.getByTestId('language-menu-button');
|
||||
await expect(languageMenuButton).toBeVisible();
|
||||
const actual = await languageMenuButton.innerText();
|
||||
expect(actual).toEqual('English');
|
||||
await assertCurrentWorkspaceFlavour('local', page);
|
||||
});
|
||||
|
@ -5,56 +5,52 @@ import { waitMarkdownImported } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
import { createWorkspace } from '../libs/workspace';
|
||||
|
||||
test.describe('Open AFFiNE', () => {
|
||||
test('Open last workspace when back to affine', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await createWorkspace({ name: 'New Workspace 2' }, page);
|
||||
// FIXME: can not get when the new workspace is surely created, hack a timeout to wait
|
||||
// waiting for page loading end
|
||||
await page.waitForTimeout(3000);
|
||||
// show workspace list
|
||||
await page.getByTestId('workspace-name').click();
|
||||
test('Open last workspace when back to affine', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await createWorkspace({ name: 'New Workspace 2' }, page);
|
||||
// FIXME: can not get when the new workspace is surely created, hack a timeout to wait
|
||||
// waiting for page loading end
|
||||
await page.waitForTimeout(3000);
|
||||
// show workspace list
|
||||
await page.getByTestId('workspace-name').click();
|
||||
|
||||
//check workspace list length
|
||||
const workspaceCards = await page.$$('data-testid=workspace-card');
|
||||
expect(workspaceCards.length).toBe(2);
|
||||
await workspaceCards[1].click();
|
||||
await openHomePage(page);
|
||||
//check workspace list length
|
||||
const workspaceCards = await page.$$('data-testid=workspace-card');
|
||||
expect(workspaceCards.length).toBe(2);
|
||||
await workspaceCards[1].click();
|
||||
await openHomePage(page);
|
||||
|
||||
const workspaceNameDom = await page.getByTestId('workspace-name');
|
||||
const currentWorkspaceName = await workspaceNameDom.evaluate(
|
||||
node => node.textContent
|
||||
);
|
||||
expect(currentWorkspaceName).toEqual('New Workspace 2');
|
||||
});
|
||||
const workspaceNameDom = await page.getByTestId('workspace-name');
|
||||
const currentWorkspaceName = await workspaceNameDom.evaluate(
|
||||
node => node.textContent
|
||||
);
|
||||
expect(currentWorkspaceName).toEqual('New Workspace 2');
|
||||
});
|
||||
|
||||
test.describe('AFFiNE change log', () => {
|
||||
test('Open affine in first time after updated', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
const changeLogItem = page.locator('[data-testid=change-log]');
|
||||
await expect(changeLogItem).toBeVisible();
|
||||
const closeButton = page.locator('[data-testid=change-log-close-button]');
|
||||
await closeButton.click();
|
||||
await expect(changeLogItem).not.toBeVisible();
|
||||
await page.goto('http://localhost:8080');
|
||||
const currentChangeLogItem = page.locator('[data-testid=change-log]');
|
||||
await expect(currentChangeLogItem).not.toBeVisible();
|
||||
});
|
||||
test('Click right-bottom corner change log icon', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.locator('[data-testid=help-island]').click();
|
||||
const editorRightBottomChangeLog = page.locator(
|
||||
'[data-testid=right-bottom-change-log-icon]'
|
||||
);
|
||||
await page.waitForTimeout(50);
|
||||
expect(await editorRightBottomChangeLog.isVisible()).toEqual(true);
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const normalRightBottomChangeLog = page.locator(
|
||||
'[data-testid=right-bottom-change-log-icon]'
|
||||
);
|
||||
expect(await normalRightBottomChangeLog.isVisible()).toEqual(true);
|
||||
});
|
||||
test('Open affine in first time after updated', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
const changeLogItem = page.locator('[data-testid=change-log]');
|
||||
await expect(changeLogItem).toBeVisible();
|
||||
const closeButton = page.locator('[data-testid=change-log-close-button]');
|
||||
await closeButton.click();
|
||||
await expect(changeLogItem).not.toBeVisible();
|
||||
await page.goto('http://localhost:8080');
|
||||
const currentChangeLogItem = page.locator('[data-testid=change-log]');
|
||||
await expect(currentChangeLogItem).not.toBeVisible();
|
||||
});
|
||||
test('Click right-bottom corner change log icon', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.locator('[data-testid=help-island]').click();
|
||||
const editorRightBottomChangeLog = page.locator(
|
||||
'[data-testid=right-bottom-change-log-icon]'
|
||||
);
|
||||
await page.waitForTimeout(50);
|
||||
expect(await editorRightBottomChangeLog.isVisible()).toEqual(true);
|
||||
await page.getByRole('link', { name: 'All pages' }).click();
|
||||
const normalRightBottomChangeLog = page.locator(
|
||||
'[data-testid=right-bottom-change-log-icon]'
|
||||
);
|
||||
expect(await normalRightBottomChangeLog.isVisible()).toEqual(true);
|
||||
});
|
||||
|
@ -24,232 +24,225 @@ async function checkIsChildInsertToParentInEditor(page: Page, pageId: string) {
|
||||
expect(referenceLink).not.toBeNull();
|
||||
}
|
||||
|
||||
test.describe('PinBoard interaction', () => {
|
||||
test('Have initial root pinboard page when first in', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
expect(rootPinboardMeta).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test('Root pinboard page have no operation of "trash" ,"rename" and "move to"', async ({
|
||||
page,
|
||||
}) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
|
||||
expect(
|
||||
await page
|
||||
.locator(`[data-testid="pinboard-operation-move-to-trash"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-operation-rename"]`).count()
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-operation-move-to"]`).count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
test('Click Pinboard in sidebar should open root pinboard page', async ({
|
||||
page,
|
||||
}) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await page.getByTestId(`pinboard-${rootPinboardMeta?.id}`).click();
|
||||
await page.waitForTimeout(200);
|
||||
expect(await page.locator('affine-editor-container')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('Add pinboard by header operation menu', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const meta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
expect(meta).not.toBeUndefined();
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('[data-testid="sidebar-pinboard-container"]')
|
||||
.getByTestId(`pinboard-${meta?.id}`)
|
||||
).not.toBeNull();
|
||||
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
|
||||
});
|
||||
|
||||
test('Add pinboard by sidebar operation menu', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
|
||||
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
|
||||
await page.getByTestId('pinboard-operation-add').click();
|
||||
const newPageMeta = (await getMetas(page)).find(
|
||||
m => m.id !== rootPinboardMeta?.id
|
||||
);
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.getByTestId(`pinboard-${newPageMeta?.id}`)
|
||||
).not.toBeNull();
|
||||
console.log('rootPinboardMeta', rootPinboardMeta);
|
||||
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
|
||||
});
|
||||
|
||||
test('Move pinboard to another in sidebar', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2');
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
await page.getByTestId('pinboard-operation-move-to').click();
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.getByTestId(`pinboard-${childMeta2?.id}`)
|
||||
.click();
|
||||
expect(
|
||||
(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);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', '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
|
||||
.getByTestId(`page-list-item-${childMeta?.id}`)
|
||||
.getByTestId('page-list-operation-button')
|
||||
.click();
|
||||
await page.getByTestId('move-to-menu-item').click();
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.locator(`[data-testid="pinboard-${childMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
});
|
||||
test('Move pinboard to another in page list', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', '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
|
||||
.getByTestId(`page-list-item-${childMeta?.id}`)
|
||||
.getByTestId('page-list-operation-button')
|
||||
.click();
|
||||
await page.getByTestId('move-to-menu-item').click();
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.getByTestId(`pinboard-${childMeta2?.id}`)
|
||||
.click();
|
||||
expect(
|
||||
(await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('Remove from pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('pinboard-operation-move-to').click();
|
||||
await page.getByTestId('remove-from-pinboard-button').click();
|
||||
await page.waitForTimeout(1000);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-${childMeta?.id}"]`).count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
test('search pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
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"]', 'test2');
|
||||
expect(
|
||||
await page.locator('[data-testid="pinboard-search-result"]').count()
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
test('Rename pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
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')
|
||||
.textContent()) as string;
|
||||
|
||||
expect(title).toEqual('test2');
|
||||
});
|
||||
|
||||
test('Move pinboard to trash', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
await createPinboardPage(page, childMeta?.id ?? '', 'test2');
|
||||
const grandChildMeta = (await getMetas(page)).find(
|
||||
m => m.title === 'test2'
|
||||
);
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('pinboard-operation-move-to-trash').click();
|
||||
(
|
||||
await page.waitForSelector('[data-testid="move-to-trash-confirm"]')
|
||||
).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.locator(`[data-testid="pinboard-${childMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.locator(`[data-testid="pinboard-${grandChildMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
// FIXME
|
||||
test.skip('Copy link', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('copy-link').click();
|
||||
|
||||
await page.evaluate(() => {
|
||||
const input = document.createElement('input');
|
||||
input.id = 'paste-input';
|
||||
document.body.appendChild(input);
|
||||
input.focus();
|
||||
});
|
||||
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());
|
||||
});
|
||||
test('Have initial root pinboard page when first in', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
expect(rootPinboardMeta).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test('Root pinboard page have no operation of "trash" ,"rename" and "move to"', async ({
|
||||
page,
|
||||
}) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
|
||||
expect(
|
||||
await page
|
||||
.locator(`[data-testid="pinboard-operation-move-to-trash"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-operation-rename"]`).count()
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-operation-move-to"]`).count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
test('Click Pinboard in sidebar should open root pinboard page', async ({
|
||||
page,
|
||||
}) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await page.getByTestId(`pinboard-${rootPinboardMeta?.id}`).click();
|
||||
await page.waitForTimeout(200);
|
||||
expect(await page.locator('affine-editor-container')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('Add pinboard by header operation menu', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const meta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
expect(meta).not.toBeUndefined();
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('[data-testid="sidebar-pinboard-container"]')
|
||||
.getByTestId(`pinboard-${meta?.id}`)
|
||||
).not.toBeNull();
|
||||
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
|
||||
});
|
||||
|
||||
test('Add pinboard by sidebar operation menu', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
|
||||
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
|
||||
await page.getByTestId('pinboard-operation-add').click();
|
||||
const newPageMeta = (await getMetas(page)).find(
|
||||
m => m.id !== rootPinboardMeta?.id
|
||||
);
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.getByTestId(`pinboard-${newPageMeta?.id}`)
|
||||
).not.toBeNull();
|
||||
await checkIsChildInsertToParentInEditor(page, rootPinboardMeta?.id ?? '');
|
||||
});
|
||||
|
||||
test('Move pinboard to another in sidebar', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2');
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
await page.getByTestId('pinboard-operation-move-to').click();
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.getByTestId(`pinboard-${childMeta2?.id}`)
|
||||
.click();
|
||||
expect(
|
||||
(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);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', '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
|
||||
.getByTestId(`page-list-item-${childMeta?.id}`)
|
||||
.getByTestId('page-list-operation-button')
|
||||
.click();
|
||||
await page.getByTestId('move-to-menu-item').click();
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.locator(`[data-testid="pinboard-${childMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
});
|
||||
test('Move pinboard to another in page list', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', '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
|
||||
.getByTestId(`page-list-item-${childMeta?.id}`)
|
||||
.getByTestId('page-list-operation-button')
|
||||
.click();
|
||||
await page.getByTestId('move-to-menu-item').click();
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.getByTestId(`pinboard-${childMeta2?.id}`)
|
||||
.click();
|
||||
expect(
|
||||
(await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('Remove from pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('pinboard-operation-move-to').click();
|
||||
await page.getByTestId('remove-from-pinboard-button').click();
|
||||
await page.waitForTimeout(1000);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-${childMeta?.id}"]`).count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
test('search pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
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"]', 'test2');
|
||||
expect(
|
||||
await page.locator('[data-testid="pinboard-search-result"]').count()
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
test('Rename pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
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')
|
||||
.textContent()) as string;
|
||||
|
||||
expect(title).toEqual('test2');
|
||||
});
|
||||
|
||||
test('Move pinboard to trash', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
await createPinboardPage(page, childMeta?.id ?? '', 'test2');
|
||||
const grandChildMeta = (await getMetas(page)).find(m => m.title === 'test2');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('pinboard-operation-move-to-trash').click();
|
||||
(await page.waitForSelector('[data-testid="move-to-trash-confirm"]')).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.locator(`[data-testid="pinboard-${childMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.locator(`[data-testid="pinboard-${grandChildMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
// FIXME
|
||||
test.skip('Copy link', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('copy-link').click();
|
||||
|
||||
await page.evaluate(() => {
|
||||
const input = document.createElement('input');
|
||||
input.id = 'paste-input';
|
||||
document.body.appendChild(input);
|
||||
input.focus();
|
||||
});
|
||||
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());
|
||||
});
|
||||
|
@ -20,10 +20,12 @@ async function assertTitle(page: Page, text: string) {
|
||||
expect(actual).toBe(text);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertResultList(page: Page, texts: string[]) {
|
||||
const actual = await page.locator('[cmdk-item]').allInnerTexts();
|
||||
expect(actual).toEqual(texts);
|
||||
}
|
||||
|
||||
async function titleIsFocused(page: Page) {
|
||||
const edgeless = page.locator('affine-edgeless-page');
|
||||
if (!edgeless) {
|
||||
@ -33,229 +35,221 @@ async function titleIsFocused(page: Page) {
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Open quick search', () => {
|
||||
test('Click slider bar button', 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('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('Click slider bar button', 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.describe('Add new page in quick search', () => {
|
||||
test('Create a new page without keyword', 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 page.waitForTimeout(300);
|
||||
await assertTitle(page, '');
|
||||
});
|
||||
|
||||
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('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.describe('Search and select', () => {
|
||||
test('Enter a keyword to search for', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
await page.keyboard.insertText('test123456');
|
||||
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('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('Focus event for quick search', () => {
|
||||
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('Create a new page without keyword', 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 page.waitForTimeout(300);
|
||||
await assertTitle(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 }) => {
|
||||
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');
|
||||
});
|
||||
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('Enter a keyword to search for', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await newPage(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
await page.keyboard.insertText('test123456');
|
||||
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);
|
||||
// input title and create new page
|
||||
await page.keyboard.insertText('test123456');
|
||||
await page.waitForTimeout(300);
|
||||
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(300);
|
||||
await assertResultList(page, ['test123456']);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(300);
|
||||
await assertTitle(page, 'test123456');
|
||||
});
|
||||
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('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');
|
||||
});
|
||||
|
@ -4,19 +4,17 @@ import { openHomePage } from '../libs/load-page';
|
||||
import { waitMarkdownImported } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('Shortcuts Modal', () => {
|
||||
test('Open shortcuts modal', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.locator('[data-testid=help-island]').click();
|
||||
test('Open shortcuts modal', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.locator('[data-testid=help-island]').click();
|
||||
|
||||
const shortcutsIcon = page.locator('[data-testid=shortcuts-icon]');
|
||||
await page.waitForTimeout(1000);
|
||||
expect(await shortcutsIcon.isVisible()).toEqual(true);
|
||||
const shortcutsIcon = page.locator('[data-testid=shortcuts-icon]');
|
||||
await page.waitForTimeout(1000);
|
||||
expect(await shortcutsIcon.isVisible()).toEqual(true);
|
||||
|
||||
await shortcutsIcon.click();
|
||||
await page.waitForTimeout(1000);
|
||||
const shortcutsModal = page.locator('[data-testid=shortcuts-modal]');
|
||||
await expect(shortcutsModal).toContainText('Keyboard Shortcuts');
|
||||
});
|
||||
await shortcutsIcon.click();
|
||||
await page.waitForTimeout(1000);
|
||||
const shortcutsModal = page.locator('[data-testid=shortcuts-modal]');
|
||||
await expect(shortcutsModal).toContainText('Keyboard Shortcuts');
|
||||
});
|
||||
|
@ -7,23 +7,19 @@ async function openStorybook(page: Page, storyName?: string) {
|
||||
return page.goto(`http://localhost:6006`);
|
||||
}
|
||||
|
||||
test.describe('storybook - Button', () => {
|
||||
test('Basic', async ({ page }) => {
|
||||
await openStorybook(page);
|
||||
await page.click('#storybook-explorer-tree >> #affine-button');
|
||||
await page.click('#affine-button--test');
|
||||
test('Basic', async ({ page }) => {
|
||||
await openStorybook(page);
|
||||
await page.click('#storybook-explorer-tree >> #affine-button');
|
||||
await page.click('#affine-button--test');
|
||||
|
||||
const iframe = page.frameLocator('iframe');
|
||||
await iframe
|
||||
.locator('input[data-testid="test-input"]')
|
||||
.type('Hello World!');
|
||||
const iframe = page.frameLocator('iframe');
|
||||
await iframe.locator('input[data-testid="test-input"]').type('Hello World!');
|
||||
|
||||
expect(
|
||||
await iframe.locator('input[data-testid="test-input"]').inputValue()
|
||||
).toBe('Hello World!');
|
||||
await iframe.locator('[data-testid="clear-button"]').click();
|
||||
expect(
|
||||
await iframe.locator('input[data-testid="test-input"]').textContent()
|
||||
).toBe('');
|
||||
});
|
||||
expect(
|
||||
await iframe.locator('input[data-testid="test-input"]').inputValue()
|
||||
).toBe('Hello World!');
|
||||
await iframe.locator('[data-testid="clear-button"]').click();
|
||||
expect(
|
||||
await iframe.locator('input[data-testid="test-input"]').textContent()
|
||||
).toBe('');
|
||||
});
|
||||
|
@ -4,12 +4,10 @@ import { openHomePage } from '../libs/load-page';
|
||||
import { waitMarkdownImported } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
|
||||
test.describe('subpage', () => {
|
||||
test('Create subpage', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
test('Create subpage', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
await page.getByTestId('sliderBar-arrowButton-collapse').click();
|
||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
|
@ -1,61 +1,55 @@
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { openHomePage } from '../libs/load-page';
|
||||
import { waitMarkdownImported } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
import { testResultDir } from '../libs/utils';
|
||||
|
||||
test.describe('Change Theme', () => {
|
||||
// default could be anything according to the system
|
||||
test('default white', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
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);
|
||||
// default could be anything according to the system
|
||||
test('default white', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
colorScheme: 'light',
|
||||
});
|
||||
|
||||
// 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');
|
||||
// });
|
||||
const page = await context.newPage();
|
||||
await openHomePage(page);
|
||||
await waitMarkdownImported(page);
|
||||
const root = page.locator('html');
|
||||
const themeMode = await root.evaluate(element =>
|
||||
element.getAttribute('data-theme')
|
||||
);
|
||||
expect(themeMode).toBe('light');
|
||||
const prev = await page.screenshot({
|
||||
path: resolve(testResultDir, 'affine-light-theme.png'),
|
||||
});
|
||||
await page.getByTestId('change-theme-dark').click();
|
||||
await page.waitForTimeout(50);
|
||||
const after = await page.screenshot({
|
||||
path: resolve(testResultDir, 'affine-dark-theme.png'),
|
||||
});
|
||||
expect(prev).not.toEqual(after);
|
||||
});
|
||||
|
||||
// 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
388
yarn.lock
@ -224,6 +224,7 @@ __metadata:
|
||||
eslint: ^8.38.0
|
||||
eslint-config-next: ^13.3.0
|
||||
jotai: ^2.0.4
|
||||
jotai-devtools: ^0.4.0
|
||||
lit: ^2.7.2
|
||||
lottie-web: ^5.11.0
|
||||
next: =13.2.3
|
||||
@ -1703,7 +1704,7 @@ __metadata:
|
||||
languageName: node
|
||||
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
|
||||
resolution: "@babel/runtime@npm:7.21.0"
|
||||
dependencies:
|
||||
@ -3216,6 +3217,48 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.1.3
|
||||
resolution: "@gar/promisify@npm:1.1.3"
|
||||
@ -3782,6 +3825,71 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.3.0
|
||||
resolution: "@mdx-js/react@npm:2.3.0"
|
||||
@ -4422,6 +4530,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/primitive@npm:1.0.0"
|
||||
@ -4495,6 +4612,17 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.0"
|
||||
@ -4590,6 +4718,19 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.2
|
||||
resolution: "@radix-ui/react-primitive@npm:1.0.2"
|
||||
@ -4603,6 +4744,27 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-slot@npm:1.0.0"
|
||||
@ -6165,6 +6327,21 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 8.20.0
|
||||
resolution: "@testing-library/dom@npm:8.20.0"
|
||||
@ -7831,7 +8008,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aria-hidden@npm:^1.1.1":
|
||||
"aria-hidden@npm:^1.1.1, aria-hidden@npm:^1.1.3":
|
||||
version: 1.2.3
|
||||
resolution: "aria-hidden@npm:1.2.3"
|
||||
dependencies:
|
||||
@ -7937,6 +8114,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.0.0
|
||||
resolution: "assert@npm:2.0.0"
|
||||
@ -8937,6 +9123,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.2.1
|
||||
resolution: "clsx@npm:1.2.1"
|
||||
@ -9296,6 +9489,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 3.3.3
|
||||
resolution: "copy-to-clipboard@npm:3.3.3"
|
||||
@ -9424,6 +9626,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 3.1.2
|
||||
resolution: "csstype@npm:3.1.2"
|
||||
@ -9455,6 +9664,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.29.3
|
||||
resolution: "date-fns@npm:2.29.3"
|
||||
@ -11680,6 +11896,16 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.0.1
|
||||
resolution: "get-stream@npm:6.0.1"
|
||||
@ -13004,6 +13230,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.2.0
|
||||
resolution: "is-windows@npm:0.2.0"
|
||||
@ -13880,6 +14113,27 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.0.4
|
||||
resolution: "jotai@npm:2.0.4"
|
||||
@ -16259,6 +16513,22 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.29.0
|
||||
resolution: "prismjs@npm:1.29.0"
|
||||
@ -16699,6 +16969,17 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.0.3
|
||||
resolution: "react-error-boundary@npm:4.0.3"
|
||||
@ -16807,6 +17088,35 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.2.1
|
||||
resolution: "react-style-singleton@npm:2.2.1"
|
||||
@ -16839,6 +17149,19 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.4.5
|
||||
resolution: "react-transition-group@npm:4.4.5"
|
||||
@ -18035,6 +18358,16 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.0.1
|
||||
resolution: "statuses@npm:2.0.1"
|
||||
@ -18355,6 +18688,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 5.5.0
|
||||
resolution: "supports-color@npm:5.5.0"
|
||||
@ -18434,6 +18776,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.8.1
|
||||
resolution: "table@npm:6.8.1"
|
||||
@ -19275,6 +19624,41 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 9.1.0
|
||||
resolution: "use-resize-observer@npm:9.1.0"
|
||||
|
Loading…
Reference in New Issue
Block a user