mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 18:02:47 +08:00
fix(core): fix ui flashing (#7056)
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
||||
import { AppSidebarFallback } from '../app-sidebar';
|
||||
import type { WorkspaceRootProps } from '../workspace';
|
||||
import { AppContainer as AppContainerWithoutSettings } from '../workspace';
|
||||
import {
|
||||
AppContainer as AppContainerWithoutSettings,
|
||||
MainContainer,
|
||||
} from '../workspace';
|
||||
|
||||
export const AppContainer = (props: WorkspaceRootProps) => {
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
@@ -17,3 +23,12 @@ export const AppContainer = (props: WorkspaceRootProps) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppFallback = (): ReactElement => {
|
||||
return (
|
||||
<AppContainer>
|
||||
<AppSidebarFallback />
|
||||
<MainContainer />
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
|
||||
@@ -277,7 +277,6 @@ const WorkspaceListItem = ({
|
||||
UserFeatureService,
|
||||
});
|
||||
const information = useWorkspaceInfo(meta);
|
||||
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const isCurrent = currentWorkspace.id === meta.id;
|
||||
@@ -318,9 +317,10 @@ const WorkspaceListItem = ({
|
||||
onClick={onClickPreference}
|
||||
data-testid="workspace-list-item"
|
||||
>
|
||||
<Avatar
|
||||
<WorkspaceAvatar
|
||||
key={meta.id}
|
||||
meta={meta}
|
||||
size={16}
|
||||
url={avatarUrl}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
style={{
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { FlexWrapper, Input, notify, Wrapper } from '@affine/component';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { Upload } from '@affine/core/components/pure/file-upload';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
@@ -28,18 +27,13 @@ export const ProfilePanel = () => {
|
||||
}, [permissionService]);
|
||||
const workspaceIsReady = useLiveData(workspace?.engine.rootDocState$)?.ready;
|
||||
|
||||
const [avatarBlob, setAvatarBlob] = useState<string | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const avatarUrl = useWorkspaceBlobObjectUrl(workspace?.meta, avatarBlob);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace?.docCollection) {
|
||||
setAvatarBlob(workspace.docCollection.meta.avatar ?? null);
|
||||
setName(workspace.docCollection.meta.name ?? UNTITLED_WORKSPACE_NAME);
|
||||
const dispose = workspace.docCollection.meta.commonFieldsUpdated.on(
|
||||
() => {
|
||||
setAvatarBlob(workspace.docCollection.meta.avatar ?? null);
|
||||
setName(workspace.docCollection.meta.name ?? UNTITLED_WORKSPACE_NAME);
|
||||
}
|
||||
);
|
||||
@@ -47,7 +41,6 @@ export const ProfilePanel = () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
} else {
|
||||
setAvatarBlob(null);
|
||||
setName(UNTITLED_WORKSPACE_NAME);
|
||||
}
|
||||
return;
|
||||
@@ -139,7 +132,7 @@ export const ProfilePanel = () => {
|
||||
[setWorkspaceAvatar]
|
||||
);
|
||||
|
||||
const canAdjustAvatar = workspaceIsReady && avatarUrl && isOwner;
|
||||
const canAdjustAvatar = workspaceIsReady && isOwner;
|
||||
|
||||
return (
|
||||
<div className={style.profileWrapper}>
|
||||
@@ -149,9 +142,9 @@ export const ProfilePanel = () => {
|
||||
data-testid="upload-avatar"
|
||||
disabled={!isOwner}
|
||||
>
|
||||
<Avatar
|
||||
<WorkspaceAvatar
|
||||
meta={workspace.meta}
|
||||
size={56}
|
||||
url={avatarUrl}
|
||||
name={name}
|
||||
imageProps={avatarImageProps}
|
||||
fallbackProps={avatarImageProps}
|
||||
|
||||
@@ -4,7 +4,6 @@ export const floatingMaxWidth = 768;
|
||||
export const navWrapperStyle = style({
|
||||
zIndex: 3,
|
||||
paddingBottom: '8px',
|
||||
backgroundColor: cssVar('backgroundPrimaryColor'),
|
||||
'@media': {
|
||||
print: {
|
||||
display: 'none',
|
||||
@@ -15,6 +14,9 @@ export const navWrapperStyle = style({
|
||||
'&[data-has-border=true]': {
|
||||
borderRight: `1px solid ${cssVar('borderColor')}`,
|
||||
},
|
||||
'&[data-is-floating="true"]': {
|
||||
backgroundColor: cssVar('backgroundPrimaryColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
export const navHeaderButton = style({
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback, useEffect } from 'react';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
authAtom,
|
||||
@@ -130,11 +130,6 @@ const UserWithWorkspaceListInner = ({
|
||||
const workspaceManager = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspaceManager.list.workspaces$);
|
||||
|
||||
// revalidate workspace list when mounted
|
||||
useEffect(() => {
|
||||
workspaceManager.list.revalidate();
|
||||
}, [workspaceManager]);
|
||||
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
{isAuthenticated ? (
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Divider } from '@affine/component/ui/divider';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
|
||||
import {
|
||||
useWorkspaceAvatar,
|
||||
useWorkspaceInfo,
|
||||
useWorkspaceName,
|
||||
} from '@affine/core/hooks/use-workspace-info';
|
||||
@@ -76,7 +75,6 @@ const CloudWorkSpaceList = ({
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
useIsWorkspaceOwner={useIsWorkspaceOwner}
|
||||
useWorkspaceName={useWorkspaceName}
|
||||
useWorkspaceAvatar={useWorkspaceAvatar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -115,7 +113,6 @@ const LocalWorkspaces = ({
|
||||
onEnableCloudClick={onClickEnableCloud}
|
||||
useIsWorkspaceOwner={useIsWorkspaceOwner}
|
||||
useWorkspaceName={useWorkspaceName}
|
||||
useWorkspaceAvatar={useWorkspaceAvatar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -186,8 +183,18 @@ export const AFFiNEWorkspaceList = ({
|
||||
|
||||
const onClickWorkspace = useCallback(
|
||||
(workspaceMetadata: WorkspaceMetadata) => {
|
||||
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||
onEventEnd?.();
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||
onEventEnd?.();
|
||||
return new Promise(resolve =>
|
||||
setTimeout(resolve, 150)
|
||||
); /* start transition after 150ms */
|
||||
});
|
||||
} else {
|
||||
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||
onEventEnd?.();
|
||||
}
|
||||
},
|
||||
[jumpToSubPath, onEventEnd]
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { notify, Tooltip } from '@affine/component';
|
||||
import { Avatar, type AvatarProps } from '@affine/component/ui/avatar';
|
||||
import { type AvatarProps } from '@affine/component/ui/avatar';
|
||||
import { Loading } from '@affine/component/ui/loading';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { useDocEngineStatus } from '@affine/core/hooks/affine/use-doc-engine-status';
|
||||
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
@@ -284,11 +284,6 @@ export const WorkspaceCard = forwardRef<
|
||||
|
||||
const information = useWorkspaceInfo(currentWorkspace.meta);
|
||||
|
||||
const avatarUrl = useWorkspaceBlobObjectUrl(
|
||||
currentWorkspace.meta,
|
||||
information?.avatar
|
||||
);
|
||||
|
||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
|
||||
return (
|
||||
@@ -301,12 +296,13 @@ export const WorkspaceCard = forwardRef<
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<Avatar
|
||||
<WorkspaceAvatar
|
||||
key={currentWorkspace.id}
|
||||
meta={currentWorkspace.meta}
|
||||
imageProps={avatarImageProps}
|
||||
fallbackProps={avatarImageProps}
|
||||
data-testid="workspace-avatar"
|
||||
size={32}
|
||||
url={avatarUrl}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import { forwardRef, useCallback, useEffect } from 'react';
|
||||
import { forwardRef, memo, useCallback, useEffect } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
||||
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||
@@ -89,174 +89,177 @@ RouteMenuLinkItem.displayName = 'RouteMenuLinkItem';
|
||||
*
|
||||
* @todo(himself65): rewrite all styled component into @vanilla-extract/css
|
||||
*/
|
||||
export const RootAppSidebar = ({
|
||||
currentWorkspace,
|
||||
openPage,
|
||||
createPage,
|
||||
paths,
|
||||
onOpenQuickSearchModal,
|
||||
onOpenSettingModal,
|
||||
}: RootAppSidebarProps): ReactElement => {
|
||||
const currentWorkspaceId = currentWorkspace.id;
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
const t = useAFFiNEI18N();
|
||||
const currentPath = useLiveData(
|
||||
useService(WorkbenchService).workbench.location$.map(
|
||||
location => location.pathname
|
||||
)
|
||||
);
|
||||
export const RootAppSidebar = memo(
|
||||
({
|
||||
currentWorkspace,
|
||||
openPage,
|
||||
createPage,
|
||||
paths,
|
||||
onOpenQuickSearchModal,
|
||||
onOpenSettingModal,
|
||||
}: RootAppSidebarProps): ReactElement => {
|
||||
const currentWorkspaceId = currentWorkspace.id;
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
const t = useAFFiNEI18N();
|
||||
const currentPath = useLiveData(
|
||||
useService(WorkbenchService).workbench.location$.map(
|
||||
location => location.pathname
|
||||
)
|
||||
);
|
||||
|
||||
const allPageActive = currentPath === '/all';
|
||||
const allPageActive = currentPath === '/all';
|
||||
|
||||
const trashActive = currentPath === '/trash';
|
||||
const trashActive = currentPath === '/trash';
|
||||
|
||||
const onClickNewPage = useAsyncCallback(async () => {
|
||||
const page = createPage();
|
||||
page.load();
|
||||
openPage(page.id);
|
||||
mixpanel.track('DocCreated', {
|
||||
page: allPageActive ? 'all' : trashActive ? 'trash' : 'other',
|
||||
segment: 'navigation panel',
|
||||
module: 'bottom button',
|
||||
control: 'new doc button',
|
||||
category: 'page',
|
||||
type: 'doc',
|
||||
const onClickNewPage = useAsyncCallback(async () => {
|
||||
const page = createPage();
|
||||
page.load();
|
||||
openPage(page.id);
|
||||
mixpanel.track('DocCreated', {
|
||||
page: allPageActive ? 'all' : trashActive ? 'trash' : 'other',
|
||||
segment: 'navigation panel',
|
||||
module: 'bottom button',
|
||||
control: 'new doc button',
|
||||
category: 'page',
|
||||
type: 'doc',
|
||||
});
|
||||
}, [allPageActive, createPage, openPage, trashActive]);
|
||||
|
||||
const { trashModal, setTrashModal, handleOnConfirm } =
|
||||
useTrashModalHelper(docCollection);
|
||||
const deletePageTitles = trashModal.pageTitles;
|
||||
const trashConfirmOpen = trashModal.open;
|
||||
const onTrashConfirmOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setTrashModal({
|
||||
...trashModal,
|
||||
open,
|
||||
});
|
||||
},
|
||||
[trashModal, setTrashModal]
|
||||
);
|
||||
|
||||
const navigateHelper = useNavigateHelper();
|
||||
// Listen to the "New Page" action from the menu
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop) {
|
||||
return events?.applicationMenu.onNewPageAction(onClickNewPage);
|
||||
}
|
||||
return;
|
||||
}, [onClickNewPage]);
|
||||
|
||||
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop) {
|
||||
apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}, [sidebarOpen]);
|
||||
|
||||
const dropItemId = getDNDId('sidebar-trash', 'container', 'trash');
|
||||
const trashDroppable = useDroppable({
|
||||
id: dropItemId,
|
||||
});
|
||||
}, [allPageActive, createPage, openPage, trashActive]);
|
||||
|
||||
const { trashModal, setTrashModal, handleOnConfirm } =
|
||||
useTrashModalHelper(docCollection);
|
||||
const deletePageTitles = trashModal.pageTitles;
|
||||
const trashConfirmOpen = trashModal.open;
|
||||
const onTrashConfirmOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setTrashModal({
|
||||
...trashModal,
|
||||
open,
|
||||
});
|
||||
},
|
||||
[trashModal, setTrashModal]
|
||||
);
|
||||
const collection = useService(CollectionService);
|
||||
const { node, open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.createCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
const handleCreateCollection = useCallback(() => {
|
||||
open('')
|
||||
.then(name => {
|
||||
const id = nanoid();
|
||||
collection.addCollection(createEmptyCollection(id, { name }));
|
||||
navigateHelper.jumpToCollection(docCollection.id, id);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [docCollection.id, collection, navigateHelper, open]);
|
||||
|
||||
const navigateHelper = useNavigateHelper();
|
||||
// Listen to the "New Page" action from the menu
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop) {
|
||||
return events?.applicationMenu.onNewPageAction(onClickNewPage);
|
||||
}
|
||||
return;
|
||||
}, [onClickNewPage]);
|
||||
|
||||
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop) {
|
||||
apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}, [sidebarOpen]);
|
||||
|
||||
const dropItemId = getDNDId('sidebar-trash', 'container', 'trash');
|
||||
const trashDroppable = useDroppable({
|
||||
id: dropItemId,
|
||||
});
|
||||
|
||||
const collection = useService(CollectionService);
|
||||
const { node, open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.createCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
const handleCreateCollection = useCallback(() => {
|
||||
open('')
|
||||
.then(name => {
|
||||
const id = nanoid();
|
||||
collection.addCollection(createEmptyCollection(id, { name }));
|
||||
navigateHelper.jumpToCollection(docCollection.id, id);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [docCollection.id, collection, navigateHelper, open]);
|
||||
|
||||
return (
|
||||
<AppSidebar
|
||||
clientBorder={appSettings.clientBorder}
|
||||
translucentUI={appSettings.enableBlurBackground}
|
||||
>
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={trashConfirmOpen}
|
||||
onConfirm={handleOnConfirm}
|
||||
onOpenChange={onTrashConfirmOpenChange}
|
||||
titles={deletePageTitles}
|
||||
/>
|
||||
<SidebarContainer>
|
||||
<div className={workspaceAndUserWrapper}>
|
||||
<div className={workspaceWrapper}>
|
||||
<WorkspaceSelector />
|
||||
return (
|
||||
<AppSidebar
|
||||
clientBorder={appSettings.clientBorder}
|
||||
translucentUI={appSettings.enableBlurBackground}
|
||||
>
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={trashConfirmOpen}
|
||||
onConfirm={handleOnConfirm}
|
||||
onOpenChange={onTrashConfirmOpenChange}
|
||||
titles={deletePageTitles}
|
||||
/>
|
||||
<SidebarContainer>
|
||||
<div className={workspaceAndUserWrapper}>
|
||||
<div className={workspaceWrapper}>
|
||||
<WorkspaceSelector />
|
||||
</div>
|
||||
<UserInfo />
|
||||
</div>
|
||||
<UserInfo />
|
||||
</div>
|
||||
<QuickSearchInput
|
||||
data-testid="slider-bar-quick-search-button"
|
||||
onClick={onOpenQuickSearchModal}
|
||||
/>
|
||||
<RouteMenuLinkItem
|
||||
icon={<FolderIcon />}
|
||||
active={allPageActive}
|
||||
path={paths.all(currentWorkspaceId)}
|
||||
>
|
||||
<span data-testid="all-pages">
|
||||
{t['com.affine.workspaceSubPath.all']()}
|
||||
</span>
|
||||
</RouteMenuLinkItem>
|
||||
<AppSidebarJournalButton
|
||||
docCollection={currentWorkspace.docCollection}
|
||||
/>
|
||||
{runtimeConfig.enableNewSettingModal ? (
|
||||
<MenuItem
|
||||
data-testid="slider-bar-workspace-setting-button"
|
||||
icon={<SettingsIcon />}
|
||||
onClick={onOpenSettingModal}
|
||||
>
|
||||
<span data-testid="settings-modal-trigger">
|
||||
{t['com.affine.settingSidebar.title']()}
|
||||
</span>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
</SidebarContainer>
|
||||
|
||||
<SidebarScrollableContainer>
|
||||
<FavoriteList docCollection={docCollection} />
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.collections']()}>
|
||||
<AddCollectionButton node={node} onClick={handleCreateCollection} />
|
||||
</CategoryDivider>
|
||||
<CollectionsList
|
||||
docCollection={docCollection}
|
||||
onCreate={handleCreateCollection}
|
||||
/>
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
|
||||
{/* fixme: remove the following spacer */}
|
||||
<div style={{ height: '4px' }} />
|
||||
<div style={{ padding: '0 8px' }}>
|
||||
<QuickSearchInput
|
||||
data-testid="slider-bar-quick-search-button"
|
||||
onClick={onOpenQuickSearchModal}
|
||||
/>
|
||||
<RouteMenuLinkItem
|
||||
ref={trashDroppable.setNodeRef}
|
||||
icon={<AnimatedDeleteIcon closed={trashDroppable.isOver} />}
|
||||
active={trashActive || trashDroppable.isOver}
|
||||
path={paths.trash(currentWorkspaceId)}
|
||||
icon={<FolderIcon />}
|
||||
active={allPageActive}
|
||||
path={paths.all(currentWorkspaceId)}
|
||||
>
|
||||
<span data-testid="trash-page">
|
||||
{t['com.affine.workspaceSubPath.trash']()}
|
||||
<span data-testid="all-pages">
|
||||
{t['com.affine.workspaceSubPath.all']()}
|
||||
</span>
|
||||
</RouteMenuLinkItem>
|
||||
<ImportPage docCollection={docCollection} />
|
||||
</div>
|
||||
</SidebarScrollableContainer>
|
||||
<SidebarContainer>
|
||||
{environment.isDesktop ? <UpdaterButton /> : <AppDownloadButton />}
|
||||
<div style={{ height: '4px' }} />
|
||||
<AddPageButton onClick={onClickNewPage} />
|
||||
</SidebarContainer>
|
||||
</AppSidebar>
|
||||
);
|
||||
};
|
||||
<AppSidebarJournalButton
|
||||
docCollection={currentWorkspace.docCollection}
|
||||
/>
|
||||
{runtimeConfig.enableNewSettingModal ? (
|
||||
<MenuItem
|
||||
data-testid="slider-bar-workspace-setting-button"
|
||||
icon={<SettingsIcon />}
|
||||
onClick={onOpenSettingModal}
|
||||
>
|
||||
<span data-testid="settings-modal-trigger">
|
||||
{t['com.affine.settingSidebar.title']()}
|
||||
</span>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
</SidebarContainer>
|
||||
<SidebarScrollableContainer>
|
||||
<FavoriteList docCollection={docCollection} />
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.collections']()}>
|
||||
<AddCollectionButton node={node} onClick={handleCreateCollection} />
|
||||
</CategoryDivider>
|
||||
<CollectionsList
|
||||
docCollection={docCollection}
|
||||
onCreate={handleCreateCollection}
|
||||
/>
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
|
||||
{/* fixme: remove the following spacer */}
|
||||
<div style={{ height: '4px' }} />
|
||||
<div style={{ padding: '0 8px' }}>
|
||||
<RouteMenuLinkItem
|
||||
ref={trashDroppable.setNodeRef}
|
||||
icon={<AnimatedDeleteIcon closed={trashDroppable.isOver} />}
|
||||
active={trashActive || trashDroppable.isOver}
|
||||
path={paths.trash(currentWorkspaceId)}
|
||||
>
|
||||
<span data-testid="trash-page">
|
||||
{t['com.affine.workspaceSubPath.trash']()}
|
||||
</span>
|
||||
</RouteMenuLinkItem>
|
||||
<ImportPage docCollection={docCollection} />
|
||||
</div>
|
||||
</SidebarScrollableContainer>
|
||||
<SidebarContainer>
|
||||
{environment.isDesktop ? <UpdaterButton /> : <AppDownloadButton />}
|
||||
<div style={{ height: '4px' }} />
|
||||
<AddPageButton onClick={onClickNewPage} />
|
||||
</SidebarContainer>
|
||||
</AppSidebar>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RootAppSidebar.displayName = 'memo(RootAppSidebar)';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Menu } from '@affine/component';
|
||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { openWorkspaceListModalAtom } from '../../atoms';
|
||||
import { mixpanel } from '../../utils';
|
||||
@@ -21,16 +22,21 @@ export const WorkspaceSelector = () => {
|
||||
setOpenUserWorkspaceList(true);
|
||||
}, [setOpenUserWorkspaceList]);
|
||||
|
||||
const workspaceManager = useService(WorkspacesService);
|
||||
|
||||
// revalidate workspace list when open workspace list
|
||||
useEffect(() => {
|
||||
if (isUserWorkspaceListOpened) {
|
||||
workspaceManager.list.revalidate();
|
||||
}
|
||||
}, [workspaceManager, isUserWorkspaceListOpened]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
rootOptions={{
|
||||
open: isUserWorkspaceListOpened,
|
||||
}}
|
||||
items={
|
||||
<Suspense>
|
||||
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
|
||||
</Suspense>
|
||||
}
|
||||
items={<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />}
|
||||
contentOptions={{
|
||||
// hide trigger
|
||||
sideOffset: -58,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useAtomValue } from 'jotai';
|
||||
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { AppSidebarFallback, appSidebarOpenAtom } from '../app-sidebar';
|
||||
import { appSidebarOpenAtom } from '../app-sidebar';
|
||||
import { appStyle, mainContainerStyle, toolStyle } from './index.css';
|
||||
|
||||
export type WorkspaceRootProps = PropsWithChildren<{
|
||||
@@ -87,12 +87,3 @@ export const ToolContainer = (props: PropsWithChildren): ReactElement => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceFallback = (): ReactElement => {
|
||||
return (
|
||||
<AppContainer>
|
||||
<AppSidebarFallback />
|
||||
<MainContainer />
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useWorkspaceBlobObjectUrl(
|
||||
meta?: WorkspaceMetadata,
|
||||
blobKey?: string | null
|
||||
) {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
|
||||
const [blob, setBlob] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
setBlob(undefined);
|
||||
if (!blobKey || !meta) {
|
||||
return;
|
||||
}
|
||||
let canceled = false;
|
||||
let objectUrl: string = '';
|
||||
workspacesService
|
||||
.getWorkspaceBlob(meta, blobKey)
|
||||
.then(blob => {
|
||||
if (blob && !canceled) {
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setBlob(objectUrl);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('get workspace blob error: ' + err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
}, [meta, blobKey, workspacesService]);
|
||||
|
||||
return blob;
|
||||
}
|
||||
@@ -4,24 +4,16 @@ import {
|
||||
useService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useWorkspaceBlobObjectUrl } from './use-workspace-blob';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useWorkspaceInfo(meta: WorkspaceMetadata) {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
|
||||
const [profile, setProfile] = useState(() =>
|
||||
workspacesService.getProfile(meta)
|
||||
);
|
||||
const profile = workspacesService.getProfile(meta);
|
||||
|
||||
useEffect(() => {
|
||||
const profile = workspacesService.getProfile(meta);
|
||||
|
||||
profile.revalidate();
|
||||
|
||||
setProfile(profile);
|
||||
}, [meta, workspacesService]);
|
||||
}, [meta, profile]);
|
||||
|
||||
return useLiveData(profile.profile$);
|
||||
}
|
||||
@@ -31,10 +23,3 @@ export function useWorkspaceName(meta: WorkspaceMetadata) {
|
||||
|
||||
return information?.name;
|
||||
}
|
||||
|
||||
export function useWorkspaceAvatar(meta: WorkspaceMetadata) {
|
||||
const information = useWorkspaceInfo(meta);
|
||||
const avatar = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Map as YMap } from 'yjs';
|
||||
|
||||
@@ -24,14 +24,11 @@ import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
|
||||
import { WorkspaceAIOnboarding } from '../components/affine/ai-onboarding';
|
||||
import { AppContainer } from '../components/affine/app-container';
|
||||
import { SyncAwareness } from '../components/affine/awareness';
|
||||
import {
|
||||
AppSidebarFallback,
|
||||
appSidebarResizingAtom,
|
||||
} from '../components/app-sidebar';
|
||||
import { appSidebarResizingAtom } from '../components/app-sidebar';
|
||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||
import type { DraggableTitleCellData } from '../components/page-list';
|
||||
import { RootAppSidebar } from '../components/root-app-sidebar';
|
||||
import { MainContainer, WorkspaceFallback } from '../components/workspace';
|
||||
import { MainContainer } from '../components/workspace';
|
||||
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
||||
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
||||
import {
|
||||
@@ -93,15 +90,11 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
||||
return (
|
||||
<SWRConfigProvider>
|
||||
{/* load all workspaces is costly, do not block the whole UI */}
|
||||
<Suspense>
|
||||
<AllWorkspaceModals />
|
||||
<CurrentWorkspaceModals />
|
||||
</Suspense>
|
||||
<Suspense fallback={<WorkspaceFallback />}>
|
||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||
{/* should show after workspace loaded */}
|
||||
<WorkspaceAIOnboarding />
|
||||
</Suspense>
|
||||
<AllWorkspaceModals />
|
||||
<CurrentWorkspaceModals />
|
||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||
{/* should show after workspace loaded */}
|
||||
<WorkspaceAIOnboarding />
|
||||
</SWRConfigProvider>
|
||||
);
|
||||
};
|
||||
@@ -177,11 +170,18 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
const resizing = useAtomValue(appSidebarResizingAtom);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
distance: 10,
|
||||
},
|
||||
})
|
||||
useSensor(
|
||||
MouseSensor,
|
||||
useMemo(
|
||||
/* useMemo is necessary to avoid re-render */
|
||||
() => ({
|
||||
activationConstraint: {
|
||||
distance: 10,
|
||||
},
|
||||
}),
|
||||
[]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const { handleDragEnd } = useGlobalDNDHelper();
|
||||
@@ -192,27 +192,23 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
||||
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
||||
<AppContainer data-current-path={currentPath} resizing={resizing}>
|
||||
<Suspense fallback={<AppSidebarFallback />}>
|
||||
<RootAppSidebar
|
||||
isPublicWorkspace={false}
|
||||
onOpenQuickSearchModal={handleOpenQuickSearchModal}
|
||||
onOpenSettingModal={handleOpenSettingModal}
|
||||
currentWorkspace={currentWorkspace}
|
||||
openPage={useCallback(
|
||||
(pageId: string) => {
|
||||
assertExists(currentWorkspace);
|
||||
return openPage(currentWorkspace.id, pageId);
|
||||
},
|
||||
[currentWorkspace, openPage]
|
||||
)}
|
||||
createPage={handleCreatePage}
|
||||
paths={pathGenerator}
|
||||
/>
|
||||
</Suspense>
|
||||
<RootAppSidebar
|
||||
isPublicWorkspace={false}
|
||||
onOpenQuickSearchModal={handleOpenQuickSearchModal}
|
||||
onOpenSettingModal={handleOpenSettingModal}
|
||||
currentWorkspace={currentWorkspace}
|
||||
openPage={useCallback(
|
||||
(pageId: string) => {
|
||||
assertExists(currentWorkspace);
|
||||
return openPage(currentWorkspace.id, pageId);
|
||||
},
|
||||
[currentWorkspace, openPage]
|
||||
)}
|
||||
createPage={handleCreatePage}
|
||||
paths={pathGenerator}
|
||||
/>
|
||||
<MainContainer clientBorder={appSettings.clientBorder}>
|
||||
<Suspense>
|
||||
{needUpgrade || upgrading ? <WorkspaceUpgrade /> : children}
|
||||
</Suspense>
|
||||
{needUpgrade || upgrading ? <WorkspaceUpgrade /> : children}
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
<GlobalDragOverlay />
|
||||
|
||||
@@ -16,7 +16,14 @@ import type {
|
||||
PropsWithChildren,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { View } from '../../entities/view';
|
||||
import { WorkbenchService } from '../../services/workbench';
|
||||
@@ -57,7 +64,7 @@ export const SplitViewPanel = memo(function SplitViewPanel({
|
||||
const isDragging = dndIsDragging || indicatorPressed;
|
||||
const isActive = activeView === view;
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
setSlots?.(slots => ({ ...slots, [view.id]: ref }));
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes, RefObject } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { View } from '../../entities/view';
|
||||
@@ -52,11 +52,18 @@ export const SplitView = ({
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 0,
|
||||
},
|
||||
})
|
||||
useSensor(
|
||||
PointerSensor,
|
||||
useMemo(
|
||||
/* avoid re-rendering */
|
||||
() => ({
|
||||
activationConstraint: {
|
||||
distance: 0,
|
||||
},
|
||||
}),
|
||||
[]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const onResizing = useCallback(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FrameworkScope, useLiveData } from '@toeverything/infra';
|
||||
import { lazy as reactLazy, useEffect, useMemo } from 'react';
|
||||
import { lazy as reactLazy, useLayoutEffect, useMemo } from 'react';
|
||||
import {
|
||||
createMemoryRouter,
|
||||
RouterProvider,
|
||||
@@ -34,7 +34,7 @@ export const ViewRoot = ({ view }: { view: View }) => {
|
||||
|
||||
const location = useLiveData(view.location$);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
viewRouter.navigate(location).catch(err => {
|
||||
console.error('navigate error', err);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import type { View } from '../entities/view';
|
||||
@@ -14,7 +14,7 @@ const useAdapter = environment.isDesktop
|
||||
? useBindWorkbenchToDesktopRouter
|
||||
: useBindWorkbenchToBrowserRouter;
|
||||
|
||||
export const WorkbenchRoot = () => {
|
||||
export const WorkbenchRoot = memo(() => {
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
// for debugging
|
||||
@@ -50,7 +50,9 @@ export const WorkbenchRoot = () => {
|
||||
onMove={onMove}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
WorkbenchRoot.displayName = 'memo(WorkbenchRoot)';
|
||||
|
||||
const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type WorkspaceProfileInfo,
|
||||
} from '@toeverything/infra';
|
||||
import { effect, globalBlockSuiteSchema, Service } from '@toeverything/infra';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { EMPTY, map, mergeMap } from 'rxjs';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
@@ -148,11 +149,16 @@ export class CloudWorkspaceFlavourProviderService
|
||||
mergeMap(data => {
|
||||
if (data) {
|
||||
const { accountId, workspaces } = data;
|
||||
const sorted = workspaces.sort((a, b) => {
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
this.globalState.set(
|
||||
CLOUD_WORKSPACES_CACHE_KEY + accountId,
|
||||
workspaces
|
||||
sorted
|
||||
);
|
||||
this.workspaces$.next(workspaces);
|
||||
if (!isEqual(this.workspaces$.value, sorted)) {
|
||||
this.workspaces$.next(sorted);
|
||||
}
|
||||
} else {
|
||||
this.workspaces$.next([]);
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@ export class CloudBlobStorage implements BlobStorage {
|
||||
? key
|
||||
: `/api/workspaces/${this.workspaceId}/blobs/${key}`;
|
||||
|
||||
return fetch(getBaseUrl() + suffix).then(async res => {
|
||||
if (!res.ok) {
|
||||
// status not in the range 200-299
|
||||
return null;
|
||||
return fetch(getBaseUrl() + suffix, { cache: 'default' }).then(
|
||||
async res => {
|
||||
if (!res.ok) {
|
||||
// status not in the range 200-299
|
||||
return null;
|
||||
}
|
||||
return bufferToBlob(await res.arrayBuffer());
|
||||
}
|
||||
return bufferToBlob(await res.arrayBuffer());
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
async set(key: string, value: Blob) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
WorkspaceProfileInfo,
|
||||
} from '@toeverything/infra';
|
||||
import { globalBlockSuiteSchema, LiveData, Service } from '@toeverything/infra';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Observable } from 'rxjs';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
@@ -96,12 +97,14 @@ export class LocalWorkspaceFlavourProvider
|
||||
}
|
||||
workspaces$ = LiveData.from(
|
||||
new Observable<WorkspaceMetadata[]>(subscriber => {
|
||||
let last: WorkspaceMetadata[] | null = null;
|
||||
const emit = () => {
|
||||
subscriber.next(
|
||||
JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
).map((id: string) => ({ id, flavour: WorkspaceFlavour.LOCAL }))
|
||||
);
|
||||
const value = JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
).map((id: string) => ({ id, flavour: WorkspaceFlavour.LOCAL }));
|
||||
if (isEqual(last, value)) return;
|
||||
subscriber.next(value);
|
||||
last = value;
|
||||
};
|
||||
|
||||
emit();
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
buildShowcaseWorkspace,
|
||||
createFirstAppData,
|
||||
} from '../bootstrap/first-app-data';
|
||||
import { AppFallback } from '../components/affine/app-container';
|
||||
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
||||
import { WorkspaceFallback } from '../components/workspace';
|
||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
@@ -141,7 +141,7 @@ export const Component = () => {
|
||||
}, [jumpToPage, openPage, workspacesService]);
|
||||
|
||||
if (navigating || creating) {
|
||||
return <WorkspaceFallback></WorkspaceFallback>;
|
||||
return <AppFallback></AppFallback>;
|
||||
}
|
||||
|
||||
// TODO: We need a no workspace page
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
||||
import { ZipTransformer } from '@blocksuite/blocks';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
@@ -9,11 +10,10 @@ import {
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||
import { WorkspaceFallback } from '../../components/workspace';
|
||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
import { RightSidebarContainer } from '../../modules/right-sidebar';
|
||||
import { WorkbenchRoot } from '../../modules/workbench';
|
||||
@@ -121,13 +121,13 @@ export const Component = (): ReactElement => {
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
if (!workspace) {
|
||||
return <WorkspaceFallback key="workspaceLoading" />;
|
||||
return <AppFallback key="workspaceLoading" />;
|
||||
}
|
||||
|
||||
if (!isRootDocReady) {
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<WorkspaceFallback key="workspaceLoading" />
|
||||
<AppFallback key="workspaceLoading" />
|
||||
<AllWorkspaceModals />
|
||||
</FrameworkScope>
|
||||
);
|
||||
@@ -135,14 +135,12 @@ export const Component = (): ReactElement => {
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<Suspense fallback={<WorkspaceFallback key="workspaceFallback" />}>
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<WorkspaceLayout>
|
||||
<WorkbenchRoot />
|
||||
<RightSidebarContainer />
|
||||
</WorkspaceLayout>
|
||||
</AffineErrorBoundary>
|
||||
</Suspense>
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<WorkspaceLayout>
|
||||
<WorkbenchRoot />
|
||||
<RightSidebarContainer />
|
||||
</WorkspaceLayout>
|
||||
</AffineErrorBoundary>
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user