From 7bbe67af431e8bf6a65a697e41c6f7a83b71505d Mon Sep 17 00:00:00 2001 From: Himself65 Date: Sun, 16 Apr 2023 16:02:41 -0500 Subject: [PATCH] refactor: workspace loading logic (#1966) --- apps/web/next.config.mjs | 1 + apps/web/package.json | 1 + apps/web/src/atoms/index.ts | 78 ++- apps/web/src/atoms/root.ts | 78 +++ .../components/__tests__/PinBoard.spec.tsx | 23 +- .../__tests__/WorkSpaceSliderBar.spec.tsx | 17 +- .../pure/quick-search-modal/Footer.tsx | 34 +- apps/web/src/hooks/__tests__/index.spec.tsx | 124 +---- .../affine/use-toggle-workspace-publish.ts | 4 +- .../src/hooks/current/use-current-page-id.ts | 39 +- .../hooks/current/use-current-workspace.ts | 22 +- .../src/hooks/use-create-first-workspace.ts | 30 -- ...uter-and-workspace-with-page-id-defense.ts | 84 ++++ .../use-router-with-workspace-id-defense.ts | 48 ++ .../use-sync-router-with-current-page-id.ts | 18 + ...-router-with-current-workspace-and-page.ts | 215 --------- ...e-sync-router-with-current-workspace-id.ts | 51 ++ .../use-sync-router-with-current-workspace.ts | 53 --- apps/web/src/hooks/use-transform-workspace.ts | 16 +- apps/web/src/hooks/use-workspaces.ts | 11 +- .../{index.tsx => workspace-layout.tsx} | 197 ++++++-- apps/web/src/pages/_app.tsx | 24 +- apps/web/src/pages/_debug/init-page.dev.tsx | 2 +- apps/web/src/pages/index.tsx | 22 +- .../[workspaceId]/[pageId].tsx | 2 +- .../workspace/[workspaceId]/[pageId].tsx | 30 +- .../src/pages/workspace/[workspaceId]/all.tsx | 50 +- .../workspace/[workspaceId]/favorite.tsx | 6 +- .../pages/workspace/[workspaceId]/setting.tsx | 10 +- .../pages/workspace/[workspaceId]/shared.tsx | 6 +- .../pages/workspace/[workspaceId]/trash.tsx | 6 +- apps/web/src/plugins/affine/fetcher.ts | 4 +- apps/web/src/plugins/affine/index.tsx | 11 +- apps/web/src/plugins/index.tsx | 4 +- apps/web/src/plugins/local/index.tsx | 48 +- apps/web/src/providers/ModalProvider.tsx | 15 +- apps/web/src/shared/apis.ts | 4 +- apps/web/src/shared/index.ts | 3 +- apps/web/src/utils/index.ts | 1 - docs/contributing/behind-the-code.md | 27 +- packages/env/package.json | 3 +- .../utils => packages/env/src}/blocksuite.ts | 21 +- packages/env/src/constant.ts | 1 + packages/env/src/types.d.ts | 10 + ...use-blocksuite-workspace-page-is-public.ts | 5 +- .../src/use-blocksuite-workspace-page.ts | 30 ++ packages/workspace/src/affine/sync.ts | 4 +- packages/workspace/src/atom.ts | 45 +- packages/workspace/src/local/crud.ts | 25 +- packages/workspace/src/providers/index.ts | 34 +- packages/workspace/src/type.ts | 7 +- playwright.config.ts | 2 +- tests/libs/load-page.ts | 1 - tests/libs/page-logic.ts | 7 +- tests/libs/playwright.ts | 5 - .../affine/affine-built-in-workspace.spec.ts | 56 ++- .../affine/affine-public-single-page.spec.ts | 108 ++--- .../affine/affine-public-workspace.spec.ts | 114 +++-- .../parallels/affine/affine-workspace.spec.ts | 78 ++- tests/parallels/change-page-mode.spec.ts | 38 +- tests/parallels/contact-us.spec.ts | 26 +- tests/parallels/debug-init-page.spec.ts | 18 +- tests/parallels/debug-page-broadcast.spec.ts | 32 +- tests/parallels/exception-page.spec.ts | 10 +- tests/parallels/invite-code-page.spec.ts | 10 +- tests/parallels/layout.spec.ts | 138 +++--- tests/parallels/local-first-avatar.spec.ts | 64 ++- .../parallels/local-first-delete-page.spec.ts | 86 ++-- .../local-first-delete-workspace.spec.ts | 42 +- .../parallels/local-first-export-page.spec.ts | 108 ++--- .../local-first-favorite-page.spec.ts | 164 ++++--- .../local-first-favorites-items.spec.ts | 106 ++--- tests/parallels/local-first-new-page.spec.ts | 42 +- .../local-first-openpage-newtab.spec.ts | 42 +- .../local-first-restore-page.spec.ts | 90 ++-- .../local-first-setting-page.spec.ts | 44 +- .../local-first-show-delete-modal.spec.ts | 90 ++-- .../parallels/local-first-trash-page.spec.ts | 64 ++- .../local-first-workspace-list.spec.ts | 188 ++++---- tests/parallels/local-first-workspace.spec.ts | 50 +- tests/parallels/open-affine.spec.ts | 92 ++-- tests/parallels/pin-board.spec.ts | 449 +++++++++--------- tests/parallels/quick-search.spec.ts | 428 ++++++++--------- tests/parallels/shortcuts.spec.ts | 24 +- tests/parallels/storybook/button.spec.ts | 30 +- tests/parallels/subpage.spec.ts | 14 +- tests/parallels/theme.spec.ts | 100 ++-- yarn.lock | 388 ++++++++++++++- 88 files changed, 2684 insertions(+), 2268 deletions(-) create mode 100644 apps/web/src/atoms/root.ts delete mode 100644 apps/web/src/hooks/use-create-first-workspace.ts create mode 100644 apps/web/src/hooks/use-router-and-workspace-with-page-id-defense.ts create mode 100644 apps/web/src/hooks/use-router-with-workspace-id-defense.ts create mode 100644 apps/web/src/hooks/use-sync-router-with-current-page-id.ts delete mode 100644 apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts create mode 100644 apps/web/src/hooks/use-sync-router-with-current-workspace-id.ts delete mode 100644 apps/web/src/hooks/use-sync-router-with-current-workspace.ts rename apps/web/src/layouts/{index.tsx => workspace-layout.tsx} (67%) rename {apps/web/src/utils => packages/env/src}/blocksuite.ts (82%) create mode 100644 packages/env/src/types.d.ts create mode 100644 packages/hooks/src/use-blocksuite-workspace-page.ts diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 9778ac4a94..ad36deace3 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -78,6 +78,7 @@ const nextConfig = { }, reactStrictMode: true, transpilePackages: [ + 'jotai-devtools', '@affine/component', '@affine/i18n', '@affine/debug', diff --git a/apps/web/package.json b/apps/web/package.json index 9eb6ac57f9..a10aa4d761 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index 8c74c30b60..89b35f7187 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -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(null); -export const currentPageIdAtom = atom(null); -export const currentEditorAtom = atom | 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>(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' }; diff --git a/apps/web/src/atoms/root.ts b/apps/web/src/atoms/root.ts new file mode 100644 index 0000000000..d9be5caa44 --- /dev/null +++ b/apps/web/src/atoms/root.ts @@ -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>(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>( + 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 diff --git a/apps/web/src/components/__tests__/PinBoard.spec.tsx b/apps/web/src/components/__tests__/PinBoard.spec.tsx index dfd73a22e4..9fec085e9c 100644 --- a/apps/web/src/components/__tests__/PinBoard.spec.tsx +++ b/apps/web/src/components/__tests__/PinBoard.spec.tsx @@ -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, { diff --git a/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx b/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx index ace40f5d36..7d6e69af35 100644 --- a/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx +++ b/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx @@ -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 diff --git a/apps/web/src/components/pure/quick-search-modal/Footer.tsx b/apps/web/src/components/pure/quick-search-modal/Footer.tsx index 7835f75348..9b8bac8677 100644 --- a/apps/web/src/components/pure/quick-search-modal/Footer.tsx +++ b/apps/web/src/components/pure/quick-search-modal/Footer.tsx @@ -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 = ({ return ( { - 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])} > diff --git a/apps/web/src/hooks/__tests__/index.spec.tsx b/apps/web/src/hooks/__tests__/index.spec.tsx index 4cb0c1d917..4a138feeb5 100644 --- a/apps/web/src/hooks/__tests__/index.spec.tsx +++ b/apps/web/src/hooks/__tests__/index.spec.tsx @@ -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, diff --git a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts index 87938305d7..7334653ed2 100644 --- a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts +++ b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts @@ -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] ); diff --git a/apps/web/src/hooks/current/use-current-page-id.ts b/apps/web/src/hooks/current/use-current-page-id.ts index 8c98c2de79..076433d556 100644 --- a/apps/web/src/hooks/current/use-current-page-id.ts +++ b/apps/web/src/hooks/current/use-current-page-id.ts @@ -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>(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); } diff --git a/apps/web/src/hooks/current/use-current-workspace.ts b/apps/web/src/hooks/current/use-current-workspace.ts index 4992bb8e06..632a47ff0c 100644 --- a/apps/web/src/hooks/current/use-current-workspace.ts +++ b/apps/web/src/hooks/current/use-current-workspace.ts @@ -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>( - 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( '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); diff --git a/apps/web/src/hooks/use-create-first-workspace.ts b/apps/web/src/hooks/use-create-first-workspace.ts deleted file mode 100644 index 052bd493f2..0000000000 --- a/apps/web/src/hooks/use-create-first-workspace.ts +++ /dev/null @@ -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']?.(); - } - } - }); - }, []); -} diff --git a/apps/web/src/hooks/use-router-and-workspace-with-page-id-defense.ts b/apps/web/src/hooks/use-router-and-workspace-with-page-id-defense.ts new file mode 100644 index 0000000000..69c43a9491 --- /dev/null +++ b/apps/web/src/hooks/use-router-and-workspace-with-page-id-defense.ts @@ -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, + ]); +} diff --git a/apps/web/src/hooks/use-router-with-workspace-id-defense.ts b/apps/web/src/hooks/use-router-with-workspace-id-defense.ts new file mode 100644 index 0000000000..d66ea3b324 --- /dev/null +++ b/apps/web/src/hooks/use-router-with-workspace-id-defense.ts @@ -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, + ]); +} diff --git a/apps/web/src/hooks/use-sync-router-with-current-page-id.ts b/apps/web/src/hooks/use-sync-router-with-current-page-id.ts new file mode 100644 index 0000000000..b4373dfb43 --- /dev/null +++ b/apps/web/src/hooks/use-sync-router-with-current-page-id.ts @@ -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]); +} diff --git a/apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts b/apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts deleted file mode 100644 index b8bab63151..0000000000 --- a/apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts +++ /dev/null @@ -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[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, - ]); -} diff --git a/apps/web/src/hooks/use-sync-router-with-current-workspace-id.ts b/apps/web/src/hooks/use-sync-router-with-current-workspace-id.ts new file mode 100644 index 0000000000..75c17a8295 --- /dev/null +++ b/apps/web/src/hooks/use-sync-router-with-current-workspace-id.ts @@ -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]); +} diff --git a/apps/web/src/hooks/use-sync-router-with-current-workspace.ts b/apps/web/src/hooks/use-sync-router-with-current-workspace.ts deleted file mode 100644 index c1751aad96..0000000000 --- a/apps/web/src/hooks/use-sync-router-with-current-workspace.ts +++ /dev/null @@ -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[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]); -} diff --git a/apps/web/src/hooks/use-transform-workspace.ts b/apps/web/src/hooks/use-transform-workspace.ts index 294477f2f1..056e938235 100644 --- a/apps/web/src/hooks/use-transform-workspace.ts +++ b/apps/web/src/hooks/use-transform-workspace.ts @@ -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: From, @@ -35,9 +35,9 @@ export function useTransformWorkspace() { }); return [...workspaces]; }); - await helper.jumpToWorkspace(newId); + setCurrentWorkspaceId(newId); return newId; }, - [helper, set] + [set, setCurrentWorkspaceId] ); } diff --git a/apps/web/src/hooks/use-workspaces.ts b/apps/web/src/hooks/use-workspaces.ts index 27a6eb3df4..d31ea35ff6 100644 --- a/apps/web/src/hooks/use-workspaces.ts +++ b/apps/web/src/hooks/use-workspaces.ts @@ -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) => { diff --git a/apps/web/src/layouts/index.tsx b/apps/web/src/layouts/workspace-layout.tsx similarity index 67% rename from apps/web/src/layouts/index.tsx rename to apps/web/src/layouts/workspace-layout.tsx index c6e13b2573..2c5790e4ac 100644 --- a/apps/web/src/layouts/index.tsx +++ b/apps/web/src/layouts/workspace-layout.tsx @@ -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 ; + } + if (!workspaceId) { + return ; + } + if (!exist) { + return ; + } + return <>{children}; +}; + export const WorkspaceLayout: FC = function WorkspacesSuspense({ children }) { const { i18n } = useTranslation(); @@ -122,10 +173,9 @@ export const WorkspaceLayout: FC = // 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 = .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 = return ( <> {/* fixme(himself65): don't re-render whole modals */} - - }> - {children} - + + + + + + + }> + {children} + + ); }; export const WorkspaceLayoutInner: FC = ({ 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 = ({ 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 = ({ children }) => { - {children} + }> + {isLoading ? : children} + {/* fixme(himself65): remove this */}
diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index d613067a49..1becf0cfc6 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -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' && } + {children} + + ); +}; + const App = function App({ Component, pageProps, @@ -55,10 +65,12 @@ const App = function App({ }> [ - , - , - ], + () => + [ + , + , + , + ].filter(Boolean), [] )} > diff --git a/apps/web/src/pages/_debug/init-page.dev.tsx b/apps/web/src/pages/_debug/init-page.dev.tsx index 29e3c0de80..5f5e7578bb 100644 --- a/apps/web/src/pages/_debug/init-page.dev.tsx +++ b/apps/web/src/pages/_debug/init-page.dev.tsx @@ -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 => ({ diff --git a/apps/web/src/pages/index.tsx b/apps/web/src/pages/index.tsx index 23eb280205..01977d62d7 100644 --- a/apps/web/src/pages/index.tsx +++ b/apps/web/src/pages/index.tsx @@ -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 ; }; const IndexPage: NextPage = () => { - useCreateFirstWorkspace(); return ( - }> + }> ); diff --git a/apps/web/src/pages/public-workspace/[workspaceId]/[pageId].tsx b/apps/web/src/pages/public-workspace/[workspaceId]/[pageId].tsx index d9f4002573..7f44dfbc78 100644 --- a/apps/web/src/pages/public-workspace/[workspaceId]/[pageId].tsx +++ b/apps/web/src/pages/public-workspace/[workspaceId]/[pageId].tsx @@ -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 { diff --git a/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx b/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx index b865942584..4149a2983f 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx @@ -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 ; } if (!currentPageId) { - return ; + return ; } 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 ; - } else if ( - typeof router.query.pageId !== 'string' || - typeof router.query.workspaceId !== 'string' - ) { - throw new Error('Invalid router query'); + return ; + } else if (!currentPageId || !page) { + return ; } return ; }; diff --git a/apps/web/src/pages/workspace/[workspaceId]/all.tsx b/apps/web/src/pages/workspace/[workspaceId]/all.tsx index 65751e15e7..a1ac34d8a4 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/all.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/all.tsx @@ -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); diff --git a/apps/web/src/pages/workspace/[workspaceId]/favorite.tsx b/apps/web/src/pages/workspace/[workspaceId]/favorite.tsx index 275c6fc211..6d58b73a92 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/favorite.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/favorite.tsx @@ -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); diff --git a/apps/web/src/pages/workspace/[workspaceId]/setting.tsx b/apps/web/src/pages/workspace/[workspaceId]/setting.tsx index 245e72a9ea..62b9a7a070 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/setting.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/setting.tsx @@ -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); diff --git a/apps/web/src/pages/workspace/[workspaceId]/shared.tsx b/apps/web/src/pages/workspace/[workspaceId]/shared.tsx index 3238953b7b..473942fd90 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/shared.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/shared.tsx @@ -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); diff --git a/apps/web/src/pages/workspace/[workspaceId]/trash.tsx b/apps/web/src/pages/workspace/[workspaceId]/trash.tsx index 169d2e0edd..be395a3acd 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/trash.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/trash.tsx @@ -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); diff --git a/apps/web/src/plugins/affine/fetcher.ts b/apps/web/src/plugins/affine/fetcher.ts index 302f609ec5..7c0fc5ae04 100644 --- a/apps/web/src/plugins/affine/fetcher.ts +++ b/apps/web/src/plugins/affine/fetcher.ts @@ -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; diff --git a/apps/web/src/plugins/affine/index.tsx b/apps/web/src/plugins/affine/index.tsx index f4985e5392..be2b23bb03 100644 --- a/apps/web/src/plugins/affine/index.tsx +++ b/apps/web/src/plugins/affine/index.tsx @@ -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 = { 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: { diff --git a/apps/web/src/plugins/index.tsx b/apps/web/src/plugins/index.tsx index e6d12cdf24..94a1e0d01c 100644 --- a/apps/web/src/plugins/index.tsx +++ b/apps/web/src/plugins/index.tsx @@ -27,9 +27,7 @@ export const WorkspacePlugins = { [WorkspaceFlavour.PUBLIC]: { flavour: WorkspaceFlavour.PUBLIC, loadPriority: LoadPriority.LOW, - Events: { - 'app:first-init': async () => {}, - }, + Events: {} as Partial, // todo: implement this CRUD: { get: unimplemented, diff --git a/apps/web/src/plugins/local/index.tsx b/apps/web/src/plugins/local/index.tsx index 3347e5718b..c9e9577e4b 100644 --- a/apps/web/src/plugins/local/index.tsx +++ b/apps/web/src/plugins/local/index.tsx @@ -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 = { 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, diff --git a/apps/web/src/providers/ModalProvider.tsx b/apps/web/src/providers/ModalProvider.tsx index 95c9bb82fa..47aa9ad646 100644 --- a/apps/web/src/providers/ModalProvider.tsx +++ b/apps/web/src/providers/ModalProvider.tsx @@ -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 = ({ - children, -}) => { +export const ModalProvider = (): ReactElement => { return ( <> - {children} ); }; diff --git a/apps/web/src/shared/apis.ts b/apps/web/src/shared/apis.ts index 90fafcba2b..434ab40378 100644 --- a/apps/web/src/shared/apis.ts +++ b/apps/web/src/shared/apis.ts @@ -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 & ReturnType; @@ -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 () => { diff --git a/apps/web/src/shared/index.ts b/apps/web/src/shared/index.ts index 830208508e..c34167b9a4 100644 --- a/apps/web/src/shared/index.ts +++ b/apps/web/src/shared/index.ts @@ -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

, IP = P> = NextPage< P, diff --git a/apps/web/src/utils/index.ts b/apps/web/src/utils/index.ts index cb84088028..196e778438 100644 --- a/apps/web/src/utils/index.ts +++ b/apps/web/src/utils/index.ts @@ -1,4 +1,3 @@ -export * from './blocksuite'; export * from './create-emotion-cache'; export * from './string2color'; export * from './toast'; diff --git a/docs/contributing/behind-the-code.md b/docs/contributing/behind-the-code.md index a73f8c523c..c3346070bc 100644 --- a/docs/contributing/behind-the-code.md +++ b/docs/contributing/behind-the-code.md @@ -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 +``` diff --git a/packages/env/package.json b/packages/env/package.json index 8147fce250..20c38c9b2e 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -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" diff --git a/apps/web/src/utils/blocksuite.ts b/packages/env/src/blocksuite.ts similarity index 82% rename from apps/web/src/utils/blocksuite.ts rename to packages/env/src/blocksuite.ts index d2ae4f2079..e86e1582ed 100644 --- a/apps/web/src/utils/blocksuite.ts +++ b/packages/env/src/blocksuite.ts @@ -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); diff --git a/packages/env/src/constant.ts b/packages/env/src/constant.ts index 3551c50a40..e5cfd471b0 100644 --- a/packages/env/src/constant.ts +++ b/packages/env/src/constant.ts @@ -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, diff --git a/packages/env/src/types.d.ts b/packages/env/src/types.d.ts new file mode 100644 index 0000000000..a459f45e71 --- /dev/null +++ b/packages/env/src/types.d.ts @@ -0,0 +1,10 @@ +/// + +// not using import because it will break the declare module line below +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +declare module '*.md' { + const text: string; + export default text; +} diff --git a/packages/hooks/src/use-blocksuite-workspace-page-is-public.ts b/packages/hooks/src/use-blocksuite-workspace-page-is-public.ts index 7200e0e9e9..e8c941b5e9 100644 --- a/packages/hooks/src/use-blocksuite-workspace-page-is-public.ts +++ b/packages/hooks/src/use-blocksuite-workspace-page-is-public.ts @@ -10,9 +10,12 @@ declare module '@blocksuite/store' { export function useBlockSuiteWorkspacePageIsPublic(page: Page) { const [isPublic, set] = useState(() => 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) => { diff --git a/packages/hooks/src/use-blocksuite-workspace-page.ts b/packages/hooks/src/use-blocksuite-workspace-page.ts new file mode 100644 index 0000000000..6f55b4a022 --- /dev/null +++ b/packages/hooks/src/use-blocksuite-workspace-page.ts @@ -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; +} diff --git a/packages/workspace/src/affine/sync.ts b/packages/workspace/src/affine/sync.ts index 3fd7bb761c..60501febeb 100644 --- a/packages/workspace/src/affine/sync.ts +++ b/packages/workspace/src/affine/sync.ts @@ -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]; diff --git a/packages/workspace/src/atom.ts b/packages/workspace/src/atom.ts index 91210d34f8..9e49a3a465 100644 --- a/packages/workspace/src/atom.ts +++ b/packages/workspace/src/atom.ts @@ -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( +/** + * 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( + 'root-current-workspace-id', + null, + createJSONStorage(() => sessionStorage) +); +export const rootCurrentPageIdAtom = atomWithStorage( + '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 | null>( + null +); +//#endregion + +// global store +export const rootStore = createStore(); diff --git a/packages/workspace/src/local/crud.ts b/packages/workspace/src/local/crud.ts index e07e5c1bb5..33cccc4d26 100644 --- a/packages/workspace/src/local/crud.ts +++ b/packages/workspace/src/local/crud.ts @@ -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; + const id = data.find(id => id === workspaceId); + if (!id) { + logger.debug('saveWorkspaceToLocalStorage', workspaceId); + storage.setItem(kStoreKey, [...data, workspaceId]); + } +} + export const CRUD: WorkspaceCRUD = { 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 = { 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; const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdateV2(doc); const id = nanoid(); const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( @@ -52,11 +70,11 @@ export const CRUD: WorkspaceCRUD = { 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 = { storage.setItem(kStoreKey, [...data]); }, list: async () => { + logger.debug('list'); const storage = getStorage(); !Array.isArray(storage.getItem(kStoreKey)) && storage.setItem(kStoreKey, []); diff --git a/packages/workspace/src/providers/index.ts b/packages/workspace/src/providers/index.ts index 941109a4c9..4d57dea9ef 100644 --- a/packages/workspace/src/providers/index.ts +++ b/packages/workspace/src/providers/index.ts @@ -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; }, }; }; diff --git a/packages/workspace/src/type.ts b/packages/workspace/src/type.ts index 4efb1b88c5..de8e3fe276 100644 --- a/packages/workspace/src/type.ts +++ b/packages/workspace/src/type.ts @@ -2,8 +2,11 @@ /// 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; + export type BaseProvider = { flavour: string; // if this is true, we will connect the provider on the background @@ -141,9 +144,9 @@ export interface WorkspaceUISchema { } export interface AppEvents { - // event when app is first initialized + // event there is no workspace // usually used to initialize workspace plugin - 'app:first-init': () => Promise; + 'app:init': () => string[]; // request to gain access to workspace plugin 'workspace:access': () => Promise; // request to revoke access to workspace plugin diff --git a/playwright.config.ts b/playwright.config.ts index 2f1540754d..44460de742 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -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 diff --git a/tests/libs/load-page.ts b/tests/libs/load-page.ts index cd066c33c4..eae07759d5 100644 --- a/tests/libs/load-page.ts +++ b/tests/libs/load-page.ts @@ -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) { diff --git a/tests/libs/page-logic.ts b/tests/libs/page-logic.ts index 877e65169f..c215af28b0 100644 --- a/tests/libs/page-logic.ts +++ b/tests/libs/page-logic.ts @@ -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) { diff --git a/tests/libs/playwright.ts b/tests/libs/playwright.ts index 211e16df80..02d2798e1d 100644 --- a/tests/libs/playwright.ts +++ b/tests/libs/playwright.ts @@ -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) { diff --git a/tests/parallels/affine/affine-built-in-workspace.spec.ts b/tests/parallels/affine/affine-built-in-workspace.spec.ts index 3aae021fd5..4c8e493adc 100644 --- a/tests/parallels/affine/affine-built-in-workspace.spec.ts +++ b/tests/parallels/affine/affine-built-in-workspace.spec.ts @@ -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'); }); diff --git a/tests/parallels/affine/affine-public-single-page.spec.ts b/tests/parallels/affine/affine-public-single-page.spec.ts index e605a7fde4..54c705f511 100644 --- a/tests/parallels/affine/affine-public-single-page.spec.ts +++ b/tests/parallels/affine/affine-public-single-page.spec.ts @@ -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); }); diff --git a/tests/parallels/affine/affine-public-workspace.spec.ts b/tests/parallels/affine/affine-public-workspace.spec.ts index 3981f2b592..effe84d5e5 100644 --- a/tests/parallels/affine/affine-public-workspace.spec.ts +++ b/tests/parallels/affine/affine-public-workspace.spec.ts @@ -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'); }); diff --git a/tests/parallels/affine/affine-workspace.spec.ts b/tests/parallels/affine/affine-workspace.spec.ts index 67e9258ec2..9b244d8225 100644 --- a/tests/parallels/affine/affine-workspace.spec.ts +++ b/tests/parallels/affine/affine-workspace.spec.ts @@ -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); }); diff --git a/tests/parallels/change-page-mode.spec.ts b/tests/parallels/change-page-mode.spec.ts index 4d76bcbda0..2b8830ad3b 100644 --- a/tests/parallels/change-page-mode.spec.ts +++ b/tests/parallels/change-page-mode.spec.ts @@ -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); }); diff --git a/tests/parallels/contact-us.spec.ts b/tests/parallels/contact-us.spec.ts index 4c29676bfe..7b98e44ab5 100644 --- a/tests/parallels/contact-us.spec.ts +++ b/tests/parallels/contact-us.spec.ts @@ -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'); }); diff --git a/tests/parallels/debug-init-page.spec.ts b/tests/parallels/debug-init-page.spec.ts index 6bb4718913..d2cf632ae1 100644 --- a/tests/parallels/debug-init-page.spec.ts +++ b/tests/parallels/debug-init-page.spec.ts @@ -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'); }); diff --git a/tests/parallels/debug-page-broadcast.spec.ts b/tests/parallels/debug-page-broadcast.spec.ts index bcf3232e41..da1c2da780 100644 --- a/tests/parallels/debug-page-broadcast.spec.ts +++ b/tests/parallels/debug-page-broadcast.spec.ts @@ -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); }); diff --git a/tests/parallels/exception-page.spec.ts b/tests/parallels/exception-page.spec.ts index e7f51b515f..81088c5231 100644 --- a/tests/parallels/exception-page.spec.ts +++ b/tests/parallels/exception-page.spec.ts @@ -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(); }); diff --git a/tests/parallels/invite-code-page.spec.ts b/tests/parallels/invite-code-page.spec.ts index 18499165af..d627f4df7b 100644 --- a/tests/parallels/invite-code-page.spec.ts +++ b/tests/parallels/invite-code-page.spec.ts @@ -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(); }); diff --git a/tests/parallels/layout.spec.ts b/tests/parallels/layout.spec.ts index 23059115fc..f65144a427 100644 --- a/tests/parallels/layout.spec.ts +++ b/tests/parallels/layout.spec.ts @@ -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(); }); diff --git a/tests/parallels/local-first-avatar.spec.ts b/tests/parallels/local-first-avatar.spec.ts index b70b118ca6..607526845c 100644 --- a/tests/parallels/local-first-avatar.spec.ts +++ b/tests/parallels/local-first-avatar.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-delete-page.spec.ts b/tests/parallels/local-first-delete-page.spec.ts index 26a7d895ab..32f30eae29 100644 --- a/tests/parallels/local-first-delete-page.spec.ts +++ b/tests/parallels/local-first-delete-page.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-delete-workspace.spec.ts b/tests/parallels/local-first-delete-workspace.spec.ts index 7456199394..0b9a2525a3 100644 --- a/tests/parallels/local-first-delete-workspace.spec.ts +++ b/tests/parallels/local-first-delete-workspace.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-export-page.spec.ts b/tests/parallels/local-first-export-page.spec.ts index 5b5e423ed5..c4e1276860 100644 --- a/tests/parallels/local-first-export-page.spec.ts +++ b/tests/parallels/local-first-export-page.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-favorite-page.spec.ts b/tests/parallels/local-first-favorite-page.spec.ts index e03be1fb3e..4f20c6250a 100644 --- a/tests/parallels/local-first-favorite-page.spec.ts +++ b/tests/parallels/local-first-favorite-page.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-favorites-items.spec.ts b/tests/parallels/local-first-favorites-items.spec.ts index b228a5bd4d..9721eb1466 100644 --- a/tests/parallels/local-first-favorites-items.spec.ts +++ b/tests/parallels/local-first-favorites-items.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-new-page.spec.ts b/tests/parallels/local-first-new-page.spec.ts index 8da6827764..8533966919 100644 --- a/tests/parallels/local-first-new-page.spec.ts +++ b/tests/parallels/local-first-new-page.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-openpage-newtab.spec.ts b/tests/parallels/local-first-openpage-newtab.spec.ts index b043271cf7..03a4c88b1d 100644 --- a/tests/parallels/local-first-openpage-newtab.spec.ts +++ b/tests/parallels/local-first-openpage-newtab.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-restore-page.spec.ts b/tests/parallels/local-first-restore-page.spec.ts index 874bd87f18..744a83a0a5 100644 --- a/tests/parallels/local-first-restore-page.spec.ts +++ b/tests/parallels/local-first-restore-page.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-setting-page.spec.ts b/tests/parallels/local-first-setting-page.spec.ts index 4babbb8800..d7b6d275f4 100644 --- a/tests/parallels/local-first-setting-page.spec.ts +++ b/tests/parallels/local-first-setting-page.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-show-delete-modal.spec.ts b/tests/parallels/local-first-show-delete-modal.spec.ts index d69a0f780b..f924ac84cc 100644 --- a/tests/parallels/local-first-show-delete-modal.spec.ts +++ b/tests/parallels/local-first-show-delete-modal.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-trash-page.spec.ts b/tests/parallels/local-first-trash-page.spec.ts index 0c2766a0cb..a75c69c285 100644 --- a/tests/parallels/local-first-trash-page.spec.ts +++ b/tests/parallels/local-first-trash-page.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-workspace-list.spec.ts b/tests/parallels/local-first-workspace-list.spec.ts index 7550e82a11..34c5b36d86 100644 --- a/tests/parallels/local-first-workspace-list.spec.ts +++ b/tests/parallels/local-first-workspace-list.spec.ts @@ -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); }); diff --git a/tests/parallels/local-first-workspace.spec.ts b/tests/parallels/local-first-workspace.spec.ts index 8fd96eca66..de08f12596 100644 --- a/tests/parallels/local-first-workspace.spec.ts +++ b/tests/parallels/local-first-workspace.spec.ts @@ -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); }); diff --git a/tests/parallels/open-affine.spec.ts b/tests/parallels/open-affine.spec.ts index 020dd48d03..36fac0587e 100644 --- a/tests/parallels/open-affine.spec.ts +++ b/tests/parallels/open-affine.spec.ts @@ -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); }); diff --git a/tests/parallels/pin-board.spec.ts b/tests/parallels/pin-board.spec.ts index 0dc2c5f12a..dded1d3ffb 100644 --- a/tests/parallels/pin-board.spec.ts +++ b/tests/parallels/pin-board.spec.ts @@ -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()); }); diff --git a/tests/parallels/quick-search.spec.ts b/tests/parallels/quick-search.spec.ts index c3881d093e..31f5108de8 100644 --- a/tests/parallels/quick-search.spec.ts +++ b/tests/parallels/quick-search.spec.ts @@ -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'); }); diff --git a/tests/parallels/shortcuts.spec.ts b/tests/parallels/shortcuts.spec.ts index 420dd50ff8..5f3fbb3588 100644 --- a/tests/parallels/shortcuts.spec.ts +++ b/tests/parallels/shortcuts.spec.ts @@ -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'); }); diff --git a/tests/parallels/storybook/button.spec.ts b/tests/parallels/storybook/button.spec.ts index 54eb978a5a..b023275ef0 100644 --- a/tests/parallels/storybook/button.spec.ts +++ b/tests/parallels/storybook/button.spec.ts @@ -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(''); }); diff --git a/tests/parallels/subpage.spec.ts b/tests/parallels/subpage.spec.ts index d2891cdc22..e5a8d08e1b 100644 --- a/tests/parallels/subpage.spec.ts +++ b/tests/parallels/subpage.spec.ts @@ -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(); }); diff --git a/tests/parallels/theme.spec.ts b/tests/parallels/theme.spec.ts index 1a821826c2..ae29cc8b3e 100644 --- a/tests/parallels/theme.spec.ts +++ b/tests/parallels/theme.spec.ts @@ -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'); +// }); diff --git a/yarn.lock b/yarn.lock index f8d2899b71..fb920e02d1 100644 --- a/yarn.lock +++ b/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"