Files
AFFiNE-Mirror/apps/web/src/layouts/workspace-layout.tsx
Alex Yang 8f590ff360 fix: first workspace not found (#3258)
(cherry picked from commit 071d582250)
2023-07-17 13:32:16 +08:00

370 lines
12 KiB
TypeScript

import { Content, displayFlex } from '@affine/component';
import { AffineWatermark } from '@affine/component/affine-watermark';
import { appSidebarResizingAtom } from '@affine/component/app-sidebar';
import { BlockHubWrapper } from '@affine/component/block-hub';
import { NotificationCenter } from '@affine/component/notification-center';
import type { DraggableTitleCellData } from '@affine/component/page-list';
import { StyledTitleLink } from '@affine/component/page-list';
import {
MainContainer,
ToolContainer,
WorkspaceFallback,
} from '@affine/component/workspace';
import {
DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX,
isDesktop,
} from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
rootBlockHubAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils';
import { nanoid } from '@blocksuite/store';
import type { DragEndEvent } from '@dnd-kit/core';
import {
DndContext,
DragOverlay,
MouseSensor,
pointerWithin,
useDndContext,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/workspace';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/plugin-infra/manager';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
import { useRouter } from 'next/router';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
import { WorkspaceAdapters } from '../adapters/workspace';
import {
openQuickSearchModalAtom,
openSettingModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { useTrackRouterHistoryEffect } from '../atoms/history';
import { useAppSetting } from '../atoms/settings';
import { AppContainer } from '../components/affine/app-container';
import type { IslandItemNames } from '../components/pure/help-island';
import { HelpIsland } from '../components/pure/help-island';
import { processCollectionsDrag } from '../components/pure/workspace-slider-bar/collections';
import {
DROPPABLE_SIDEBAR_TRASH,
RootAppSidebar,
} from '../components/root-app-sidebar';
import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useRouterHelper } from '../hooks/use-router-helper';
import { useRouterTitle } from '../hooks/use-router-title';
import {
AllWorkspaceModals,
CurrentWorkspaceModals,
} from '../providers/modal-provider';
import { pathGenerator, publicPathGenerator } from '../shared';
import { toast } from '../utils';
const QuickSearchModal = lazy(() =>
import('../components/pure/quick-search-modal').then(module => ({
default: module.QuickSearchModal,
}))
);
function DefaultProvider({ children }: PropsWithChildren) {
return <>{children}</>;
}
export const QuickSearch: FC = () => {
const [currentWorkspace] = useCurrentWorkspace();
const router = useRouter();
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
openQuickSearchModalAtom
);
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
if (!blockSuiteWorkspace) {
return null;
}
return (
<QuickSearchModal
workspace={currentWorkspace}
open={openQuickSearchModal}
setOpen={setOpenQuickSearchModalAtom}
router={router}
/>
);
};
declare global {
// eslint-disable-next-line no-var
var HALTING_PROBLEM_TIMEOUT: number;
}
if (globalThis.HALTING_PROBLEM_TIMEOUT === undefined) {
globalThis.HALTING_PROBLEM_TIMEOUT = 1000;
}
export const CurrentWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
const router = useRouter();
const push = router.push;
// fixme(himself65): this is not a good way to handle this,
// need a better way to check whether this workspace really exist.
useEffect(() => {
const id = setTimeout(() => {
if (!exist) {
push('/').catch(err => {
console.error(err);
});
globalThis.HALTING_PROBLEM_TIMEOUT <<= 1;
}
}, globalThis.HALTING_PROBLEM_TIMEOUT);
return () => {
clearTimeout(id);
};
}, [push, exist, metadata.length]);
if (metadata.length === 0) {
return <WorkspaceFallback key="no-workspace" />;
}
if (!router.isReady) {
return <WorkspaceFallback key="router-is-loading" />;
}
if (!workspaceId) {
return <WorkspaceFallback key="finding-workspace-id" />;
}
if (!exist) {
return <WorkspaceFallback key="workspace-not-found" />;
}
return <>{children}</>;
};
export const WorkspaceLayout: FC<PropsWithChildren> =
function WorkspacesSuspense({ children }) {
useTrackRouterHistoryEffect();
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const meta = useMemo(
() => jotaiWorkspaces.find(x => x.id === currentWorkspaceId),
[currentWorkspaceId, jotaiWorkspaces]
);
const Provider =
(meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider;
return (
<>
{/* load all workspaces is costly, do not block the whole UI */}
<Suspense fallback={null}>
<AllWorkspaceModals />
<CurrentWorkspaceContext>
{/* fixme(himself65): don't re-render whole modals */}
<CurrentWorkspaceModals key={currentWorkspaceId} />
</CurrentWorkspaceContext>
</Suspense>
<CurrentWorkspaceContext>
<Suspense fallback={<WorkspaceFallback />}>
<Provider>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</Provider>
</Suspense>
</CurrentWorkspaceContext>
</>
);
};
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
useEffect(() => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(
`${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
);
if (page && page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
jumpOnce: false,
});
setCurrentPageId(currentPageId);
jumpToPage(currentWorkspace.id, page.id).catch(err => {
console.error(err);
});
}
}, [
currentPageId,
currentWorkspace,
jumpToPage,
router.query.pageId,
setCurrentPageId,
]);
const { openPage } = useRouterHelper(router);
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace
);
const isPublicWorkspace =
router.pathname.split('/')[1] === 'public-workspace';
const title = useRouterTitle(router);
const handleCreatePage = useCallback(() => {
return helper.createPage(nanoid());
}, [helper]);
const handleOpenWorkspaceListModal = useCallback(() => {
setOpenWorkspacesModal(true);
}, [setOpenWorkspacesModal]);
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
const handleOpenQuickSearchModal = useCallback(() => {
setOpenQuickSearchModalAtom(true);
}, [setOpenQuickSearchModalAtom]);
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
const handleOpenSettingModal = useCallback(() => {
setOpenSettingModalAtom({
activeTab: 'appearance',
workspaceId: null,
open: true,
});
}, [setOpenSettingModalAtom]);
const resizing = useAtomValue(appSidebarResizingAtom);
const sensors = useSensors(
// Delay 10ms after mousedown
// Otherwise clicks would be intercepted
useSensor(MouseSensor, {
activationConstraint: {
delay: 500,
tolerance: 10,
},
})
);
const { removeToTrash: moveToTrash } = useBlockSuiteMetaHelper(
currentWorkspace.blockSuiteWorkspace
);
const t = useAFFiNEI18N();
const showList: IslandItemNames[] = isDesktop
? ['whatNew', 'contact', 'guide']
: ['whatNew', 'contact'];
const handleDragEnd = useCallback(
(e: DragEndEvent) => {
// Drag page into trash folder
if (
e.over?.id === DROPPABLE_SIDEBAR_TRASH &&
String(e.active.id).startsWith('page-list-item-')
) {
const { pageId } = e.active.data.current as DraggableTitleCellData;
// TODO-Doma
// Co-locate `moveToTrash` with the toast for reuse, as they're always used together
moveToTrash(pageId);
toast(t['Successfully deleted']());
}
// Drag page into Collections
processCollectionsDrag(e);
},
[moveToTrash, t]
);
const [appSetting] = useAppSetting();
return (
<>
<Head>
<title>{title}</title>
</Head>
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
<DndContext
sensors={sensors}
collisionDetection={pointerWithin}
onDragEnd={handleDragEnd}
>
<AppContainer resizing={resizing}>
<RootAppSidebar
isPublicWorkspace={isPublicWorkspace}
onOpenQuickSearchModal={handleOpenQuickSearchModal}
onOpenSettingModal={handleOpenSettingModal}
currentWorkspace={currentWorkspace}
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={useCallback(
(pageId: string) => {
assertExists(currentWorkspace);
return openPage(currentWorkspace.id, pageId);
},
[currentWorkspace, openPage]
)}
createPage={handleCreatePage}
currentPath={router.asPath.split('?')[0]}
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
/>
<MainContainer padding={appSetting.clientBorder}>
{children}
<ToolContainer>
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
{!isPublicWorkspace && (
<HelpIsland
showList={router.query.pageId ? undefined : showList}
/>
)}
</ToolContainer>
<AffineWatermark />
</MainContainer>
</AppContainer>
<PageListTitleCellDragOverlay />
</DndContext>
<QuickSearch />
{runtimeConfig.enableNotificationCenter && <NotificationCenter />}
</>
);
};
function PageListTitleCellDragOverlay() {
const { active } = useDndContext();
const renderChildren = useCallback(
({ icon, pageTitle }: DraggableTitleCellData) => {
return (
<StyledTitleLink>
{icon}
<Content ellipsis={true} color="inherit">
{pageTitle}
</Content>
</StyledTitleLink>
);
},
[]
);
return (
<DragOverlay
style={{
zIndex: 1001,
backgroundColor: 'var(--affine-black-10)',
padding: '0 30px',
cursor: 'default',
borderRadius: 10,
...displayFlex('flex-start', 'center'),
}}
dropAnimation={null}
>
{active
? renderChildren(active.data.current as DraggableTitleCellData)
: null}
</DragOverlay>
);
}