From 9a199eb9a1e6a361d319a6405848a02937903d41 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Sat, 4 Mar 2023 20:11:15 -0600 Subject: [PATCH] refactor: support suspense mode in workspaces (#1304) --- apps/web/src/atoms/index.ts | 37 ++- apps/web/src/atoms/public-workspace/index.ts | 23 +- apps/web/src/blocksuite/providers/index.ts | 1 + .../__tests__/WorkSpaceSliderBar.spec.tsx | 43 +++- .../pure/quick-search-modal/index.tsx | 7 +- .../pure/workspace-slider-bar/index.tsx | 1 + apps/web/src/hooks/__tests__/index.spec.tsx | 128 +++++++--- .../use-blocksuite-workspace-helper.spec.ts | 2 - .../affine/use-toggle-workspace-publish.ts | 7 +- .../src/hooks/current/use-current-page-id.ts | 35 ++- .../hooks/current/use-current-workspace.ts | 22 +- .../src/hooks/use-create-first-workspace.ts | 53 +++++ .../src/hooks/use-last-opened-workspace.ts | 41 ---- ...-router-with-current-workspace-and-page.ts | 70 ++++-- apps/web/src/hooks/use-workspaces.ts | 218 +++++------------- apps/web/src/layouts/index.tsx | 104 +++++++-- apps/web/src/pages/_app.tsx | 2 - apps/web/src/pages/index.tsx | 83 ++++--- .../workspace/[workspaceId]/[pageId].tsx | 19 +- apps/web/src/plugins/affine/index.tsx | 137 ++++------- apps/web/src/plugins/index.tsx | 45 ++-- apps/web/src/plugins/local/index.tsx | 186 ++++++--------- apps/web/src/providers/ModalProvider.tsx | 37 +-- apps/web/src/utils/index.ts | 10 +- package.json | 2 + pnpm-lock.yaml | 50 +++- tests/local-first-avatar.spec.ts | 2 +- 27 files changed, 713 insertions(+), 652 deletions(-) create mode 100644 apps/web/src/hooks/use-create-first-workspace.ts delete mode 100644 apps/web/src/hooks/use-last-opened-workspace.ts diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index e9d5126345..a24bab2672 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -1,7 +1,11 @@ -import { atom } from 'jotai'; -import { createStore } from 'jotai'; +import { assertExists } from '@blocksuite/store'; +import { atom, createStore } from 'jotai'; +import { atomWithStorage } from 'jotai/utils'; import { unstable_batchedUpdates } from 'react-dom'; +import { WorkspacePlugins } from '../plugins'; +import { RemWorkspace, RemWorkspaceFlavour } from '../shared'; + // workspace necessary atoms export const currentWorkspaceIdAtom = atom(null); export const currentPageIdAtom = atom(null); @@ -27,3 +31,32 @@ export const openCreateWorkspaceModalAtom = atom(false); export const openQuickSearchModalAtom = atom(false); export const jotaiStore = createStore(); + +type JotaiWorkspace = { + id: string; + flavour: RemWorkspaceFlavour; +}; + +export const jotaiWorkspacesAtom = atomWithStorage( + 'jotai-workspaces', + [] +); + +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 RemWorkspace[]; +}); diff --git a/apps/web/src/atoms/public-workspace/index.ts b/apps/web/src/atoms/public-workspace/index.ts index 21c28a1098..adffc93d29 100644 --- a/apps/web/src/atoms/public-workspace/index.ts +++ b/apps/web/src/atoms/public-workspace/index.ts @@ -1,10 +1,6 @@ import { atom } from 'jotai/index'; -import { - BlockSuiteWorkspace, - LocalWorkspace, - RemWorkspaceFlavour, -} from '../../shared'; +import { BlockSuiteWorkspace } from '../../shared'; import { apis } from '../../shared/apis'; import { createEmptyBlockSuiteWorkspace } from '../../utils'; @@ -35,14 +31,15 @@ export const publicBlockSuiteAtom = atom>( blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false); return new Promise(resolve => { setTimeout(() => { - const workspace: LocalWorkspace = { - id: workspaceId, - blockSuiteWorkspace, - flavour: RemWorkspaceFlavour.LOCAL, - providers: [], - }; - dataCenter.workspaces.push(workspace); - dataCenter.callbacks.forEach(cb => cb()); + // const workspace: LocalWorkspace = { + // id: workspaceId, + // blockSuiteWorkspace, + // flavour: RemWorkspaceFlavour.LOCAL, + // providers: [], + // }; + // fixme: quick search won't work, ASAP + // dataCenter.workspaces.push(workspace); + // dataCenter.callbacks.forEach(cb => cb()); resolve(blockSuiteWorkspace); }, 0); }); diff --git a/apps/web/src/blocksuite/providers/index.ts b/apps/web/src/blocksuite/providers/index.ts index c22b223f22..7190d035c7 100644 --- a/apps/web/src/blocksuite/providers/index.ts +++ b/apps/web/src/blocksuite/providers/index.ts @@ -63,6 +63,7 @@ const createIndexedDBProvider = ( cleanup: () => { assertExists(indexeddbProvider); indexeddbProvider.clearData(); + indexeddbProvider = null; }, connect: () => { providerLogger.info( diff --git a/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx b/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx index a6d3127a21..77b7d08427 100644 --- a/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx +++ b/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx @@ -5,22 +5,36 @@ import 'fake-indexeddb/auto'; import { assertExists } from '@blocksuite/store'; import { render, renderHook } from '@testing-library/react'; +import { createStore, getDefaultStore, Provider } from 'jotai'; import { useRouter } from 'next/router'; -import { useCallback, useState } from 'react'; -import { describe, expect, test, vi } from 'vitest'; +import React, { useCallback, useState } from 'react'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import createFetchMock from 'vitest-fetch-mock'; +import { workspacesAtom } from '../../atoms'; import { useCurrentPageId } from '../../hooks/current/use-current-page-id'; -import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace'; +import { + currentWorkspaceAtom, + useCurrentWorkspace, +} from '../../hooks/current/use-current-workspace'; import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper'; import { useWorkspacesHelper } from '../../hooks/use-workspaces'; import { ThemeProvider } from '../../providers/ThemeProvider'; -import { pathGenerator } from '../../shared'; +import { pathGenerator, RemWorkspaceFlavour } from '../../shared'; import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar'; const fetchMocker = createFetchMock(vi); // fetchMocker.enableMocks(); +let store = getDefaultStore(); +beforeEach(async () => { + store = createStore(); + await store.get(workspacesAtom); +}); + +const ProviderWrapper: React.FC = ({ children }) => { + return {children}; +}; describe('WorkSpaceSliderBar', () => { test('basic', async () => { @@ -28,10 +42,17 @@ describe('WorkSpaceSliderBar', () => { const onOpenWorkspaceListModalFn = vi.fn(); const onOpenQuickSearchModalFn = vi.fn(); - const mutationHook = renderHook(() => useWorkspacesHelper()); - const id = mutationHook.result.current.createRemLocalWorkspace('test0'); + const mutationHook = renderHook(() => useWorkspacesHelper(), { + wrapper: ProviderWrapper, + }); + const id = await mutationHook.result.current.createLocalWorkspace('test0'); + await store.get(workspacesAtom); + mutationHook.rerender(); mutationHook.result.current.createWorkspacePage(id, 'test1'); - const currentWorkspaceHook = renderHook(() => useCurrentWorkspace()); + await store.get(currentWorkspaceAtom); + const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), { + wrapper: ProviderWrapper, + }); let i = 0; const Component = () => { const [show, setShow] = useState(false); @@ -63,11 +84,17 @@ describe('WorkSpaceSliderBar', () => { const App = () => { return ( - + + + ); }; currentWorkspaceHook.result.current[1](id); + const currentWorkspace = await store.get(currentWorkspaceAtom); + expect(currentWorkspace).toBeDefined(); + expect(currentWorkspace?.flavour).toBe(RemWorkspaceFlavour.LOCAL); + expect(currentWorkspace?.id).toBe(id); const app = render(); const card = await app.findByTestId('current-workspace'); expect(onOpenWorkspaceListModalFn).toBeCalledTimes(0); diff --git a/apps/web/src/components/pure/quick-search-modal/index.tsx b/apps/web/src/components/pure/quick-search-modal/index.tsx index 270614bf25..cd42b1e0e5 100644 --- a/apps/web/src/components/pure/quick-search-modal/index.tsx +++ b/apps/web/src/components/pure/quick-search-modal/index.tsx @@ -30,7 +30,6 @@ const isMac = () => { export type QuickSearchModalProps = { blockSuiteWorkspace: BlockSuiteWorkspace; - enableShortCut: boolean; open: boolean; setOpen: (value: boolean) => void; router: NextRouter; @@ -40,7 +39,6 @@ export const QuickSearchModal: React.FC = ({ open, setOpen, router, - enableShortCut, blockSuiteWorkspace, }) => { const [loading, startTransition] = useTransition(); @@ -65,9 +63,6 @@ export const QuickSearchModal: React.FC = ({ }, [setOpen, setQuery]); // Add ‘⌘+K’ shortcut keys as switches useEffect(() => { - if (!enableShortCut) { - return; - } const keydown = (e: KeyboardEvent) => { if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) { const selection = window.getSelection(); @@ -86,7 +81,7 @@ export const QuickSearchModal: React.FC = ({ document.addEventListener('keydown', keydown, { capture: true }); return () => document.removeEventListener('keydown', keydown, { capture: true }); - }, [enableShortCut, open, router, setOpen, setQuery]); + }, [open, router, setOpen, setQuery]); return ( = ({ currentPath === (currentWorkspaceId && paths.setting(currentWorkspaceId)) } + data-testid="slider-bar-workspace-setting-button" > { @@ -32,9 +36,26 @@ beforeAll(() => { ); }); +beforeEach(() => { + localStorage.clear(); +}); + +async function getJotaiContext() { + const store = createStore(); + const ProviderWrapper: React.FC = + function ProviderWrapper({ children }) { + return {children}; + }; + const workspaces = await store.get(workspacesAtom); + expect(workspaces.length).toBe(0); + return { + store, + ProviderWrapper, + initialWorkspaces: workspaces, + } as const; +} + beforeEach(async () => { - vitestRefreshWorkspaces(); - dataCenter.isLoaded = true; return new Promise(resolve => { blockSuiteWorkspace = new BlockSuiteWorkspace({ room: 'test', @@ -104,16 +125,50 @@ describe('usePageMetas', async () => { }); }); +describe('useWorkspacesHelper', () => { + test('basic', async () => { + const { ProviderWrapper, store } = await getJotaiContext(); + const workspaceHelperHook = renderHook(() => useWorkspacesHelper(), { + 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); + const workspacesHook = renderHook(() => useWorkspaces(), { + wrapper: ProviderWrapper, + }); + await store.get(currentWorkspaceAtom); + const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), { + wrapper: ProviderWrapper, + }); + currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id); + }); +}); + describe('useWorkspaces', () => { - test('basic', () => { - const { result } = renderHook(() => useWorkspaces()); + test('basic', async () => { + const { ProviderWrapper } = await getJotaiContext(); + const { result } = renderHook(() => useWorkspaces(), { + wrapper: ProviderWrapper, + }); expect(result.current).toEqual([]); }); - test('mutation', () => { - const { result } = renderHook(() => useWorkspacesHelper()); - result.current.createRemLocalWorkspace('test'); - const { result: result2 } = renderHook(() => useWorkspaces()); + test('mutation', async () => { + const { ProviderWrapper, store } = await getJotaiContext(); + const { result } = renderHook(() => useWorkspacesHelper(), { + wrapper: ProviderWrapper, + }); + await result.current.createLocalWorkspace('test'); + const workspaces = await store.get(workspacesAtom); + console.log(workspaces); + expect(workspaces.length).toEqual(1); + const { result: result2 } = renderHook(() => useWorkspaces(), { + wrapper: ProviderWrapper, + }); expect(result2.current.length).toEqual(1); const firstWorkspace = result2.current[0]; expect(firstWorkspace.flavour).toBe('local'); @@ -124,8 +179,13 @@ describe('useWorkspaces', () => { describe('useSyncRouterWithCurrentWorkspaceAndPage', () => { test('from "/"', async () => { - const mutationHook = renderHook(() => useWorkspacesHelper()); - const id = mutationHook.result.current.createRemLocalWorkspace('test0'); + 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('/'); @@ -134,6 +194,7 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => { renderHook( ({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router), { + wrapper: ProviderWrapper, initialProps: { router: routerHook.result.current, }, @@ -144,8 +205,14 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => { }); test('from incorrect "/workspace/[workspaceId]/[pageId]"', async () => { - const mutationHook = renderHook(() => useWorkspacesHelper()); - const id = mutationHook.result.current.createRemLocalWorkspace('test0'); + 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`); @@ -154,29 +221,16 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => { renderHook( ({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router), { + wrapper: ProviderWrapper, initialProps: { router: routerHook.result.current, }, } ); - expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`); - }); -}); + await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT)); -describe('useLastOpenedWorkspace', () => { - test('basic', async () => { - const workspaceHelperHook = renderHook(() => useWorkspacesHelper()); - workspaceHelperHook.result.current.createRemLocalWorkspace('test'); - const workspacesHook = renderHook(() => useWorkspaces()); - const currentWorkspaceHook = renderHook(() => useCurrentWorkspace()); - currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id); - const lastOpenedWorkspace = renderHook(() => useLastOpenedWorkspace()); - expect(lastOpenedWorkspace.result.current[0]).toBe(null); - const lastOpenedWorkspace2 = renderHook(() => useLastOpenedWorkspace()); - expect(lastOpenedWorkspace2.result.current[0]).toBe( - workspacesHook.result.current[0].id - ); + expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`); }); }); diff --git a/apps/web/src/hooks/__tests__/use-blocksuite-workspace-helper.spec.ts b/apps/web/src/hooks/__tests__/use-blocksuite-workspace-helper.spec.ts index 801833cf32..408fe5e5b6 100644 --- a/apps/web/src/hooks/__tests__/use-blocksuite-workspace-helper.spec.ts +++ b/apps/web/src/hooks/__tests__/use-blocksuite-workspace-helper.spec.ts @@ -11,12 +11,10 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { BlockSuiteWorkspace } from '../../shared'; import { useBlockSuiteWorkspaceHelper } from '../use-blocksuite-workspace-helper'; import { usePageMeta } from '../use-page-meta'; -import { vitestRefreshWorkspaces } from '../use-workspaces'; let blockSuiteWorkspace: BlockSuiteWorkspace; beforeEach(() => { - vitestRefreshWorkspaces(); blockSuiteWorkspace = new BlockSuiteWorkspace({ room: 'test', }) 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 9d63eadce0..c7c3998ed2 100644 --- a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts +++ b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts @@ -1,10 +1,10 @@ import { useCallback } from 'react'; import { mutate } from 'swr'; +import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms'; import { QueryKey } from '../../plugins/affine/fetcher'; import { AffineWorkspace } from '../../shared'; import { apis } from '../../shared/apis'; -import { refreshDataCenter } from '../use-workspaces'; export function useToggleWorkspacePublish(workspace: AffineWorkspace) { return useCallback( @@ -14,7 +14,10 @@ export function useToggleWorkspacePublish(workspace: AffineWorkspace) { public: isPublish, }); await mutate(QueryKey.getWorkspaces); - await refreshDataCenter(); + // force update + jotaiStore.set(jotaiWorkspacesAtom, [ + ...jotaiStore.get(jotaiWorkspacesAtom), + ]); }, [workspace] ); 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 27e84adf76..540dea5637 100644 --- a/apps/web/src/hooks/current/use-current-page-id.ts +++ b/apps/web/src/hooks/current/use-current-page-id.ts @@ -1,7 +1,40 @@ -import { useAtom } from 'jotai'; +import { 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); +} + +/** + * @deprecated + */ export function useCurrentPageId(): [ string | null, (newId: string | null) => void diff --git a/apps/web/src/hooks/current/use-current-workspace.ts b/apps/web/src/hooks/current/use-current-workspace.ts index 88bf5be2bf..293573d829 100644 --- a/apps/web/src/hooks/current/use-current-workspace.ts +++ b/apps/web/src/hooks/current/use-current-workspace.ts @@ -1,18 +1,30 @@ -import { useAtom } from 'jotai'; +import { atom, useAtom, useAtomValue } from 'jotai'; import { useCallback } from 'react'; -import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms'; +import { + currentPageIdAtom, + currentWorkspaceIdAtom, + workspacesAtom, +} from '../../atoms'; import { RemWorkspace } from '../../shared'; -import { useWorkspace } from '../use-workspace'; + +export const currentWorkspaceAtom = atom>( + async get => { + const id = get(currentWorkspaceIdAtom); + const workspaces = await get(workspacesAtom); + return workspaces.find(workspace => workspace.id === id) ?? null; + } +); export function useCurrentWorkspace(): [ RemWorkspace | null, (id: string | null) => void ] { - const [id, setId] = useAtom(currentWorkspaceIdAtom); + const currentWorkspace = useAtomValue(currentWorkspaceAtom); + const [, setId] = useAtom(currentWorkspaceIdAtom); const [, setPageId] = useAtom(currentPageIdAtom); return [ - useWorkspace(id), + currentWorkspace, useCallback( (id: string | null) => { setPageId(null); diff --git a/apps/web/src/hooks/use-create-first-workspace.ts b/apps/web/src/hooks/use-create-first-workspace.ts new file mode 100644 index 0000000000..dcef22b6e5 --- /dev/null +++ b/apps/web/src/hooks/use-create-first-workspace.ts @@ -0,0 +1,53 @@ +import { DEFAULT_WORKSPACE_NAME } from '@affine/env'; +import { assertEquals, assertExists, nanoid } from '@blocksuite/store'; +import { useAtom } from 'jotai/index'; +import { useEffect } from 'react'; + +import { jotaiWorkspacesAtom } from '../atoms'; +import { LocalPlugin } from '../plugins/local'; +import { RemWorkspaceFlavour } from '../shared'; +import { createEmptyBlockSuiteWorkspace } from '../utils'; + +export function useCreateFirstWorkspace() { + const [jotaiWorkspaces, set] = useAtom(jotaiWorkspacesAtom); + useEffect(() => { + const controller = new AbortController(); + + /** + * Create a first workspace, only just once for a browser + */ + async function createFirst() { + 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); + const newPageId = nanoid(); + workspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => { + assertEquals(pageId, newPageId); + set(workspaces => [ + ...workspaces, + { + id: workspace.id, + flavour: RemWorkspaceFlavour.LOCAL, + }, + ]); + }); + workspace.blockSuiteWorkspace.createPage(newPageId); + } + if ( + jotaiWorkspaces.length === 0 && + localStorage.getItem('first') !== 'true' + ) { + localStorage.setItem('first', 'true'); + createFirst(); + } + return () => { + controller.abort(); + }; + }, [jotaiWorkspaces.length, set]); +} diff --git a/apps/web/src/hooks/use-last-opened-workspace.ts b/apps/web/src/hooks/use-last-opened-workspace.ts deleted file mode 100644 index 7f77c747bd..0000000000 --- a/apps/web/src/hooks/use-last-opened-workspace.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; - -import { useCurrentPageId } from './current/use-current-page-id'; -import { useCurrentWorkspace } from './current/use-current-workspace'; - -const kLastOpenedWorkspaceKey = 'affine-last-opened-workspace'; -const kLastOpenedPageKey = 'affine-last-opened-page'; - -export function useLastOpenedWorkspace(): [ - string | null, - string | null, - () => void -] { - const [currentWorkspace] = useCurrentWorkspace(); - const [currentPageId] = useCurrentPageId(); - const [lastWorkspaceId, setLastWorkspaceId] = useState(null); - const [lastPageId, setLastPageId] = useState(null); - useEffect(() => { - const lastWorkspaceId = localStorage.getItem(kLastOpenedWorkspaceKey); - if (lastWorkspaceId) { - setLastWorkspaceId(lastWorkspaceId); - } - const lastPageId = localStorage.getItem(kLastOpenedPageKey); - if (lastPageId) { - setLastPageId(lastPageId); - } - }, []); - useEffect(() => { - if (currentWorkspace) { - localStorage.setItem(kLastOpenedWorkspaceKey, currentWorkspace.id); - } - if (currentPageId) { - localStorage.setItem(kLastOpenedPageKey, currentPageId); - } - }, [currentPageId, currentWorkspace]); - const refresh = useCallback(() => { - localStorage.removeItem(kLastOpenedWorkspaceKey); - localStorage.removeItem(kLastOpenedPageKey); - }, []); - return [lastWorkspaceId, lastPageId, refresh]; -} 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 index 158e58c8f1..0c9b46aa22 100644 --- 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 @@ -1,10 +1,11 @@ import { NextRouter } from 'next/router'; import { useEffect } from 'react'; +import { currentPageIdAtom, jotaiStore } from '../atoms'; import { RemWorkspace, RemWorkspaceFlavour } from '../shared'; import { useCurrentPageId } from './current/use-current-page-id'; import { useCurrentWorkspace } from './current/use-current-workspace'; -import { useWorkspaces, useWorkspacesIsLoaded } from './use-workspaces'; +import { useWorkspaces } from './use-workspaces'; export function findSuitablePageId( workspace: RemWorkspace, @@ -32,11 +33,11 @@ export function findSuitablePageId( } } +export const REDIRECT_TIMEOUT = 1000; export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) { const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace(); const [currentPageId, setCurrentPageId] = useCurrentPageId(); const workspaces = useWorkspaces(); - const isLoaded = useWorkspacesIsLoaded(); useEffect(() => { const listener: Parameters[1] = (url: string) => { if (url.startsWith('/')) { @@ -66,7 +67,7 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) { }; }, [currentWorkspace, router, setCurrentPageId, setCurrentWorkspaceId]); useEffect(() => { - if (!router.isReady || !isLoaded) { + if (!router.isReady) { return; } if ( @@ -130,19 +131,55 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) { } } else { if (!currentPageId && currentWorkspace) { - if ('blockSuiteWorkspace' in currentWorkspace) { - const targetId = findSuitablePageId(currentWorkspace, targetPageId); - if (targetId) { - setCurrentPageId(targetId); - router.push({ - query: { - ...router.query, - workspaceId: currentWorkspace.id, - pageId: targetId, - }, - }); - return; - } + 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: targetId, + }, + }); + } + } + ); + const clearId = setTimeout(() => { + if (jotaiStore.get(currentPageIdAtom) === 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(); + }, REDIRECT_TIMEOUT); + return () => { + clearTimeout(clearId); + dispose.dispose(); + }; } } } @@ -156,6 +193,5 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) { setCurrentWorkspaceId, workspaces, router, - isLoaded, ]); } diff --git a/apps/web/src/hooks/use-workspaces.ts b/apps/web/src/hooks/use-workspaces.ts index 131d290cb7..9d09e31801 100644 --- a/apps/web/src/hooks/use-workspaces.ts +++ b/apps/web/src/hooks/use-workspaces.ts @@ -1,172 +1,25 @@ -import { Workspace } from '@affine/datacenter'; -import { config, getEnvironment } from '@affine/env'; import { nanoid } from '@blocksuite/store'; -import { useCallback, useMemo, useSyncExternalStore } from 'react'; -import useSWR from 'swr'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { useCallback } from 'react'; -import { lockMutex } from '../atoms'; -import { createLocalProviders } from '../blocksuite'; +import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms'; import { WorkspacePlugins } from '../plugins'; -import { QueryKey } from '../plugins/affine/fetcher'; -import { kStoreKey } from '../plugins/local'; +import { LocalPlugin } from '../plugins/local'; import { LocalWorkspace, RemWorkspace, RemWorkspaceFlavour } from '../shared'; import { createEmptyBlockSuiteWorkspace } from '../utils'; -// fixme(himself65): refactor with jotai atom using async -export const dataCenter = { - workspaces: [] as RemWorkspace[], - isLoaded: false, - callbacks: new Set<() => void>(), -}; - -export function vitestRefreshWorkspaces() { - dataCenter.workspaces = []; - dataCenter.callbacks.clear(); -} - -declare global { - // eslint-disable-next-line no-var - var dataCenter: { - workspaces: RemWorkspace[]; - isLoaded: boolean; - callbacks: Set<() => void>; - }; -} - -globalThis.dataCenter = dataCenter; - -function createRemLocalWorkspace(name: string) { - const id = nanoid(); - const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( - id, - (_: string) => undefined - ); - blockSuiteWorkspace.meta.setName(name); - const workspace: LocalWorkspace = { - flavour: RemWorkspaceFlavour.LOCAL, - blockSuiteWorkspace: blockSuiteWorkspace, - providers: [...createLocalProviders(blockSuiteWorkspace)], - id, - }; - if (config.enableIndexedDBProvider) { - let ids: string[]; - try { - ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]'); - if (!Array.isArray(ids)) { - localStorage.setItem(kStoreKey, '[]'); - ids = []; - } - } catch (e) { - localStorage.setItem(kStoreKey, '[]'); - ids = []; - } - ids.push(id); - localStorage.setItem(kStoreKey, JSON.stringify(ids)); - } - dataCenter.workspaces = [...dataCenter.workspaces, workspace]; - dataCenter.callbacks.forEach(cb => cb()); - return id; -} - -const emptyWorkspaces: RemWorkspace[] = []; - -export async function refreshDataCenter(signal?: AbortSignal) { - dataCenter.isLoaded = false; - dataCenter.callbacks.forEach(cb => cb()); - if (getEnvironment().isServer) { - return; - } - // fixme(himself65): `prefetchWorkspace` is not used - // use `config.enablePlugin = ['affine', 'local']` instead - // if (!config.prefetchWorkspace) { - // console.info('prefetchNecessaryData: skip prefetching'); - // return; - // } - const plugins = Object.values(WorkspacePlugins).sort( - (a, b) => a.loadPriority - b.loadPriority - ); - // prefetch data in order - for (const plugin of plugins) { - console.info('prefetchNecessaryData: plugin', plugin.flavour); - try { - if (signal?.aborted) { - break; - } - const oldData = dataCenter.workspaces; - await plugin.prefetchData(dataCenter, signal); - const newData = dataCenter.workspaces; - if (!Object.is(oldData, newData)) { - console.info('prefetchNecessaryData: data changed'); - } - } catch (e) { - console.error('error prefetch data', plugin.flavour, e); - } - } - dataCenter.isLoaded = true; - dataCenter.callbacks.forEach(cb => cb()); -} - export function useWorkspaces(): RemWorkspace[] { - return useSyncExternalStore( - useCallback(onStoreChange => { - dataCenter.callbacks.add(onStoreChange); - return () => { - dataCenter.callbacks.delete(onStoreChange); - }; - }, []), - useCallback(() => dataCenter.workspaces, []), - useCallback(() => emptyWorkspaces, []) - ); -} - -export function useWorkspacesIsLoaded(): boolean { - return useSyncExternalStore( - useCallback(onStoreChange => { - dataCenter.callbacks.add(onStoreChange); - return () => { - dataCenter.callbacks.delete(onStoreChange); - }; - }, []), - useCallback(() => dataCenter.isLoaded, []), - useCallback(() => true, []) - ); -} - -export function useSyncWorkspaces() { - return useSWR(QueryKey.getWorkspaces, { - fallbackData: [], - revalidateOnReconnect: true, - revalidateOnFocus: false, - revalidateOnMount: true, - revalidateIfStale: false, - }); -} - -async function deleteWorkspace(workspaceId: string) { - return lockMutex(async () => { - console.warn('deleting workspace'); - const idx = dataCenter.workspaces.findIndex( - workspace => workspace.id === workspaceId - ); - if (idx === -1) { - throw new Error('workspace not found'); - } - try { - const [workspace] = dataCenter.workspaces.splice(idx, 1); - // @ts-expect-error - await WorkspacePlugins[workspace.flavour].deleteWorkspace(workspace); - dataCenter.callbacks.forEach(cb => cb()); - } catch (e) { - console.error('error deleting workspace', e); - } - }); + return useAtomValue(workspacesAtom); } export function useWorkspacesHelper() { - return useMemo( - () => ({ - createWorkspacePage: (workspaceId: string, pageId: string) => { - const workspace = dataCenter.workspaces.find( + const workspaces = useWorkspaces(); + const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom); + const set = useSetAtom(jotaiWorkspacesAtom); + return { + createWorkspacePage: useCallback( + (workspaceId: string, pageId: string) => { + const workspace = workspaces.find( ws => ws.id === workspaceId ) as LocalWorkspace; if (workspace && 'blockSuiteWorkspace' in workspace) { @@ -175,9 +28,46 @@ export function useWorkspacesHelper() { throw new Error('cannot create page. blockSuiteWorkspace not found'); } }, - createRemLocalWorkspace, - deleteWorkspace, - }), - [] - ); + [workspaces] + ), + createLocalWorkspace: useCallback( + async (name: string): Promise => { + const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( + nanoid(), + _ => undefined + ); + blockSuiteWorkspace.meta.setName(name); + const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace); + set(workspaces => [ + ...workspaces, + { + id, + flavour: RemWorkspaceFlavour.LOCAL, + }, + ]); + return id; + }, + [set] + ), + deleteWorkspace: useCallback( + async (workspaceId: string) => { + const targetJotaiWorkspace = jotaiWorkspaces.find( + ws => ws.id === workspaceId + ); + const targetWorkspace = workspaces.find(ws => ws.id === workspaceId); + if (!targetJotaiWorkspace || !targetWorkspace) { + throw new Error('page cannot be found'); + } + + // delete workspace from plugin + await WorkspacePlugins[targetWorkspace.flavour].CRUD.delete( + // fixme: type casting + targetWorkspace as any + ); + // delete workspace from jotai storage + set(workspaces => workspaces.filter(ws => ws.id !== workspaceId)); + }, + [jotaiWorkspaces, set, workspaces] + ), + }; } diff --git a/apps/web/src/layouts/index.tsx b/apps/web/src/layouts/index.tsx index 0c3250541e..c6f4de42c9 100644 --- a/apps/web/src/layouts/index.tsx +++ b/apps/web/src/layouts/index.tsx @@ -1,57 +1,100 @@ +import { DebugLogger } from '@affine/debug'; import { setUpLanguage, useTranslation } from '@affine/i18n'; import { assertExists, nanoid } from '@blocksuite/store'; -import { useAtom, useAtomValue } from 'jotai'; +import { NoSsr } from '@mui/material'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; import { useRouter } from 'next/router'; -import React, { useCallback, useEffect } from 'react'; +import React, { Suspense, useCallback, useEffect } from 'react'; import { Helmet } from 'react-helmet-async'; import { + currentWorkspaceIdAtom, + jotaiWorkspacesAtom, openQuickSearchModalAtom, openWorkspacesModalAtom, workspaceLockAtom, } from '../atoms'; import { HelpIsland } from '../components/pure/help-island'; import { PageLoading } from '../components/pure/loading'; +import QuickSearchModal from '../components/pure/quick-search-modal'; 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 { useRouterTitle } from '../hooks/use-router-title'; -import { - refreshDataCenter, - useSyncWorkspaces, - useWorkspaces, -} from '../hooks/use-workspaces'; +import { useWorkspaces } from '../hooks/use-workspaces'; +import { WorkspacePlugins } from '../plugins'; +import { ModalProvider } from '../providers/ModalProvider'; import { pathGenerator, publicPathGenerator } from '../shared'; import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles'; const sideBarOpenAtom = atomWithStorage('sideBarOpen', true); -refreshDataCenter(); +const logger = new DebugLogger('workspace-layout'); +export const WorkspaceLayout: React.FC = + function WorkspacesSuspense({ children }) { + const { i18n } = useTranslation(); + useEffect(() => { + document.documentElement.lang = i18n.language; + // todo(himself65): this is a hack, we should use a better way to set the language + setUpLanguage(i18n); + }, [i18n]); + useCreateFirstWorkspace(); + const set = useSetAtom(jotaiWorkspacesAtom); + useEffect(() => { + logger.info('mount'); + const controller = new AbortController(); + const lists = Object.values(WorkspacePlugins) + .sort((a, b) => a.loadPriority - b.loadPriority) + .map(({ CRUD }) => CRUD.list); + async function fetch() { + const items = []; + for (const list of lists) { + try { + const item = await list(); + items.push(...item.map(x => ({ id: x.id, flavour: x.flavour }))); + } catch (e) { + logger.error('list data error:', e); + } + } + if (controller.signal.aborted) { + return; + } + set([...items]); + logger.info('mount first data:', items); + } + fetch(); + return () => { + controller.abort(); + logger.info('unmount'); + }; + }, [set]); + const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); + return ( + + {/* fixme(himself65): don't re-render whole modals */} + + }> + {children} + + + ); + }; -export const WorkspaceLayout: React.FC = ({ +export const WorkspaceLayoutInner: React.FC = ({ children, }) => { - const { i18n } = useTranslation(); - useEffect(() => { - document.documentElement.lang = i18n.language; - // todo(himself65): this is a hack, we should use a better way to set the language - setUpLanguage(i18n); - }, [i18n]); - useEffect(() => { - const controller = new AbortController(); - refreshDataCenter(controller.signal); - return () => { - controller.abort(); - }; - }, []); - const [show, setShow] = useAtom(sideBarOpenAtom); - useSyncWorkspaces(); const [currentWorkspace] = useCurrentWorkspace(); const [currentPageId] = useCurrentPageId(); const workspaces = useWorkspaces(); + + useEffect(() => { + console.log(workspaces); + }, [workspaces]); + useEffect(() => { const providers = workspaces.flatMap(workspace => workspace.providers.filter(provider => provider.background) @@ -91,7 +134,6 @@ export const WorkspaceLayout: React.FC = ({ const isPublicWorkspace = router.pathname.split('/')[1] === 'public-workspace'; const title = useRouterTitle(router); - const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom); const handleOpenPage = useCallback( (pageId: string) => { assertExists(currentWorkspace); @@ -113,6 +155,10 @@ export const WorkspaceLayout: React.FC = ({ const handleOpenWorkspaceListModal = useCallback(() => { setOpenWorkspacesModal(true); }, [setOpenWorkspacesModal]); + + const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom( + openQuickSearchModalAtom + ); const handleOpenQuickSearchModal = useCallback(() => { setOpenQuickSearchModalAtom(true); }, [setOpenQuickSearchModalAtom]); @@ -153,6 +199,14 @@ export const WorkspaceLayout: React.FC = ({ + {currentWorkspace?.blockSuiteWorkspace && ( + + )} ); }; diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index fa6c57ff6c..35803b1d98 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -18,7 +18,6 @@ import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary' import { ProviderComposer } from '../components/provider-composer'; import { PageLoading } from '../components/pure/loading'; import { AffineSWRConfigProvider } from '../providers/AffineSWRConfigProvider'; -import { ModalProvider } from '../providers/ModalProvider'; import { ThemeProvider } from '../providers/ThemeProvider'; import { NextPageWithLayout } from '../shared'; import createEmotionCache from '../utils/create-emotion-cache'; @@ -81,7 +80,6 @@ const App = function App({ , , , - , ], [] )} diff --git a/apps/web/src/pages/index.tsx b/apps/web/src/pages/index.tsx index 3ded0c95b8..ab61156487 100644 --- a/apps/web/src/pages/index.tsx +++ b/apps/web/src/pages/index.tsx @@ -1,50 +1,18 @@ -import { useAtom } from 'jotai'; import { NextPage } from 'next'; import { useRouter } from 'next/router'; -import React, { useEffect } from 'react'; +import React, { Suspense, useEffect } from 'react'; -import { currentWorkspaceIdAtom } from '../atoms'; import { PageLoading } from '../components/pure/loading'; -import { refreshDataCenter, useWorkspaces } from '../hooks/use-workspaces'; +import { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace'; +import { useWorkspaces } from '../hooks/use-workspaces'; -const IndexPage: NextPage = () => { +const IndexPageInner = () => { const router = useRouter(); - useEffect(() => { - const controller = new AbortController(); - refreshDataCenter(controller.signal); - return () => { - controller.abort(); - }; - }, []); - const [workspaceId] = useAtom(currentWorkspaceIdAtom); const workspaces = useWorkspaces(); useEffect(() => { if (!router.isReady) { return; } - const targetWorkspace = workspaces.find(w => w.id === workspaceId); - if (workspaceId && targetWorkspace) { - const pageId = - targetWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id; - if (pageId) { - router.replace({ - pathname: '/workspace/[workspaceId]/[pageId]', - query: { - workspaceId, - pageId, - }, - }); - return; - } else { - router.replace({ - pathname: '/workspace/[workspaceId]/all', - query: { - workspaceId, - }, - }); - return; - } - } const firstWorkspace = workspaces.at(0); if (firstWorkspace) { const pageId = @@ -59,17 +27,44 @@ const IndexPage: NextPage = () => { }); return; } else { - router.replace({ - pathname: '/workspace/[workspaceId]/all', - query: { - workspaceId: firstWorkspace.id, - }, - }); - return; + const clearId = setTimeout(() => { + dispose.dispose(); + router.replace({ + pathname: '/workspace/[workspaceId]/all', + query: { + workspaceId: firstWorkspace.id, + }, + }); + }, 1000); + const dispose = firstWorkspace.blockSuiteWorkspace.slots.pageAdded.once( + pageId => { + clearTimeout(clearId); + router.replace({ + pathname: '/workspace/[workspaceId]/[pageId]', + query: { + workspaceId: firstWorkspace.id, + pageId, + }, + }); + } + ); + return () => { + clearTimeout(clearId); + dispose.dispose(); + }; } } - }, [router, workspaceId, workspaces]); + }, [router, workspaces]); return ; }; +const IndexPage: NextPage = () => { + useCreateFirstWorkspace(); + return ( + }> + + + ); +}; + export default IndexPage; diff --git a/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx b/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx index 3fdea02056..4f558b0617 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/[pageId].tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { Unreachable } from '../../../components/affine/affine-error-eoundary'; import { PageLoading } from '../../../components/pure/loading'; @@ -27,20 +27,6 @@ function enableFullFlags(blockSuiteWorkspace: BlockSuiteWorkspace) { const WorkspaceDetail: React.FC = () => { const [pageId] = useCurrentPageId(); const [currentWorkspace] = useCurrentWorkspace(); - const [, rerender] = useState(false); - // fixme(himself65): this is a hack - useEffect(() => { - const dispose = currentWorkspace?.blockSuiteWorkspace.slots.pageAdded.on( - id => { - if (pageId === id) { - rerender(prev => !prev); - } - } - ); - return () => { - dispose?.dispose(); - }; - }, [currentWorkspace?.blockSuiteWorkspace.slots.pageAdded, pageId]); useEffect(() => { if (currentWorkspace) { enableFullFlags(currentWorkspace.blockSuiteWorkspace); @@ -52,9 +38,6 @@ const WorkspaceDetail: React.FC = () => { if (!pageId) { return ; } - if (!currentWorkspace.blockSuiteWorkspace.getPage(pageId)) { - return ; - } if (currentWorkspace.flavour === RemWorkspaceFlavour.AFFINE) { const PageDetail = WorkspacePlugins[currentWorkspace.flavour].PageDetail; return ( diff --git a/apps/web/src/plugins/affine/index.tsx b/apps/web/src/plugins/affine/index.tsx index e4d07f5658..3bec7a3376 100644 --- a/apps/web/src/plugins/affine/index.tsx +++ b/apps/web/src/plugins/affine/index.tsx @@ -1,4 +1,4 @@ -import { assertEquals } from '@blocksuite/store'; +import { createJSONStorage } from 'jotai/utils'; import React from 'react'; import { preload } from 'swr'; import { z } from 'zod'; @@ -19,6 +19,7 @@ import { createEmptyBlockSuiteWorkspace } from '../../utils'; import { WorkspacePlugin } from '..'; import { fetcher, QueryKey } from './fetcher'; +const storage = createJSONStorage(() => localStorage); const kAffineLocal = 'affine-local-storage-v2'; const schema = z.object({ id: z.string(), @@ -31,33 +32,48 @@ const schema = z.object({ export const AffinePlugin: WorkspacePlugin = { flavour: RemWorkspaceFlavour.AFFINE, loadPriority: LoadPriority.HIGH, - createWorkspace: async (blockSuiteWorkspace: BlockSuiteWorkspace) => { - const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate( - blockSuiteWorkspace.doc - ); - const { id } = await apis.createWorkspace(new Blob([binary.buffer])); - return id; - }, - deleteWorkspace: async workspace => { - await apis.deleteWorkspace({ - id: workspace.id, - }); - workspace.providers.forEach(p => p.cleanup()); - }, - prefetchData: async dataCenter => { - if (localStorage.getItem(kAffineLocal)) { - const localData = JSON.parse(localStorage.getItem(kAffineLocal) || '[]'); - if (Array.isArray(localData)) { - const workspacesDump = localData - .map((item: any) => { - const result = schema.safeParse(item); - if (result.success) { - return result.data; - } - return null; - }) - .filter(Boolean) as z.infer[]; - const workspaces = workspacesDump.map(workspace => { + CRUD: { + create: async blockSuiteWorkspace => { + const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate( + blockSuiteWorkspace.doc + ); + const { id } = await apis.createWorkspace(new Blob([binary.buffer])); + return id; + }, + delete: async workspace => { + await apis.deleteWorkspace({ + id: workspace.id, + }); + }, + get: async workspaceId => { + const workspaces: AffineWorkspace[] = await preload( + QueryKey.getWorkspaces, + fetcher + ); + + const workspace = workspaces.find( + workspace => workspace.id === workspaceId + ); + const dump = workspaces.map(workspace => { + return { + id: workspace.id, + type: workspace.type, + public: workspace.public, + permission: workspace.permission, + create_at: workspace.create_at, + } satisfies z.infer; + }); + storage.setItem(kAffineLocal, dump); + if (!workspace) { + return null; + } + return workspace; + }, + list: async () => { + // fixme: refactor auth check + if (!apis.auth.isLogin) return []; + return await apis.getWorkspaces().then(workspaces => { + return workspaces.map(workspace => { const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( workspace.id, (k: string) => @@ -66,75 +82,14 @@ export const AffinePlugin: WorkspacePlugin = { ); const affineWorkspace: AffineWorkspace = { ...workspace, + flavour: RemWorkspaceFlavour.AFFINE, blockSuiteWorkspace, providers: [...createAffineProviders(blockSuiteWorkspace)], - flavour: RemWorkspaceFlavour.AFFINE, }; return affineWorkspace; }); - - // fixme: refactor to a function - workspaces.forEach(workspace => { - const exist = dataCenter.workspaces.findIndex( - ws => ws.id === workspace.id - ); - if (exist !== -1) { - dataCenter.workspaces.splice(exist, 1, workspace); - dataCenter.workspaces = [...dataCenter.workspaces]; - } else { - dataCenter.workspaces = [...dataCenter.workspaces, workspace]; - } - }); - dataCenter.callbacks.forEach(cb => cb()); - } else { - localStorage.removeItem(kAffineLocal); - } - } - const promise: Promise = preload( - QueryKey.getWorkspaces, - fetcher - ); - return promise - .then(async workspaces => { - const promises = workspaces.map(workspace => { - assertEquals(workspace.flavour, RemWorkspaceFlavour.AFFINE); - return workspace; - }); - return Promise.all(promises) - .then(workspaces => { - workspaces.forEach(workspace => { - if (workspace === null) { - return; - } - const exist = dataCenter.workspaces.findIndex( - ws => ws.id === workspace.id - ); - if (exist !== -1) { - dataCenter.workspaces.splice(exist, 1, workspace); - dataCenter.workspaces = [...dataCenter.workspaces]; - } else { - dataCenter.workspaces = [...dataCenter.workspaces, workspace]; - } - }); - return workspaces; - }) - .then(ws => { - const workspaces = ws.filter(Boolean) as AffineWorkspace[]; - const dump = workspaces.map(workspace => { - return { - id: workspace.id, - type: workspace.type, - public: workspace.public, - permission: workspace.permission, - create_at: workspace.create_at, - } satisfies z.infer; - }); - localStorage.setItem(kAffineLocal, JSON.stringify(dump)); - }); - }) - .catch(error => { - console.error(error); }); + }, }, PageDetail: ({ currentWorkspace, currentPageId }) => { const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); diff --git a/apps/web/src/plugins/index.tsx b/apps/web/src/plugins/index.tsx index edf22b831d..dc1f409ec3 100644 --- a/apps/web/src/plugins/index.tsx +++ b/apps/web/src/plugins/index.tsx @@ -1,12 +1,10 @@ -import { assertExists } from '@blocksuite/store'; import React from 'react'; -import { refreshDataCenter } from '../hooks/use-workspaces'; +import { jotaiStore, jotaiWorkspacesAtom } from '../atoms'; import { BlockSuiteWorkspace, FlavourToWorkspace, LoadPriority, - RemWorkspace, RemWorkspaceFlavour, SettingPanel, } from '../shared'; @@ -45,19 +43,14 @@ export interface WorkspacePlugin { // Plugin will be loaded according to the priority loadPriority: LoadPriority; // Fetch necessary data for the first render - prefetchData: ( - dataCenter: { - workspaces: RemWorkspace[]; - callbacks: Set<() => void>; - }, - signal?: AbortSignal - ) => Promise; - - createWorkspace: ( - blockSuiteWorkspace: BlockSuiteWorkspace - ) => Promise; - - deleteWorkspace: (workspace: FlavourToWorkspace[Flavour]) => Promise; + CRUD: { + create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise; + delete: (workspace: FlavourToWorkspace[Flavour]) => Promise; + get: (workspaceId: string) => Promise; + // not supported yet + // update: (workspace: FlavourToWorkspace[Flavour]) => Promise; + list: () => Promise; + }; //#region UI PageDetail: React.FC>; @@ -73,18 +66,26 @@ export const WorkspacePlugins = { [Key in RemWorkspaceFlavour]: WorkspacePlugin; }; +/** + * Transform workspace from one flavour to another + * + * The logic here is to delete the old workspace and create a new one. + */ export async function transformWorkspace< From extends RemWorkspaceFlavour, To extends RemWorkspaceFlavour >(from: From, to: To, workspace: FlavourToWorkspace[From]): Promise { // fixme: type cast - await WorkspacePlugins[from].deleteWorkspace(workspace as any); - const newId = await WorkspacePlugins[to].createWorkspace( + await WorkspacePlugins[from].CRUD.delete(workspace as any); + const newId = await WorkspacePlugins[to].CRUD.create( workspace.blockSuiteWorkspace ); - // refresh the data center - dataCenter.workspaces = []; - await refreshDataCenter(); - assertExists(dataCenter.workspaces.some(w => w.id === newId)); + const workspaces = jotaiStore.get(jotaiWorkspacesAtom); + const idx = workspaces.findIndex(ws => ws.id === workspace.id); + workspaces.splice(idx, 1, { + id: newId, + flavour: to, + }); + jotaiStore.set(jotaiWorkspacesAtom, [...workspaces]); return newId; } diff --git a/apps/web/src/plugins/local/index.tsx b/apps/web/src/plugins/local/index.tsx index c92658fcbf..f11f0ef36a 100644 --- a/apps/web/src/plugins/local/index.tsx +++ b/apps/web/src/plugins/local/index.tsx @@ -1,8 +1,9 @@ -import { DebugLogger } from '@affine/debug'; -import { config, DEFAULT_WORKSPACE_NAME } from '@affine/env'; -import { assertEquals, nanoid } from '@blocksuite/store'; +import { DEFAULT_WORKSPACE_NAME } from '@affine/env'; +import { nanoid } from '@blocksuite/store'; +import { createJSONStorage } from 'jotai/utils'; import React from 'react'; import { IndexeddbPersistence } from 'y-indexeddb'; +import { z } from 'zod'; import { createLocalProviders } from '../../blocksuite'; import { PageNotFoundError } from '../../components/affine/affine-error-eoundary'; @@ -18,139 +19,90 @@ import { import { createEmptyBlockSuiteWorkspace } from '../../utils'; import { WorkspacePlugin } from '..'; -const logger = new DebugLogger('local-plugin'); +const getStorage = () => createJSONStorage(() => localStorage); export const kStoreKey = 'affine-local-workspace'; -// fixme(himself65): this is a hacking that first workspace will disappear somehow -const hashMap = new Map(); +const schema = z.array(z.string()); export const LocalPlugin: WorkspacePlugin = { flavour: RemWorkspaceFlavour.LOCAL, loadPriority: LoadPriority.LOW, - createWorkspace: async blockSuiteWorkspace => { - let ids: string[] = []; - try { - ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]'); - if (!Array.isArray(ids)) { - localStorage.setItem(kStoreKey, '[]'); - ids = []; + CRUD: { + get: async workspaceId => { + 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) { + return null; } - } catch (e) { - localStorage.setItem(kStoreKey, '[]'); - ids = []; - } - const id = nanoid(); - const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc); - await persistence.whenSynced.then(() => { - persistence.destroy(); - }); - ids.push(id); - localStorage.setItem(kStoreKey, JSON.stringify(ids)); - return id; - }, - deleteWorkspace: async workspace => { - const id = workspace.id; - let ids: string[]; - try { - ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]'); - if (!Array.isArray(ids)) { - localStorage.setItem(kStoreKey, '[]'); - ids = []; - } - } catch (e) { - localStorage.setItem(kStoreKey, '[]'); - ids = []; - } - const idx = ids.findIndex(x => x === id); - if (idx === -1) { - throw new Error('cannot find local workspace from localStorage'); - } - workspace.providers.forEach(p => p.cleanup()); - ids.splice(idx, 1); - assertEquals( - ids.every(id => typeof id === 'string'), - true - ); - localStorage.setItem(kStoreKey, JSON.stringify(ids)); - }, - prefetchData: async (dataCenter, signal) => { - if (typeof window === 'undefined') { - // SSR mode, no local data - return; - } - if (signal?.aborted) { - return; - } - let ids: string[]; - try { - ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]'); - if (!Array.isArray(ids)) { - localStorage.setItem(kStoreKey, '[]'); - ids = []; - } - } catch (e) { - localStorage.setItem(kStoreKey, '[]'); - ids = []; - } - if (config.enableIndexedDBProvider) { - const workspaces = await Promise.all( - ids.map(id => { - const blockSuiteWorkspace = hashMap.has(id) - ? (hashMap.get(id) as BlockSuiteWorkspace) - : createEmptyBlockSuiteWorkspace(id, (_: string) => undefined); - hashMap.set(id, blockSuiteWorkspace); - const workspace: LocalWorkspace = { - id, - flavour: RemWorkspaceFlavour.LOCAL, - blockSuiteWorkspace: blockSuiteWorkspace, - providers: [...createLocalProviders(blockSuiteWorkspace)], - }; - return workspace; - }) - ); - workspaces.forEach(workspace => { - if (workspace) { - const exist = dataCenter.workspaces.findIndex( - w => w.id === workspace.id - ); - if (exist === -1) { - dataCenter.workspaces = [...dataCenter.workspaces, workspace]; - } else { - dataCenter.workspaces[exist] = workspace; - dataCenter.workspaces = [...dataCenter.workspaces]; - } - } - }); - } - if (dataCenter.workspaces.length === 0) { - if (signal?.aborted) { - return; - } - logger.info('no local workspace found, create a new one'); - const workspaceId = nanoid(); const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( - workspaceId, + id, (_: string) => undefined ); - hashMap.set(workspaceId, blockSuiteWorkspace); - blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME); - localStorage.setItem(kStoreKey, JSON.stringify([workspaceId])); - blockSuiteWorkspace.createPage(nanoid()); const workspace: LocalWorkspace = { - id: workspaceId, + id, flavour: RemWorkspaceFlavour.LOCAL, blockSuiteWorkspace: blockSuiteWorkspace, providers: [...createLocalProviders(blockSuiteWorkspace)], }; - const persistence = new IndexeddbPersistence( - blockSuiteWorkspace.room as string, - blockSuiteWorkspace.doc + return workspace; + }, + create: async ({ 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( + id, + (_: string) => undefined ); + BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary); + const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc); await persistence.whenSynced.then(() => { persistence.destroy(); }); - dataCenter.workspaces = [workspace]; - } + storage.setItem(kStoreKey, [...data, id]); + console.log('create', id, storage.getItem(kStoreKey)); + return id; + }, + delete: async workspace => { + const storage = getStorage(); + !Array.isArray(storage.getItem(kStoreKey)) && + storage.setItem(kStoreKey, []); + const data = storage.getItem(kStoreKey) as z.infer; + const idx = data.findIndex(id => id === workspace.id); + if (idx === -1) { + throw new Error('workspace not found'); + } + data.splice(idx, 1); + storage.setItem(kStoreKey, [...data]); + }, + list: async () => { + const storage = getStorage(); + !Array.isArray(storage.getItem(kStoreKey)) && + storage.setItem(kStoreKey, []); + const data = ( + await Promise.all( + (storage.getItem(kStoreKey) as z.infer).map(id => + LocalPlugin.CRUD.get(id) + ) + ) + ).filter(item => item !== null) as LocalWorkspace[]; + if (data.length === 0) { + const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( + nanoid(), + (_: string) => undefined + ); + blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME); + await LocalPlugin.CRUD.create(blockSuiteWorkspace); + return LocalPlugin.CRUD.list(); + } + return data; + }, }, PageDetail: ({ currentWorkspace, currentPageId }) => { const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); diff --git a/apps/web/src/providers/ModalProvider.tsx b/apps/web/src/providers/ModalProvider.tsx index ddcc6e59bf..f341fd4196 100644 --- a/apps/web/src/providers/ModalProvider.tsx +++ b/apps/web/src/providers/ModalProvider.tsx @@ -1,17 +1,15 @@ -import { useAtom } from 'jotai'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useRouter } from 'next/router'; import React, { useCallback } from 'react'; import { + currentWorkspaceIdAtom, openCreateWorkspaceModalAtom, - openQuickSearchModalAtom, openWorkspacesModalAtom, } from '../atoms'; import { CreateWorkspaceModal } from '../components/pure/create-workspace-modal'; -import QuickSearchModal from '../components/pure/quick-search-modal'; import { WorkspaceListModal } from '../components/pure/workspace-list-modal'; import { useCurrentUser } from '../hooks/current/use-current-user'; -import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces'; import { apis } from '../shared/apis'; @@ -22,28 +20,27 @@ export function Modals() { const [openCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom( openCreateWorkspaceModalAtom ); - const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom( - openQuickSearchModalAtom - ); + const router = useRouter(); const user = useCurrentUser(); const workspaces = useWorkspaces(); - const [currentWorkspace, setCurrentWorkspace] = useCurrentWorkspace(); - const { createRemLocalWorkspace } = useWorkspacesHelper(); + const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); + const setCurrentWorkspace = useSetAtom(currentWorkspaceIdAtom); + const { createLocalWorkspace } = useWorkspacesHelper(); - const disableShortCut = router.pathname.startsWith('/404'); return ( <> { setOpenWorkspacesModal(false); }, [setOpenWorkspacesModal])} onClickWorkspace={useCallback( workspace => { + setOpenWorkspacesModal(false); setCurrentWorkspace(workspace.id); router.push({ pathname: `/workspace/[workspaceId]/all`, @@ -51,7 +48,6 @@ export function Modals() { workspaceId: workspace.id, }, }); - setOpenWorkspacesModal(false); }, [router, setCurrentWorkspace, setOpenWorkspacesModal] )} @@ -74,11 +70,11 @@ export function Modals() { setOpenCreateWorkspaceModal(false); }, [setOpenCreateWorkspaceModal])} onCreate={useCallback( - name => { - const id = createRemLocalWorkspace(name); + async name => { + const id = await createLocalWorkspace(name); setOpenCreateWorkspaceModal(false); setOpenWorkspacesModal(false); - router.push({ + return router.push({ pathname: '/workspace/[workspaceId]/all', query: { workspaceId: id, @@ -86,22 +82,13 @@ export function Modals() { }); }, [ - createRemLocalWorkspace, + createLocalWorkspace, router, setOpenCreateWorkspaceModal, setOpenWorkspacesModal, ] )} /> - {currentWorkspace?.blockSuiteWorkspace && ( - - )} ); } diff --git a/apps/web/src/utils/index.ts b/apps/web/src/utils/index.ts index dfde32aab9..af55255749 100644 --- a/apps/web/src/utils/index.ts +++ b/apps/web/src/utils/index.ts @@ -24,15 +24,21 @@ export function stringToColour(str: string) { return colour; } +const hashMap = new Map(); export const createEmptyBlockSuiteWorkspace = ( room: string, blobOptionsGetter?: BlobOptionsGetter -) => { - return new BlockSuiteWorkspace({ +): BlockSuiteWorkspace => { + if (hashMap.has(room)) { + return hashMap.get(room) as BlockSuiteWorkspace; + } + const workspace = new BlockSuiteWorkspace({ room, isSSR: typeof window === 'undefined', blobOptionsGetter, }) .register(builtInSchemas) .register(__unstableSchemas); + hashMap.set(room, workspace); + return workspace; }; diff --git a/package.json b/package.json index a7b360f2f3..2e750a49db 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test": "playwright test", "test:coverage": "cross-env COVERAGE=true pnpm test -- --forbid-only", "test:unit": "vitest --run", + "test:unit:ui": "vitest --ui", "test:unit:coverage": "vitest run --coverage", "postinstall": "husky install", "notify": "node scripts/notify.mjs", @@ -37,6 +38,7 @@ "@typescript-eslint/parser": "^5.54.0", "@vitejs/plugin-react": "^3.1.0", "@vitest/coverage-istanbul": "^0.28.5", + "@vitest/ui": "^0.29.2", "concurrently": "^7.6.0", "cross-env": "^7.0.3", "eslint": "^8.35.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21a3574ed4..6dcc1d8fb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,7 @@ importers: '@typescript-eslint/parser': ^5.54.0 '@vitejs/plugin-react': ^3.1.0 '@vitest/coverage-istanbul': ^0.28.5 + '@vitest/ui': ^0.29.2 concurrently: ^7.6.0 cross-env: ^7.0.3 eslint: ^8.35.0 @@ -49,7 +50,8 @@ importers: '@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi '@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu '@vitejs/plugin-react': 3.1.0_vite@4.1.4 - '@vitest/coverage-istanbul': 0.28.5_happy-dom@8.9.0 + '@vitest/coverage-istanbul': 0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi + '@vitest/ui': 0.29.2 concurrently: 7.6.0 cross-env: 7.0.3 eslint: 8.35.0 @@ -70,7 +72,7 @@ importers: react-dom: 18.2.0_react@18.2.0 typescript: 4.9.5 vite: 4.1.4_@types+node@18.14.4 - vitest: 0.28.5_happy-dom@8.9.0 + vitest: 0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi vitest-fetch-mock: 0.2.2_vitest@0.28.5 apps/desktop: @@ -3908,6 +3910,10 @@ packages: fsevents: 2.3.2 dev: true + /@polka/url/1.0.0-next.21: + resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + dev: true + /@popperjs/core/2.11.6: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false @@ -5894,12 +5900,12 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.12 magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 4.1.4_@types+node@18.14.4 + vite: 4.1.4 transitivePeerDependencies: - supports-color dev: true - /@vitest/coverage-istanbul/0.28.5_happy-dom@8.9.0: + /@vitest/coverage-istanbul/0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi: resolution: {integrity: sha512-na1pkr3AVrdFflzuBXsBh1MvBfhSMrv4nfd4N8rm0HEJlvlbQc+GiqNwtwzfO8TPsXxcjNphSIMp5wvCy+0xrQ==} dependencies: istanbul-lib-coverage: 3.2.0 @@ -5908,7 +5914,7 @@ packages: istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 test-exclude: 6.0.0 - vitest: 0.28.5_happy-dom@8.9.0 + vitest: 0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi transitivePeerDependencies: - '@edge-runtime/vm' - '@vitest/browser' @@ -5945,6 +5951,16 @@ packages: tinyspy: 1.1.1 dev: true + /@vitest/ui/0.29.2: + resolution: {integrity: sha512-GpCExCMptrS1z3Xf6kz35Xdvjc2eTBy9OIIwW3HjePVxw9Q++ZoEaIBVimRTTGzSe40XiAI/ZyR0H0Ya9brqLA==} + dependencies: + fast-glob: 3.2.12 + flatted: 3.2.7 + pathe: 1.1.0 + picocolors: 1.0.0 + sirv: 2.0.2 + dev: true + /@vitest/utils/0.28.5: resolution: {integrity: sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==} dependencies: @@ -10740,6 +10756,11 @@ packages: engines: {node: '>=4'} dev: true + /mrmime/1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + /ms/2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -12596,6 +12617,15 @@ packages: semver: 7.0.0 dev: true + /sirv/2.0.2: + resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.0 + dev: true + /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -13277,6 +13307,11 @@ packages: ieee754: 1.2.1 dev: true + /totalist/3.0.0: + resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==} + engines: {node: '>=6'} + dev: true + /tr46/0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -13835,12 +13870,12 @@ packages: vitest: '>=0.16.0' dependencies: cross-fetch: 3.1.5 - vitest: 0.28.5_happy-dom@8.9.0 + vitest: 0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi transitivePeerDependencies: - encoding dev: true - /vitest/0.28.5_happy-dom@8.9.0: + /vitest/0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi: resolution: {integrity: sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==} engines: {node: '>=v14.16.0'} hasBin: true @@ -13868,6 +13903,7 @@ packages: '@vitest/expect': 0.28.5 '@vitest/runner': 0.28.5 '@vitest/spy': 0.28.5 + '@vitest/ui': 0.29.2 '@vitest/utils': 0.28.5 acorn: 8.8.2 acorn-walk: 8.2.0 diff --git a/tests/local-first-avatar.spec.ts b/tests/local-first-avatar.spec.ts index 2ebb54efec..2da026a1c3 100644 --- a/tests/local-first-avatar.spec.ts +++ b/tests/local-first-avatar.spec.ts @@ -17,7 +17,7 @@ test.describe('Local first create page', () => { await page.getByTestId('create-workspace-button').click(); await page.getByTestId('workspace-name').click(); await page.getByTestId('workspace-card').nth(1).click(); - await page.getByText('Workspace Setting').click(); + await page.getByTestId('slider-bar-workspace-setting-button').click(); await page .getByTestId('upload-avatar') .setInputFiles('./tests/fixtures/smile.png');