mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 17:46:18 +08:00
refactor: workspace manager (#5060)
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
|
||||
export const AdapterProviderWrapper: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
|
||||
const Provider = WorkspaceAdapters[currentWorkspace.flavour].UI.Provider;
|
||||
assertExists(Provider);
|
||||
|
||||
+9
-9
@@ -1,8 +1,8 @@
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
currentWorkspaceAtom,
|
||||
workspaceListAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
@@ -13,8 +13,8 @@ export interface DumpInfoProps {
|
||||
|
||||
export const DumpInfo = (_props: DumpInfoProps) => {
|
||||
const location = useLocation();
|
||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||
const workspaceList = useAtomValue(workspaceListAtom);
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const path = location.pathname;
|
||||
const query = useParams();
|
||||
@@ -22,10 +22,10 @@ export const DumpInfo = (_props: DumpInfoProps) => {
|
||||
console.info('DumpInfo', {
|
||||
path,
|
||||
query,
|
||||
currentWorkspaceId,
|
||||
currentWorkspaceId: currentWorkspace?.id,
|
||||
currentPageId,
|
||||
metadata,
|
||||
workspaceList,
|
||||
});
|
||||
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
|
||||
}, [path, query, currentWorkspace, currentPageId, workspaceList]);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ const UserPlanButtonWithData = () => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
workspaceId: null,
|
||||
});
|
||||
},
|
||||
[setSettingModalAtom]
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
|
||||
const SyncAwarenessInnerLoggedIn = () => {
|
||||
const currentUser = useCurrentUser();
|
||||
const [{ blockSuiteWorkspace: workspace }] = useCurrentWorkspace();
|
||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser && workspace) {
|
||||
workspace.awarenessStore.awareness.setLocalStateField('user', {
|
||||
name: currentUser.name,
|
||||
// todo: add avatar?
|
||||
});
|
||||
if (currentUser && currentWorkspace) {
|
||||
currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
|
||||
'user',
|
||||
{
|
||||
name: currentUser.name,
|
||||
// todo: add avatar?
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
workspace.awarenessStore.awareness.setLocalStateField('user', null);
|
||||
currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
|
||||
'user',
|
||||
null
|
||||
);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [currentUser, workspace]);
|
||||
}, [currentUser, currentWorkspace]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import { Input, toast } from '@affine/component';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import {
|
||||
ConfirmModal,
|
||||
type ConfirmModalProps,
|
||||
Modal,
|
||||
} from '@affine/component/ui/modal';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { HelpIcon } from '@blocksuite/icons';
|
||||
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import type {
|
||||
LoadDBFileResult,
|
||||
SelectDBFileLocationResult,
|
||||
} from '@toeverything/infra/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import {
|
||||
buildShowcaseWorkspace,
|
||||
initEmptyPage,
|
||||
} from '@toeverything/infra/blocksuite';
|
||||
import type { LoadDBFileResult } from '@toeverything/infra/type';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { useAppHelper } from '../../../hooks/use-workspaces';
|
||||
import { setPageModeAtom } from '../../../atoms';
|
||||
import * as style from './index.css';
|
||||
|
||||
type CreateWorkspaceStep =
|
||||
@@ -94,159 +93,14 @@ const NameWorkspaceContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface SetDBLocationContentProps {
|
||||
onConfirmLocation: (dir?: string) => void;
|
||||
}
|
||||
|
||||
const useDefaultDBLocation = () => {
|
||||
const [defaultDBLocation, setDefaultDBLocation] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
window.apis?.db
|
||||
.getDefaultStorageLocation()
|
||||
.then(dir => {
|
||||
setDefaultDBLocation(dir);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return defaultDBLocation;
|
||||
};
|
||||
|
||||
const SetDBLocationContent = ({
|
||||
onConfirmLocation,
|
||||
}: SetDBLocationContentProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const defaultDBLocation = useDefaultDBLocation();
|
||||
const [opening, setOpening] = useState(false);
|
||||
|
||||
const handleSelectDBFileLocation = useCallback(() => {
|
||||
if (opening) {
|
||||
return;
|
||||
}
|
||||
setOpening(true);
|
||||
(async function () {
|
||||
const result: SelectDBFileLocationResult =
|
||||
await window.apis?.dialog.selectDBFileLocation();
|
||||
setOpening(false);
|
||||
if (result?.filePath) {
|
||||
onConfirmLocation(result.filePath);
|
||||
} else if (result?.error) {
|
||||
toast(t[result.error]());
|
||||
}
|
||||
})().catch(err => {
|
||||
logger.error(err);
|
||||
});
|
||||
}, [onConfirmLocation, opening, t]);
|
||||
|
||||
return (
|
||||
<div className={style.content}>
|
||||
<div className={style.contentTitle}>
|
||||
{t['com.affine.setDBLocation.title']()}
|
||||
</div>
|
||||
<p>{t['com.affine.setDBLocation.description']()}</p>
|
||||
<div className={style.buttonGroup}>
|
||||
<Button
|
||||
disabled={opening}
|
||||
data-testid="create-workspace-customize-button"
|
||||
type="primary"
|
||||
onClick={handleSelectDBFileLocation}
|
||||
>
|
||||
{t['com.affine.setDBLocation.button.customize']()}
|
||||
</Button>
|
||||
<Tooltip
|
||||
content={t['com.affine.setDBLocation.tooltip.defaultLocation']({
|
||||
location: defaultDBLocation,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
data-testid="create-workspace-default-location-button"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onConfirmLocation();
|
||||
}}
|
||||
icon={<HelpIcon />}
|
||||
iconPosition="end"
|
||||
>
|
||||
{t['com.affine.setDBLocation.button.defaultLocation']()}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SetSyncingModeContentProps {
|
||||
mode: CreateWorkspaceMode;
|
||||
onConfirmMode: (enableCloudSyncing: boolean) => void;
|
||||
}
|
||||
|
||||
const SetSyncingModeContent = ({
|
||||
mode,
|
||||
onConfirmMode,
|
||||
}: SetSyncingModeContentProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [enableCloudSyncing, setEnableCloudSyncing] = useState(false);
|
||||
return (
|
||||
<div className={style.content}>
|
||||
<div className={style.contentTitle}>
|
||||
{mode === 'new'
|
||||
? t['com.affine.setSyncingMode.title.created']()
|
||||
: t['com.affine.setSyncingMode.title.added']()}
|
||||
</div>
|
||||
|
||||
<div className={style.radioGroup}>
|
||||
<label onClick={() => setEnableCloudSyncing(false)}>
|
||||
<input
|
||||
className={style.radio}
|
||||
type="radio"
|
||||
readOnly
|
||||
checked={!enableCloudSyncing}
|
||||
/>
|
||||
{t['com.affine.setSyncingMode.deviceOnly']()}
|
||||
</label>
|
||||
<label onClick={() => setEnableCloudSyncing(true)}>
|
||||
<input
|
||||
className={style.radio}
|
||||
type="radio"
|
||||
readOnly
|
||||
checked={enableCloudSyncing}
|
||||
/>
|
||||
{t['com.affine.setSyncingMode.cloud']()}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={style.buttonGroup}>
|
||||
<Button
|
||||
data-testid="create-workspace-continue-button"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onConfirmMode(enableCloudSyncing);
|
||||
}}
|
||||
>
|
||||
{t['com.affine.setSyncingMode.button.continue']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateWorkspaceModal = ({
|
||||
mode,
|
||||
onClose,
|
||||
onCreate,
|
||||
}: ModalProps) => {
|
||||
const { createLocalWorkspace, addLocalWorkspace } = useAppHelper();
|
||||
const [step, setStep] = useState<CreateWorkspaceStep>();
|
||||
const [addedId, setAddedId] = useState<string>();
|
||||
const [workspaceName, setWorkspaceName] = useState<string>();
|
||||
const [dbFileLocation, setDBFileLocation] = useState<string>();
|
||||
const setOpenDisableCloudAlertModal = useSetAtom(
|
||||
openDisableCloudAlertModalAtom
|
||||
);
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||
|
||||
// todo: maybe refactor using xstate?
|
||||
useLayoutEffect(() => {
|
||||
@@ -265,9 +119,8 @@ export const CreateWorkspaceModal = ({
|
||||
setStep(undefined);
|
||||
const result: LoadDBFileResult = await window.apis.dialog.loadDBFile();
|
||||
if (result.workspaceId && !canceled) {
|
||||
setAddedId(result.workspaceId);
|
||||
const newWorkspaceId = await addLocalWorkspace(result.workspaceId);
|
||||
onCreate(newWorkspaceId);
|
||||
workspaceManager._addLocalWorkspace(result.workspaceId);
|
||||
onCreate(result.workspaceId);
|
||||
} else if (result.error || result.canceled) {
|
||||
if (result.error) {
|
||||
toast(t[result.error]());
|
||||
@@ -285,77 +138,38 @@ export const CreateWorkspaceModal = ({
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [addLocalWorkspace, mode, onClose, onCreate, t]);
|
||||
|
||||
const onConfirmEnableCloudSyncing = useCallback(
|
||||
(enableCloudSyncing: boolean) => {
|
||||
(async function () {
|
||||
if (!runtimeConfig.enableCloud && enableCloudSyncing) {
|
||||
setOpenDisableCloudAlertModal(true);
|
||||
} else {
|
||||
let id = addedId;
|
||||
// syncing mode is also the last step
|
||||
if (addedId && mode === 'add') {
|
||||
await addLocalWorkspace(addedId);
|
||||
} else if (mode === 'new' && workspaceName) {
|
||||
id = await createLocalWorkspace(workspaceName);
|
||||
// if dbFileLocation is set, move db file to that location
|
||||
if (dbFileLocation) {
|
||||
await window.apis?.dialog.moveDBFile(id, dbFileLocation);
|
||||
}
|
||||
} else {
|
||||
logger.error('invalid state');
|
||||
return;
|
||||
}
|
||||
if (id) {
|
||||
onCreate(id);
|
||||
}
|
||||
}
|
||||
})().catch(e => {
|
||||
logger.error(e);
|
||||
});
|
||||
},
|
||||
[
|
||||
addLocalWorkspace,
|
||||
addedId,
|
||||
createLocalWorkspace,
|
||||
dbFileLocation,
|
||||
mode,
|
||||
onCreate,
|
||||
setOpenDisableCloudAlertModal,
|
||||
workspaceName,
|
||||
]
|
||||
);
|
||||
}, [mode, onClose, onCreate, t, workspaceManager]);
|
||||
|
||||
const onConfirmName = useAsyncCallback(
|
||||
async (name: string) => {
|
||||
setWorkspaceName(name);
|
||||
// this will be the last step for web for now
|
||||
// fix me later
|
||||
const id = await createLocalWorkspace(name);
|
||||
const id = await workspaceManager.createWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
async workspace => {
|
||||
workspace.meta.setName(name);
|
||||
if (runtimeConfig.enablePreloading) {
|
||||
await buildShowcaseWorkspace(workspace, {
|
||||
store: getCurrentStore(),
|
||||
atoms: {
|
||||
pageMode: setPageModeAtom,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const page = workspace.createPage();
|
||||
workspace.setPageMeta(page.id, {
|
||||
jumpOnce: true,
|
||||
});
|
||||
await initEmptyPage(page);
|
||||
}
|
||||
logger.debug('create first workspace');
|
||||
}
|
||||
);
|
||||
onCreate(id);
|
||||
},
|
||||
[createLocalWorkspace, onCreate]
|
||||
[onCreate, workspaceManager]
|
||||
);
|
||||
|
||||
const setDBLocationNode =
|
||||
step === 'set-db-location' ? (
|
||||
<SetDBLocationContent
|
||||
onConfirmLocation={dir => {
|
||||
setDBFileLocation(dir);
|
||||
setStep('name-workspace');
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const setSyncingModeNode =
|
||||
step === 'set-syncing-mode' ? (
|
||||
<SetSyncingModeContent
|
||||
mode={mode}
|
||||
onConfirmMode={onConfirmEnableCloudSyncing}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) {
|
||||
@@ -384,8 +198,6 @@ export const CreateWorkspaceModal = ({
|
||||
}}
|
||||
>
|
||||
<div className={style.header}></div>
|
||||
{setDBLocationNode}
|
||||
{setSyncingModeNode}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-8
@@ -3,28 +3,28 @@ import {
|
||||
ConfirmModal,
|
||||
type ConfirmModalProps,
|
||||
} from '@affine/component/ui/modal';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
interface WorkspaceDeleteProps extends ConfirmModalProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
}
|
||||
|
||||
export const WorkspaceDeleteModal = ({
|
||||
workspace,
|
||||
workspaceMetadata,
|
||||
...props
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const { onConfirm } = props;
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
const [deleteStr, setDeleteStr] = useState<string>('');
|
||||
const info = useWorkspaceInfo(workspaceMetadata);
|
||||
const workspaceName = info?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
const allowDelete = deleteStr === workspaceName;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -46,7 +46,7 @@ export const WorkspaceDeleteModal = ({
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
{workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
<Trans i18nKey="com.affine.workspaceDelete.description">
|
||||
Deleting (
|
||||
<span className={styles.workspaceName}>
|
||||
|
||||
+59
-21
@@ -1,29 +1,44 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
workspaceListAtom,
|
||||
workspaceManagerAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../../atoms';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../../../../hooks/use-navigate-helper';
|
||||
import type { WorkspaceSettingDetailProps } from '../types';
|
||||
import { WorkspaceDeleteModal } from './delete';
|
||||
|
||||
export interface DeleteLeaveWorkspaceProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
export interface DeleteLeaveWorkspaceProps
|
||||
extends WorkspaceSettingDetailProps {}
|
||||
|
||||
export const DeleteLeaveWorkspace = ({
|
||||
workspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onLeaveWorkspace,
|
||||
workspaceMetadata,
|
||||
isOwner,
|
||||
}: DeleteLeaveWorkspaceProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
||||
// fixme: cloud regression
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [showLeave, setShowLeave] = useState(false);
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||
const workspaceList = useAtomValue(workspaceListAtom);
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const onLeaveOrDelete = useCallback(() => {
|
||||
if (isOwner) {
|
||||
@@ -33,18 +48,41 @@ export const DeleteLeaveWorkspace = ({
|
||||
}
|
||||
}, [isOwner]);
|
||||
|
||||
const onLeaveConfirm = useCallback(() => {
|
||||
return onLeaveWorkspace();
|
||||
}, [onLeaveWorkspace]);
|
||||
const onDeleteConfirm = useAsyncCallback(async () => {
|
||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||
|
||||
const onDeleteConfirm = useCallback(() => {
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return onDeleteLocalWorkspace();
|
||||
if (currentWorkspace?.id === workspaceMetadata.id) {
|
||||
const backWorkspace = workspaceList.find(
|
||||
ws => ws.id !== workspaceMetadata.id
|
||||
);
|
||||
// TODO: if there is no workspace, jump to a new page(wait for design)
|
||||
if (backWorkspace) {
|
||||
jumpToSubPath(
|
||||
backWorkspace?.id || '',
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}
|
||||
}
|
||||
if (workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
return onDeleteCloudWorkspace();
|
||||
}
|
||||
}, [onDeleteCloudWorkspace, onDeleteLocalWorkspace, workspace.flavour]);
|
||||
|
||||
await workspaceManager.deleteWorkspace(workspaceMetadata);
|
||||
pushNotification({
|
||||
title: t['Successfully deleted'](),
|
||||
type: 'success',
|
||||
});
|
||||
}, [
|
||||
currentWorkspace?.id,
|
||||
jumpToIndex,
|
||||
jumpToSubPath,
|
||||
pushNotification,
|
||||
setSettingModal,
|
||||
t,
|
||||
workspaceList,
|
||||
workspaceManager,
|
||||
workspaceMetadata,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -68,13 +106,13 @@ export const DeleteLeaveWorkspace = ({
|
||||
onConfirm={onDeleteConfirm}
|
||||
open={showDelete}
|
||||
onOpenChange={setShowDelete}
|
||||
workspace={workspace}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
/>
|
||||
) : (
|
||||
<ConfirmModal
|
||||
open={showLeave}
|
||||
cancelText={t['com.affine.confirmModal.button.cancel']()}
|
||||
onConfirm={onLeaveConfirm}
|
||||
onConfirm={onDeleteConfirm}
|
||||
onOpenChange={setShowLeave}
|
||||
title={`${t['com.affine.deleteLeaveWorkspace.leave']()}?`}
|
||||
description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()}
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@affine/workspace';
|
||||
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../atoms';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export interface PublishPanelProps extends WorkspaceSettingDetailProps {
|
||||
workspace: Workspace | null;
|
||||
}
|
||||
|
||||
export const EnableCloudPanel = ({
|
||||
workspaceMetadata,
|
||||
workspace,
|
||||
}: PublishPanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { openPage } = useNavigateHelper();
|
||||
|
||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleEnableCloud = useAsyncCallback(async () => {
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
const { id: newId } =
|
||||
await workspaceManager.transformLocalToCloud(workspace);
|
||||
openPage(newId, WorkspaceSubPath.ALL);
|
||||
setOpen(false);
|
||||
setSettingModal(settings => ({
|
||||
...settings,
|
||||
open: false,
|
||||
}));
|
||||
}, [openPage, setSettingModal, workspace, workspaceManager]);
|
||||
|
||||
if (workspaceMetadata.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={t['Workspace saved locally']({
|
||||
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
|
||||
})}
|
||||
desc={t['Enable cloud hint']()}
|
||||
spreadCol={false}
|
||||
style={{
|
||||
padding: '10px',
|
||||
background: 'var(--affine-background-secondary-color)',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
data-testid="publish-enable-affine-cloud-button"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
style={{ marginTop: '12px' }}
|
||||
>
|
||||
{t['Enable AFFiNE Cloud']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
{runtimeConfig.enableCloud ? (
|
||||
<EnableAffineCloudModal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onConfirm={handleEnableCloud}
|
||||
/>
|
||||
) : (
|
||||
<TmpDisableAffineCloudModal open={open} onOpenChange={setOpen} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+17
-51
@@ -1,69 +1,35 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import type { Doc } from 'yjs';
|
||||
import { encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
if (window.apis && environment.isDesktop) {
|
||||
const bs = workspace.blockSuiteWorkspace.blob;
|
||||
const blobsInDb = await window.apis.db.getBlobKeys(workspace.id);
|
||||
const blobsInStorage = await bs.list();
|
||||
const blobsToSync = blobsInStorage.filter(
|
||||
blob => !blobsInDb.includes(blob)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
blobsToSync.map(async blobKey => {
|
||||
const blob = await bs.get(blobKey);
|
||||
if (blob) {
|
||||
const bin = new Uint8Array(await blob.arrayBuffer());
|
||||
await window.apis.db.addBlob(workspace.id, blobKey, bin);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncDocsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
if (window.apis && environment.isDesktop) {
|
||||
const workspaceId = workspace.blockSuiteWorkspace.doc.guid;
|
||||
const syncDoc = async (doc: Doc) => {
|
||||
await window.apis.db.applyDocUpdate(
|
||||
workspace.id,
|
||||
encodeStateAsUpdate(doc),
|
||||
doc.guid === workspaceId ? undefined : doc.guid
|
||||
);
|
||||
await Promise.all([...doc.subdocs].map(subdoc => syncDoc(subdoc)));
|
||||
};
|
||||
|
||||
return syncDoc(workspace.blockSuiteWorkspace.doc);
|
||||
}
|
||||
}
|
||||
|
||||
interface ExportPanelProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
workspace: Workspace | null;
|
||||
}
|
||||
|
||||
export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
||||
const workspaceId = workspace.id;
|
||||
export const ExportPanel = ({
|
||||
workspaceMetadata,
|
||||
workspace,
|
||||
}: ExportPanelProps) => {
|
||||
const workspaceId = workspaceMetadata.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const onExport = useAsyncCallback(async () => {
|
||||
if (syncing) {
|
||||
if (saving || !workspace) {
|
||||
return;
|
||||
}
|
||||
setSyncing(true);
|
||||
setSaving(true);
|
||||
try {
|
||||
await syncBlobsToSqliteDb(workspace);
|
||||
await syncDocsToSqliteDb(workspace);
|
||||
await workspace.engine.sync.waitForSynced();
|
||||
await workspace.engine.blob.sync();
|
||||
const result: SaveDBFileResult =
|
||||
await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||
if (result?.error) {
|
||||
@@ -81,16 +47,16 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
||||
message: e.message,
|
||||
});
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
setSaving(false);
|
||||
}
|
||||
}, [pushNotification, syncing, t, workspace, workspaceId]);
|
||||
}, [pushNotification, saving, t, workspace, workspaceId]);
|
||||
|
||||
return (
|
||||
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
|
||||
<Button
|
||||
data-testid="export-affine-backup"
|
||||
onClick={onExport}
|
||||
disabled={syncing}
|
||||
disabled={saving}
|
||||
>
|
||||
{t['Export']()}
|
||||
</Button>
|
||||
|
||||
+28
-31
@@ -3,47 +3,38 @@ import {
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useMemo } from 'react';
|
||||
import { useWorkspace } from '@toeverything/hooks/use-workspace';
|
||||
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||
|
||||
import { useSelfHosted } from '../../../hooks/affine/use-server-flavor';
|
||||
import { useWorkspace } from '../../../hooks/use-workspace';
|
||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||
import { EnableCloudPanel } from './enable-cloud';
|
||||
import { ExportPanel } from './export';
|
||||
import { LabelsPanel } from './labels';
|
||||
import { MembersPanel } from './members';
|
||||
import { ProfilePanel } from './profile';
|
||||
import { PublishPanel } from './publish';
|
||||
import { StoragePanel } from './storage';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
const { workspaceId } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
const isSelfHosted = useSelfHosted();
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
const workspaceMetadata = props.workspaceMetadata;
|
||||
|
||||
const storageAndExportSetting = useMemo(() => {
|
||||
if (environment.isDesktop) {
|
||||
return (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
{runtimeConfig.enableMoveDatabase ? (
|
||||
<StoragePanel workspace={workspace} />
|
||||
) : null}
|
||||
<ExportPanel workspace={workspace} />
|
||||
</SettingWrapper>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [t, workspace]);
|
||||
// useWorkspace hook is a vary heavy operation here, but we need syncing name and avatar changes here,
|
||||
// we don't have a better way to do this now
|
||||
const workspace = useWorkspace(workspaceMetadata);
|
||||
|
||||
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t[`Workspace Settings with name`]({ name })}
|
||||
title={t[`Workspace Settings with name`]({
|
||||
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
|
||||
})}
|
||||
subtitle={t['com.affine.settings.workspace.description']()}
|
||||
/>
|
||||
<SettingWrapper title={t['Info']()}>
|
||||
@@ -53,20 +44,26 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
spreadCol={false}
|
||||
>
|
||||
<ProfilePanel workspace={workspace} {...props} />
|
||||
<LabelsPanel workspace={workspace} {...props} />
|
||||
<LabelsPanel {...props} />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
|
||||
<PublishPanel workspace={workspace} {...props} />
|
||||
<MembersPanel
|
||||
workspace={workspace}
|
||||
upgradable={!isSelfHosted}
|
||||
{...props}
|
||||
/>
|
||||
<EnableCloudPanel workspace={workspace} {...props} />
|
||||
<MembersPanel upgradable={!isSelfHosted} {...props} />
|
||||
</SettingWrapper>
|
||||
{storageAndExportSetting}
|
||||
{environment.isDesktop && (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
{runtimeConfig.enableMoveDatabase ? (
|
||||
<StoragePanel workspaceMetadata={workspaceMetadata} />
|
||||
) : null}
|
||||
<ExportPanel
|
||||
workspace={workspace}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
/>
|
||||
</SettingWrapper>
|
||||
)}
|
||||
<SettingWrapper>
|
||||
<DeleteLeaveWorkspace workspace={workspace} {...props} />
|
||||
<DeleteLeaveWorkspace {...props} />
|
||||
</SettingWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
+8
-9
@@ -1,12 +1,9 @@
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as style from './style.css';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export interface LabelsPanelProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
export interface LabelsPanelProps extends WorkspaceSettingDetailProps {}
|
||||
|
||||
type WorkspaceStatus =
|
||||
| 'local'
|
||||
@@ -38,7 +35,10 @@ const Label = ({ value, background }: LabelProps) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const LabelsPanel = ({ workspace, isOwner }: LabelsPanelProps) => {
|
||||
export const LabelsPanel = ({
|
||||
workspaceMetadata,
|
||||
isOwner,
|
||||
}: LabelsPanelProps) => {
|
||||
const labelMap: LabelMap = useMemo(
|
||||
() => ({
|
||||
local: {
|
||||
@@ -74,11 +74,10 @@ export const LabelsPanel = ({ workspace, isOwner }: LabelsPanelProps) => {
|
||||
);
|
||||
const labelConditions: labelConditionsProps[] = [
|
||||
{ condition: !isOwner, label: 'joinedWorkspace' },
|
||||
{ condition: workspace.flavour === 'local', label: 'local' },
|
||||
{ condition: workspace.flavour === 'affine-cloud', label: 'syncCloud' },
|
||||
{ condition: workspaceMetadata.flavour === 'local', label: 'local' },
|
||||
{
|
||||
condition: workspace.flavour === 'affine-public',
|
||||
label: 'publishedToWeb',
|
||||
condition: workspaceMetadata.flavour === 'affine-cloud',
|
||||
label: 'syncCloud',
|
||||
},
|
||||
//TODO: add these labels
|
||||
// { status==="synced", label: 'availableOffline' }
|
||||
|
||||
+3
-6
@@ -13,7 +13,6 @@ import { Button, IconButton } from '@affine/component/ui/button';
|
||||
import { Loading } from '@affine/component/ui/loading';
|
||||
import { Menu, MenuItem } from '@affine/component/ui/menu';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Permission } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -45,7 +44,6 @@ import type { WorkspaceSettingDetailProps } from './types';
|
||||
const COUNT_PER_PAGE = 8;
|
||||
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
||||
upgradable: boolean;
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
type OnRevoke = (memberId: string) => void;
|
||||
const MembersPanelLocal = () => {
|
||||
@@ -62,11 +60,11 @@ const MembersPanelLocal = () => {
|
||||
};
|
||||
|
||||
export const CloudWorkspaceMembersPanel = ({
|
||||
workspace,
|
||||
isOwner,
|
||||
upgradable,
|
||||
workspaceMetadata,
|
||||
}: MembersPanelProps) => {
|
||||
const workspaceId = workspace.id;
|
||||
const workspaceId = workspaceMetadata.id;
|
||||
const memberCount = useMemberCount(workspaceId);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -138,7 +136,6 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
workspaceId: null,
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
@@ -345,7 +342,7 @@ const MemberItem = ({
|
||||
};
|
||||
|
||||
export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <MembersPanelLocal />;
|
||||
}
|
||||
return (
|
||||
|
||||
+91
-22
@@ -2,51 +2,120 @@ import { FlexWrapper, Input, Wrapper } from '@affine/component';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@affine/workspace';
|
||||
import { SyncPeerStep } from '@affine/workspace';
|
||||
import { CameraIcon } from '@blocksuite/icons';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
|
||||
import { useWorkspaceStatus } from '@toeverything/hooks/use-workspace-status';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
type MouseEvent,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { validateAndReduceImage } from '../../../utils/reduce-image';
|
||||
import { Upload } from '../../pure/file-upload';
|
||||
import * as style from './style.css';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
workspace: Workspace | null;
|
||||
}
|
||||
|
||||
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
export const ProfilePanel = ({ isOwner, workspace }: ProfilePanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const [workspaceAvatar, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
workspace.blockSuiteWorkspace
|
||||
const workspaceIsLoading =
|
||||
useWorkspaceStatus(
|
||||
workspace,
|
||||
status =>
|
||||
!status.engine.sync.local ||
|
||||
status.engine.sync.local?.step <= SyncPeerStep.LoadingRootDoc
|
||||
) ?? true;
|
||||
|
||||
const [avatarBlob, setAvatarBlob] = useState<string | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const avatarUrl = useWorkspaceBlobObjectUrl(workspace?.meta, avatarBlob);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace?.blockSuiteWorkspace) {
|
||||
setAvatarBlob(workspace.blockSuiteWorkspace.meta.avatar ?? null);
|
||||
setName(
|
||||
workspace.blockSuiteWorkspace.meta.name ?? UNTITLED_WORKSPACE_NAME
|
||||
);
|
||||
const dispose = workspace.blockSuiteWorkspace.meta.commonFieldsUpdated.on(
|
||||
() => {
|
||||
setAvatarBlob(workspace.blockSuiteWorkspace.meta.avatar ?? null);
|
||||
setName(
|
||||
workspace.blockSuiteWorkspace.meta.name ?? UNTITLED_WORKSPACE_NAME
|
||||
);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
} else {
|
||||
setAvatarBlob(null);
|
||||
setName(UNTITLED_WORKSPACE_NAME);
|
||||
}
|
||||
return;
|
||||
}, [workspace]);
|
||||
|
||||
const setWorkspaceAvatar = useCallback(
|
||||
async (file: File | null) => {
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
if (!file) {
|
||||
workspace.blockSuiteWorkspace.meta.setAvatar('');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const reducedFile = await validateAndReduceImage(file);
|
||||
const blobs = workspace.blockSuiteWorkspace.blob;
|
||||
const blobId = await blobs.set(reducedFile);
|
||||
workspace.blockSuiteWorkspace.meta.setAvatar(blobId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[workspace]
|
||||
);
|
||||
|
||||
const [name, setName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace
|
||||
const setWorkspaceName = useCallback(
|
||||
(name: string) => {
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
workspace.blockSuiteWorkspace.meta.setName(name);
|
||||
},
|
||||
[workspace]
|
||||
);
|
||||
|
||||
const [input, setInput] = useState<string>(name);
|
||||
const [input, setInput] = useState<string>('');
|
||||
useEffect(() => {
|
||||
setInput(name);
|
||||
}, [name]);
|
||||
|
||||
const handleUpdateWorkspaceName = useCallback(
|
||||
(name: string) => {
|
||||
setName(name);
|
||||
setWorkspaceName(name);
|
||||
pushNotification({
|
||||
title: t['Update workspace name success'](),
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
[pushNotification, setName, t]
|
||||
[pushNotification, setWorkspaceName, t]
|
||||
);
|
||||
|
||||
const handleSetInput = useCallback((value: string) => {
|
||||
@@ -68,17 +137,17 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
handleUpdateWorkspaceName(input);
|
||||
}, [handleUpdateWorkspaceName, input]);
|
||||
|
||||
const handleRemoveUserAvatar = useCallback(
|
||||
const handleRemoveUserAvatar = useAsyncCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
await update(null);
|
||||
await setWorkspaceAvatar(null);
|
||||
},
|
||||
[update]
|
||||
[setWorkspaceAvatar]
|
||||
);
|
||||
|
||||
const handleUploadAvatar = useCallback(
|
||||
(file: File) => {
|
||||
update(file)
|
||||
setWorkspaceAvatar(file)
|
||||
.then(() => {
|
||||
pushNotification({
|
||||
title: 'Update workspace avatar success',
|
||||
@@ -93,10 +162,10 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
});
|
||||
});
|
||||
},
|
||||
[pushNotification, update]
|
||||
[pushNotification, setWorkspaceAvatar]
|
||||
);
|
||||
|
||||
const canAdjustAvatar = workspaceAvatar && isOwner;
|
||||
const canAdjustAvatar = !workspaceIsLoading && avatarUrl && isOwner;
|
||||
|
||||
return (
|
||||
<div className={style.profileWrapper}>
|
||||
@@ -108,7 +177,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
>
|
||||
<Avatar
|
||||
size={56}
|
||||
url={workspaceAvatar}
|
||||
url={avatarUrl}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
hoverIcon={isOwner ? <CameraIcon /> : undefined}
|
||||
@@ -132,10 +201,10 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
<div className={style.label}>{t['Workspace Name']()}</div>
|
||||
<FlexWrapper alignItems="center" flexGrow="1">
|
||||
<Input
|
||||
disabled={!isOwner}
|
||||
disabled={workspaceIsLoading || !isOwner}
|
||||
width={280}
|
||||
height={32}
|
||||
defaultValue={input}
|
||||
value={input}
|
||||
data-testid="workspace-name-input"
|
||||
placeholder={t['Workspace Name']()}
|
||||
maxLength={64}
|
||||
@@ -143,7 +212,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
onChange={handleSetInput}
|
||||
onKeyUp={handleKeyUp}
|
||||
/>
|
||||
{input === workspace.blockSuiteWorkspace.meta.name ? null : (
|
||||
{input === name ? null : (
|
||||
<Button
|
||||
data-testid="save-workspace-name"
|
||||
onClick={handleClick}
|
||||
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
import { FlexWrapper, Input, Switch } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
AffinePublicWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { noop } from 'foxact/noop';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { toast } from '../../../utils';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
||||
import * as style from './style.css';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
export interface PublishPanelProps
|
||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
export interface PublishPanelLocalProps
|
||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||
workspace: LocalWorkspace;
|
||||
}
|
||||
export interface PublishPanelAffineProps
|
||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||
workspace: AffineCloudWorkspace | AffinePublicWorkspace;
|
||||
}
|
||||
|
||||
const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
||||
const { workspace } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
||||
const isPublic = useMemo(() => {
|
||||
return workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC;
|
||||
}, [workspace]);
|
||||
const [origin, setOrigin] = useState('');
|
||||
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
||||
|
||||
useEffect(() => {
|
||||
setOrigin(
|
||||
typeof window !== 'undefined' && window.location.origin
|
||||
? window.location.origin
|
||||
: ''
|
||||
);
|
||||
}, []);
|
||||
|
||||
const copyUrl = useAsyncCallback(async () => {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
toast(t['Copied link to clipboard']());
|
||||
}, [shareUrl, t]);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'none' }}>
|
||||
<SettingRow
|
||||
name={t['Publish']()}
|
||||
desc={isPublic ? t['Unpublished hint']() : t['Published hint']()}
|
||||
style={{
|
||||
marginBottom: isPublic ? '12px' : '25px',
|
||||
}}
|
||||
>
|
||||
<Switch checked={isPublic} />
|
||||
</SettingRow>
|
||||
{isPublic ? (
|
||||
<FlexWrapper justifyContent="space-between" marginBottom={25}>
|
||||
<Input value={shareUrl} disabled />
|
||||
<Button
|
||||
onClick={copyUrl}
|
||||
style={{
|
||||
marginLeft: '20px',
|
||||
}}
|
||||
>
|
||||
{t['Copy']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FakePublishPanelAffineProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
|
||||
const FakePublishPanelAffine = (_props: FakePublishPanelAffineProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<Tooltip content={t['com.affine.settings.workspace.publish-tooltip']()}>
|
||||
<div className={style.fakeWrapper}>
|
||||
<SettingRow name={t['Publish']()} desc={t['Unpublished hint']()}>
|
||||
<Switch checked={false} onChange={noop} />
|
||||
</SettingRow>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const PublishPanelLocal = ({
|
||||
workspace,
|
||||
onTransferWorkspace,
|
||||
}: PublishPanelLocalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={t['Workspace saved locally']({ name })}
|
||||
desc={t['Enable cloud hint']()}
|
||||
spreadCol={false}
|
||||
style={{
|
||||
padding: '10px',
|
||||
background: 'var(--affine-background-secondary-color)',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
data-testid="publish-enable-affine-cloud-button"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
style={{ marginTop: '12px' }}
|
||||
>
|
||||
{t['Enable AFFiNE Cloud']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<FakePublishPanelAffine workspace={workspace} />
|
||||
{runtimeConfig.enableCloud ? (
|
||||
<EnableAffineCloudModal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onConfirm={() => {
|
||||
onTransferWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
workspace
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TmpDisableAffineCloudModal open={open} onOpenChange={setOpen} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PublishPanel = (props: PublishPanelProps) => {
|
||||
if (
|
||||
props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ||
|
||||
props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC
|
||||
) {
|
||||
return <PublishPanelAffine {...props} workspace={props.workspace} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <PublishPanelLocal {...props} workspace={props.workspace} />;
|
||||
}
|
||||
throw new Unreachable();
|
||||
};
|
||||
+4
-4
@@ -2,8 +2,8 @@ import { FlexWrapper, toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||
import type { MoveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -33,11 +33,11 @@ const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
};
|
||||
|
||||
interface StoragePanelProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
}
|
||||
|
||||
export const StoragePanel = ({ workspace }: StoragePanelProps) => {
|
||||
const workspaceId = workspace.id;
|
||||
export const StoragePanel = ({ workspaceMetadata }: StoragePanelProps) => {
|
||||
const workspaceId = workspaceMetadata.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const secondaryPath = useDBFileSecondaryPath(workspaceId);
|
||||
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
import type {
|
||||
WorkspaceFlavour,
|
||||
WorkspaceRegistry,
|
||||
} from '@affine/env/workspace';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||
|
||||
export interface WorkspaceSettingDetailProps {
|
||||
workspaceId: string;
|
||||
isOwner: boolean;
|
||||
onDeleteLocalWorkspace: () => void;
|
||||
onDeleteCloudWorkspace: () => void;
|
||||
onLeaveWorkspace: () => void;
|
||||
onTransferWorkspace: <
|
||||
From extends WorkspaceFlavour,
|
||||
To extends WorkspaceFlavour,
|
||||
>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
) => void;
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ import {
|
||||
listHistoryQuery,
|
||||
recoverDocMutation,
|
||||
} from '@affine/graphql';
|
||||
import {
|
||||
createAffineCloudBlobStorage,
|
||||
globalBlockSuiteSchema,
|
||||
} from '@affine/workspace';
|
||||
import {
|
||||
useMutateQueryResource,
|
||||
useMutation,
|
||||
useQueryInfinite,
|
||||
} from '@affine/workspace/affine/gql';
|
||||
import { createAffineCloudBlobEngine } from '@affine/workspace/blob';
|
||||
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
@@ -107,27 +109,13 @@ const workspaceMap = new Map<string, Workspace>();
|
||||
const getOrCreateWorkspace = (workspaceId: string) => {
|
||||
let workspace = workspaceMap.get(workspaceId);
|
||||
if (!workspace) {
|
||||
const blobEngine = createAffineCloudBlobEngine(workspaceId);
|
||||
const blobStorage = createAffineCloudBlobStorage(workspaceId);
|
||||
workspace = new Workspace({
|
||||
id: workspaceId,
|
||||
providerCreators: [],
|
||||
blobStorages: [
|
||||
() => ({
|
||||
crud: {
|
||||
async get(key) {
|
||||
return (await blobEngine.get(key)) ?? null;
|
||||
},
|
||||
async set(key, value) {
|
||||
await blobEngine.set(key, value);
|
||||
return key;
|
||||
},
|
||||
async delete(key) {
|
||||
return blobEngine.delete(key);
|
||||
},
|
||||
async list() {
|
||||
return blobEngine.list();
|
||||
},
|
||||
},
|
||||
crud: blobStorage,
|
||||
}),
|
||||
],
|
||||
schema: globalBlockSuiteSchema,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from '@affine/component/ui/button';
|
||||
import { ConfirmModal, Modal } from '@affine/component/ui/modal';
|
||||
import type { PageMode } from '@affine/core/atoms';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import { pageHistoryModalAtom } from '../../../atoms/page-history';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
|
||||
import {
|
||||
EdgelessSwitchItem,
|
||||
@@ -423,7 +423,7 @@ export const PageHistoryModal = ({
|
||||
|
||||
export const GlobalPageHistoryModal = () => {
|
||||
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { validateAndReduceImage } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import bytes from 'bytes';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import {
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor';
|
||||
import { useUserSubscription } from '../../../../hooks/use-subscription';
|
||||
import { validateAndReduceImage } from '../../../../utils/reduce-image';
|
||||
import { Upload } from '../../../pure/file-upload';
|
||||
import * as style from './style.css';
|
||||
|
||||
@@ -187,7 +187,6 @@ const StoragePanel = () => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
workspaceId: null,
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
|
||||
-1
@@ -119,7 +119,6 @@ const SubscriptionSettings = () => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
workspaceId: null,
|
||||
});
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
||||
import { Modal, type ModalProps } from '@affine/component/ui/modal';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';
|
||||
@@ -20,16 +21,16 @@ type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
|
||||
|
||||
export interface SettingProps extends ModalProps {
|
||||
activeTab: ActiveTab;
|
||||
workspaceId: string | null;
|
||||
workspaceMetadata?: WorkspaceMetadata | null;
|
||||
onSettingClick: (params: {
|
||||
activeTab: ActiveTab;
|
||||
workspaceId: string | null;
|
||||
workspaceMetadata: WorkspaceMetadata | null;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const SettingModal = ({
|
||||
activeTab = 'appearance',
|
||||
workspaceId = null,
|
||||
workspaceMetadata = null,
|
||||
onSettingClick,
|
||||
...modalProps
|
||||
}: SettingProps) => {
|
||||
@@ -75,22 +76,22 @@ export const SettingModal = ({
|
||||
(key: GeneralSettingKeys) => {
|
||||
onSettingClick({
|
||||
activeTab: key,
|
||||
workspaceId: null,
|
||||
workspaceMetadata: null,
|
||||
});
|
||||
},
|
||||
[onSettingClick]
|
||||
);
|
||||
const onWorkspaceSettingClick = useCallback(
|
||||
(workspaceId: string) => {
|
||||
(workspaceMetadata: WorkspaceMetadata) => {
|
||||
onSettingClick({
|
||||
activeTab: 'workspace',
|
||||
workspaceId,
|
||||
workspaceMetadata,
|
||||
});
|
||||
},
|
||||
[onSettingClick]
|
||||
);
|
||||
const onAccountSettingClick = useCallback(() => {
|
||||
onSettingClick({ activeTab: 'account', workspaceId: null });
|
||||
onSettingClick({ activeTab: 'account', workspaceMetadata: null });
|
||||
}, [onSettingClick]);
|
||||
|
||||
return (
|
||||
@@ -114,7 +115,7 @@ export const SettingModal = ({
|
||||
onGeneralSettingClick={onGeneralSettingClick}
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedGeneralKey={activeTab}
|
||||
selectedWorkspaceId={workspaceId}
|
||||
selectedWorkspaceId={workspaceMetadata?.id ?? null}
|
||||
onAccountSettingClick={onAccountSettingClick}
|
||||
/>
|
||||
|
||||
@@ -125,9 +126,12 @@ export const SettingModal = ({
|
||||
>
|
||||
<div ref={modalContentRef} className={style.centerContainer}>
|
||||
<div className={style.content}>
|
||||
{activeTab === 'workspace' && workspaceId ? (
|
||||
{activeTab === 'workspace' && workspaceMetadata ? (
|
||||
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
||||
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
|
||||
<WorkspaceSetting
|
||||
key={workspaceMetadata.id}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
/>
|
||||
</Suspense>
|
||||
) : null}
|
||||
{generalSettingList.some(v => v.key === activeTab) ? (
|
||||
|
||||
+25
-28
@@ -4,22 +4,23 @@ import {
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace';
|
||||
import {
|
||||
waitForCurrentWorkspaceAtom,
|
||||
workspaceListAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
|
||||
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useAtomValue } from 'jotai/react';
|
||||
import { type ReactElement, Suspense, useCallback, useMemo } from 'react';
|
||||
import { type ReactElement, Suspense, useCallback } from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms';
|
||||
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { UserPlanButton } from '../../auth/user-plan-button';
|
||||
import type {
|
||||
GeneralSettingKeys,
|
||||
@@ -109,7 +110,7 @@ export const SettingSidebar = ({
|
||||
}: {
|
||||
generalSettingList: GeneralSettingList;
|
||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
onWorkspaceSettingClick: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
selectedGeneralKey: string | null;
|
||||
onAccountSettingClick: () => void;
|
||||
@@ -182,25 +183,20 @@ export const WorkspaceList = ({
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
}: {
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
onWorkspaceSettingClick: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
}) => {
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const workspaceList = useMemo(() => {
|
||||
return workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.AFFINE_PUBLIC
|
||||
);
|
||||
}, [workspaces]);
|
||||
const workspaces = useAtomValue(workspaceListAtom);
|
||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
return (
|
||||
<>
|
||||
{workspaceList.map(workspace => {
|
||||
{workspaces.map(workspace => {
|
||||
return (
|
||||
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
||||
<WorkspaceListItem
|
||||
meta={workspace}
|
||||
onClick={() => {
|
||||
onWorkspaceSettingClick(workspace.id);
|
||||
onWorkspaceSettingClick(workspace);
|
||||
}}
|
||||
isCurrent={workspace.id === currentWorkspace.id}
|
||||
isActive={workspace.id === selectedWorkspaceId}
|
||||
@@ -218,33 +214,34 @@ const WorkspaceListItem = ({
|
||||
isCurrent,
|
||||
isActive,
|
||||
}: {
|
||||
meta: RootWorkspaceMetadata;
|
||||
meta: WorkspaceMetadata;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(meta.id);
|
||||
const workspace = useAtomValue(workspaceAtom);
|
||||
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
||||
const information = useWorkspaceInfo(meta);
|
||||
|
||||
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
||||
|
||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(sidebarSelectItem, { active: isActive })}
|
||||
title={workspaceName}
|
||||
title={name}
|
||||
onClick={onClick}
|
||||
data-testid="workspace-list-item"
|
||||
>
|
||||
<Avatar
|
||||
size={14}
|
||||
url={workspaceAvatar}
|
||||
name={workspaceName}
|
||||
url={avatarUrl}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
style={{
|
||||
marginRight: '10px',
|
||||
}}
|
||||
/>
|
||||
<span className="setting-name">{workspaceName}</span>
|
||||
<span className="setting-name">{name}</span>
|
||||
{isCurrent ? (
|
||||
<Tooltip content="Current" side="top">
|
||||
<div
|
||||
|
||||
+12
-107
@@ -1,113 +1,18 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||
|
||||
import { getUIAdapter } from '../../../../adapters/workspace';
|
||||
import { openSettingModalAtom } from '../../../../atoms';
|
||||
import { useLeaveWorkspace } from '../../../../hooks/affine/use-leave-workspace';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../../../../hooks/use-navigate-helper';
|
||||
import { useWorkspace } from '../../../../hooks/use-workspace';
|
||||
import { useAppHelper } from '../../../../hooks/use-workspaces';
|
||||
|
||||
export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const leaveWorkspace = useLeaveWorkspace();
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const { deleteWorkspace } = useAppHelper();
|
||||
|
||||
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
|
||||
|
||||
const closeAndJumpOut = useCallback(() => {
|
||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||
|
||||
if (currentWorkspace.id === workspaceId) {
|
||||
const backWorkspace = workspaces.find(ws => ws.id !== workspaceId);
|
||||
// TODO: if there is no workspace, jump to a new page(wait for design)
|
||||
if (backWorkspace) {
|
||||
jumpToSubPath(
|
||||
backWorkspace?.id || '',
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentWorkspace.id,
|
||||
jumpToIndex,
|
||||
jumpToSubPath,
|
||||
setSettingModal,
|
||||
workspaceId,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
const handleDeleteWorkspace = useAsyncCallback(async () => {
|
||||
closeAndJumpOut();
|
||||
await deleteWorkspace(workspaceId);
|
||||
|
||||
pushNotification({
|
||||
title: t['Successfully deleted'](),
|
||||
type: 'success',
|
||||
});
|
||||
}, [closeAndJumpOut, deleteWorkspace, pushNotification, t, workspaceId]);
|
||||
|
||||
const handleLeaveWorkspace = useAsyncCallback(async () => {
|
||||
closeAndJumpOut();
|
||||
await leaveWorkspace(workspaceId, workspaceName);
|
||||
|
||||
pushNotification({
|
||||
title: 'Successfully leave',
|
||||
type: 'success',
|
||||
});
|
||||
}, [
|
||||
closeAndJumpOut,
|
||||
leaveWorkspace,
|
||||
pushNotification,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
]);
|
||||
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
// const handleDelete = useCallback(async () => {
|
||||
// await onDeleteWorkspace();
|
||||
// toast(t['Successfully deleted'](), {
|
||||
// portal: document.body,
|
||||
// });
|
||||
// onClose();
|
||||
// }, [onClose, onDeleteWorkspace, t, workspace.id]);
|
||||
import { NewWorkspaceSettingDetail } from '../../../../adapters/shared';
|
||||
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
|
||||
|
||||
export const WorkspaceSetting = ({
|
||||
workspaceMetadata,
|
||||
}: {
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
}) => {
|
||||
const isOwner = useIsWorkspaceOwner(workspaceMetadata);
|
||||
return (
|
||||
<NewSettingsDetail
|
||||
onDeleteCloudWorkspace={handleDeleteWorkspace}
|
||||
onDeleteLocalWorkspace={handleDeleteWorkspace}
|
||||
onLeaveWorkspace={handleLeaveWorkspace}
|
||||
onTransformWorkspace={onTransformWorkspace}
|
||||
currentWorkspaceId={workspaceId}
|
||||
<NewWorkspaceSettingDetail
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
import {
|
||||
type AffineOfficialWorkspace,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { Workspace } from '@affine/workspace';
|
||||
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
import { ShareMenu } from './share-menu';
|
||||
|
||||
type SharePageModalProps = {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
workspace: Workspace;
|
||||
page: Page;
|
||||
};
|
||||
|
||||
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
const { openPage } = useNavigateHelper();
|
||||
|
||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||
|
||||
const handleConfirm = useAsyncCallback(async () => {
|
||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
return;
|
||||
}
|
||||
onTransformWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
workspace
|
||||
);
|
||||
const { id: newId } =
|
||||
await workspaceManager.transformLocalToCloud(workspace);
|
||||
openPage(newId, page.id);
|
||||
setOpen(false);
|
||||
}, [onTransformWorkspace, workspace]);
|
||||
}, [openPage, page.id, workspace, workspaceManager]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShareMenu
|
||||
workspace={workspace}
|
||||
workspaceMetadata={workspace.meta}
|
||||
currentPage={page}
|
||||
onEnableAffineCloud={() => setOpen(true)}
|
||||
/>
|
||||
|
||||
+4
-1
@@ -10,7 +10,10 @@ import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
import { useSharingUrl } from './use-share-url';
|
||||
|
||||
export const ShareExport = ({ workspace, currentPage }: ShareMenuProps) => {
|
||||
export const ShareExport = ({
|
||||
workspaceMetadata: workspace,
|
||||
currentPage,
|
||||
}: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaceId = workspace.id;
|
||||
const pageId = currentPage.id;
|
||||
|
||||
+7
-17
@@ -1,14 +1,9 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { Menu } from '@affine/component/ui/menu';
|
||||
import {
|
||||
type AffineCloudWorkspace,
|
||||
type AffineOfficialWorkspace,
|
||||
type AffinePublicWorkspace,
|
||||
type LocalWorkspace,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace';
|
||||
import { WebIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
|
||||
@@ -17,13 +12,8 @@ import * as styles from './index.css';
|
||||
import { ShareExport } from './share-export';
|
||||
import { SharePage } from './share-page';
|
||||
|
||||
export interface ShareMenuProps<
|
||||
Workspace extends AffineOfficialWorkspace =
|
||||
| AffineCloudWorkspace
|
||||
| LocalWorkspace
|
||||
| AffinePublicWorkspace,
|
||||
> {
|
||||
workspace: Workspace;
|
||||
export interface ShareMenuProps {
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
currentPage: Page;
|
||||
onEnableAffineCloud: () => void;
|
||||
}
|
||||
@@ -70,7 +60,7 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const {
|
||||
workspace: { id: workspaceId },
|
||||
workspaceMetadata: { id: workspaceId },
|
||||
currentPage,
|
||||
} = props;
|
||||
const { isSharedPage } = useIsSharedPage(workspaceId, currentPage.id);
|
||||
@@ -96,9 +86,9 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
};
|
||||
|
||||
export const ShareMenu = (props: ShareMenuProps) => {
|
||||
const { workspace } = props;
|
||||
const { workspaceMetadata } = props;
|
||||
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalShareMenu {...props} />;
|
||||
}
|
||||
return <CloudShareMenu {...props} />;
|
||||
|
||||
+5
-3
@@ -69,7 +69,7 @@ export const LocalSharePage = (props: ShareMenuProps) => {
|
||||
|
||||
export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
const {
|
||||
workspace: { id: workspaceId },
|
||||
workspaceMetadata: { id: workspaceId },
|
||||
currentPage,
|
||||
} = props;
|
||||
const pageId = currentPage.id;
|
||||
@@ -239,9 +239,11 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
};
|
||||
|
||||
export const SharePage = (props: ShareMenuProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalSharePage {...props} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
} else if (
|
||||
props.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) {
|
||||
return <AffineSharePage {...props} />;
|
||||
}
|
||||
throw new Error('Unreachable');
|
||||
|
||||
+10
-10
@@ -1,4 +1,4 @@
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
@@ -18,7 +18,7 @@ import { PageHeaderMenuButton } from './operation-menu';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface BlockSuiteHeaderTitleProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
pageId: string;
|
||||
isPublic?: boolean;
|
||||
publicMode?: PageMode;
|
||||
@@ -53,7 +53,7 @@ const EditableTitle = ({
|
||||
};
|
||||
|
||||
const StableTitle = ({
|
||||
workspace,
|
||||
blockSuiteWorkspace: workspace,
|
||||
pageId,
|
||||
onRename,
|
||||
isPublic,
|
||||
@@ -61,8 +61,8 @@ const StableTitle = ({
|
||||
}: BlockSuiteHeaderTitleProps & {
|
||||
onRename?: () => void;
|
||||
}) => {
|
||||
const currentPage = workspace.blockSuiteWorkspace.getPage(pageId);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
||||
const currentPage = workspace.getPage(pageId);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace).find(
|
||||
meta => meta.id === currentPage?.id
|
||||
);
|
||||
|
||||
@@ -77,7 +77,7 @@ const StableTitle = ({
|
||||
return (
|
||||
<div className={styles.headerTitleContainer}>
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
blockSuiteWorkspace={workspace}
|
||||
pageId={pageId}
|
||||
isPublic={isPublic}
|
||||
publicMode={publicMode}
|
||||
@@ -97,12 +97,12 @@ const StableTitle = ({
|
||||
};
|
||||
|
||||
const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
||||
const { workspace, pageId } = props;
|
||||
const currentPage = workspace.blockSuiteWorkspace.getPage(pageId);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
||||
const { blockSuiteWorkspace: workspace, pageId } = props;
|
||||
const currentPage = workspace.getPage(pageId);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace).find(
|
||||
meta => meta.id === currentPage?.id
|
||||
);
|
||||
const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
||||
const pageTitleMeta = usePageMetaHelper(workspace);
|
||||
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const [title, setPageTitle] = useState(pageMeta?.title || 'Untitled');
|
||||
|
||||
+9
-7
@@ -7,6 +7,7 @@ import {
|
||||
} from '@affine/component/ui/menu';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
DuplicateIcon,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
ImportIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
@@ -27,7 +27,6 @@ import { currentModeAtom } from '../../../atoms/mode';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import { useExportPage } from '../../../hooks/affine/use-export-page';
|
||||
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { toast } from '../../../utils';
|
||||
import { PageHistoryModal } from '../../affine/page-history-modal/history-modal';
|
||||
import { HeaderDropDownButton } from '../../pure/header-drop-down-button';
|
||||
@@ -42,16 +41,16 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
// fixme(himself65): remove these hooks ASAP
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(currentPage);
|
||||
|
||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
meta => meta.id === pageId
|
||||
) as PageMeta;
|
||||
);
|
||||
const currentMode = useAtomValue(currentModeAtom);
|
||||
const favorite = pageMeta.favorite ?? false;
|
||||
const favorite = pageMeta?.favorite ?? false;
|
||||
|
||||
const { togglePageMode, toggleFavorite, duplicate } =
|
||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
@@ -65,12 +64,15 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
||||
}, []);
|
||||
|
||||
const handleOpenTrashModal = useCallback(() => {
|
||||
if (!pageMeta) {
|
||||
return;
|
||||
}
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: [pageId],
|
||||
pageTitles: [pageMeta.title],
|
||||
});
|
||||
}, [pageId, pageMeta.title, setTrashModal]);
|
||||
}, [pageId, pageMeta, setTrashModal]);
|
||||
|
||||
const handleFavorite = useCallback(() => {
|
||||
toggleFavorite(pageId);
|
||||
@@ -205,7 +207,7 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
if (pageMeta.trash) {
|
||||
if (pageMeta?.trash) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { CSSProperties } from 'react';
|
||||
@@ -44,8 +43,7 @@ export const EditorModeSwitch = ({
|
||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
assertExists(pageMeta);
|
||||
const { trash } = pageMeta;
|
||||
const trash = pageMeta?.trash ?? false;
|
||||
|
||||
const { togglePageMode, switchToEdgelessMode, switchToPageMode } =
|
||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import './page-detail-editor.css';
|
||||
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { pluginEditorAtom } from '@toeverything/infra/__internal__/plugin';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
@@ -51,10 +49,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
||||
isPublic,
|
||||
publishMode,
|
||||
}: PageDetailEditorProps & { page: Page }) {
|
||||
const meta = useBlockSuitePageMeta(workspace).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
|
||||
const { switchToEdgelessMode, switchToPageMode } =
|
||||
useBlockSuiteMetaHelper(workspace);
|
||||
|
||||
@@ -73,7 +67,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
||||
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
assertExists(meta);
|
||||
const value = useMemo(() => {
|
||||
const fontStyle = fontStyleOptions.find(
|
||||
option => option.key === appSettings.fontStyle
|
||||
@@ -171,9 +164,8 @@ export const PageDetailEditor = (props: PageDetailEditorProps) => {
|
||||
const { workspace, pageId } = props;
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(workspace, pageId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<PageDetailEditorMain {...props} page={page} />
|
||||
|
||||
@@ -2,21 +2,17 @@ import { commandScore } from '@affine/cmdk';
|
||||
import { useCollectionManager } from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
waitForCurrentWorkspaceAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
|
||||
import type { Page, PageMeta } from '@blocksuite/store';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
getWorkspace,
|
||||
waitForWorkspace,
|
||||
} from '@toeverything/infra/__internal__/workspace';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
getCurrentStore,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { currentPageIdAtom, getCurrentStore } from '@toeverything/infra/atom';
|
||||
import {
|
||||
type AffineCommand,
|
||||
AffineCommandRegistry,
|
||||
@@ -33,7 +29,6 @@ import {
|
||||
recentPageIdsBaseAtom,
|
||||
} from '../../../atoms';
|
||||
import { collectionsCRUDAtom } from '../../../atoms/collections';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { WorkspaceSubPath } from '../../../shared';
|
||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||
@@ -53,8 +48,8 @@ export const cmdkValueAtom = atom('');
|
||||
|
||||
// like currentWorkspaceAtom, but not throw error
|
||||
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
|
||||
const currentWorkspaceId = get(currentWorkspaceIdAtom);
|
||||
if (!currentWorkspaceId) {
|
||||
const currentWorkspace = get(currentWorkspaceAtom);
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,9 +59,7 @@ const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = getWorkspace(currentWorkspaceId);
|
||||
await waitForWorkspace(workspace);
|
||||
const page = workspace.getPage(currentPageId);
|
||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
|
||||
if (!page) {
|
||||
return;
|
||||
@@ -132,7 +125,7 @@ export const filteredAffineCommands = atom(async get => {
|
||||
});
|
||||
|
||||
const useWorkspacePages = () => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||
return pages;
|
||||
};
|
||||
@@ -166,7 +159,7 @@ export const pageToCommand = (
|
||||
blockId?: string
|
||||
): CMDKCommand => {
|
||||
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
|
||||
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
|
||||
const currentWorkspace = store.get(currentWorkspaceAtom);
|
||||
|
||||
const title = page.title || t['Untitled']();
|
||||
const commandLabel = label || {
|
||||
@@ -191,18 +184,18 @@ export const pageToCommand = (
|
||||
originalValue: title,
|
||||
category: category,
|
||||
run: () => {
|
||||
if (!currentWorkspaceId) {
|
||||
if (!currentWorkspace) {
|
||||
console.error('current workspace not found');
|
||||
return;
|
||||
}
|
||||
if (blockId) {
|
||||
return navigationHelper.jumpToPageBlock(
|
||||
currentWorkspaceId,
|
||||
currentWorkspace.id,
|
||||
page.id,
|
||||
blockId
|
||||
);
|
||||
}
|
||||
return navigationHelper.jumpToPage(currentWorkspaceId, page.id);
|
||||
return navigationHelper.jumpToPage(currentWorkspace.id, page.id);
|
||||
},
|
||||
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
|
||||
timestamp: page.updatedDate,
|
||||
@@ -217,7 +210,7 @@ export const usePageCommands = () => {
|
||||
const recentPages = useRecentPages();
|
||||
const pages = useWorkspacePages();
|
||||
const store = getCurrentStore();
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
|
||||
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
||||
const query = useAtomValue(cmdkQueryAtom);
|
||||
@@ -359,7 +352,7 @@ export const collectionToCommand = (
|
||||
selectCollection: (id: string) => void,
|
||||
t: ReturnType<typeof useAFFiNEI18N>
|
||||
): CMDKCommand => {
|
||||
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
|
||||
const currentWorkspace = store.get(currentWorkspaceAtom);
|
||||
const label = collection.name || t['Untitled']();
|
||||
const category = 'affine:collections';
|
||||
return {
|
||||
@@ -377,11 +370,11 @@ export const collectionToCommand = (
|
||||
originalValue: label,
|
||||
category: category,
|
||||
run: () => {
|
||||
if (!currentWorkspaceId) {
|
||||
if (!currentWorkspace) {
|
||||
console.error('current workspace not found');
|
||||
return;
|
||||
}
|
||||
navigationHelper.jumpToSubPath(currentWorkspaceId, WorkspaceSubPath.ALL);
|
||||
navigationHelper.jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
|
||||
selectCollection(collection.id);
|
||||
},
|
||||
icon: <ViewLayersIcon />,
|
||||
@@ -395,7 +388,7 @@ export const useCollectionsCommands = () => {
|
||||
const query = useAtomValue(cmdkQueryAtom);
|
||||
const navigationHelper = useNavigateHelper();
|
||||
const t = useAFFiNEI18N();
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
const selectCollection = useCallback(
|
||||
(id: string) => {
|
||||
navigationHelper.jumpToCollection(workspace.id, id);
|
||||
|
||||
@@ -40,7 +40,6 @@ export const HelpIsland = () => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: tab,
|
||||
workspaceId: null,
|
||||
});
|
||||
},
|
||||
[setOpenSettingModalAtom]
|
||||
|
||||
@@ -3,21 +3,21 @@ import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { toast } from '../../../utils';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
|
||||
// fixme(himself65): remove these hooks ASAP
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
assertExists(workspace);
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
|
||||
+7
-10
@@ -15,7 +15,6 @@ import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -44,9 +43,9 @@ const CollectionRenderer = ({
|
||||
const t = useAFFiNEI18N();
|
||||
const dragItemId = getDropItemId('collections', collection.id);
|
||||
|
||||
const removeFromAllowList = useAsyncCallback(
|
||||
async (id: string) => {
|
||||
await setting.updateCollection({
|
||||
const removeFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
setting.updateCollection({
|
||||
...collection,
|
||||
allowList: collection.allowList?.filter(v => v !== id),
|
||||
});
|
||||
@@ -66,9 +65,7 @@ const CollectionRenderer = ({
|
||||
} else {
|
||||
toast(t['com.affine.collection.addPage.success']());
|
||||
}
|
||||
setting.addPage(collection.id, id).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
setting.addPage(collection.id, id);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -90,9 +87,9 @@ const CollectionRenderer = ({
|
||||
const currentPath = location.pathname.split('?')[0];
|
||||
const path = `/workspace/${workspace.id}/collection/${collection.id}`;
|
||||
|
||||
const onRename = useAsyncCallback(
|
||||
async (name: string) => {
|
||||
await setting.updateCollection({
|
||||
const onRename = useCallback(
|
||||
(name: string) => {
|
||||
setting.updateCollection({
|
||||
...collection,
|
||||
name,
|
||||
});
|
||||
|
||||
+16
-5
@@ -1,12 +1,16 @@
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { MenuItem } from '@affine/component/ui/menu';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import {
|
||||
workspaceListAtom,
|
||||
workspaceManagerAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
authAtom,
|
||||
@@ -81,9 +85,16 @@ export const UserWithWorkspaceList = ({
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
|
||||
delay: 0,
|
||||
});
|
||||
const workspaces = useAtomValue(workspaceListAtom);
|
||||
|
||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||
|
||||
// revalidate workspace list when mounted
|
||||
useEffect(() => {
|
||||
workspaceManager.list.revalidate().catch(err => {
|
||||
throw new Unreachable('revlidate should never throw, ' + err);
|
||||
});
|
||||
}, [workspaceManager]);
|
||||
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
|
||||
+27
-52
@@ -1,39 +1,29 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import type { WorkspaceMetadata } from '@affine/workspace';
|
||||
import { currentWorkspaceAtom } from '@affine/workspace/atom';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { startTransition, useCallback, useMemo, useTransition } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
openCreateWorkspaceModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '../../../../../atoms';
|
||||
import type { AllWorkspace } from '../../../../../shared';
|
||||
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
|
||||
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
||||
import * as styles from './index.css';
|
||||
interface WorkspaceModalProps {
|
||||
disabled?: boolean;
|
||||
workspaces: (AffineCloudWorkspace | LocalWorkspace)[];
|
||||
currentWorkspaceId: AllWorkspace['id'] | null;
|
||||
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
workspaces: WorkspaceMetadata[];
|
||||
currentWorkspaceId?: string | null;
|
||||
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onClickWorkspaceSetting: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onNewWorkspace: () => void;
|
||||
onAddWorkspace: () => void;
|
||||
onDragEnd: (event: DragEndEvent) => void;
|
||||
@@ -102,22 +92,14 @@ export const AFFiNEWorkspaceList = ({
|
||||
workspaces,
|
||||
onEventEnd,
|
||||
}: {
|
||||
workspaces: RootWorkspaceMetadata[];
|
||||
workspaces: WorkspaceMetadata[];
|
||||
onEventEnd?: () => void;
|
||||
}) => {
|
||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
||||
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
|
||||
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
|
||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
|
||||
currentWorkspaceIdAtom
|
||||
);
|
||||
|
||||
const setCurrentPageId = useSetAtom(currentPageIdAtom);
|
||||
|
||||
const [, startCloseTransition] = useTransition();
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
@@ -130,7 +112,7 @@ export const AFFiNEWorkspaceList = ({
|
||||
() =>
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[],
|
||||
) as WorkspaceMetadata[],
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
@@ -138,44 +120,37 @@ export const AFFiNEWorkspaceList = ({
|
||||
() =>
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[],
|
||||
) as WorkspaceMetadata[],
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
const onClickWorkspaceSetting = useCallback(
|
||||
(workspaceId: string) => {
|
||||
(workspaceMetadata: WorkspaceMetadata) => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'workspace',
|
||||
workspaceId,
|
||||
workspaceMetadata,
|
||||
});
|
||||
onEventEnd?.();
|
||||
},
|
||||
[onEventEnd, setOpenSettingModalAtom]
|
||||
);
|
||||
|
||||
const onMoveWorkspace = useCallback(
|
||||
(activeId: string, overId: string) => {
|
||||
const oldIndex = workspaces.findIndex(w => w.id === activeId);
|
||||
|
||||
const newIndex = workspaces.findIndex(w => w.id === overId);
|
||||
startTransition(() => {
|
||||
setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
|
||||
});
|
||||
},
|
||||
[setWorkspaces, workspaces]
|
||||
);
|
||||
const onMoveWorkspace = useCallback((_activeId: string, _overId: string) => {
|
||||
// TODO: order
|
||||
// const oldIndex = workspaces.findIndex(w => w.id === activeId);
|
||||
// const newIndex = workspaces.findIndex(w => w.id === overId);
|
||||
// startTransition(() => {
|
||||
// setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
|
||||
// });
|
||||
}, []);
|
||||
|
||||
const onClickWorkspace = useCallback(
|
||||
(workspaceId: string) => {
|
||||
startCloseTransition(() => {
|
||||
setCurrentWorkspaceId(workspaceId);
|
||||
setCurrentPageId(null);
|
||||
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
|
||||
});
|
||||
(workspaceMetadata: WorkspaceMetadata) => {
|
||||
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||
onEventEnd?.();
|
||||
},
|
||||
[jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId]
|
||||
[jumpToSubPath, onEventEnd]
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
@@ -211,7 +186,7 @@ export const AFFiNEWorkspaceList = ({
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
currentWorkspaceId={currentWorkspace?.id}
|
||||
onDragEnd={onDragEnd}
|
||||
/>
|
||||
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
||||
@@ -225,7 +200,7 @@ export const AFFiNEWorkspaceList = ({
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
currentWorkspaceId={currentWorkspace?.id}
|
||||
onDragEnd={onDragEnd}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
|
||||
+24
-28
@@ -1,12 +1,10 @@
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Loading } from '@affine/component/ui/loading';
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { useCurrentSyncEngine } from '@affine/core/hooks/current/use-current-sync-engine';
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
type SyncEngineStatus,
|
||||
SyncEngineStep,
|
||||
} from '@affine/workspace/providers';
|
||||
import { type SyncEngineStatus, SyncEngineStep } from '@affine/workspace';
|
||||
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||
import {
|
||||
CloudWorkspaceIcon,
|
||||
InformationFillDuotoneIcon,
|
||||
@@ -14,8 +12,9 @@ import {
|
||||
NoNetworkIcon,
|
||||
UnsyncIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
|
||||
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { debounce } from 'lodash-es';
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -27,7 +26,6 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import { useSystemOnline } from '../../../../hooks/use-system-online';
|
||||
import type { AllWorkspace } from '../../../../shared';
|
||||
import {
|
||||
StyledSelectorContainer,
|
||||
StyledSelectorWrapper,
|
||||
@@ -87,21 +85,18 @@ const OfflineStatus = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const WorkspaceStatus = ({
|
||||
currentWorkspace,
|
||||
}: {
|
||||
currentWorkspace: AllWorkspace;
|
||||
}) => {
|
||||
const WorkspaceStatus = () => {
|
||||
const isOnline = useSystemOnline();
|
||||
|
||||
const [syncEngineStatus, setSyncEngineStatus] =
|
||||
useState<SyncEngineStatus | null>(null);
|
||||
|
||||
const syncEngine = useCurrentSyncEngine();
|
||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
|
||||
// debounce sync engine status
|
||||
useEffect(() => {
|
||||
setSyncEngineStatus(syncEngine?.status ?? null);
|
||||
const disposable = syncEngine?.onStatusChange.on(
|
||||
setSyncEngineStatus(currentWorkspace.engine.sync.status);
|
||||
const disposable = currentWorkspace.engine.sync.onStatusChange.on(
|
||||
debounce(status => {
|
||||
setSyncEngineStatus(status);
|
||||
}, 500)
|
||||
@@ -109,7 +104,7 @@ const WorkspaceStatus = ({
|
||||
return () => {
|
||||
disposable?.dispose();
|
||||
};
|
||||
}, [syncEngine]);
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
// TODO: add i18n
|
||||
@@ -162,17 +157,18 @@ const WorkspaceStatus = ({
|
||||
|
||||
export const WorkspaceCard = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
currentWorkspace: AllWorkspace;
|
||||
} & HTMLAttributes<HTMLDivElement>
|
||||
>(({ currentWorkspace, ...props }, ref) => {
|
||||
const [name] = useBlockSuiteWorkspaceName(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ ...props }, ref) => {
|
||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
|
||||
const information = useWorkspaceInfo(currentWorkspace.meta);
|
||||
|
||||
const avatarUrl = useWorkspaceBlobObjectUrl(
|
||||
currentWorkspace.meta,
|
||||
information?.avatar
|
||||
);
|
||||
|
||||
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||
|
||||
return (
|
||||
<StyledSelectorContainer
|
||||
@@ -186,7 +182,7 @@ export const WorkspaceCard = forwardRef<
|
||||
<Avatar
|
||||
data-testid="workspace-avatar"
|
||||
size={40}
|
||||
url={workspaceAvatar}
|
||||
url={avatarUrl}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
/>
|
||||
@@ -194,7 +190,7 @@ export const WorkspaceCard = forwardRef<
|
||||
<StyledWorkspaceName data-testid="workspace-name">
|
||||
{name}
|
||||
</StyledWorkspaceName>
|
||||
<WorkspaceStatus currentWorkspace={currentWorkspace} />
|
||||
<WorkspaceStatus />
|
||||
</StyledSelectorWrapper>
|
||||
</StyledSelectorContainer>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Menu } from '@affine/component/ui/menu';
|
||||
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@affine/workspace';
|
||||
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
||||
import { type Page } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
@@ -40,7 +41,6 @@ import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
|
||||
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import type { AllWorkspace } from '../../shared';
|
||||
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
||||
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
|
||||
import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button';
|
||||
@@ -53,7 +53,7 @@ export type RootAppSidebarProps = {
|
||||
isPublicWorkspace: boolean;
|
||||
onOpenQuickSearchModal: () => void;
|
||||
onOpenSettingModal: () => void;
|
||||
currentWorkspace: AllWorkspace;
|
||||
currentWorkspace: Workspace;
|
||||
openPage: (pageId: string) => void;
|
||||
createPage: () => Page;
|
||||
currentPath: string;
|
||||
@@ -185,9 +185,9 @@ export const RootAppSidebar = ({
|
||||
});
|
||||
const handleCreateCollection = useCallback(() => {
|
||||
open('')
|
||||
.then(async name => {
|
||||
.then(name => {
|
||||
const id = nanoid();
|
||||
await setting.createCollection(createEmptyCollection(id, { name }));
|
||||
setting.createCollection(createEmptyCollection(id, { name }));
|
||||
navigateHelper.jumpToCollection(blockSuiteWorkspace.id, id);
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -230,7 +230,6 @@ export const RootAppSidebar = ({
|
||||
}}
|
||||
>
|
||||
<WorkspaceCard
|
||||
currentWorkspace={currentWorkspace}
|
||||
onClick={useCallback(() => {
|
||||
setOpenUserWorkspaceList(true);
|
||||
}, [setOpenUserWorkspaceList])}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { BrowserWarning } from '@affine/component/affine-banner';
|
||||
import { LocalDemoTips } from '@affine/component/affine-banner';
|
||||
import {
|
||||
type AffineOfficialWorkspace,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { Workspace } from '@affine/workspace';
|
||||
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { useOnTransformWorkspace } from '../hooks/root/use-on-transform-workspace';
|
||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
|
||||
|
||||
const minimumChromeVersion = 106;
|
||||
@@ -57,9 +58,11 @@ const OSWarningMessage = () => {
|
||||
};
|
||||
|
||||
export const TopTip = ({
|
||||
pageId,
|
||||
workspace,
|
||||
}: {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
pageId?: string;
|
||||
workspace: Workspace;
|
||||
}) => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const isLoggedIn = loginStatus === 'authenticated';
|
||||
@@ -73,18 +76,18 @@ export const TopTip = ({
|
||||
setAuthModal({ openModal: true, state: 'signIn' });
|
||||
}, [setAuthModal]);
|
||||
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
const handleConfirm = useCallback(() => {
|
||||
const { openPage } = useNavigateHelper();
|
||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||
const handleConfirm = useAsyncCallback(async () => {
|
||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
return;
|
||||
}
|
||||
onTransformWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
workspace
|
||||
);
|
||||
// TODO: we need to transform local to cloud
|
||||
const { id: newId } =
|
||||
await workspaceManager.transformLocalToCloud(workspace);
|
||||
openPage(newId, pageId || WorkspaceSubPath.ALL);
|
||||
setOpen(false);
|
||||
}, [onTransformWorkspace, workspace]);
|
||||
}, [openPage, pageId, workspace, workspaceManager]);
|
||||
|
||||
if (
|
||||
showLocalDemoTips &&
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||
import {
|
||||
migrateLocalBlobStorage,
|
||||
migrateWorkspace,
|
||||
} from '@toeverything/infra/blocksuite';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { WorkspaceAdapters } from '../../adapters/workspace';
|
||||
import { useCurrentSyncEngine } from '../../hooks/current/use-current-sync-engine';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
|
||||
export type UpgradeState = 'pending' | 'upgrading' | 'done' | 'error';
|
||||
|
||||
function applyDoc(target: YDoc, result: YDoc) {
|
||||
applyUpdate(target, encodeStateAsUpdate(result));
|
||||
for (const targetSubDoc of target.subdocs.values()) {
|
||||
const resultSubDocs = Array.from(result.subdocs.values());
|
||||
const resultSubDoc = resultSubDocs.find(
|
||||
item => item.guid === targetSubDoc.guid
|
||||
);
|
||||
if (resultSubDoc) {
|
||||
applyDoc(targetSubDoc, resultSubDoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpgradeWorkspace(migration: MigrationPoint) {
|
||||
const [state, setState] = useState<UpgradeState>('pending');
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [newWorkspaceId, setNewWorkspaceId] = useState<string | null>(null);
|
||||
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const syncEngine = useCurrentSyncEngine();
|
||||
const rootStore = getCurrentStore();
|
||||
|
||||
const upgradeWorkspace = useAsyncCallback(async () => {
|
||||
setState('upgrading');
|
||||
setError(null);
|
||||
try {
|
||||
// Migration need to wait for root doc and all subdocs loaded.
|
||||
await syncEngine?.waitForSynced();
|
||||
|
||||
// Clone a new doc to prevent change events.
|
||||
const clonedDoc = new YDoc({
|
||||
guid: workspace.blockSuiteWorkspace.doc.guid,
|
||||
});
|
||||
applyDoc(clonedDoc, workspace.blockSuiteWorkspace.doc);
|
||||
const schema = workspace.blockSuiteWorkspace.schema;
|
||||
let newWorkspace: Workspace | null = null;
|
||||
|
||||
const resultDoc = await migrateWorkspace(migration, {
|
||||
doc: clonedDoc,
|
||||
schema,
|
||||
createWorkspace: () => {
|
||||
// Migrate to subdoc version need to create a new workspace.
|
||||
// It will only happened for old local workspace.
|
||||
newWorkspace = getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL);
|
||||
return Promise.resolve(newWorkspace);
|
||||
},
|
||||
});
|
||||
|
||||
if (newWorkspace) {
|
||||
const localMetaString =
|
||||
localStorage.getItem('jotai-workspaces') ?? '[]';
|
||||
const localMetadataList = JSON.parse(
|
||||
localMetaString
|
||||
) as RootWorkspaceMetadata[];
|
||||
const currentLocalMetadata = localMetadataList.find(
|
||||
item => item.id === workspace.id
|
||||
);
|
||||
const flavour = currentLocalMetadata?.flavour ?? WorkspaceFlavour.LOCAL;
|
||||
|
||||
// Legacy logic moved from `setup.ts`.
|
||||
// It works well before, should be refactor or remove in the future.
|
||||
const adapter = WorkspaceAdapters[flavour];
|
||||
const newId = await adapter.CRUD.create(newWorkspace);
|
||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(newId);
|
||||
await rootStore.get(workspaceAtom); // Trigger provider sync to persist data.
|
||||
|
||||
await adapter.CRUD.delete(workspace.blockSuiteWorkspace);
|
||||
await migrateLocalBlobStorage(workspace.id, newId);
|
||||
setNewWorkspaceId(newId);
|
||||
|
||||
const index = localMetadataList.findIndex(
|
||||
meta => meta.id === workspace.id
|
||||
);
|
||||
localMetadataList[index] = {
|
||||
...currentLocalMetadata,
|
||||
id: newId,
|
||||
flavour,
|
||||
};
|
||||
localStorage.setItem(
|
||||
'jotai-workspaces',
|
||||
JSON.stringify(localMetadataList)
|
||||
);
|
||||
localStorage.setItem('last_workspace_id', newId);
|
||||
localStorage.removeItem('last_page_id');
|
||||
} else {
|
||||
applyDoc(workspace.blockSuiteWorkspace.doc, resultDoc);
|
||||
}
|
||||
|
||||
await syncEngine?.waitForSynced();
|
||||
|
||||
setState('done');
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setError(e);
|
||||
setState('error');
|
||||
}
|
||||
}, [rootStore, workspace, syncEngine, migration]);
|
||||
|
||||
return [state, error, upgradeWorkspace, newWorkspaceId] as const;
|
||||
}
|
||||
@@ -1,90 +1,84 @@
|
||||
import { AffineShapeIcon } from '@affine/component/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management.
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
waitForCurrentWorkspaceAtom,
|
||||
workspaceManagerAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useWorkspaceStatus } from '@toeverything/hooks/use-workspace-status';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { pathGenerator } from '../../shared';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import * as styles from './upgrade.css';
|
||||
import { type UpgradeState, useUpgradeWorkspace } from './upgrade-hooks';
|
||||
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
||||
|
||||
const UPGRADE_TIPS_KEYS = {
|
||||
pending: 'com.affine.upgrade.tips.normal',
|
||||
upgrading: 'com.affine.upgrade.tips.normal',
|
||||
done: 'com.affine.upgrade.tips.done',
|
||||
error: 'com.affine.upgrade.tips.error',
|
||||
} as const;
|
||||
|
||||
const BUTTON_TEXT_KEYS = {
|
||||
pending: 'com.affine.upgrade.button-text.pending',
|
||||
upgrading: 'com.affine.upgrade.button-text.upgrading',
|
||||
done: 'com.affine.upgrade.button-text.done',
|
||||
error: 'com.affine.upgrade.button-text.error',
|
||||
} as const;
|
||||
|
||||
function UpgradeIcon({ upgradeState }: { upgradeState: UpgradeState }) {
|
||||
if (upgradeState === 'error') {
|
||||
return <HeartBreakIcon />;
|
||||
}
|
||||
return (
|
||||
<ArrowCircleIcon
|
||||
className={upgradeState === 'upgrading' ? styles.loadingIcon : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface WorkspaceUpgradeProps {
|
||||
migration: MigrationPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Help info is not implemented yet.
|
||||
*/
|
||||
export const WorkspaceUpgrade = function WorkspaceUpgrade(
|
||||
props: WorkspaceUpgradeProps
|
||||
) {
|
||||
const [upgradeState, error, upgradeWorkspace, newWorkspaceId] =
|
||||
useUpgradeWorkspace(props.migration);
|
||||
export const WorkspaceUpgrade = function WorkspaceUpgrade() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
|
||||
const { openPage } = useNavigateHelper();
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const refreshPage = useCallback(() => {
|
||||
window.location.reload();
|
||||
}, []);
|
||||
const onButtonClick = useAsyncCallback(async () => {
|
||||
if (upgradeStatus?.upgrading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onButtonClick = useMemo(() => {
|
||||
if (upgradeState === 'done') {
|
||||
try {
|
||||
const newWorkspaceId =
|
||||
await currentWorkspace.upgrade.upgrade(workspaceManager);
|
||||
if (newWorkspaceId) {
|
||||
return () => {
|
||||
window.location.replace(pathGenerator.all(newWorkspaceId));
|
||||
};
|
||||
openPage(newWorkspaceId, WorkspaceSubPath.ALL);
|
||||
} else {
|
||||
// blocksuite may enter an incorrect state, reload to reset it.
|
||||
location.reload();
|
||||
}
|
||||
|
||||
return refreshPage;
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : '' + error);
|
||||
}
|
||||
|
||||
if (upgradeState === 'pending') {
|
||||
return upgradeWorkspace;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [upgradeState, upgradeWorkspace, refreshPage, newWorkspaceId]);
|
||||
}, [
|
||||
upgradeStatus?.upgrading,
|
||||
currentWorkspace.upgrade,
|
||||
workspaceManager,
|
||||
openPage,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
<div className={styles.upgradeBox}>
|
||||
<AffineShapeIcon width={180} height={180} />
|
||||
<p className={styles.upgradeTips}>
|
||||
{error ? error.message : t[UPGRADE_TIPS_KEYS[upgradeState]]()}
|
||||
{error ? error : t['com.affine.upgrade.tips.normal']()}
|
||||
</p>
|
||||
<Button
|
||||
data-testid="upgrade-workspace-button"
|
||||
onClick={onButtonClick}
|
||||
size="extraLarge"
|
||||
icon={<UpgradeIcon upgradeState={upgradeState} />}
|
||||
type={upgradeState === 'error' ? 'error' : 'default'}
|
||||
icon={
|
||||
error ? (
|
||||
<HeartBreakIcon />
|
||||
) : (
|
||||
<ArrowCircleIcon
|
||||
className={
|
||||
upgradeStatus?.upgrading ? styles.loadingIcon : undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
type={error ? 'error' : 'default'}
|
||||
>
|
||||
{t[BUTTON_TEXT_KEYS[upgradeState]]()}
|
||||
{error
|
||||
? t['com.affine.upgrade.button-text.error']()
|
||||
: upgradeStatus?.upgrading
|
||||
? t['com.affine.upgrade.button-text.upgrading']()
|
||||
: t['com.affine.upgrade.button-text.pending']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user