diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index 8685bc2434..449e9e7aeb 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -46,7 +46,11 @@ rootWorkspacesMetadataAtom.onMount = setAtom => { const id = setTimeout(() => { setAtom(metadata => { if (abortController.signal.aborted) return metadata; - if (metadata.length === 0) { + if ( + metadata.length === 0 && + localStorage.getItem('is-first-open') === null + ) { + localStorage.setItem('is-first-open', 'false'); const newMetadata = createFirst(); logger.info('create first workspace', newMetadata); return newMetadata; diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/general/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/general/index.tsx index d7ff2bef5d..b2945caf6d 100644 --- a/apps/web/src/components/affine/workspace-setting-detail/panel/general/index.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/general/index.tsx @@ -1,7 +1,6 @@ import { Button, toast } from '@affine/component'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { ArrowRightSmallIcon, DeleteIcon, @@ -12,7 +11,6 @@ import { import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import clsx from 'clsx'; -import { useAtomValue } from 'jotai'; import type React from 'react'; import { useEffect, useState } from 'react'; @@ -88,7 +86,6 @@ export const GeneralPanel: React.FC = ({ workspace.blockSuiteWorkspace ); - const isLastWorkspace = useAtomValue(rootWorkspacesMetadataAtom).length === 1; return ( <>
@@ -225,14 +222,6 @@ export const GeneralPanel: React.FC = ({
{t['Delete Workspace Label Hint']()}
- {isOwner && isLastWorkspace && ( -
- {t['com.affine.workspace.cannot-delete']()} -
- )}
@@ -243,7 +232,6 @@ export const GeneralPanel: React.FC = ({ type="warning" data-testid="delete-workspace-button" size="middle" - disabled={isLastWorkspace} icon={} onClick={() => { setShowDelete(true); diff --git a/apps/web/src/hooks/__tests__/index.spec.tsx b/apps/web/src/hooks/__tests__/index.spec.tsx index bff431806a..78dbee0277 100644 --- a/apps/web/src/hooks/__tests__/index.spec.tsx +++ b/apps/web/src/hooks/__tests__/index.spec.tsx @@ -191,22 +191,20 @@ describe('useWorkspaces', () => { const { result } = renderHook(() => useAppHelper(), { wrapper: ProviderWrapper, }); - // next tick - await new Promise(resolve => setTimeout(resolve, 100)); { const workspaces = await store.get(workspacesAtom); - expect(workspaces.length).toEqual(1); + expect(workspaces.length).toEqual(0); } await result.current.createLocalWorkspace('test'); { const workspaces = await store.get(workspacesAtom); - expect(workspaces.length).toEqual(2); + expect(workspaces.length).toEqual(1); } const { result: result2 } = renderHook(() => useWorkspaces(), { wrapper: ProviderWrapper, }); - expect(result2.current.length).toEqual(2); - const firstWorkspace = result2.current[1]; + expect(result2.current.length).toEqual(1); + const firstWorkspace = result2.current[0]; expect(firstWorkspace.flavour).toBe('local'); assert(firstWorkspace.flavour === WorkspaceFlavour.LOCAL); expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test'); diff --git a/apps/web/src/layouts/workspace-layout.tsx b/apps/web/src/layouts/workspace-layout.tsx index 090bb8d72c..ee8fbb59b0 100644 --- a/apps/web/src/layouts/workspace-layout.tsx +++ b/apps/web/src/layouts/workspace-layout.tsx @@ -58,7 +58,10 @@ import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useRouterHelper } from '../hooks/use-router-helper'; import { useRouterTitle } from '../hooks/use-router-title'; import { useWorkspaces } from '../hooks/use-workspaces'; -import { ModalProvider } from '../providers/modal-provider'; +import { + AllWorkspaceModals, + CurrentWorkspaceModals, +} from '../providers/modal-provider'; import { pathGenerator, publicPathGenerator } from '../shared'; import { toast } from '../utils'; @@ -178,7 +181,10 @@ export const CurrentWorkspaceContext = ({ return () => { clearTimeout(id); }; - }, [push, exist]); + }, [push, exist, metadata.length]); + if (metadata.length === 0) { + return ; + } if (!router.isReady) { return ; } @@ -266,9 +272,10 @@ export const WorkspaceLayout: FC = {/* load all workspaces is costly, do not block the whole UI */} + {/* fixme(himself65): don't re-render whole modals */} - + diff --git a/apps/web/src/pages/index.tsx b/apps/web/src/pages/index.tsx index 3642e63361..87f2f6a51f 100644 --- a/apps/web/src/pages/index.tsx +++ b/apps/web/src/pages/index.tsx @@ -1,3 +1,4 @@ +import { WorkspaceFallback } from '@affine/component/workspace'; import { DebugLogger } from '@affine/debug'; import { WorkspaceSubPath } from '@affine/workspace/type'; import type { NextPage } from 'next'; @@ -7,6 +8,8 @@ import { Suspense, useEffect } from 'react'; import { PageLoading } from '../components/pure/loading'; import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper'; import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces'; +import { AllWorkspaceContext } from '../layouts/workspace-layout'; +import { AllWorkspaceModals } from '../providers/modal-provider'; const logger = new DebugLogger('index-page'); @@ -62,7 +65,13 @@ const IndexPageInner = () => { } }, [helper, jumpToPage, jumpToSubPath, router, workspaces]); - return ; + return ( + }> + + + + + ); }; const IndexPage: NextPage = () => { diff --git a/apps/web/src/pages/workspace/[workspaceId]/setting.tsx b/apps/web/src/pages/workspace/[workspaceId]/setting.tsx index 2f50d44d8c..a983aaf4f1 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/setting.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/setting.tsx @@ -1,5 +1,4 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import type { SettingPanel } from '@affine/workspace/type'; import { settingPanel, @@ -7,7 +6,7 @@ import { WorkspaceSubPath, } from '@affine/workspace/type'; import { assertExists } from '@blocksuite/store'; -import { useAtom, useAtomValue } from 'jotai'; +import { useAtom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; import Head from 'next/head'; import type { NextRouter } from 'next/router'; @@ -21,7 +20,6 @@ import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-wo import { useAppHelper } from '../../../hooks/use-workspaces'; import { WorkspaceLayout } from '../../../layouts/workspace-layout'; import type { NextPageWithLayout } from '../../../shared'; -import { toast } from '../../../utils'; const settingPanelAtom = atomWithStorage( 'workspaceId', @@ -77,7 +75,6 @@ function useTabRouterSync( const SettingPage: NextPageWithLayout = () => { const router = useRouter(); - const workspaceIds = useAtomValue(rootWorkspacesMetadataAtom); const [currentWorkspace] = useCurrentWorkspace(); const t = useAFFiNEI18N(); const [currentTab, setCurrentTab] = useAtom(settingPanelAtom); @@ -102,13 +99,8 @@ const SettingPage: NextPageWithLayout = () => { const onDeleteWorkspace = useCallback(async () => { assertExists(currentWorkspace); const workspaceId = currentWorkspace.id; - if (workspaceIds.length === 1 && workspaceId === workspaceIds[0].id) { - toast(t['You cannot delete the last workspace']()); - throw new Error('You cannot delete the last workspace'); - } else { - return await helper.deleteWorkspace(workspaceId); - } - }, [currentWorkspace, helper, t, workspaceIds]); + return helper.deleteWorkspace(workspaceId); + }, [currentWorkspace, helper]); const onTransformWorkspace = useOnTransformWorkspace(); if (!router.isReady) { return ; diff --git a/apps/web/src/providers/modal-provider.tsx b/apps/web/src/providers/modal-provider.tsx index 04a1084453..2e64588540 100644 --- a/apps/web/src/providers/modal-provider.tsx +++ b/apps/web/src/providers/modal-provider.tsx @@ -1,14 +1,16 @@ import { getEnvironment } from '@affine/env'; -import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; +import { + rootCurrentWorkspaceIdAtom, + rootWorkspacesMetadataAtom, +} from '@affine/workspace/atom'; import { WorkspaceSubPath } from '@affine/workspace/type'; import { arrayMove } from '@dnd-kit/sortable'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useAtom, useSetAtom } from 'jotai'; import { useRouter } from 'next/router'; import type { ReactElement } from 'react'; import { lazy, Suspense, useCallback, useTransition } from 'react'; import { - currentWorkspaceIdAtom, openCreateWorkspaceModalAtom, openDisableCloudAlertModalAtom, openOnboardingModalAtom, @@ -17,7 +19,6 @@ import { import { useAffineLogIn } from '../hooks/affine/use-affine-log-in'; 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 } from '../hooks/use-workspaces'; @@ -47,14 +48,7 @@ const OnboardingModal = lazy(() => })) ); -export function Modals() { - const [openWorkspacesModal, setOpenWorkspacesModal] = useAtom( - openWorkspacesModalAtom - ); - const [openCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom( - openCreateWorkspaceModalAtom - ); - +export function CurrentWorkspaceModals() { const [openDisableCloudAlertModal, setOpenDisableCloudAlertModal] = useAtom( openDisableCloudAlertModalAtom ); @@ -62,14 +56,6 @@ export function Modals() { openOnboardingModalAtom ); - const router = useRouter(); - const { jumpToSubPath } = useRouterHelper(router); - const user = useCurrentUser(); - const workspaces = useWorkspaces(); - const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom); - const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); - const [, setCurrentWorkspace] = useCurrentWorkspace(); - const [transitioning, transition] = useTransition(); const env = getEnvironment(); const onCloseOnboardingModal = useCallback(() => { setOpenOnboardingModal(false); @@ -92,14 +78,39 @@ export function Modals() { /> )} + + ); +} +export const AllWorkspaceModals = (): ReactElement => { + const [openWorkspacesModal, setOpenWorkspacesModal] = useAtom( + openWorkspacesModalAtom + ); + const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom( + openCreateWorkspaceModalAtom + ); + + const router = useRouter(); + const { jumpToSubPath } = useRouterHelper(router); + const user = useCurrentUser(); + const workspaces = useWorkspaces(); + const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom); + const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom( + rootCurrentWorkspaceIdAtom + ); + const [transitioning, transition] = useTransition(); + return ( + <> { setOpenWorkspacesModal(false); }, [setOpenWorkspacesModal])} @@ -118,18 +129,18 @@ export function Modals() { onClickWorkspace={useCallback( workspace => { setOpenWorkspacesModal(false); - setCurrentWorkspace(workspace.id); + setCurrentWorkspaceId(workspace.id); jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); }, - [jumpToSubPath, setCurrentWorkspace, setOpenWorkspacesModal] + [jumpToSubPath, setCurrentWorkspaceId, setOpenWorkspacesModal] )} onClickWorkspaceSetting={useCallback( workspace => { setOpenWorkspacesModal(false); - setCurrentWorkspace(workspace.id); + setCurrentWorkspaceId(workspace.id); jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING); }, - [jumpToSubPath, setCurrentWorkspace, setOpenWorkspacesModal] + [jumpToSubPath, setCurrentWorkspaceId, setOpenWorkspacesModal] )} onClickLogin={useAffineLogIn()} onClickLogout={useAffineLogOut()} @@ -143,7 +154,7 @@ export function Modals() { { setOpenCreateWorkspaceModal(false); }, [setOpenCreateWorkspaceModal])} @@ -151,12 +162,12 @@ export function Modals() { async id => { setOpenCreateWorkspaceModal(false); setOpenWorkspacesModal(false); - setCurrentWorkspace(id); + setCurrentWorkspaceId(id); return jumpToSubPath(id, WorkspaceSubPath.ALL); }, [ jumpToSubPath, - setCurrentWorkspace, + setCurrentWorkspaceId, setOpenCreateWorkspaceModal, setOpenWorkspacesModal, ] @@ -165,12 +176,4 @@ export function Modals() { ); -} - -export const ModalProvider = (): ReactElement => { - return ( - <> - - - ); }; diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json index a9a96ce892..d7a5ac31fc 100644 --- a/packages/i18n/src/resources/en.json +++ b/packages/i18n/src/resources/en.json @@ -290,7 +290,6 @@ "com.affine.lastMonth": "Last month", "com.affine.lastYear": "Last year", "com.affine.earlier": "Earlier", - "com.affine.workspace.cannot-delete": "You cannot delete the last workspace", "FILE_ALREADY_EXISTS": "File already exists", "others": "Others", "Update Available": "Update available", diff --git a/packages/workspace/src/atom.ts b/packages/workspace/src/atom.ts index 70d9224514..3b09430b6a 100644 --- a/packages/workspace/src/atom.ts +++ b/packages/workspace/src/atom.ts @@ -36,6 +36,9 @@ rootCurrentWorkspaceIdAtom.onMount = set => { const value = url.split('/')[2]; if (value) { set(value); + if (typeof window !== 'undefined') { + localStorage.setItem('last_workspace_id', value); + } } else { set(null); } diff --git a/tests/parallels/affine/affine-lost-auth.spec.ts b/tests/parallels/affine/affine-lost-auth.spec.ts index 5eff02f451..a83c3eac18 100644 --- a/tests/parallels/affine/affine-lost-auth.spec.ts +++ b/tests/parallels/affine/affine-lost-auth.spec.ts @@ -1,4 +1,5 @@ import { test } from '@affine-test/kit/playwright'; +import { expect } from '@playwright/test'; import { openHomePage } from '../../libs/load-page'; import { waitMarkdownImported } from '../../libs/page-logic'; @@ -15,5 +16,5 @@ test('authorization expired', async ({ page }) => { await clickSideBarAllPageButton(page); await page.evaluate(() => localStorage.removeItem('affine-login-v2')); await openHomePage(page); - await waitMarkdownImported(page); + await expect(page.getByTestId('new-workspace')).toBeVisible(); }); diff --git a/tests/parallels/local-first-delete-workspace.spec.ts b/tests/parallels/local-first-delete-workspace.spec.ts index c6fe2e4b44..ddf4b91b33 100644 --- a/tests/parallels/local-first-delete-workspace.spec.ts +++ b/tests/parallels/local-first-delete-workspace.spec.ts @@ -47,12 +47,31 @@ test('Create new workspace, then delete it', async ({ page }) => { await assertCurrentWorkspaceFlavour('local', page); }); -test('Should not delete the last one workspace', async ({ page }) => { +test('Delete last workspace', async ({ page }) => { await openHomePage(page); await waitMarkdownImported(page); await clickSideBarSettingButton(page); - await expect( - page.getByTestId('warn-cannot-delete-last-workspace').isVisible() - ).toBeTruthy(); - await assertCurrentWorkspaceFlavour('local', 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); + const promise = page + .getByTestId('affine-toast') + .waitFor({ state: 'attached' }); + await page.getByTestId('delete-workspace-confirm-button').click(); + await promise; + await page.reload(); + await expect(page.getByTestId('new-workspace')).toBeVisible(); + await page.getByTestId('new-workspace').click(); + await page.type('[data-testid="create-workspace-input"]', 'Test Workspace'); + await page.getByTestId('create-workspace-create-button').click(); + await page.waitForTimeout(1000); + await page.waitForSelector('[data-testid="workspace-name"]'); + expect(await page.getByTestId('workspace-name').textContent()).toBe( + 'Test Workspace' + ); });