refactor: workspace manager (#5060)

This commit is contained in:
EYHN
2023-12-15 07:20:50 +00:00
parent af15aa06d4
commit fe2851d3e9
217 changed files with 3605 additions and 4244 deletions
@@ -4,16 +4,17 @@ import {
FavoriteTag,
} from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useCallback, useMemo } from 'react';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
export const useAllPageListConfig = () => {
const [currentWorkspace] = useCurrentWorkspace();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = currentWorkspace.blockSuiteWorkspace;
const pageMetas = useBlockSuitePageMeta(workspace);
const { isPreferredEdgeless } = usePageHelper(workspace);
@@ -1,13 +1,23 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getIsOwnerQuery } from '@affine/graphql';
import { useQueryImmutable } from '@affine/workspace/affine/gql';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
export function useIsWorkspaceOwner(workspaceId: string) {
const { data } = useQueryImmutable({
query: getIsOwnerQuery,
variables: {
workspaceId,
},
});
export function useIsWorkspaceOwner(workspaceMetadata: WorkspaceMetadata) {
const { data } = useQueryImmutable(
workspaceMetadata.flavour !== WorkspaceFlavour.LOCAL
? {
query: getIsOwnerQuery,
variables: {
workspaceId: workspaceMetadata.id,
},
}
: undefined
);
if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
return true;
}
return data.isOwner;
}
@@ -1,26 +0,0 @@
import { leaveWorkspaceMutation } from '@affine/graphql';
import { useMutation } from '@affine/workspace/affine/gql';
import { useCallback } from 'react';
import { useAppHelper } from '../use-workspaces';
export function useLeaveWorkspace() {
const { deleteWorkspaceMeta } = useAppHelper();
const { trigger: leaveWorkspace } = useMutation({
mutation: leaveWorkspaceMutation,
});
return useCallback(
async (workspaceId: string, workspaceName: string) => {
deleteWorkspaceMeta(workspaceId);
await leaveWorkspace({
workspaceId,
workspaceName,
sendLeaveMail: true,
});
},
[deleteWorkspaceMeta, leaveWorkspace]
);
}
@@ -1,6 +1,7 @@
import { toast } from '@affine/component';
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 { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
@@ -8,11 +9,10 @@ import {
PreconditionStrategy,
registerAffineCommand,
} from '@toeverything/infra/command';
import { useSetAtom } from 'jotai';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import { pageHistoryModalAtom } from '../../atoms/page-history';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
import { useExportPage } from './use-export-page';
import { useTrashModalHelper } from './use-trash-modal-helper';
@@ -22,7 +22,7 @@ export function useRegisterBlocksuiteEditorCommands(
mode: 'page' | 'edgeless'
) {
const t = useAFFiNEI18N();
const [workspace] = useCurrentWorkspace();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const currentPage = blockSuiteWorkspace.getPage(pageId);
@@ -1,11 +1,12 @@
import { toast } from '@affine/component';
import type { DraggableTitleCellData } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
import { useTrashModalHelper } from './use-trash-modal-helper';
@@ -68,7 +69,7 @@ export function getDragItemId(
export const useSidebarDrag = () => {
const t = useAFFiNEI18N();
const [currentWorkspace] = useCurrentWorkspace();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = currentWorkspace.blockSuiteWorkspace;
const { setTrashModal } = useTrashModalHelper(workspace);
const { addToFavorite, removeFromFavorite } =
@@ -1,12 +1,14 @@
import {
currentPageIdAtom,
currentWorkspaceAtom,
} from '@toeverything/infra/atom';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
export const useCurrentPage = () => {
const currentPageId = useAtomValue(currentPageIdAtom);
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
return currentPageId ? currentWorkspace.getPage(currentPageId) : null;
return useBlockSuiteWorkspacePage(
currentWorkspace?.blockSuiteWorkspace,
currentPageId
);
};
@@ -1,33 +0,0 @@
import type { SyncEngine, SyncEngineStatus } from '@affine/workspace/providers';
import { useEffect, useState } from 'react';
import { useCurrentWorkspace } from './use-current-workspace';
export function useCurrentSyncEngine(): SyncEngine | undefined {
const [workspace] = useCurrentWorkspace();
// FIXME: This is a hack to get the sync engine, we need refactor this in the future.
const syncEngine = (
workspace.blockSuiteWorkspace.providers[0] as { engine?: SyncEngine }
)?.engine;
return syncEngine;
}
export function useCurrentSyncEngineStatus(): SyncEngineStatus | undefined {
const syncEngine = useCurrentSyncEngine();
const [status, setStatus] = useState<SyncEngineStatus>();
useEffect(() => {
if (syncEngine) {
setStatus(syncEngine.status);
return syncEngine.onStatusChange.on(status => {
setStatus(status);
}).dispose;
} else {
setStatus(undefined);
}
return;
}, [syncEngine]);
return status;
}
@@ -1,54 +0,0 @@
import { assertExists } from '@blocksuite/global/utils';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtom, useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import type { AllWorkspace } from '../../shared';
import { useWorkspace, useWorkspaceEffect } from '../use-workspace';
declare global {
/**
* @internal debug only
*/
// eslint-disable-next-line no-var
var currentWorkspace: AllWorkspace | undefined;
interface WindowEventMap {
'affine:workspace:change': CustomEvent<{ id: string }>;
}
}
export function useCurrentWorkspace(): [
AllWorkspace,
(id: string | null) => void,
] {
const [id, setId] = useAtom(currentWorkspaceIdAtom);
assertExists(id);
const currentWorkspace = useWorkspace(id);
// when you call current workspace, effect is always called
useWorkspaceEffect(currentWorkspace.id);
useEffect(() => {
globalThis.currentWorkspace = currentWorkspace;
globalThis.dispatchEvent(
new CustomEvent('affine:workspace:change', {
detail: { id: currentWorkspace.id },
})
);
}, [currentWorkspace]);
const setPageId = useSetAtom(currentPageIdAtom);
return [
currentWorkspace,
useCallback(
(id: string | null) => {
if (environment.isBrowser && id) {
localStorage.setItem('last_workspace_id', id);
}
setPageId(null);
setId(id);
},
[setId, setPageId]
),
];
}
@@ -1,90 +0,0 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import type { WorkspaceRegistry } from '@affine/env/workspace';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
import { useAtomValue, useSetAtom } from 'jotai';
import { openSettingModalAtom } from '../../atoms';
import { useNavigateHelper } from '../use-navigate-helper';
export function useOnTransformWorkspace() {
const t = useAFFiNEI18N();
const setSettingModal = useSetAtom(openSettingModalAtom);
const WorkspaceAdapters = useAtomValue(workspaceAdaptersAtom);
const setMetadata = useSetAtom(rootWorkspacesMetadataAtom);
const { openPage } = useNavigateHelper();
const currentPageId = useAtomValue(currentPageIdAtom);
const pushNotification = useSetAtom(pushNotificationAtom);
return useAsyncCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
): Promise<void> => {
// create first, then delete, in case of failure
const newId = await WorkspaceAdapters[to].CRUD.create(
workspace.blockSuiteWorkspace
);
await WorkspaceAdapters[from].CRUD.delete(workspace.blockSuiteWorkspace);
setMetadata(workspaces => {
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
workspaces.splice(idx, 1, {
id: newId,
flavour: to,
version: WorkspaceVersion.SubDoc,
});
return [...workspaces];
}, newId);
// fixme(himself65): setting modal could still open and open the non-exist workspace
setSettingModal(settings => ({
...settings,
open: false,
}));
window.dispatchEvent(
new CustomEvent('affine-workspace:transform', {
detail: {
from,
to,
oldId: workspace.id,
newId: newId,
},
})
);
openPage(newId, currentPageId ?? WorkspaceSubPath.ALL);
pushNotification({
title: t['Successfully enabled AFFiNE Cloud'](),
type: 'success',
});
},
[
WorkspaceAdapters,
setMetadata,
setSettingModal,
openPage,
currentPageId,
pushNotification,
t,
]
);
}
declare global {
// global Events
interface WindowEventMap {
'affine-workspace:transform': CustomEvent<{
from: WorkspaceFlavour;
to: WorkspaceFlavour;
oldId: string;
newId: string;
}>;
}
}
@@ -1,5 +1,6 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtom, useStore } from 'jotai';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { useAtom, useAtomValue, useStore } from 'jotai';
import { useTheme } from 'next-themes';
import { useEffect } from 'react';
@@ -14,14 +15,13 @@ import {
} from '../commands';
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
import { useLanguageHelper } from './affine/use-language-helper';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { useNavigateHelper } from './use-navigate-helper';
export function useRegisterWorkspaceCommands() {
const store = useStore();
const t = useAFFiNEI18N();
const theme = useTheme();
const [currentWorkspace] = useCurrentWorkspace();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const languageHelper = useLanguageHelper();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const navigationHelper = useNavigateHelper();
@@ -1,58 +0,0 @@
import { DebugLogger } from '@affine/debug';
import type { BlobManager } from '@blocksuite/store';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { BlockSuiteWorkspace } from '../shared';
const logger = new DebugLogger('useWorkspaceBlob');
export function useWorkspaceBlob(
blockSuiteWorkspace: BlockSuiteWorkspace
): BlobManager {
return useMemo(() => blockSuiteWorkspace.blob, [blockSuiteWorkspace.blob]);
}
export function useWorkspaceBlobImage(
key: string | null,
blockSuiteWorkspace: BlockSuiteWorkspace
) {
const blobManager = useWorkspaceBlob(blockSuiteWorkspace);
const [blob, setBlob] = useState<Blob | null>(null);
useEffect(() => {
const controller = new AbortController();
if (key === null) {
setBlob(null);
return;
}
blobManager
?.get(key)
.then(blob => {
if (controller.signal.aborted) {
return;
}
if (blob) {
setBlob(blob);
}
})
.catch(err => {
logger.error('Failed to get blob', err);
});
return () => {
controller.abort();
};
}, [blobManager, key]);
const [url, setUrl] = useState<string | null>(null);
const ref = useRef<string | null>(null);
useEffect(() => {
if (ref.current) {
URL.revokeObjectURL(ref.current);
}
if (blob) {
const url = URL.createObjectURL(blob);
setUrl(url);
ref.current = url;
}
}, [blob]);
return url;
}
@@ -1,53 +0,0 @@
import {
type AffineOfficialWorkspace,
WorkspaceFlavour,
} from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { Workspace } from '@blocksuite/store';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import type { Atom } from 'jotai';
import { atom, useAtomValue } from 'jotai';
const workspaceWeakMap = new WeakMap<
Workspace,
Atom<Promise<AffineOfficialWorkspace>>
>();
// workspace effect is the side effect like connect to the server/indexeddb,
// this will save the workspace updates permanently.
export function useWorkspaceEffect(workspaceId: string): void {
const [, effectAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
useAtomValue(effectAtom);
}
// todo(himself65): remove this hook
export function useWorkspace(workspaceId: string): AffineOfficialWorkspace {
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
const workspace = useAtomValue(workspaceAtom);
if (!workspaceWeakMap.has(workspace)) {
const baseAtom = atom(async get => {
const metadataList = await get(rootWorkspacesMetadataAtom);
const flavour = metadataList.find(({ id }) => id === workspaceId)
?.flavour;
if (!flavour) {
// when last workspace is removed, we may encounter this warning. it should be fine
console.warn(
'workspace not found in rootWorkspacesMetadataAtom, maybe it is removed',
workspaceId
);
}
return {
id: workspaceId,
flavour: flavour ?? WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: workspace,
};
});
workspaceWeakMap.set(workspace, baseAtom);
}
return useAtomValue(
workspaceWeakMap.get(workspace) as Atom<Promise<AffineOfficialWorkspace>>
);
}
@@ -1,122 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
buildShowcaseWorkspace,
WorkspaceVersion,
} from '@toeverything/infra/blocksuite';
import { useAtomValue, useSetAtom } from 'jotai';
import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { LocalAdapter } from '../adapters/local';
import { WorkspaceAdapters } from '../adapters/workspace';
import { setPageModeAtom } from '../atoms';
const logger = new DebugLogger('use-workspaces');
/**
* This hook has the permission to all workspaces. Be careful when using it.
*/
export function useAppHelper() {
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
return {
addLocalWorkspace: useCallback(
async (workspaceId: string): Promise<string> => {
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
saveWorkspaceToLocalStorage(workspaceId);
set(workspaces => [
...workspaces,
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.DatabaseV3,
},
]);
logger.debug('imported local workspace', workspaceId);
return workspaceId;
},
[set]
),
addCloudWorkspace: useCallback(
(workspaceId: string) => {
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.AFFINE_CLOUD);
set(workspaces => [
...workspaces,
{
id: workspaceId,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
version: WorkspaceVersion.DatabaseV3,
},
]);
logger.debug('imported cloud workspace', workspaceId);
},
[set]
),
createLocalWorkspace: useCallback(
async (name: string): Promise<string> => {
const blockSuiteWorkspace = getOrCreateWorkspace(
nanoid(),
WorkspaceFlavour.LOCAL
);
blockSuiteWorkspace.meta.setName(name);
const id = await LocalAdapter.CRUD.create(blockSuiteWorkspace);
{
// this is hack, because CRUD doesn't return the workspace
const blockSuiteWorkspace = getOrCreateWorkspace(
id,
WorkspaceFlavour.LOCAL
);
await buildShowcaseWorkspace(blockSuiteWorkspace, {
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
},
});
}
set(workspaces => [
...workspaces,
{
id,
flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.DatabaseV3,
},
]);
logger.debug('created local workspace', id);
return id;
},
[set]
),
deleteWorkspace: useCallback(
async (workspaceId: string) => {
const targetJotaiWorkspace = jotaiWorkspaces.find(
ws => ws.id === workspaceId
);
if (!targetJotaiWorkspace) {
throw new Error('page cannot be found');
}
const targetWorkspace = getWorkspace(targetJotaiWorkspace.id);
// delete workspace from plugin
await WorkspaceAdapters[targetJotaiWorkspace.flavour].CRUD.delete(
targetWorkspace
);
// delete workspace from jotai storage
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
},
[jotaiWorkspaces, set]
),
deleteWorkspaceMeta: useCallback(
(workspaceId: string) => {
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
},
[set]
),
};
}