feat(core): import template (#8000)

This commit is contained in:
EYHN
2024-08-29 04:01:35 +00:00
parent 4ec45a247e
commit b96ad57568
67 changed files with 1835 additions and 974 deletions
@@ -0,0 +1,33 @@
import { Entity, LiveData } from '@toeverything/infra';
import type { CreateWorkspaceCallbackPayload } from '../types';
export class CreateWorkspaceDialog extends Entity {
readonly mode$ = new LiveData<'new' | 'add'>('new');
readonly isOpen$ = new LiveData(false);
readonly callback$ = new LiveData<
(data: CreateWorkspaceCallbackPayload | undefined) => void
>(() => {});
open(
mode: 'new' | 'add',
callback?: (data: CreateWorkspaceCallbackPayload | undefined) => void
) {
this.callback(undefined);
this.mode$.next(mode);
this.isOpen$.next(true);
if (callback) {
this.callback$.next(callback);
}
}
callback(payload: CreateWorkspaceCallbackPayload | undefined) {
this.callback$.value(payload);
this.callback$.next(() => {});
}
close() {
this.isOpen$.next(false);
this.callback(undefined);
}
}
@@ -0,0 +1,12 @@
import type { Framework } from '@toeverything/infra';
import { CreateWorkspaceDialog } from './entities/dialog';
import { CreateWorkspaceDialogService } from './services/dialog';
export { CreateWorkspaceDialogService } from './services/dialog';
export type { CreateWorkspaceCallbackPayload } from './types';
export { CreateWorkspaceDialogProvider } from './views/dialog';
export function configureCreateWorkspaceModule(framework: Framework) {
framework.service(CreateWorkspaceDialogService).entity(CreateWorkspaceDialog);
}
@@ -0,0 +1,7 @@
import { Service } from '@toeverything/infra';
import { CreateWorkspaceDialog } from '../entities/dialog';
export class CreateWorkspaceDialogService extends Service {
dialog = this.framework.createEntity(CreateWorkspaceDialog);
}
@@ -0,0 +1,7 @@
import type { WorkspaceMetadata } from '@toeverything/infra';
export type CreateWorkspaceMode = 'add' | 'new';
export type CreateWorkspaceCallbackPayload = {
meta: WorkspaceMetadata;
defaultDocId?: string;
};
@@ -0,0 +1,77 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const header = style({
position: 'relative',
marginTop: '44px',
});
export const subTitle = style({
fontSize: cssVar('fontSm'),
color: cssVar('textPrimaryColor'),
fontWeight: 600,
});
export const avatarWrapper = style({
display: 'flex',
margin: '10px 0',
});
export const workspaceNameWrapper = style({
display: 'flex',
flexDirection: 'column',
gap: '8px',
padding: '12px 0',
});
export const affineCloudWrapper = style({
display: 'flex',
flexDirection: 'column',
gap: '6px',
paddingTop: '10px',
});
export const card = style({
padding: '12px',
display: 'flex',
alignItems: 'center',
borderRadius: '8px',
backgroundColor: cssVar('backgroundSecondaryColor'),
minHeight: '114px',
position: 'relative',
});
export const cardText = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '100%',
gap: '12px',
});
export const cardTitle = style({
fontSize: cssVar('fontBase'),
color: cssVar('textPrimaryColor'),
display: 'flex',
justifyContent: 'space-between',
});
export const cardDescription = style({
fontSize: cssVar('fontXs'),
color: cssVar('textSecondaryColor'),
maxWidth: '288px',
});
export const cloudTips = style({
fontSize: cssVar('fontXs'),
color: cssVar('textSecondaryColor'),
});
export const cloudSvgContainer = style({
width: '146px',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
position: 'absolute',
bottom: '0',
right: '0',
pointerEvents: 'none',
});
@@ -0,0 +1,272 @@
import { Avatar, ConfirmModal, Input, Switch, toast } from '@affine/component';
import type { ConfirmModalProps } from '@affine/component/ui/modal';
import { authAtom } from '@affine/core/atoms';
import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { track } from '@affine/core/mixpanel';
import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import {
initEmptyPage,
useLiveData,
useService,
WorkspacesService,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import type { KeyboardEvent } from 'react';
import { useCallback, useLayoutEffect, useState } from 'react';
import { buildShowcaseWorkspace } from '../../../bootstrap/first-app-data';
import { AuthService } from '../../../modules/cloud';
import { _addLocalWorkspace } from '../../../modules/workspace-engine';
import { CreateWorkspaceDialogService } from '../services/dialog';
import * as styles from './dialog.css';
const logger = new DebugLogger('CreateWorkspaceModal');
interface NameWorkspaceContentProps extends ConfirmModalProps {
loading: boolean;
onConfirmName: (
name: string,
workspaceFlavour: WorkspaceFlavour,
avatar?: File
) => void;
}
const shouldEnableCloud = !runtimeConfig.allowLocalWorkspace;
const NameWorkspaceContent = ({
loading,
onConfirmName,
...props
}: NameWorkspaceContentProps) => {
const t = useI18n();
const [workspaceName, setWorkspaceName] = useState('');
const [enable, setEnable] = useState(shouldEnableCloud);
const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$);
const setOpenSignIn = useSetAtom(authAtom);
const openSignInModal = useCallback(() => {
setOpenSignIn(state => ({
...state,
openModal: true,
}));
}, [setOpenSignIn]);
const onSwitchChange = useCallback(
(checked: boolean) => {
if (loginStatus !== 'authenticated') {
return openSignInModal();
}
return setEnable(checked);
},
[loginStatus, openSignInModal]
);
const handleCreateWorkspace = useCallback(() => {
onConfirmName(
workspaceName,
enable ? WorkspaceFlavour.AFFINE_CLOUD : WorkspaceFlavour.LOCAL
);
}, [enable, onConfirmName, workspaceName]);
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && workspaceName) {
handleCreateWorkspace();
}
},
[handleCreateWorkspace, workspaceName]
);
// Currently, when we create a new workspace and upload an avatar at the same time,
// an error occurs after the creation is successful: get blob 404 not found
return (
<ConfirmModal
defaultOpen={true}
title={t['com.affine.nameWorkspace.title']()}
description={t['com.affine.nameWorkspace.description']()}
cancelText={t['com.affine.nameWorkspace.button.cancel']()}
confirmText={t['com.affine.nameWorkspace.button.create']()}
confirmButtonOptions={{
variant: 'primary',
loading,
disabled: !workspaceName,
['data-testid' as string]: 'create-workspace-create-button',
}}
closeButtonOptions={{
['data-testid' as string]: 'create-workspace-close-button',
}}
onConfirm={handleCreateWorkspace}
{...props}
>
<div className={styles.avatarWrapper}>
<Avatar size={56} name={workspaceName} colorfulFallback />
</div>
<div className={styles.workspaceNameWrapper}>
<div className={styles.subTitle}>
{t['com.affine.nameWorkspace.subtitle.workspace-name']()}
</div>
<Input
autoFocus
data-testid="create-workspace-input"
onKeyDown={handleKeyDown}
placeholder={t['com.affine.nameWorkspace.placeholder']()}
maxLength={64}
minLength={0}
onChange={setWorkspaceName}
size="large"
/>
</div>
<div className={styles.affineCloudWrapper}>
<div className={styles.subTitle}>{t['AFFiNE Cloud']()}</div>
<div className={styles.card}>
<div className={styles.cardText}>
<div className={styles.cardTitle}>
<span>{t['com.affine.nameWorkspace.affine-cloud.title']()}</span>
<Switch
checked={enable}
onChange={onSwitchChange}
disabled={shouldEnableCloud}
/>
</div>
<div className={styles.cardDescription}>
{t['com.affine.nameWorkspace.affine-cloud.description']()}
</div>
</div>
<div className={styles.cloudSvgContainer}>
<CloudSvg />
</div>
</div>
{shouldEnableCloud ? (
<a
className={styles.cloudTips}
href={runtimeConfig.downloadUrl}
target="_blank"
rel="noreferrer"
>
{t['com.affine.nameWorkspace.affine-cloud.web-tips']()}
</a>
) : null}
</div>
</ConfirmModal>
);
};
const CreateWorkspaceDialog = () => {
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
const mode = useLiveData(createWorkspaceDialogService.dialog.mode$);
const t = useI18n();
const workspacesService = useService(WorkspacesService);
const [loading, setLoading] = useState(false);
// TODO(@Peng): maybe refactor using xstate?
useLayoutEffect(() => {
let canceled = false;
// if mode changed, reset step
if (mode === 'add') {
// a hack for now
// when adding a workspace, we will immediately let user select a db file
// after it is done, it will effectively add a new workspace to app-data folder
// so after that, we will be able to load it via importLocalWorkspace
(async () => {
if (!apis) {
return;
}
logger.info('load db file');
const result = await apis.dialog.loadDBFile();
if (result.workspaceId && !canceled) {
_addLocalWorkspace(result.workspaceId);
workspacesService.list.revalidate();
createWorkspaceDialogService.dialog.callback({
meta: {
flavour: WorkspaceFlavour.LOCAL,
id: result.workspaceId,
},
});
} else if (result.error || result.canceled) {
if (result.error) {
toast(t[result.error]());
}
createWorkspaceDialogService.dialog.callback(undefined);
createWorkspaceDialogService.dialog.close();
}
})().catch(err => {
console.error(err);
});
}
return () => {
canceled = true;
};
}, [createWorkspaceDialogService, mode, t, workspacesService]);
const onConfirmName = useAsyncCallback(
async (name: string, workspaceFlavour: WorkspaceFlavour) => {
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
if (loading) return;
setLoading(true);
// this will be the last step for web for now
// fix me later
if (runtimeConfig.enablePreloading) {
const { meta, defaultDocId } = await buildShowcaseWorkspace(
workspacesService,
workspaceFlavour,
name
);
createWorkspaceDialogService.dialog.callback({ meta, defaultDocId });
} else {
let defaultDocId: string | undefined = undefined;
const meta = await workspacesService.create(
workspaceFlavour,
async workspace => {
workspace.meta.initialize();
workspace.meta.setName(name);
const page = workspace.createDoc();
defaultDocId = page.id;
initEmptyPage(page);
}
);
createWorkspaceDialogService.dialog.callback({ meta, defaultDocId });
}
createWorkspaceDialogService.dialog.close();
setLoading(false);
},
[createWorkspaceDialogService.dialog, loading, workspacesService]
);
const onOpenChange = useCallback(
(open: boolean) => {
if (!open) {
createWorkspaceDialogService.dialog.close();
}
},
[createWorkspaceDialogService]
);
if (mode === 'new') {
return (
<NameWorkspaceContent
loading={loading}
open
onOpenChange={onOpenChange}
onConfirmName={onConfirmName}
/>
);
} else {
return null;
}
};
export const CreateWorkspaceDialogProvider = () => {
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
const isOpen = useLiveData(createWorkspaceDialogService.dialog.isOpen$);
return isOpen ? <CreateWorkspaceDialog /> : null;
};
@@ -0,0 +1,19 @@
import { Entity, LiveData } from '@toeverything/infra';
export class ImportTemplateDialog extends Entity {
readonly isOpen$ = new LiveData(false);
readonly template$ = new LiveData<{
workspaceId: string;
docId: string;
templateName: string;
} | null>(null);
open(workspaceId: string, docId: string, templateName: string) {
this.template$.next({ workspaceId, docId, templateName });
this.isOpen$.next(true);
}
close() {
this.isOpen$.next(false);
}
}
@@ -0,0 +1,51 @@
import {
backoffRetry,
catchErrorInto,
effect,
Entity,
fromPromise,
LiveData,
onComplete,
onStart,
} from '@toeverything/infra';
import { EMPTY, mergeMap, switchMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../../cloud';
import type { TemplateDownloaderStore } from '../store/downloader';
export class TemplateDownloader extends Entity {
constructor(private readonly store: TemplateDownloaderStore) {
super();
}
readonly isDownloading$ = new LiveData<boolean>(false);
readonly data$ = new LiveData<Uint8Array | null>(null);
readonly error$ = new LiveData<any | null>(null);
readonly download = effect(
switchMap(
({ workspaceId, docId }: { workspaceId: string; docId: string }) => {
return fromPromise(() => this.store.download(workspaceId, docId)).pipe(
mergeMap(({ data }) => {
this.data$.next(data);
return EMPTY;
}),
backoffRetry({
when: isNetworkError,
count: Infinity,
}),
backoffRetry({
when: isBackendError,
}),
catchErrorInto(this.error$),
onStart(() => {
this.isDownloading$.next(true);
this.data$.next(null);
this.error$.next(null);
}),
onComplete(() => this.isDownloading$.next(false))
);
}
)
);
}
@@ -0,0 +1,22 @@
import { type Framework, WorkspacesService } from '@toeverything/infra';
import { FetchService } from '../cloud';
import { ImportTemplateDialog } from './entities/dialog';
import { TemplateDownloader } from './entities/downloader';
import { ImportTemplateDialogService } from './services/dialog';
import { TemplateDownloaderService } from './services/downloader';
import { ImportTemplateService } from './services/import';
import { TemplateDownloaderStore } from './store/downloader';
export { ImportTemplateDialogService } from './services/dialog';
export { ImportTemplateDialogProvider } from './views/dialog';
export function configureImportTemplateModule(framework: Framework) {
framework
.service(ImportTemplateDialogService)
.entity(ImportTemplateDialog)
.service(TemplateDownloaderService)
.entity(TemplateDownloader, [TemplateDownloaderStore])
.store(TemplateDownloaderStore, [FetchService])
.service(ImportTemplateService, [WorkspacesService]);
}
@@ -0,0 +1,7 @@
import { Service } from '@toeverything/infra';
import { ImportTemplateDialog } from '../entities/dialog';
export class ImportTemplateDialogService extends Service {
dialog = this.framework.createEntity(ImportTemplateDialog);
}
@@ -0,0 +1,7 @@
import { Service } from '@toeverything/infra';
import { TemplateDownloader } from '../entities/downloader';
export class TemplateDownloaderService extends Service {
downloader = this.framework.createEntity(TemplateDownloader);
}
@@ -0,0 +1,47 @@
import type { WorkspaceFlavour } from '@affine/env/workspace';
import type { WorkspaceMetadata, WorkspacesService } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
export class ImportTemplateService extends Service {
constructor(private readonly workspacesService: WorkspacesService) {
super();
}
async importToWorkspace(
workspaceMetadata: WorkspaceMetadata,
docBinary: Uint8Array
) {
const { workspace, dispose: disposeWorkspace } =
this.workspacesService.open({
metadata: workspaceMetadata,
});
await workspace.engine.waitForRootDocReady();
const newDoc = workspace.docCollection.createDoc({});
await workspace.engine.doc.storage.behavior.doc.set(
newDoc.spaceDoc.guid,
docBinary
);
disposeWorkspace();
return newDoc.id;
}
async importToNewWorkspace(
flavour: WorkspaceFlavour,
workspaceName: string,
docBinary: Uint8Array
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let docId: string = null!;
const { id: workspaceId } = await this.workspacesService.create(
flavour,
async (docCollection, _, docStorage) => {
docCollection.meta.initialize();
docCollection.meta.setName(workspaceName);
const doc = docCollection.createDoc();
docId = doc.id;
await docStorage.doc.set(doc.spaceDoc.guid, docBinary);
}
);
return { workspaceId, docId };
}
}
@@ -0,0 +1,21 @@
import { Store } from '@toeverything/infra';
import type { FetchService } from '../../cloud';
export class TemplateDownloaderStore extends Store {
constructor(private readonly fetchService: FetchService) {
super();
}
async download(workspaceId: string, docId: string) {
const response = await this.fetchService.fetch(
`/api/workspaces/${workspaceId}/docs/${docId}`,
{
priority: 'high',
} as any
);
const arrayBuffer = await response.arrayBuffer();
return { data: new Uint8Array(arrayBuffer) };
}
}
@@ -0,0 +1,48 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const dialogContainer = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
color: cssVarV2('text/primary'),
padding: '16px',
});
export const mainIcon = style({
width: 36,
height: 36,
color: cssVarV2('icon/primary'),
});
export const mainTitle = style({
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
marginTop: '16px',
fontWeight: 600,
});
export const desc = style({
textAlign: 'center',
color: cssVarV2('text/secondary'),
marginBottom: '20px',
});
export const mainButton = style({
width: '100%',
fontSize: '14px',
height: '42px',
});
export const modal = style({
maxWidth: '400px',
});
export const workspaceSelector = style({
margin: '0 -16px',
width: 'calc(100% + 32px)',
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
padding: '0 16px',
});
@@ -0,0 +1,249 @@
import { Button, Modal } from '@affine/component';
import { WorkspaceSelector } from '@affine/core/components/workspace-selector';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useWorkspaceName } from '@affine/core/hooks/use-workspace-info';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import { AllDocsIcon } from '@blocksuite/icons/rc';
import {
useLiveData,
useService,
type WorkspaceMetadata,
WorkspacesService,
} from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useCallback, useEffect, useState } from 'react';
import { AuthService } from '../../cloud';
import type { CreateWorkspaceCallbackPayload } from '../../create-workspace';
import { ImportTemplateDialogService } from '../services/dialog';
import { TemplateDownloaderService } from '../services/downloader';
import { ImportTemplateService } from '../services/import';
import * as styles from './dialog.css';
const Dialog = ({
workspaceId,
docId,
templateName,
onClose,
}: {
workspaceId: string;
docId: string;
templateName: string;
onClose?: () => void;
}) => {
const t = useI18n();
const session = useService(AuthService).session;
const notLogin = useLiveData(session.status$) === 'unauthenticated';
const isSessionRevalidating = useLiveData(session.isRevalidating$);
const [importing, setImporting] = useState(false);
const [importingError, setImportingError] = useState<any>(null);
const workspacesService = useService(WorkspacesService);
const templateDownloaderService = useService(TemplateDownloaderService);
const importTemplateService = useService(ImportTemplateService);
const templateDownloader = templateDownloaderService.downloader;
const isDownloading = useLiveData(templateDownloader.isDownloading$);
const downloadError = useLiveData(templateDownloader.error$);
const workspaces = useLiveData(workspacesService.list.workspaces$);
const [rawSelectedWorkspace, setSelectedWorkspace] =
useState<WorkspaceMetadata | null>(null);
const selectedWorkspace =
rawSelectedWorkspace ??
workspaces.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
workspaces.at(0);
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
const { openPage, jumpToSignIn } = useNavigateHelper();
const noWorkspace = workspaces.length === 0;
useEffect(() => {
workspacesService.list.revalidate();
}, [workspacesService]);
useEffect(() => {
session.revalidate();
}, [session]);
useEffect(() => {
if (!isSessionRevalidating && notLogin) {
jumpToSignIn(
'/template/import?workspaceId=' +
workspaceId +
'&docId=' +
docId +
'&name=' +
templateName
);
onClose?.();
}
}, [
docId,
isSessionRevalidating,
jumpToSignIn,
notLogin,
onClose,
templateName,
workspaceId,
]);
useEffect(() => {
templateDownloader.download({ workspaceId, docId });
}, [docId, templateDownloader, workspaceId]);
const handleSelectedWorkspace = useCallback(
(workspaceMetadata: WorkspaceMetadata) => {
return setSelectedWorkspace(workspaceMetadata);
},
[]
);
const handleCreatedWorkspace = useCallback(
(payload: CreateWorkspaceCallbackPayload) => {
return setSelectedWorkspace(payload.meta);
},
[]
);
const handleImportToSelectedWorkspace = useAsyncCallback(async () => {
if (templateDownloader.data$.value && selectedWorkspace) {
setImporting(true);
try {
const docId = await importTemplateService.importToWorkspace(
selectedWorkspace,
templateDownloader.data$.value
);
openPage(selectedWorkspace.id, docId);
onClose?.();
} catch (err) {
setImportingError(err);
} finally {
setImporting(false);
}
}
}, [
importTemplateService,
onClose,
openPage,
selectedWorkspace,
templateDownloader.data$.value,
]);
const handleImportToNewWorkspace = useAsyncCallback(async () => {
if (!templateDownloader.data$.value) {
return;
}
setImporting(true);
try {
const { workspaceId, docId } =
await importTemplateService.importToNewWorkspace(
WorkspaceFlavour.AFFINE_CLOUD,
'Workspace',
templateDownloader.data$.value
);
openPage(workspaceId, docId);
onClose?.();
} catch (err) {
setImportingError(err);
} finally {
setImporting(false);
}
}, [
importTemplateService,
onClose,
openPage,
templateDownloader.data$.value,
]);
const disabled = isDownloading || importing || notLogin;
return (
<>
<div className={styles.dialogContainer}>
<AllDocsIcon className={styles.mainIcon} />
<h6 className={styles.mainTitle}>
{t['com.affine.import-template.dialog.createDocWithTemplate']({
templateName,
})}
</h6>
{noWorkspace ? (
<p className={styles.desc}>A new workspace will be created.</p>
) : (
<>
<p className={styles.desc}>Choose a workspace.</p>
<WorkspaceSelector
workspaceMetadata={selectedWorkspace}
onSelectWorkspace={handleSelectedWorkspace}
onCreatedWorkspace={handleCreatedWorkspace}
className={styles.workspaceSelector}
showArrowDownIcon
disable={disabled}
/>
</>
)}
</div>
{importingError && (
<span style={{ color: cssVar('warningColor') }}>
{t['com.affine.import-template.dialog.errorImport']()}
</span>
)}
{downloadError ? (
<span style={{ color: cssVar('warningColor') }}>
{t['com.affine.import-template.dialog.errorLoad']()}
</span>
) : selectedWorkspace ? (
<Button
className={styles.mainButton}
variant={disabled ? 'secondary' : 'primary'}
loading={disabled}
disabled={disabled}
onClick={handleImportToSelectedWorkspace}
>
{selectedWorkspaceName &&
t['com.affine.import-template.dialog.createDocToWorkspace']({
workspace: selectedWorkspaceName,
})}
</Button>
) : (
<Button
className={styles.mainButton}
variant="primary"
loading={disabled}
disabled={disabled}
onClick={handleImportToNewWorkspace}
>
{t['com.affine.import-template.dialog.createDocToNewWorkspace']()}
</Button>
)}
</>
);
};
export const ImportTemplateDialogProvider = () => {
const importTemplateDialogService = useService(ImportTemplateDialogService);
const isOpen = useLiveData(importTemplateDialogService.dialog.isOpen$);
const template = useLiveData(importTemplateDialogService.dialog.template$);
return (
<Modal
open={isOpen}
modal={true}
persistent
withoutCloseButton
contentOptions={{
className: styles.modal,
}}
onOpenChange={() => importTemplateDialogService.dialog.close()}
>
{template && (
<Dialog
docId={template.docId}
templateName={template.templateName}
workspaceId={template.workspaceId}
onClose={() => importTemplateDialogService.dialog.close()}
/>
)}
</Modal>
);
};
@@ -3,6 +3,7 @@ import { configureInfraModules, type Framework } from '@toeverything/infra';
import { configureCloudModule } from './cloud';
import { configureCollectionModule } from './collection';
import { configureCreateWorkspaceModule } from './create-workspace';
import { configureDocLinksModule } from './doc-link';
import { configureDocsSearchModule } from './docs-search';
import { configureEditorModule } from './editor';
@@ -10,6 +11,7 @@ import { configureEditorSettingModule } from './editor-settting';
import { configureExplorerModule } from './explorer';
import { configureFavoriteModule } from './favorite';
import { configureFindInPageModule } from './find-in-page';
import { configureImportTemplateModule } from './import-template';
import { configureNavigationModule } from './navigation';
import { configureOrganizeModule } from './organize';
import { configurePeekViewModule } from './peek-view';
@@ -45,4 +47,6 @@ export function configureCommonModules(framework: Framework) {
configureEditorModule(framework);
configureSystemFontFamilyModule(framework);
configureEditorSettingModule(framework);
configureImportTemplateModule(framework);
configureCreateWorkspaceModule(framework);
}
@@ -23,7 +23,10 @@ export const useCustomTheme = (target: HTMLElement) => {
const valueMap = themeObj[mode];
// remove previous style
// TOOD(@CatsJuice): find better way to remove previous style
target.style.cssText = '';
// recover color scheme set by next-themes
target.style.colorScheme = mode;
Object.entries(valueMap).forEach(([key, value]) => {
value && target.style.setProperty(key, value);
@@ -3,6 +3,7 @@ import { RightSidebarIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useAtomValue } from 'jotai';
import { Suspense, useCallback } from 'react';
import { Outlet } from 'react-router-dom';
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
@@ -40,7 +41,7 @@ const ToggleButton = ({
);
};
export const RouteContainer = ({ route }: Props) => {
export const RouteContainer = () => {
const viewPosition = useViewPosition();
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
const workbench = useService(WorkbenchService).workbench;
@@ -74,7 +75,7 @@ export const RouteContainer = ({ route }: Props) => {
<AffineErrorBoundary>
<Suspense>
<route.Component />
<Outlet />
</Suspense>
</AffineErrorBoundary>
<ViewBodyTarget viewId={view.id} className={styles.viewBodyContainer} />
@@ -8,15 +8,8 @@ import {
useService,
} from '@toeverything/infra';
import { useAtom, useAtomValue } from 'jotai';
import {
lazy as reactLazy,
memo,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { type RouteObject, useLocation } from 'react-router-dom';
import type { View } from '../entities/view';
import { WorkbenchService } from '../services/workbench';
@@ -33,24 +26,6 @@ const useAdapter = environment.isDesktop
? useBindWorkbenchToDesktopRouter
: useBindWorkbenchToBrowserRouter;
const warpedRoutes = viewRoutes.map(({ path, lazy }) => {
const Component = reactLazy(() =>
lazy().then(m => ({
default: m.Component as React.ComponentType,
}))
);
const route = {
Component,
};
return {
path,
Component: () => {
return <RouteContainer route={route} />;
},
};
});
export const WorkbenchRoot = memo(() => {
const workbench = useService(WorkbenchService).workbench;
@@ -118,9 +93,18 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
return;
}, [handleOnFocus]);
const routes: RouteObject[] = useMemo(() => {
return [
{
element: <RouteContainer />,
children: viewRoutes,
},
] satisfies RouteObject[];
}, []);
return (
<div className={styles.workbenchViewContainer} ref={containerRef}>
<ViewRoot routes={warpedRoutes} key={view.id} view={view} />
<ViewRoot routes={routes} key={view.id} view={view} />
</div>
);
};