refactor(infra): migrate to new infra (#5565)

This commit is contained in:
EYHN
2024-01-30 07:16:39 +00:00
parent 1e3499c323
commit 329fc19852
170 changed files with 2007 additions and 4354 deletions

View File

@@ -24,7 +24,6 @@
"@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@affine/workspace-impl": "workspace:*",
"@blocksuite/block-std": "0.12.0-nightly-202401290223-b6302df",
"@blocksuite/blocks": "0.12.0-nightly-202401290223-b6302df",

View File

@@ -8,14 +8,17 @@ import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react';
import { getCurrentStore } from '@toeverything/infra/atom';
import { ServiceCollection } from '@toeverything/infra/di';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import { GlobalScopeProvider } from './modules/infra-web/global-scope';
import { CloudSessionProvider } from './providers/session-provider';
import { router } from './router';
import { performanceLogger, performanceRenderLogger } from './shared';
import createEmotionCache from './utils/create-emotion-cache';
import { configureWebServices } from './web';
const performanceI18nLogger = performanceLogger.namespace('i18n');
const cache = createEmotionCache();
@@ -52,6 +55,10 @@ async function loadLanguage() {
let languageLoadingPromise: Promise<void> | null = null;
const services = new ServiceCollection();
configureWebServices(services);
const serviceProvider = services.provider();
export const App = memo(function App() {
performanceRenderLogger.info('App');
@@ -60,20 +67,26 @@ export const App = memo(function App() {
}
return (
<CacheProvider value={cache}>
<AffineContext store={getCurrentStore()}>
<CloudSessionProvider>
<DebugProvider>
<GlobalLoading />
{runtimeConfig.enableNotificationCenter && <NotificationCenter />}
<RouterProvider
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
router={router}
future={future}
/>
</DebugProvider>
</CloudSessionProvider>
</AffineContext>
</CacheProvider>
<Suspense>
<GlobalScopeProvider provider={serviceProvider}>
<CacheProvider value={cache}>
<AffineContext store={getCurrentStore()}>
<CloudSessionProvider>
<DebugProvider>
<GlobalLoading />
{runtimeConfig.enableNotificationCenter && (
<NotificationCenter />
)}
<RouterProvider
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
router={router}
future={future}
/>
</DebugProvider>
</CloudSessionProvider>
</AffineContext>
</CacheProvider>
</GlobalScopeProvider>
</Suspense>
);
});

View File

@@ -1,231 +0,0 @@
import {
currentWorkspaceAtom,
waitForCurrentWorkspaceAtom,
} from '@affine/core/modules/workspace';
import type { Collection, DeprecatedCollection } from '@affine/env/filter';
import { DisposableGroup } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import { type DBSchema, openDB } from 'idb';
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { Observable, of } from 'rxjs';
import type {
CollectionsCRUD,
CollectionsCRUDAtom,
} from '../components/page-list';
import { getUserSetting } from '../utils/user-setting';
import { getWorkspaceSetting } from '../utils/workspace-setting';
import { sessionAtom } from './cloud-user';
/**
* @deprecated
*/
export interface PageCollectionDBV1 extends DBSchema {
view: {
key: DeprecatedCollection['id'];
value: DeprecatedCollection;
};
}
/**
* @deprecated
*/
export interface StorageCRUD<Value> {
get: (key: string) => Promise<Value | null>;
set: (key: string, value: Value) => Promise<string>;
delete: (key: string) => Promise<void>;
list: () => Promise<string[]>;
}
/**
* @deprecated
*/
const collectionDBAtom = atom(
openDB<PageCollectionDBV1>('page-view', 1, {
upgrade(database) {
database.createObjectStore('view', {
keyPath: 'id',
});
},
})
);
/**
* @deprecated
*/
const localCollectionCRUDAtom = atom(get => ({
get: async (key: string) => {
const db = await get(collectionDBAtom);
const t = db.transaction('view').objectStore('view');
return (await t.get(key)) ?? null;
},
set: async (key: string, value: DeprecatedCollection) => {
const db = await get(collectionDBAtom);
const t = db.transaction('view', 'readwrite').objectStore('view');
await t.put(value);
return key;
},
delete: async (key: string) => {
const db = await get(collectionDBAtom);
const t = db.transaction('view', 'readwrite').objectStore('view');
await t.delete(key);
},
list: async () => {
const db = await get(collectionDBAtom);
const t = db.transaction('view').objectStore('view');
return t.getAllKeys();
},
}));
/**
* @deprecated
*/
const getCollections = async (
storage: StorageCRUD<DeprecatedCollection>
): Promise<DeprecatedCollection[]> => {
return storage
.list()
.then(async keys => {
return await Promise.all(keys.map(key => storage.get(key))).then(v =>
v.filter((v): v is DeprecatedCollection => v !== null)
);
})
.catch(error => {
console.error('Failed to load collections', error);
return [];
});
};
type BaseCollectionsDataType = {
loading: boolean;
collections: Collection[];
};
export const pageCollectionBaseAtom =
atomWithObservable<BaseCollectionsDataType>(
get => {
const currentWorkspace = get(currentWorkspaceAtom);
if (!currentWorkspace) {
return of({ loading: true, collections: [] });
}
const session = get(sessionAtom);
const userId = session?.data?.user.id ?? null;
const migrateCollectionsFromIdbData = async (
workspace: Workspace
): Promise<Collection[]> => {
const localCRUD = get(localCollectionCRUDAtom);
const collections = await getCollections(localCRUD);
const result = collections.filter(v => v.workspaceId === workspace.id);
Promise.all(
result.map(collection => {
return localCRUD.delete(collection.id);
})
).catch(error => {
console.error('Failed to delete collections from indexeddb', error);
});
return result.map(v => {
return {
id: v.id,
name: v.name,
filterList: v.filterList,
allowList: v.allowList ?? [],
};
});
};
const migrateCollectionsFromUserData = async (
workspace: Workspace
): Promise<Collection[]> => {
if (userId == null) {
return [];
}
const userSetting = getUserSetting(workspace, userId);
await userSetting.loaded;
const view = userSetting.view;
if (view) {
const collections: Omit<DeprecatedCollection, 'workspaceId'>[] = [
...view.values(),
];
//delete collections
view.clear();
return collections.map(v => {
return {
id: v.id,
name: v.name,
filterList: v.filterList,
allowList: v.allowList ?? [],
};
});
}
return [];
};
return new Observable<BaseCollectionsDataType>(subscriber => {
const group = new DisposableGroup();
const workspaceSetting = getWorkspaceSetting(
currentWorkspace.blockSuiteWorkspace
);
migrateCollectionsFromIdbData(currentWorkspace.blockSuiteWorkspace)
.then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
migrateCollectionsFromUserData(currentWorkspace.blockSuiteWorkspace)
.then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
if (group.disposed) {
return;
}
const fn = () => {
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
};
workspaceSetting.setting.observeDeep(fn);
group.add(() => {
workspaceSetting.setting.unobserveDeep(fn);
});
return () => {
group.dispose();
};
});
},
{ initialValue: { loading: true, collections: [] } }
);
export const collectionsCRUDAtom: CollectionsCRUDAtom = atom(async get => {
const workspace = await get(waitForCurrentWorkspaceAtom);
return {
addCollection: (...collections) => {
getWorkspaceSetting(workspace.blockSuiteWorkspace).addCollection(
...collections
);
},
collections: get(pageCollectionBaseAtom).collections,
updateCollection: (id, updater) => {
getWorkspaceSetting(workspace.blockSuiteWorkspace).updateCollection(
id,
updater
);
},
deleteCollection: (info, ...ids) => {
getWorkspaceSetting(workspace.blockSuiteWorkspace).deleteCollection(
info,
...ids
);
},
} satisfies CollectionsCRUD;
});

View File

@@ -1,7 +1,7 @@
import { DebugLogger } from '@affine/debug';
import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { workspaceManager } from '@affine/workspace-impl';
import type { WorkspaceManager } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
buildShowcaseWorkspace,
@@ -12,12 +12,12 @@ import { setPageModeAtom } from '../atoms';
const logger = new DebugLogger('affine:first-app-data');
export async function createFirstAppData() {
export async function createFirstAppData(workspaceManager: WorkspaceManager) {
if (localStorage.getItem('is-first-open') !== null) {
return;
}
localStorage.setItem('is-first-open', 'false');
const workspaceId = await workspaceManager.createWorkspace(
const workspaceMetadata = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL,
async workspace => {
workspace.meta.setName(DEFAULT_WORKSPACE_NAME);
@@ -38,6 +38,6 @@ export async function createFirstAppData() {
logger.debug('create first workspace');
}
);
console.info('create first workspace', workspaceId);
return workspaceId;
console.info('create first workspace', workspaceMetadata);
return workspaceMetadata;
}

View File

@@ -1,12 +1,12 @@
import {
currentWorkspaceAtom,
workspaceListAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceListService } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useAtomValue } from 'jotai/react';
import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { currentPageIdAtom } from '../../../../atoms/mode';
import { CurrentWorkspaceService } from '../../../../modules/workspace/current-workspace';
export interface DumpInfoProps {
error: any;
@@ -14,8 +14,10 @@ export interface DumpInfoProps {
export const DumpInfo = (_props: DumpInfoProps) => {
const location = useLocation();
const workspaceList = useAtomValue(workspaceListAtom);
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const workspaceList = useService(WorkspaceListService);
const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname;
const query = useParams();

View File

@@ -1,13 +1,16 @@
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { Suspense, useEffect } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
const SyncAwarenessInnerLoggedIn = () => {
const currentUser = useCurrentUser();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
useEffect(() => {
if (currentUser && currentWorkspace) {

View File

@@ -5,18 +5,18 @@ import {
Modal,
} from '@affine/component/ui/modal';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { _addLocalWorkspace } from '@affine/workspace-impl';
import { WorkspaceManager } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
buildShowcaseWorkspace,
initEmptyPage,
} from '@toeverything/infra/blocksuite';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra/di';
import type { KeyboardEvent } from 'react';
import { useLayoutEffect } from 'react';
import { useCallback, useState } from 'react';
@@ -101,7 +101,7 @@ export const CreateWorkspaceModal = ({
}: ModalProps) => {
const [step, setStep] = useState<CreateWorkspaceStep>();
const t = useAFFiNEI18N();
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceManager = useService(WorkspaceManager);
// todo: maybe refactor using xstate?
useLayoutEffect(() => {
@@ -148,7 +148,7 @@ export const CreateWorkspaceModal = ({
async (name: string) => {
// this will be the last step for web for now
// fix me later
const id = await workspaceManager.createWorkspace(
const { id } = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL,
async workspace => {
workspace.meta.setName(name);

View File

@@ -8,10 +8,10 @@ import {
listHistoryQuery,
recoverDocMutation,
} from '@affine/graphql';
import { globalBlockSuiteSchema } from '@affine/workspace';
import { createAffineCloudBlobStorage } from '@affine/workspace-impl';
import { AffineCloudBlobStorage } from '@affine/workspace-impl';
import { assertEquals } from '@blocksuite/global/utils';
import { Workspace } from '@blocksuite/store';
import { globalBlockSuiteSchema } from '@toeverything/infra';
import { revertUpdate } from '@toeverything/y-indexeddb';
import { useEffect, useMemo } from 'react';
import useSWRImmutable from 'swr/immutable';
@@ -108,7 +108,7 @@ const workspaceMap = new Map<string, Workspace>();
const getOrCreateShellWorkspace = (workspaceId: string) => {
let workspace = workspaceMap.get(workspaceId);
if (!workspace) {
const blobStorage = createAffineCloudBlobStorage(workspaceId);
const blobStorage = new AffineCloudBlobStorage(workspaceId);
workspace = new Workspace({
id: workspaceId,
providerCreators: [],

View File

@@ -7,14 +7,17 @@ import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { timestampToLocalTime } from '@affine/core/utils';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons';
import type { Page, Workspace } from '@blocksuite/store';
import {
type Page,
type Workspace as BlockSuiteWorkspace,
} from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible';
import type { DialogContentProps } from '@radix-ui/react-dialog';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import {
Fragment,
@@ -29,6 +32,7 @@ import { encodeStateAsUpdate } from 'yjs';
import { currentModeAtom } from '../../../atoms/mode';
import { pageHistoryModalAtom } from '../../../atoms/page-history';
import { timestampToLocalTime } from '../../../utils';
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
import {
@@ -48,7 +52,7 @@ import * as styles from './styles.css';
export interface PageHistoryModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
workspace: Workspace;
workspace: BlockSuiteWorkspace;
pageId: string;
}
@@ -153,13 +157,12 @@ const HistoryEditorPreview = ({
const planPromptClosedAtom = atom(false);
const PlanPrompt = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
const workspaceQuota = useWorkspaceQuota(currentWorkspace.id);
const workspace = useService(Workspace);
const workspaceQuota = useWorkspaceQuota(workspace.id);
const isProWorkspace = useMemo(() => {
return workspaceQuota?.humanReadable.name.toLowerCase() !== 'free';
}, [workspaceQuota]);
const isOwner = useIsWorkspaceOwner(workspace.meta);
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
const [planPromptClosed, setPlanPromptClosed] = useAtom(planPromptClosedAtom);
@@ -412,7 +415,7 @@ const PageHistoryManager = ({
pageId,
onClose,
}: {
workspace: Workspace;
workspace: BlockSuiteWorkspace;
pageId: string;
onClose: () => void;
}) => {
@@ -536,8 +539,7 @@ export const PageHistoryModal = ({
export const GlobalPageHistoryModal = () => {
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = useService(Workspace);
const handleOpenChange = useCallback(
(open: boolean) => {
setState(prev => ({

View File

@@ -3,15 +3,15 @@ import { openQuotaModalAtom, openSettingModalAtom } from '@affine/core/atoms';
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
import { useUserQuota } from '@affine/core/hooks/use-quota';
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useService, Workspace } from '@toeverything/infra';
import bytes from 'bytes';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useSetAtom } from 'jotai';
import { useCallback, useEffect, useMemo } from 'react';
export const CloudQuotaModal = () => {
const t = useAFFiNEI18N();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const [open, setOpen] = useAtom(openQuotaModalAtom);
const workspaceQuota = useWorkspaceQuota(currentWorkspace.id);
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);

View File

@@ -1,13 +1,13 @@
import { ConfirmModal } from '@affine/component/ui/modal';
import { openQuotaModalAtom } from '@affine/core/atoms';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtom, useAtomValue } from 'jotai';
import { useService, Workspace } from '@toeverything/infra';
import { useAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
export const LocalQuotaModal = () => {
const t = useAFFiNEI18N();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const [open, setOpen] = useAtom(openQuotaModalAtom);
const onConfirm = useCallback(() => {

View File

@@ -1,8 +1,8 @@
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 type { WorkspaceMetadata } from '@toeverything/infra';
import { debounce } from 'lodash-es';
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';

View File

@@ -8,17 +8,19 @@ import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
import { useWorkspaceAvailableFeatures } from '@affine/core/hooks/use-workspace-features';
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
import {
waitForCurrentWorkspaceAtom,
workspaceListAtom,
} from '@affine/core/modules/workspace';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace';
import { Logo1Icon } from '@blocksuite/icons';
import {
Workspace,
WorkspaceManager,
type WorkspaceMetadata,
} from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import clsx from 'clsx';
import { useAtom, useAtomValue } from 'jotai/react';
import { useAtom } from 'jotai/react';
import { type ReactElement, Suspense, useCallback, useMemo } from 'react';
import { authAtom } from '../../../../atoms';
@@ -188,7 +190,9 @@ export const WorkspaceList = ({
selectedWorkspaceId: string | null;
activeSubTab: WorkspaceSubTab;
}) => {
const workspaces = useAtomValue(workspaceListAtom);
const workspaces = useLiveData(
useService(WorkspaceManager).list.workspaceList
);
return (
<>
{workspaces.map(workspace => {
@@ -236,7 +240,7 @@ const WorkspaceListItem = ({
const information = useWorkspaceInfo(meta);
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const isCurrent = currentWorkspace.id === meta.id;
const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(meta);

View File

@@ -8,7 +8,7 @@ import {
} from '@affine/core/hooks/use-workspace-features';
import { FeatureType } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { Suspense, useCallback, useState } from 'react';

View File

@@ -1,4 +1,4 @@
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
import { ExperimentalFeatures } from './experimental-features';

View File

@@ -8,7 +8,7 @@ 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 type { WorkspaceMetadata } from '@affine/workspace/metadata';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { useCallback, useState } from 'react';
import * as styles from './style.css';

View File

@@ -2,15 +2,12 @@ import { pushNotificationAtom } from '@affine/component/notification-center';
import { SettingRow } from '@affine/component/setting-components';
import { ConfirmModal } from '@affine/component/ui/modal';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import {
currentWorkspaceAtom,
workspaceListAtom,
workspaceManagerAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { useAtomValue, useSetAtom } from 'jotai';
import { Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react';
import { openSettingModalAtom } from '../../../../../../atoms';
@@ -18,6 +15,7 @@ import {
RouteLogic,
useNavigateHelper,
} from '../../../../../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../../../../../shared';
import type { WorkspaceSettingDetailProps } from '../types';
import { WorkspaceDeleteModal } from './delete';
@@ -35,9 +33,9 @@ export const DeleteLeaveWorkspace = ({
const [showLeave, setShowLeave] = useState(false);
const setSettingModal = useSetAtom(openSettingModalAtom);
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceList = useAtomValue(workspaceListAtom);
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const workspaceManager = useService(WorkspaceManager);
const workspaceList = useLiveData(workspaceManager.list.workspaceList);
const currentWorkspace = useService(Workspace);
const pushNotification = useSetAtom(pushNotificationAtom);
const onLeaveOrDelete = useCallback(() => {

View File

@@ -2,17 +2,17 @@ import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace';
import { useAtomValue, useSetAtom } from 'jotai';
import { type Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useSetAtom } from 'jotai';
import { useState } from 'react';
import { openSettingModalAtom } from '../../../../../atoms';
import { useNavigateHelper } from '../../../../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../../../../shared';
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
import type { WorkspaceSettingDetailProps } from './types';
@@ -29,7 +29,7 @@ export const EnableCloudPanel = ({
const { openPage } = useNavigateHelper();
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceManager = useService(WorkspaceManager);
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
const setSettingModal = useSetAtom(openSettingModalAtom);

View File

@@ -4,7 +4,7 @@ import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { apis } from '@affine/electron-api';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { useState } from 'react';

View File

@@ -9,9 +9,9 @@ import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
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 type { Workspace } from '@toeverything/infra';
import { SyncPeerStep } from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import {
type KeyboardEvent,

View File

@@ -4,7 +4,7 @@ import { Button } from '@affine/component/ui/button';
import { Tooltip } from '@affine/component/ui/tooltip';
import { apis, events } from '@affine/electron-api';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { useMemo } from 'react';
import { useCallback, useEffect, useState } from 'react';

View File

@@ -1,4 +1,4 @@
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import type { WorkspaceMetadata } from '@toeverything/infra';
export interface WorkspaceSettingDetailProps {
isOwner: boolean;

View File

@@ -1,9 +1,8 @@
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { Workspace } from '@affine/workspace';
import type { Page } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { type Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import { useState } from 'react';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
@@ -25,7 +24,7 @@ export const SharePageButton = ({
const { openPage } = useNavigateHelper();
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceManager = useService(WorkspaceManager);
const handleConfirm = useAsyncCallback(async () => {
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {

View File

@@ -3,9 +3,9 @@ import { Divider } from '@affine/component/ui/divider';
import { Menu } from '@affine/component/ui/menu';
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';
import type { WorkspaceMetadata } from '@toeverything/infra';
import clsx from 'clsx';
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';

View File

@@ -1,11 +1,11 @@
import { FavoriteTag } from '@affine/core/components/page-list';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { toast } from '@affine/core/utils';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { useAtomValue } from 'jotai';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useCallback } from 'react';
export interface FavoriteButtonProps {
@@ -14,7 +14,7 @@ export interface FavoriteButtonProps {
export const useFavorite = (pageId: string) => {
const t = useAFFiNEI18N();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = useService(Workspace);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage);

View File

@@ -1,3 +1,4 @@
import { toast } from '@affine/component';
import {
Menu,
MenuIcon,
@@ -11,8 +12,6 @@ import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-sui
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { toast } from '@affine/core/utils';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
@@ -26,6 +25,7 @@ import {
ImportIcon,
PageIcon,
} from '@blocksuite/icons';
import { useService, Workspace } from '@toeverything/infra';
import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react';
@@ -46,8 +46,7 @@ export const PageHeaderMenuButton = ({
}: PageMenuProps) => {
const t = useAFFiNEI18N();
// fixme(himself65): remove these hooks ASAP
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = useService(Workspace);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage);

View File

@@ -3,57 +3,46 @@
*/
import 'fake-indexeddb/auto';
import type { CollectionService } from '@affine/core/modules/collection';
import type { Collection } from '@affine/env/filter';
import { renderHook } from '@testing-library/react';
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { LiveData } from '@toeverything/infra';
import { BehaviorSubject } from 'rxjs';
import { expect, test } from 'vitest';
import { createDefaultFilter, vars } from '../filter/vars';
import {
type CollectionsCRUD,
useCollectionManager,
} from '../use-collection-manager';
import { useCollectionManager } from '../use-collection-manager';
const defaultMeta = { tags: { options: [] } };
const collectionsSubject = new BehaviorSubject<Collection[]>([]);
const baseAtom = atomWithObservable<Collection[]>(
() => {
return collectionsSubject;
},
{
initialValue: [],
}
);
const mockAtom = atom(get => {
return {
collections: get(baseAtom),
addCollection: (...collections) => {
const prev = collectionsSubject.value;
collectionsSubject.next([...collections, ...prev]);
},
deleteCollection: (...ids) => {
const prev = collectionsSubject.value;
collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
},
updateCollection: (id, updater) => {
const prev = collectionsSubject.value;
collectionsSubject.next(
prev.map(v => {
if (v.id === id) {
return updater(v);
}
return v;
})
);
},
} satisfies CollectionsCRUD;
});
const mockWorkspaceCollectionService = {
collections: LiveData.from(collectionsSubject, []),
addCollection: (...collections) => {
const prev = collectionsSubject.value;
collectionsSubject.next([...collections, ...prev]);
},
deleteCollection: (...ids) => {
const prev = collectionsSubject.value;
collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
},
updateCollection: (id, updater) => {
const prev = collectionsSubject.value;
collectionsSubject.next(
prev.map(v => {
if (v.id === id) {
return updater(v);
}
return v;
})
);
},
} as CollectionService;
test('useAllPageSetting', async () => {
const settingHook = renderHook(() => useCollectionManager(mockAtom));
const settingHook = renderHook(() =>
useCollectionManager(mockWorkspaceCollectionService)
);
const prevCollection = settingHook.result.current.currentCollection;
expect(settingHook.result.current.savedCollections).toEqual([]);
settingHook.result.current.updateCollection({

View File

@@ -1,9 +1,8 @@
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import {
type ReactElement,
useCallback,
@@ -12,6 +11,7 @@ import {
useState,
} from 'react';
import { CollectionService } from '../../../modules/collection';
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
import { collectionHeaderColsDef } from '../header-col-def';
import { CollectionOperationCell } from '../operation-cell';
@@ -69,8 +69,8 @@ export const VirtualizedCollectionList = ({
const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>(
[]
);
const setting = useCollectionManager(collectionsCRUDAtom);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const setting = useCollectionManager(useService(CollectionService));
const currentWorkspace = useService(Workspace);
const info = useDeleteCollectionInfo();
const collectionOperations = useCollectionOperationsRenderer({

View File

@@ -1,13 +1,14 @@
import { Button } from '@affine/component';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import type { Collection, Tag } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ViewLayersIcon } from '@blocksuite/icons';
import { useService } from '@toeverything/infra/di';
import { nanoid } from 'nanoid';
import { useCallback, useMemo } from 'react';
import { CollectionService } from '../../../modules/collection';
import { createTagFilter } from '../filter/utils';
import {
createEmptyCollection,
@@ -24,7 +25,7 @@ import { PageListNewPageButton } from './page-list-new-page-button';
export const PageListHeader = ({ workspaceId }: { workspaceId: string }) => {
const t = useAFFiNEI18N();
const setting = useCollectionManager(collectionsCRUDAtom);
const setting = useCollectionManager(useService(CollectionService));
const { jumpToCollections } = useNavigateHelper();
const handleJumpToCollections = useCallback(() => {
@@ -74,14 +75,16 @@ export const CollectionPageListHeader = ({
workspaceId: string;
}) => {
const t = useAFFiNEI18N();
const setting = useCollectionManager(collectionsCRUDAtom);
const setting = useCollectionManager(useService(CollectionService));
const { jumpToCollections } = useNavigateHelper();
const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspaceId);
}, [jumpToCollections, workspaceId]);
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
const { updateCollection } = useCollectionManager(
useService(CollectionService)
);
const { node, open } = useEditCollection(config);
const handleAddPage = useAsyncCallback(async () => {
@@ -121,7 +124,7 @@ export const TagPageListHeader = ({
}) => {
const t = useAFFiNEI18N();
const { jumpToTags, jumpToCollection } = useNavigateHelper();
const setting = useCollectionManager(collectionsCRUDAtom);
const setting = useCollectionManager(useService(CollectionService));
const { open, node } = useEditCollectionName({
title: t['com.affine.editCollection.saveCollection'](),
showTips: true,

View File

@@ -1,5 +1,5 @@
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAtomValue } from 'jotai';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import type { PropsWithChildren } from 'react';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
@@ -16,7 +16,7 @@ export const PageListNewPageButton = ({
size?: 'small' | 'default';
testId?: string;
}>) => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const { importFile, createEdgeless, createPage } = usePageHelper(
currentWorkspace.blockSuiteWorkspace
);

View File

@@ -2,12 +2,12 @@ import { toast } from '@affine/component';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import type { Collection } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageMeta, Tag } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { useCallback, useMemo, useRef, useState } from 'react';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
@@ -27,7 +27,7 @@ import {
} from './page-list-header';
const usePageOperationsRenderer = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const { setTrashModal } = useTrashModalHelper(
currentWorkspace.blockSuiteWorkspace
);
@@ -89,7 +89,7 @@ export const VirtualizedPageList = ({
const listRef = useRef<ItemListHandle>(null);
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const pageOperations = usePageOperationsRenderer();
const { isPreferredEdgeless } = usePageHelper(

View File

@@ -1,7 +1,7 @@
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { Trans } from '@affine/i18n';
import type { Tag } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { useCallback, useMemo, useRef, useState } from 'react';
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
@@ -26,7 +26,7 @@ export const VirtualizedTagList = ({
const listRef = useRef<ItemListHandle>(null);
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const filteredSelectedTagIds = useMemo(() => {
const ids = tags.map(tag => tag.id);

View File

@@ -1,11 +1,8 @@
import type {
Collection,
DeleteCollectionInfo,
Filter,
VariableMap,
} from '@affine/env/filter';
import type { CollectionService } from '@affine/core/modules/collection';
import type { Collection, Filter, VariableMap } from '@affine/env/filter';
import type { PageMeta } from '@blocksuite/store';
import { type Atom, useAtom, useAtomValue } from 'jotai';
import { useLiveData } from '@toeverything/infra/livedata';
import { useAtom, useAtomValue } from 'jotai';
import { atomWithReset } from 'jotai/utils';
import { useCallback } from 'react';
import { NIL } from 'uuid';
@@ -32,47 +29,28 @@ export const currentCollectionAtom = atomWithReset<string>(NIL);
export type Updater<T> = (value: T) => T;
export type CollectionUpdater = Updater<Collection>;
export type CollectionsCRUD = {
addCollection: (...collections: Collection[]) => void;
collections: Collection[];
updateCollection: (id: string, updater: CollectionUpdater) => void;
deleteCollection: (info: DeleteCollectionInfo, ...ids: string[]) => void;
};
export type CollectionsCRUDAtom = Atom<
Promise<CollectionsCRUD> | CollectionsCRUD
>;
export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => {
const [{ collections, addCollection, deleteCollection, updateCollection }] =
useAtom(collectionAtom);
export const useSavedCollections = (collectionService: CollectionService) => {
const addPage = useCallback(
(collectionId: string, pageId: string) => {
updateCollection(collectionId, old => {
collectionService.updateCollection(collectionId, old => {
return {
...old,
allowList: [pageId, ...(old.allowList ?? [])],
};
});
},
[updateCollection]
[collectionService]
);
return {
collections,
addCollection,
updateCollection,
deleteCollection,
collectionService,
addPage,
};
};
export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
const {
collections,
updateCollection,
addCollection,
deleteCollection,
addPage,
} = useSavedCollections(collectionsAtom);
export const useCollectionManager = (collectionService: CollectionService) => {
const collections = useLiveData(collectionService.collections);
const { addPage } = useSavedCollections(collectionService);
const currentCollectionId = useAtomValue(currentCollectionAtom);
const [defaultCollection, updateDefaultCollection] = useAtom(
defaultCollectionAtom
@@ -82,10 +60,10 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
if (collection.id === NIL) {
updateDefaultCollection(collection);
} else {
updateCollection(collection.id, () => collection);
collectionService.updateCollection(collection.id, () => collection);
}
},
[updateDefaultCollection, updateCollection]
[updateDefaultCollection, collectionService]
);
const setTemporaryFilter = useCallback(
(filterList: Filter[]) => {
@@ -108,9 +86,10 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
isDefault: currentCollectionId === NIL,
// actions
createCollection: addCollection,
createCollection: collectionService.addCollection.bind(collectionService),
updateCollection: update,
deleteCollection,
deleteCollection:
collectionService.deleteCollection.bind(collectionService),
addPage,
setTemporaryFilter,
};

View File

@@ -1,8 +1,9 @@
import { allPageModeSelectAtom } from '@affine/core/atoms';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { CollectionService } from '@affine/core/modules/collection';
import type { BlockSuiteWorkspace } from '@affine/core/shared';
import type { PageMeta } from '@blocksuite/store';
import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
@@ -19,8 +20,9 @@ export const useFilteredPageMetas = (
) => {
const { isPreferredEdgeless } = usePageHelper(workspace);
const pageMode = useAtomValue(allPageModeSelectAtom);
const { currentCollection, isDefault } =
useCollectionManager(collectionsCRUDAtom);
const { currentCollection, isDefault } = useCollectionManager(
useService(CollectionService)
);
const filteredPageMetas = useMemo(
() =>

View File

@@ -1,4 +1,5 @@
import { Button, Tooltip } from '@affine/component';
import type { CollectionService } from '@affine/core/modules/collection';
import type { DeleteCollectionInfo, PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -6,10 +7,7 @@ import { ViewLayersIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import { useState } from 'react';
import {
type CollectionsCRUDAtom,
useCollectionManager,
} from '../use-collection-manager';
import { useCollectionManager } from '../use-collection-manager';
import * as styles from './collection-bar.css';
import {
type AllPageListConfig,
@@ -20,16 +18,16 @@ import { useActions } from './use-action';
interface CollectionBarProps {
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
collectionsAtom: CollectionsCRUDAtom;
collectionService: CollectionService;
backToAll: () => void;
allPageListConfig: AllPageListConfig;
info: DeleteCollectionInfo;
}
export const CollectionBar = (props: CollectionBarProps) => {
const { collectionsAtom } = props;
const { collectionService } = props;
const t = useAFFiNEI18N();
const setting = useCollectionManager(collectionsAtom);
const setting = useCollectionManager(collectionService);
const collection = setting.currentCollection;
const [open, setOpen] = useState(false);
const actions = useActions({

View File

@@ -3,15 +3,11 @@ import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@affine/core/hooks/use-block-suite-page-meta';
import {
currentWorkspaceAtom,
waitForCurrentWorkspaceAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
import type { Page, PageMeta } from '@blocksuite/store';
import type { PageMeta } from '@blocksuite/store';
import { Workspace } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
type AffineCommand,
@@ -19,19 +15,17 @@ import {
type CommandCategory,
PreconditionStrategy,
} from '@toeverything/infra/command';
import { useService } from '@toeverything/infra/di';
import { commandScore } from 'cmdk';
import { atom, useAtomValue } from 'jotai';
import { groupBy } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
openQuickSearchModalAtom,
pageSettingsAtom,
recentPageIdsBaseAtom,
} from '../../../atoms';
import { collectionsCRUDAtom } from '../../../atoms/collections';
import { pageSettingsAtom, recentPageIdsBaseAtom } from '../../../atoms';
import { currentPageIdAtom } from '../../../atoms/mode';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { CollectionService } from '../../../modules/collection';
import { WorkspaceSubPath } from '../../../shared';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import type { CMDKCommand, CommandContext } from './types';
@@ -47,41 +41,6 @@ export function removeDoubleQuotes(str?: string): string | undefined {
export const cmdkQueryAtom = atom('');
export const cmdkValueAtom = atom('');
// like currentWorkspaceAtom, but not throw error
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
const currentWorkspace = get(currentWorkspaceAtom);
if (!currentWorkspace) {
return;
}
const currentPageId = get(currentPageIdAtom);
if (!currentPageId) {
return;
}
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
return;
}
if (!page.loaded) {
await page.waitForLoaded();
}
return page;
});
export const commandContextAtom = atom<Promise<CommandContext>>(async get => {
const currentPage = await get(safeCurrentPageAtom);
const pageSettings = get(pageSettingsAtom);
return {
currentPage,
pageMode: currentPage ? pageSettings[currentPage.id]?.mode : undefined,
};
});
function filterCommandByContext(
command: AffineCommand,
context: CommandContext
@@ -96,7 +55,7 @@ function filterCommandByContext(
return context.pageMode === 'page';
}
if (command.preconditionStrategy === PreconditionStrategy.InPaperOrEdgeless) {
return !!context.currentPage;
return !!context.pageMode;
}
if (command.preconditionStrategy === PreconditionStrategy.Never) {
return false;
@@ -107,27 +66,16 @@ function filterCommandByContext(
return true;
}
let quickSearchOpenCounter = 0;
const openCountAtom = atom(get => {
if (get(openQuickSearchModalAtom)) {
quickSearchOpenCounter++;
}
return quickSearchOpenCounter;
});
export const filteredAffineCommands = atom(async get => {
const context = await get(commandContextAtom);
// reset when modal open
get(openCountAtom);
function getAllCommand(context: CommandContext) {
const commands = AffineCommandRegistry.getAll();
return commands.filter(command => {
return filterCommandByContext(command, context);
});
});
}
const useWorkspacePages = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const workspace = useService(Workspace);
const pages = useBlockSuitePageMeta(workspace.blockSuiteWorkspace);
return pages;
};
@@ -153,6 +101,7 @@ export const pageToCommand = (
store: ReturnType<typeof getCurrentStore>,
navigationHelper: ReturnType<typeof useNavigateHelper>,
t: ReturnType<typeof useAFFiNEI18N>,
workspace: Workspace,
label?: {
title: string;
subTitle?: string;
@@ -160,7 +109,6 @@ export const pageToCommand = (
blockId?: string
): CMDKCommand => {
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
const currentWorkspace = store.get(currentWorkspaceAtom);
const title = page.title || t['Untitled']();
const commandLabel = label || {
@@ -186,18 +134,14 @@ export const pageToCommand = (
originalValue: title,
category: category,
run: () => {
if (!currentWorkspace) {
if (!workspace) {
console.error('current workspace not found');
return;
}
if (blockId) {
return navigationHelper.jumpToPageBlock(
currentWorkspace.id,
page.id,
blockId
);
return navigationHelper.jumpToPageBlock(workspace.id, page.id, blockId);
}
return navigationHelper.jumpToPage(currentWorkspace.id, page.id);
return navigationHelper.jumpToPage(workspace.id, page.id);
},
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
timestamp: page.updatedDate,
@@ -212,7 +156,7 @@ export const usePageCommands = () => {
const recentPages = useRecentPages();
const pages = useWorkspacePages();
const store = getCurrentStore();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = useService(Workspace);
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
const query = useAtomValue(cmdkQueryAtom);
@@ -241,7 +185,14 @@ export const usePageCommands = () => {
let results: CMDKCommand[] = [];
if (query.trim() === '') {
results = recentPages.map(page => {
return pageToCommand('affine:recent', page, store, navigationHelper, t);
return pageToCommand(
'affine:recent',
page,
store,
navigationHelper,
t,
workspace
);
});
} else {
// queried pages that has matched contents
@@ -283,6 +234,7 @@ export const usePageCommands = () => {
store,
navigationHelper,
t,
workspace,
label,
blockId
);
@@ -334,27 +286,26 @@ export const usePageCommands = () => {
}
return results;
}, [
pageHelper,
pageMetaHelper,
navigationHelper,
pages,
searchTime,
query,
recentPages,
store,
navigationHelper,
t,
workspace.blockSuiteWorkspace,
searchTime,
workspace,
pages,
pageHelper,
pageMetaHelper,
]);
};
export const collectionToCommand = (
collection: Collection,
store: ReturnType<typeof getCurrentStore>,
navigationHelper: ReturnType<typeof useNavigateHelper>,
selectCollection: (id: string) => void,
t: ReturnType<typeof useAFFiNEI18N>
t: ReturnType<typeof useAFFiNEI18N>,
workspace: Workspace
): CMDKCommand => {
const currentWorkspace = store.get(currentWorkspaceAtom);
const label = collection.name || t['Untitled']();
const category = 'affine:collections';
return {
@@ -372,11 +323,7 @@ export const collectionToCommand = (
originalValue: label,
category: category,
run: () => {
if (!currentWorkspace) {
console.error('current workspace not found');
return;
}
navigationHelper.jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
selectCollection(collection.id);
},
icon: <ViewLayersIcon />,
@@ -385,12 +332,13 @@ export const collectionToCommand = (
export const useCollectionsCommands = () => {
// todo: considering collections for searching pages
const { savedCollections } = useCollectionManager(collectionsCRUDAtom);
const store = getCurrentStore();
const { savedCollections } = useCollectionManager(
useService(CollectionService)
);
const query = useAtomValue(cmdkQueryAtom);
const navigationHelper = useNavigateHelper();
const t = useAFFiNEI18N();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = useService(Workspace);
const selectCollection = useCallback(
(id: string) => {
navigationHelper.jumpToCollection(workspace.id, id);
@@ -405,22 +353,39 @@ export const useCollectionsCommands = () => {
results = savedCollections.map(collection => {
const command = collectionToCommand(
collection,
store,
navigationHelper,
selectCollection,
t
t,
workspace
);
return command;
});
return results;
}
}, [query, savedCollections, store, navigationHelper, selectCollection, t]);
}, [
query,
savedCollections,
navigationHelper,
selectCollection,
t,
workspace,
]);
};
export const useCMDKCommandGroups = () => {
const pageCommands = usePageCommands();
const collectionCommands = useCollectionsCommands();
const affineCommands = useAtomValue(filteredAffineCommands);
const currentPageId = useAtomValue(currentPageIdAtom);
const pageSettings = useAtomValue(pageSettingsAtom);
const currentPageMode = currentPageId
? pageSettings[currentPageId]?.mode
: undefined;
const affineCommands = useMemo(() => {
return getAllCommand({
pageMode: currentPageMode,
});
}, [currentPageMode]);
return useMemo(() => {
const commands = [

View File

@@ -1,8 +1,6 @@
import type { Page } from '@blocksuite/store';
import type { CommandCategory } from '@toeverything/infra/command';
export interface CommandContext {
currentPage: Page | undefined;
pageMode: 'page' | 'edgeless' | undefined;
}

View File

@@ -2,22 +2,25 @@ import { Button } from '@affine/component/ui/button';
import { ConfirmModal } from '@affine/component/ui/modal';
import { Tooltip } from '@affine/component/ui/tooltip';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
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 { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
import { WorkspaceSubPath } from '../../../shared';
import { toast } from '../../../utils';
import * as styles from './styles.css';
export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
assertExists(workspace);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(

View File

@@ -7,21 +7,22 @@ import {
filterPage,
stopPropagation,
useCollectionManager,
useSavedCollections,
} from '@affine/core/components/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { CollectionService } from '@affine/core/modules/collection';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
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 { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra/livedata';
import { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { collectionsCRUDAtom } from '../../../../atoms/collections';
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
import { useBlockSuitePageMeta } from '../../../../hooks/use-block-suite-page-meta';
import type { CollectionsListProps } from '../index';
import { Page } from './page';
import * as styles from './styles.css';
@@ -39,7 +40,7 @@ const CollectionRenderer = ({
}) => {
const [collapsed, setCollapsed] = useState(true);
const [open, setOpen] = useState(false);
const setting = useCollectionManager(collectionsCRUDAtom);
const setting = useCollectionManager(useService(CollectionService));
const t = useAFFiNEI18N();
const dragItemId = getDropItemId('collections', collection.id);
@@ -168,7 +169,7 @@ export const CollectionsList = ({
onCreate,
}: CollectionsListProps) => {
const metas = useBlockSuitePageMeta(workspace);
const { collections } = useSavedCollections(collectionsCRUDAtom);
const collections = useLiveData(useService(CollectionService).collections);
const t = useAFFiNEI18N();
if (collections.length === 0) {
return (

View File

@@ -1,13 +1,12 @@
import { Divider } from '@affine/component/ui/divider';
import { MenuItem } from '@affine/component/ui/menu';
import {
workspaceListAtom,
workspaceManagerAtom,
} from '@affine/core/modules/workspace';
import { Unreachable } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Logo1Icon } from '@blocksuite/icons';
import { useAtomValue, useSetAtom } from 'jotai';
import { WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react';
import { useCallback, useEffect, useMemo } from 'react';
@@ -85,9 +84,8 @@ export const UserWithWorkspaceList = ({
onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]);
const workspaces = useAtomValue(workspaceListAtom);
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceManager = useService(WorkspaceManager);
const workspaces = useLiveData(workspaceManager.list.workspaceList);
// revalidate workspace list when mounted
useEffect(() => {

View File

@@ -5,16 +5,13 @@ import {
useWorkspaceAvatar,
useWorkspaceName,
} from '@affine/core/hooks/use-workspace-info';
import {
currentWorkspaceAtom,
workspaceListAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace';
import type { DragEndEvent } from '@dnd-kit/core';
import { useAtomValue, useSetAtom } from 'jotai';
import { WorkspaceManager, type WorkspaceMetadata } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react';
import { useCallback, useMemo } from 'react';
@@ -23,6 +20,8 @@ import {
openCreateWorkspaceModalAtom,
openSettingModalAtom,
} from '../../../../../atoms';
import { CurrentWorkspaceService } from '../../../../../modules/workspace/current-workspace';
import { WorkspaceSubPath } from '../../../../../shared';
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
import * as styles from './index.css';
@@ -106,13 +105,17 @@ export const AFFiNEWorkspaceList = ({
}: {
onEventEnd?: () => void;
}) => {
const workspaces = useAtomValue(workspaceListAtom);
const workspaces = useLiveData(
useService(WorkspaceManager).list.workspaceList
);
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
const { jumpToSubPath } = useNavigateHelper();
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);

View File

@@ -6,11 +6,9 @@ import { openSettingModalAtom } from '@affine/core/atoms';
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { type SyncEngineStatus, SyncEngineStep } from '@affine/workspace';
import {
CloudWorkspaceIcon,
InformationFillDuotoneIcon,
@@ -18,7 +16,13 @@ import {
NoNetworkIcon,
UnsyncIcon,
} from '@blocksuite/icons';
import { useAtomValue, useSetAtom } from 'jotai';
import {
type SyncEngineStatus,
SyncEngineStep,
Workspace,
} from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useSetAtom } from 'jotai';
import { debounce, mean } from 'lodash-es';
import {
forwardRef,
@@ -97,7 +101,7 @@ const useSyncEngineSyncProgress = () => {
useState<SyncEngineStatus | null>(null);
const [isOverCapacity, setIsOverCapacity] = useState(false);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
@@ -250,7 +254,7 @@ export const WorkspaceCard = forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement>
>(({ ...props }, ref) => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const information = useWorkspaceInfo(currentWorkspace.meta);

View File

@@ -12,15 +12,14 @@ import {
SidebarScrollableContainer,
} from '@affine/component/app-sidebar';
import { Menu } from '@affine/component/ui/menu';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { WorkspaceSubPath } from '@affine/core/shared';
import { CollectionService } from '@affine/core/modules/collection';
import { apis, events } from '@affine/electron-api';
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';
import { useService, type Workspace } from '@toeverything/infra';
import { useAtom, useAtomValue } from 'jotai';
import { nanoid } from 'nanoid';
import type { HTMLAttributes, ReactElement } from 'react';
@@ -35,6 +34,7 @@ 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 { WorkspaceSubPath } from '../../shared';
import {
createEmptyCollection,
MoveToTrash,
@@ -177,7 +177,7 @@ export const RootAppSidebar = ({
useRegisterBrowserHistoryCommands(router.back, router.forward);
const userInfo = useDeleteCollectionInfo();
const setting = useCollectionManager(collectionsCRUDAtom);
const setting = useCollectionManager(useService(CollectionService));
const { node, open } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](),
showTips: true,

View File

@@ -1,18 +1,18 @@
import { BrowserWarning } from '@affine/component/affine-banner';
import { LocalDemoTips } from '@affine/component/affine-banner';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace';
import { useAtomValue, useSetAtom } from 'jotai';
import { type Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react';
import { authAtom } from '../atoms';
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../shared';
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
const minimumChromeVersion = 106;
@@ -77,7 +77,7 @@ export const TopTip = ({
}, [setAuthModal]);
const { openPage } = useNavigateHelper();
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceManager = useService(WorkspaceManager);
const handleConfirm = useAsyncCallback(async () => {
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
return;

View File

@@ -3,15 +3,12 @@ import { AffineShapeIcon } from '@affine/core/components/page-list'; // TODO: im
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
import {
waitForCurrentWorkspaceAtom,
workspaceManagerAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtomValue } from 'jotai';
import { Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useState } from 'react';
import { WorkspaceSubPath } from '../../shared';
import * as styles from './upgrade.css';
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
@@ -20,8 +17,8 @@ import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
*/
export const WorkspaceUpgrade = function WorkspaceUpgrade() {
const [error, setError] = useState<string | null>(null);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspaceManager = useAtomValue(workspaceManagerAtom);
const currentWorkspace = useService(Workspace);
const workspaceManager = useService(WorkspaceManager);
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
const { openPage } = useNavigateHelper();
const t = useAFFiNEI18N();
@@ -32,10 +29,10 @@ export const WorkspaceUpgrade = function WorkspaceUpgrade() {
}
try {
const newWorkspaceId =
const newWorkspace =
await currentWorkspace.upgrade.upgrade(workspaceManager);
if (newWorkspaceId) {
openPage(newWorkspaceId, WorkspaceSubPath.ALL);
if (newWorkspace) {
openPage(newWorkspace.id, WorkspaceSubPath.ALL);
} else {
// blocksuite may enter an incorrect state, reload to reset it.
location.reload();

View File

@@ -3,101 +3,71 @@
*/
import 'fake-indexeddb/auto';
import {
currentWorkspaceAtom,
WorkspacePropertiesAdapter,
} from '@affine/core/modules/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { Workspace } from '@affine/workspace/workspace';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { assertExists } from '@blocksuite/global/utils';
import { type Page, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { Schema } from '@blocksuite/store';
import { WorkspacePropertiesAdapter } from '@affine/core/modules/workspace';
import { render } from '@testing-library/react';
import { Workspace } from '@toeverything/infra';
import { ServiceProviderContext, useService } from '@toeverything/infra/di';
import { createStore, Provider } from 'jotai';
import { Suspense } from 'react';
import { describe, expect, test, vi } from 'vitest';
import { beforeEach } from 'vitest';
import { configureTestingEnvironment } from '../../testing';
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
let blockSuiteWorkspace: BlockSuiteWorkspace;
const store = createStore();
const schema = new Schema();
schema.register(AffineSchemas).register(__unstableSchemas);
const Component = () => {
const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, 'page0');
const workspace = useService(Workspace);
const title = useBlockSuiteWorkspacePageTitle(
workspace.blockSuiteWorkspace,
'page0'
);
return <div>title: {title}</div>;
};
// todo: this module has some side-effects that will break the tests
vi.mock('@affine/workspace-impl', () => ({
default: {},
}));
beforeEach(async () => {
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test', schema });
const workspace = {
blockSuiteWorkspace,
flavour: WorkspaceFlavour.LOCAL,
} as Workspace;
store.set(currentWorkspaceAtom, workspace);
blockSuiteWorkspace = workspace.blockSuiteWorkspace;
blockSuiteWorkspace.doc.emit('sync', []);
const initPage = async (page: Page) => {
await page.waitForLoaded();
expect(page).not.toBeNull();
assertExists(page);
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(''),
});
const frameId = page.addBlock('affine:note', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
};
await initPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
await initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
await initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
});
describe('useBlockSuiteWorkspacePageTitle', () => {
test('basic', async () => {
const { workspace, page } = await configureTestingEnvironment();
const { findByText, rerender } = render(
<Provider store={store}>
<Suspense fallback="loading">
<Component />
</Suspense>
</Provider>
<ServiceProviderContext.Provider value={page.services}>
<Provider store={store}>
<Suspense fallback="loading">
<Component />
</Suspense>
</Provider>
</ServiceProviderContext.Provider>
);
expect(await findByText('title: Untitled')).toBeDefined();
blockSuiteWorkspace.setPageMeta('page0', { title: '1' });
workspace.blockSuiteWorkspace.setPageMeta(page.id, { title: '1' });
rerender(
<Provider store={store}>
<Suspense fallback="loading">
<Component />
</Suspense>
</Provider>
<ServiceProviderContext.Provider value={page.services}>
<Provider store={store}>
<Suspense fallback="loading">
<Component />
</Suspense>
</Provider>
</ServiceProviderContext.Provider>
);
expect(await findByText('title: 1')).toBeDefined();
});
test('journal', async () => {
const adapter = new WorkspacePropertiesAdapter(blockSuiteWorkspace);
adapter.setJournalPageDateString('page0', '2021-01-01');
const { workspace, page } = await configureTestingEnvironment();
const adapter = workspace.services.get(WorkspacePropertiesAdapter);
adapter.setJournalPageDateString(page.id, '2021-01-01');
const { findByText } = render(
<Provider store={store}>
<Suspense fallback="loading">
<Component />
</Suspense>
</Provider>
<ServiceProviderContext.Provider value={page.services}>
<Provider store={store}>
<Suspense fallback="loading">
<Component />
</Suspense>
</Provider>
</ServiceProviderContext.Provider>
);
expect(await findByText('title: Jan 1, 2021')).toBeDefined();
});

View File

@@ -4,17 +4,17 @@ import {
FavoriteTag,
} from '@affine/core/components/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageMeta } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useCallback, useMemo } from 'react';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
export const useAllPageListConfig = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const workspace = currentWorkspace.blockSuiteWorkspace;
const pageMetas = useBlockSuitePageMeta(workspace);
const { isPreferredEdgeless } = usePageHelper(workspace);

View File

@@ -4,6 +4,8 @@ import {
usePageMetaHelper,
} from '@affine/core/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
import { CollectionService } from '@affine/core/modules/collection';
import { useService } from '@toeverything/infra';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
@@ -11,7 +13,6 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { setPageModeAtom } from '../../atoms';
import { currentModeAtom } from '../../atoms/mode';
import type { BlockSuiteWorkspace } from '../../shared';
import { getWorkspaceSetting } from '../../utils/workspace-setting';
import { useNavigateHelper } from '../use-navigate-helper';
import { useReferenceLinkHelper } from './use-reference-link-helper';
@@ -26,6 +27,7 @@ export function useBlockSuiteMetaHelper(
const currentMode = useAtomValue(currentModeAtom);
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { openPage } = useNavigateHelper();
const collectionService = useService(CollectionService);
const switchToPageMode = useCallback(
(pageId: string) => {
@@ -89,9 +91,9 @@ export function useBlockSuiteMetaHelper(
trashRelate: isRoot ? parentMeta?.id : undefined,
});
setPageReadonly(pageId, true);
getWorkspaceSetting(blockSuiteWorkspace).deletePages([pageId]);
collectionService.deletePagesFromCollections([pageId]);
},
[blockSuiteWorkspace, getPageMeta, metas, setPageMeta, setPageReadonly]
[collectionService, getPageMeta, metas, setPageMeta, setPageReadonly]
);
const restoreFromTrash = useCallback(

View File

@@ -1,6 +1,6 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getIsOwnerQuery } from '@affine/graphql';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { useQueryImmutable } from '../use-query';

View File

@@ -1,15 +1,16 @@
import { toast } from '@affine/component';
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
import { Workspace } from '@toeverything/infra';
import {
PreconditionStrategy,
registerAffineCommand,
} from '@toeverything/infra/command';
import { useAtomValue, useSetAtom } from 'jotai';
import { useService } from '@toeverything/infra/di';
import { useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import { pageHistoryModalAtom } from '../../atoms/page-history';
@@ -22,7 +23,7 @@ export function useRegisterBlocksuiteEditorCommands(
mode: 'page' | 'edgeless'
) {
const t = useAFFiNEI18N();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = useService(Workspace);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const currentPage = blockSuiteWorkspace.getPage(pageId);

View File

@@ -1,10 +1,10 @@
import { toast } from '@affine/component';
import type { DraggableTitleCellData } from '@affine/core/components/page-list';
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import { useAtomValue } from 'jotai';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useCallback } from 'react';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
@@ -69,7 +69,7 @@ export function getDragItemId(
export const useSidebarDrag = () => {
const t = useAFFiNEI18N();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const workspace = currentWorkspace.blockSuiteWorkspace;
const { setTrashModal } = useTrashModalHelper(workspace);
const { addToFavorite, removeFromFavorite } =

View File

@@ -1,15 +1,15 @@
import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai';
import { currentPageIdAtom } from '../../atoms/mode';
export const useCurrentPage = () => {
const currentPageId = useAtomValue(currentPageIdAtom);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
return useBlockSuiteWorkspacePage(
currentWorkspace?.blockSuiteWorkspace,
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
};

View File

@@ -1,12 +1,7 @@
import type { Workspace } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra/di';
import { useEffect, useState } from 'react';
import type { WorkspacePropertiesAdapter } from '../modules/workspace/properties';
import {
currentWorkspacePropertiesAdapterAtom,
workspaceAdapterAtomFamily,
} from '../modules/workspace/properties';
import { WorkspacePropertiesAdapter } from '../modules/workspace/properties';
function getProxy<T extends object>(obj: T) {
return new Proxy(obj, {});
@@ -31,11 +26,6 @@ const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
};
export function useCurrentWorkspacePropertiesAdapter() {
const adapter = useAtomValue(currentWorkspacePropertiesAdapterAtom);
return useReactiveAdapter(adapter);
}
export function useWorkspacePropertiesAdapter(workspace: Workspace) {
const adapter = useAtomValue(workspaceAdapterAtomFamily(workspace));
const adapter = useService(WorkspacePropertiesAdapter);
return useReactiveAdapter(adapter);
}

View File

@@ -1,15 +0,0 @@
import { useCallback, useSyncExternalStore } from 'react';
import type { DataSourceAdapter, Status } from 'y-provider';
type UIStatus =
| Status
| {
type: 'unknown';
};
export function useDataSourceStatus(provider: DataSourceAdapter): UIStatus {
return useSyncExternalStore(
provider.subscribeStatusChange,
useCallback(() => provider.status, [provider])
);
}

View File

@@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react';
import type { BlockSuiteWorkspace } from '../shared';
import { timestampToLocalDate } from '../utils';
import { useWorkspacePropertiesAdapter } from './use-affine-adapter';
import { useCurrentWorkspacePropertiesAdapter } from './use-affine-adapter';
import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper';
import { useNavigateHelper } from './use-navigate-helper';
@@ -24,7 +24,7 @@ function toDayjs(j?: string | false) {
export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace);
const adapter = useWorkspacePropertiesAdapter(workspace);
const adapter = useCurrentWorkspacePropertiesAdapter();
/**
* @internal

View File

@@ -1,6 +1,7 @@
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtomValue, useSetAtom, useStore } from 'jotai';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useSetAtom, useStore } from 'jotai';
import { useTheme } from 'next-themes';
import { useEffect } from 'react';
@@ -22,7 +23,7 @@ export function useRegisterWorkspaceCommands() {
const store = useStore();
const t = useAFFiNEI18N();
const theme = useTheme();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const languageHelper = useLanguageHelper();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const navigationHelper = useNavigateHelper();

View File

@@ -1,13 +1,13 @@
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { useAtomValue } from 'jotai';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import { useEffect, useState } from 'react';
export function useWorkspaceBlobObjectUrl(
meta?: WorkspaceMetadata,
blobKey?: string | null
) {
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceManager = useService(WorkspaceManager);
const [blob, setBlob] = useState<string | undefined>(undefined);

View File

@@ -5,7 +5,7 @@ import {
enabledFeaturesQuery,
setWorkspaceExperimentalFeatureMutation,
} from '@affine/graphql';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { type WorkspaceMetadata } from '@toeverything/infra';
import { useAsyncCallback } from './affine-async-hooks';
import { useMutateQueryResource, useMutation } from './use-mutation';

View File

@@ -1,12 +1,11 @@
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import type { WorkspaceMetadata } from '@affine/workspace';
import { useAtomValue } from 'jotai';
import { WorkspaceManager, type WorkspaceMetadata } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useEffect, useState } from 'react';
import { useWorkspaceBlobObjectUrl } from './use-workspace-blob';
export function useWorkspaceInfo(meta: WorkspaceMetadata) {
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceManager = useService(WorkspaceManager);
const [information, setInformation] = useState(
() => workspaceManager.list.getInformation(meta).info

View File

@@ -1,4 +1,4 @@
import type { Workspace, WorkspaceStatus } from '@affine/workspace';
import type { Workspace, WorkspaceStatus } from '@toeverything/infra';
import { useEffect, useState } from 'react';
export function useWorkspaceStatus<

View File

@@ -1,14 +1,13 @@
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import type { Workspace } from '@affine/workspace';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { useAtomValue } from 'jotai';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { type Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useEffect, useState } from 'react';
/**
* definitely be careful when using this hook, open workspace is a heavy operation
*/
export function useWorkspace(meta?: WorkspaceMetadata | null) {
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceManager = useService(WorkspaceManager);
const [workspace, setWorkspace] = useState<Workspace | null>(null);
@@ -17,7 +16,7 @@ export function useWorkspace(meta?: WorkspaceMetadata | null) {
setWorkspace(null); // set to null if meta is null or undefined
return;
}
const ref = workspaceManager.use(meta);
const ref = workspaceManager.open(meta);
setWorkspace(ref.workspace);
return () => {
ref.release();

View File

@@ -5,7 +5,6 @@ import {
import { MainContainer, WorkspaceFallback } from '@affine/component/workspace';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { assertExists } from '@blocksuite/global/utils';
import {
DndContext,
@@ -16,6 +15,8 @@ import {
useSensor,
useSensors,
} from '@dnd-kit/core';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { PropsWithChildren, ReactNode } from 'react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
@@ -54,7 +55,7 @@ export const QuickSearch = () => {
openQuickSearchModalAtom
);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const { pageId } = useParams();
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const pageMeta = useBlockSuitePageMeta(
@@ -92,7 +93,7 @@ export const WorkspaceLayout = function WorkspaceLayout({
};
export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const { openPage } = useNavigateHelper();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);

View File

@@ -0,0 +1 @@
export * from './service';

View File

@@ -3,40 +3,70 @@ import type {
DeleteCollectionInfo,
DeletedCollection,
} from '@affine/env/filter';
import type { Workspace } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra';
import { LiveData } from '@toeverything/infra/livedata';
import { Observable } from 'rxjs';
import { Array as YArray } from 'yjs';
import { updateFirstOfYArray } from './yjs-utils';
const SETTING_KEY = 'setting';
const COLLECTIONS_KEY = 'collections';
const COLLECTIONS_TRASH_KEY = 'collections_trash';
const SETTING_KEY = 'setting';
export class WorkspaceSetting {
export class CollectionService {
constructor(private readonly workspace: Workspace) {}
get doc() {
return this.workspace.doc;
private get doc() {
return this.workspace.blockSuiteWorkspace.doc;
}
get setting() {
return this.workspace.doc.getMap(SETTING_KEY);
private get setting() {
return this.workspace.blockSuiteWorkspace.doc.getMap(SETTING_KEY);
}
get collectionsYArray(): YArray<Collection> | undefined {
private get collectionsYArray(): YArray<Collection> | undefined {
return this.setting.get(COLLECTIONS_KEY) as YArray<Collection>;
}
get collectionsTrashYArray(): YArray<DeletedCollection> | undefined {
private get collectionsTrashYArray(): YArray<DeletedCollection> | undefined {
return this.setting.get(COLLECTIONS_TRASH_KEY) as YArray<DeletedCollection>;
}
get collections(): Collection[] {
return this.collectionsYArray?.toArray() ?? [];
}
readonly collections = LiveData.from(
new Observable<Collection[]>(subscriber => {
subscriber.next(this.collectionsYArray?.toArray() ?? []);
const fn = () => {
subscriber.next(this.collectionsYArray?.toArray() ?? []);
};
this.setting.observeDeep(fn);
return () => {
this.setting.unobserveDeep(fn);
};
}),
[]
);
get collectionsTrash(): DeletedCollection[] {
return this.collectionsTrashYArray?.toArray() ?? [];
readonly collectionsTrash = LiveData.from(
new Observable<DeletedCollection[]>(subscriber => {
subscriber.next(this.collectionsTrashYArray?.toArray() ?? []);
const fn = () => {
subscriber.next(this.collectionsTrashYArray?.toArray() ?? []);
};
this.setting.observeDeep(fn);
return () => {
this.setting.unobserveDeep(fn);
};
}),
[]
);
addCollection(...collections: Collection[]) {
if (!this.setting.has(COLLECTIONS_KEY)) {
this.setting.set(COLLECTIONS_KEY, new YArray());
}
this.doc.transact(() => {
this.collectionsYArray?.insert(0, collections);
});
}
updateCollection(id: string, updater: (value: Collection) => Collection) {
@@ -51,22 +81,13 @@ export class WorkspaceSetting {
}
}
addCollection(...collections: Collection[]) {
if (!this.setting.has(COLLECTIONS_KEY)) {
this.setting.set(COLLECTIONS_KEY, new YArray());
}
this.doc.transact(() => {
this.collectionsYArray?.insert(0, collections);
});
}
deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) {
const collectionsYArray = this.collectionsYArray;
if (!collectionsYArray) {
return;
}
const set = new Set(ids);
this.workspace.doc.transact(() => {
this.workspace.blockSuiteWorkspace.doc.transact(() => {
const indexList: number[] = [];
const list: Collection[] = [];
collectionsYArray.forEach((collection, i) => {
@@ -100,7 +121,10 @@ export class WorkspaceSetting {
});
}
deletePagesFromCollection(collection: Collection, idSet: Set<string>) {
private deletePagesFromCollection(
collection: Collection,
idSet: Set<string>
) {
const newAllowList = collection.allowList.filter(id => !idSet.has(id));
if (newAllowList.length !== collection.allowList.length) {
this.updateCollection(collection.id, old => {
@@ -112,16 +136,29 @@ export class WorkspaceSetting {
}
}
deletePages(ids: string[]) {
deletePagesFromCollections(ids: string[]) {
const idSet = new Set(ids);
this.workspace.doc.transact(() => {
this.collections.forEach(collection => {
this.doc.transact(() => {
this.collections.value.forEach(collection => {
this.deletePagesFromCollection(collection, idSet);
});
});
}
}
export const getWorkspaceSetting = (workspace: Workspace) => {
return new WorkspaceSetting(workspace);
const updateFirstOfYArray = <T>(
array: YArray<T>,
p: (value: T) => boolean,
update: (value: T) => T
) => {
array.doc?.transact(() => {
for (let i = 0; i < array.length; i++) {
const ele = array.get(i);
if (p(ele)) {
array.delete(i);
array.insert(i, [update(ele)]);
return;
}
}
});
};

View File

@@ -0,0 +1,42 @@
import type { Page } from '@toeverything/infra';
import {
LiveData,
ServiceCollection,
type ServiceProvider,
ServiceProviderContext,
useLiveData,
useService,
useServiceOptional,
} from '@toeverything/infra';
import type React from 'react';
import { CurrentPageService } from '../../page';
import { CurrentWorkspaceService } from '../../workspace';
export const GlobalScopeProvider: React.FC<
React.PropsWithChildren<{ provider: ServiceProvider }>
> = ({ provider: rootProvider, children }) => {
const currentWorkspaceService = useService(CurrentWorkspaceService, {
provider: rootProvider,
});
const workspaceProvider = useLiveData(
currentWorkspaceService.currentWorkspace
)?.services;
const currentPageService = useServiceOptional(CurrentPageService, {
provider: workspaceProvider ?? ServiceCollection.EMPTY.provider(),
});
const pageProvider = useLiveData(
currentPageService?.currentPage ?? new LiveData<Page | null>(null)
)?.services;
return (
<ServiceProviderContext.Provider
value={pageProvider ?? workspaceProvider ?? rootProvider}
>
{children}
</ServiceProviderContext.Provider>
);
};

View File

@@ -0,0 +1,32 @@
import type { GlobalCache } from '@toeverything/infra';
import { Observable } from 'rxjs';
export class LocalStorageGlobalCache implements GlobalCache {
prefix = 'cache:';
get<T>(key: string): T | null {
const json = localStorage.getItem(this.prefix + key);
return json ? JSON.parse(json) : null;
}
watch<T>(key: string): Observable<T | null> {
return new Observable<T | null>(subscriber => {
const json = localStorage.getItem(this.prefix + key);
const first = json ? JSON.parse(json) : null;
subscriber.next(first);
const channel = new BroadcastChannel(this.prefix + key);
channel.addEventListener('message', event => {
subscriber.next(event.data);
});
return () => {
channel.close();
};
});
}
set<T>(key: string, value: T | null): void {
localStorage.setItem(this.prefix + key, JSON.stringify(value));
const channel = new BroadcastChannel(this.prefix + key);
channel.postMessage(value);
channel.close();
}
}

View File

@@ -0,0 +1,24 @@
import type { Page } from '@toeverything/infra';
import { LiveData } from '@toeverything/infra/livedata';
/**
* service to manage current page
*/
export class CurrentPageService {
currentPage = new LiveData<Page | null>(null);
/**
* open page, current page will be set to the page
* @param page
*/
openPage(page: Page) {
this.currentPage.next(page);
}
/**
* close current page, current page will be null
*/
closePage() {
this.currentPage.next(null);
}
}

View File

@@ -0,0 +1 @@
export * from './current-page';

View File

@@ -0,0 +1,27 @@
import {
GlobalCache,
type ServiceCollection,
Workspace,
WorkspaceScope,
} from '@toeverything/infra';
import { CollectionService } from './collection';
import { LocalStorageGlobalCache } from './infra-web/storage';
import { CurrentPageService } from './page';
import {
CurrentWorkspaceService,
WorkspacePropertiesAdapter,
} from './workspace';
export function configureBusinessServices(services: ServiceCollection) {
services.add(CurrentWorkspaceService);
services
.scope(WorkspaceScope)
.add(CurrentPageService)
.add(WorkspacePropertiesAdapter, [Workspace])
.add(CollectionService, [Workspace]);
}
export function configureWebInfraServices(services: ServiceCollection) {
services.addImpl(GlobalCache, LocalStorageGlobalCache);
}

View File

@@ -1,57 +0,0 @@
import { DebugLogger } from '@affine/debug';
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
import { workspaceManager } from '@affine/workspace-impl';
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { Observable } from 'rxjs';
const logger = new DebugLogger('affine:workspace:atom');
// readonly atom for workspace manager, currently only one workspace manager is supported
export const workspaceManagerAtom = atom(() => workspaceManager);
// workspace metadata list, use rxjs to push updates
export const workspaceListAtom = atomWithObservable<WorkspaceMetadata[]>(
get => {
const workspaceManager = get(workspaceManagerAtom);
return new Observable<WorkspaceMetadata[]>(subscriber => {
subscriber.next(workspaceManager.list.workspaceList);
return workspaceManager.list.onStatusChanged.on(status => {
subscriber.next(status.workspaceList);
}).dispose;
});
},
{
initialValue: [],
}
);
// workspace list loading status, if is false, UI can display not found page when workspace id is not in the list.
export const workspaceListLoadingStatusAtom = atomWithObservable<boolean>(
get => {
const workspaceManager = get(workspaceManagerAtom);
return new Observable<boolean>(subscriber => {
subscriber.next(workspaceManager.list.status.loading);
return workspaceManager.list.onStatusChanged.on(status => {
subscriber.next(status.loading);
}).dispose;
});
},
{
initialValue: true,
}
);
// current workspace
export const currentWorkspaceAtom = atom<Workspace | null>(null);
// wait for current workspace, if current workspace is null, it will suspend
export const waitForCurrentWorkspaceAtom = atom(get => {
const currentWorkspace = get(currentWorkspaceAtom);
if (!currentWorkspace) {
// suspended
logger.info('suspended for current workspace');
return new Promise<Workspace>(_ => {});
}
return currentWorkspace;
});

View File

@@ -0,0 +1,24 @@
import type { Workspace } from '@toeverything/infra';
import { LiveData } from '@toeverything/infra/livedata';
/**
* service to manage current workspace
*/
export class CurrentWorkspaceService {
currentWorkspace = new LiveData<Workspace | null>(null);
/**
* open workspace, current workspace will be set to the workspace
* @param workspace
*/
openWorkspace(workspace: Workspace) {
this.currentWorkspace.next(workspace);
}
/**
* close current workspace, current workspace will be null
*/
closeWorkspace() {
this.currentWorkspace.next(null);
}
}

View File

@@ -1,2 +1,2 @@
export * from './atoms';
export * from './current-workspace';
export * from './properties';

View File

@@ -1,6 +1,7 @@
// the adapter is to bridge the workspace rootdoc & native js bindings
import { createYProxy, type Workspace, type Y } from '@blocksuite/store';
import { createYProxy, type Y } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra';
import { defaultsDeep } from 'lodash-es';
import {
@@ -29,7 +30,7 @@ export class WorkspacePropertiesAdapter {
constructor(private readonly workspace: Workspace) {
// check if properties exists, if not, create one
const rootDoc = workspace.doc;
const rootDoc = workspace.blockSuiteWorkspace.doc;
this.properties = rootDoc.getMap(AFFINE_PROPERTIES_ID);
this.proxy = createYProxy(this.properties);
@@ -56,7 +57,9 @@ export class WorkspacePropertiesAdapter {
name: 'Tags',
source: 'system',
type: PagePropertyType.Tags,
options: this.workspace.meta.properties.tags?.options ?? [], // better use a one time migration
options:
this.workspace.blockSuiteWorkspace.meta.properties.tags
?.options ?? [], // better use a one time migration
},
},
},

View File

@@ -1,21 +0,0 @@
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';
import { waitForCurrentWorkspaceAtom } from '../atoms';
import { WorkspacePropertiesAdapter } from './adapter';
// todo: remove the inner atom when workspace is closed by using workspaceAdapterAtomFamily.remove
export const workspaceAdapterAtomFamily = atomFamily(
(workspace: BlockSuiteWorkspace) => {
return atom(async () => {
await workspace.doc.whenLoaded;
return new WorkspacePropertiesAdapter(workspace);
});
}
);
export const currentWorkspacePropertiesAdapterAtom = atom(async get => {
const workspace = await get(waitForCurrentWorkspaceAtom);
return get(workspaceAdapterAtomFamily(workspace.blockSuiteWorkspace));
});

View File

@@ -1,2 +1 @@
export * from './adapter';
export * from './atom';

View File

@@ -1,8 +1,9 @@
import { Menu } from '@affine/component/ui/menu';
import { WorkspaceFallback } from '@affine/component/workspace';
import { workspaceListAtom } from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { useAtomValue } from 'jotai';
import { WorkspaceManager } from '@toeverything/infra';
import { WorkspaceListService } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra';
import { lazy, useEffect, useLayoutEffect, useState } from 'react';
import { type LoaderFunction, redirect } from 'react-router-dom';
@@ -10,6 +11,7 @@ import { createFirstAppData } from '../bootstrap/first-app-data';
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
import { appConfigStorage } from '../hooks/use-app-config-storage';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../shared';
const AllWorkspaceModals = lazy(() =>
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
@@ -29,7 +31,7 @@ export const Component = () => {
const [navigating, setNavigating] = useState(false);
const [creating, setCreating] = useState(false);
const list = useAtomValue(workspaceListAtom);
const list = useLiveData(useService(WorkspaceListService).workspaceList);
const { openPage } = useNavigateHelper();
useLayoutEffect(() => {
@@ -44,16 +46,18 @@ export const Component = () => {
setNavigating(true);
}, [list, openPage]);
const workspaceManager = useService(WorkspaceManager);
useEffect(() => {
setCreating(true);
createFirstAppData()
createFirstAppData(workspaceManager)
.catch(err => {
console.error('Failed to create first app data', err);
})
.finally(() => {
setCreating(false);
});
}, []);
}, [workspaceManager]);
if (navigating || creating) {
return <WorkspaceFallback></WorkspaceFallback>;

View File

@@ -1,17 +1,27 @@
import { MainContainer } from '@affine/component/workspace';
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { fetchWithTraceReport } from '@affine/graphql';
import { globalBlockSuiteSchema } from '@affine/workspace';
import {
createAffineCloudBlobStorage,
createStaticBlobStorage,
AffineCloudBlobStorage,
StaticBlobStorage,
} from '@affine/workspace-impl';
import { assertExists } from '@blocksuite/global/utils';
import { type Page, Workspace } from '@blocksuite/store';
import {
EmptyBlobStorage,
LocalBlobStorage,
LocalSyncStorage,
Page,
PageManager,
ReadonlyMappingSyncStorage,
RemoteBlobStorage,
useService,
useServiceOptional,
WorkspaceIdContext,
WorkspaceManager,
WorkspaceScope,
} from '@toeverything/infra';
import { noop } from 'foxact/noop';
import type { ReactElement } from 'react';
import { useCallback } from 'react';
import { useEffect } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import {
isRouteErrorResponse,
@@ -19,12 +29,13 @@ import {
useLoaderData,
useRouteError,
} from 'react-router-dom';
import { applyUpdate } from 'yjs';
import type { PageMode } from '../../atoms';
import { AppContainer } from '../../components/affine/app-container';
import { PageDetailEditor } from '../../components/page-detail-editor';
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
import { CurrentPageService } from '../../modules/page';
import { CurrentWorkspaceService } from '../../modules/workspace';
import { ShareHeader } from './share-header';
type DocPublishMode = 'edgeless' | 'page';
@@ -57,8 +68,11 @@ export async function downloadBinaryFromCloud(
}
type LoaderData = {
page: Page;
pageId: string;
workspaceId: string;
publishMode: PageMode;
pageArrayBuffer: ArrayBuffer;
workspaceArrayBuffer: ArrayBuffer;
};
function assertDownloadResponse(
@@ -73,55 +87,104 @@ function assertDownloadResponse(
}
}
const logger = new DebugLogger('public:share-page');
export const loader: LoaderFunction = async ({ params }) => {
const workspaceId = params?.workspaceId;
const pageId = params?.pageId;
if (!workspaceId || !pageId) {
return redirect('/404');
}
const workspace = new Workspace({
id: workspaceId,
blobStorages: [
() => ({
crud: createAffineCloudBlobStorage(workspaceId),
}),
() => ({
crud: createStaticBlobStorage(),
}),
],
schema: globalBlockSuiteSchema,
});
// download root workspace
{
const response = await downloadBinaryFromCloud(workspaceId, workspaceId);
assertDownloadResponse(response);
const { arrayBuffer } = response;
applyUpdate(workspace.doc, new Uint8Array(arrayBuffer));
workspace.doc.emit('sync', []);
}
const page = workspace.getPage(pageId);
assertExists(page, 'cannot find page');
// download page
const response = await downloadBinaryFromCloud(
const [workspaceResponse, pageResponse] = await Promise.all([
downloadBinaryFromCloud(workspaceId, workspaceId),
downloadBinaryFromCloud(workspaceId, pageId),
]);
assertDownloadResponse(workspaceResponse);
const { arrayBuffer: workspaceArrayBuffer } = workspaceResponse;
assertDownloadResponse(pageResponse);
const { arrayBuffer: pageArrayBuffer, publishMode } = pageResponse;
return {
workspaceId,
page.spaceDoc.guid
);
assertDownloadResponse(response);
const { arrayBuffer, publishMode } = response;
applyUpdate(page.spaceDoc, new Uint8Array(arrayBuffer));
logger.info('workspace', workspace);
workspace.awarenessStore.setReadonly(page, true);
return { page, publishMode };
pageId,
publishMode,
workspaceArrayBuffer,
pageArrayBuffer,
} satisfies LoaderData;
};
export const Component = (): ReactElement => {
const { page, publishMode } = useLoaderData() as LoaderData;
usePageDocumentTitle(page.meta);
export const Component = () => {
const {
workspaceId,
pageId,
publishMode,
workspaceArrayBuffer,
pageArrayBuffer,
} = useLoaderData() as LoaderData;
const workspaceManager = useService(WorkspaceManager);
const currentWorkspace = useService(CurrentWorkspaceService);
useEffect(() => {
// create a workspace for share page
const workspace = workspaceManager.instantiate(
{
id: workspaceId,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
},
services => {
services
.scope(WorkspaceScope)
.addImpl(LocalBlobStorage, EmptyBlobStorage)
.addImpl(RemoteBlobStorage('affine'), AffineCloudBlobStorage, [
WorkspaceIdContext,
])
.addImpl(RemoteBlobStorage('static'), StaticBlobStorage)
.addImpl(
LocalSyncStorage,
ReadonlyMappingSyncStorage({
[workspaceId]: new Uint8Array(workspaceArrayBuffer),
[pageId]: new Uint8Array(pageArrayBuffer),
})
);
}
);
workspace.engine.sync
.waitForSynced()
.then(() => {
const { page } = workspace.services
.get(PageManager)
.openByPageId(pageId);
workspace.blockSuiteWorkspace.awarenessStore.setReadonly(
page.blockSuitePage,
true
);
const currentPage = workspace.services.get(CurrentPageService);
currentWorkspace.openWorkspace(workspace);
currentPage.openPage(page);
})
.catch(err => {
console.error(err);
});
}, [
currentWorkspace,
pageArrayBuffer,
pageId,
workspaceArrayBuffer,
workspaceId,
workspaceManager,
]);
const page = useServiceOptional(Page);
usePageDocumentTitle(page?.meta);
if (!page) {
return;
}
return (
<AppContainer>
@@ -129,14 +192,14 @@ export const Component = (): ReactElement => {
<ShareHeader
pageId={page.id}
publishMode={publishMode}
blockSuiteWorkspace={page.workspace}
blockSuiteWorkspace={page.blockSuitePage.workspace}
/>
<PageDetailEditor
isPublic
publishMode={publishMode}
workspace={page.workspace}
workspace={page.blockSuitePage.workspace}
pageId={page.id}
onLoad={useCallback(() => noop, [])}
onLoad={() => noop}
/>
</MainContainer>
</AppContainer>

View File

@@ -1,9 +1,9 @@
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { CollectionService } from '@affine/core/modules/collection';
import type { Collection, Filter } from '@affine/env/filter';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { useCallback } from 'react';
import { collectionsCRUDAtom } from '../../../atoms/collections';
import { filterContainerStyle } from '../../../components/filter-container.css';
import {
FilterList,
@@ -13,9 +13,9 @@ import {
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
export const FilterContainer = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const navigateHelper = useNavigateHelper();
const setting = useCollectionManager(collectionsCRUDAtom);
const setting = useCollectionManager(useService(CollectionService));
const saveToCollection = useCallback(
(collection: Collection) => {
setting.createCollection({

View File

@@ -1,6 +1,5 @@
import { IconButton } from '@affine/component';
import type { AllPageFilterOption } from '@affine/core/atoms';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import {
CollectionList,
PageListNewPageButton,
@@ -13,9 +12,11 @@ import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-lis
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
import { PlusIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { useService } from '@toeverything/infra/di';
import clsx from 'clsx';
import { useMemo } from 'react';
import { CollectionService } from '../../../modules/collection';
import * as styles from './all-page.css';
import { FilterContainer } from './all-page-filter';
@@ -32,7 +33,7 @@ export const AllPageHeader = ({
activeFilter: AllPageFilterOption;
onCreateCollection?: () => void;
}) => {
const setting = useCollectionManager(collectionsCRUDAtom);
const setting = useCollectionManager(useService(CollectionService));
const config = useAllPageListConfig();
const userInfo = useDeleteCollectionInfo();
const isWindowsDesktop = environment.isDesktop && environment.isWindows;

View File

@@ -1,5 +1,4 @@
import type { AllPageFilterOption } from '@affine/core/atoms';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { HubIsland } from '@affine/core/components/affine/hub-island';
import {
CollectionListHeader,
@@ -10,7 +9,6 @@ import {
useCollectionManager,
useEditCollectionName,
useFilteredPageMetas,
useSavedCollections,
useTagMetas,
VirtualizedCollectionList,
VirtualizedPageList,
@@ -22,15 +20,18 @@ import {
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { performanceRenderLogger } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtomValue, useSetAtom } from 'jotai';
import { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { nanoid } from 'nanoid';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { NIL } from 'uuid';
import { CollectionService } from '../../../modules/collection';
import {
EmptyCollectionList,
EmptyPageList,
@@ -47,13 +48,14 @@ export const AllPage = ({
}) => {
const t = useAFFiNEI18N();
const params = useParams();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
const setting = useCollectionManager(collectionsCRUDAtom);
const collectionService = useService(CollectionService);
const collections = useLiveData(collectionService.collections);
const setting = useCollectionManager(collectionService);
const config = useAllPageListConfig();
const { collections } = useSavedCollections(collectionsCRUDAtom);
const { tags, tagMetas, filterPageMetaByTag, deleteTags } = useTagMetas(
currentWorkspace.blockSuiteWorkspace,
pageMetas
@@ -212,7 +214,7 @@ export const AllPage = ({
export const Component = () => {
performanceRenderLogger.info('AllPage');
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const currentCollection = useSetAtom(currentCollectionAtom);
const navigateHelper = useNavigateHelper();

View File

@@ -12,7 +12,7 @@ import {
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { CollectionService } from '@affine/core/modules/collection';
import type { Collection } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -22,19 +22,17 @@ import {
PageIcon,
ViewLayersIcon,
} from '@blocksuite/icons';
import { Workspace } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
import {
collectionsCRUDAtom,
pageCollectionBaseAtom,
} from '../../atoms/collections';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../shared';
import { getWorkspaceSetting } from '../../utils/workspace-setting';
import { AllPage } from './all-page/all-page';
import * as styles from './collection.css';
@@ -48,18 +46,19 @@ export const loader: LoaderFunction = async args => {
};
export const Component = function CollectionPage() {
const { collections, loading } = useAtomValue(pageCollectionBaseAtom);
const collectionService = useService(CollectionService);
const collections = useLiveData(collectionService.collections);
const navigate = useNavigateHelper();
const params = useParams();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = useService(Workspace);
const collection = collections.find(v => v.id === params.collectionId);
const pushNotification = useSetAtom(pushNotificationAtom);
useEffect(() => {
if (!loading && !collection) {
if (!collection) {
navigate.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
const collection = getWorkspaceSetting(
workspace.blockSuiteWorkspace
).collectionsTrash.find(v => v.collection.id === params.collectionId);
const collection = collectionService.collectionsTrash.value.find(
v => v.collection.id === params.collectionId
);
let text = 'Collection is not exist';
if (collection) {
if (collection.userId) {
@@ -75,21 +74,18 @@ export const Component = function CollectionPage() {
}
}, [
collection,
loading,
collectionService.collectionsTrash.value,
navigate,
params.collectionId,
pushNotification,
workspace.blockSuiteWorkspace,
workspace.id,
]);
if (loading) {
return null;
}
if (!collection) {
return null;
}
return isEmpty(collection) ? (
<Placeholder collection={collection} workspaceId={workspace.id} />
<Placeholder collection={collection} />
) : (
<AllPage activeFilter="collections" />
);
@@ -97,24 +93,19 @@ export const Component = function CollectionPage() {
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const Placeholder = ({
collection,
workspaceId,
}: {
collection: Collection;
workspaceId: string;
}) => {
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
const Placeholder = ({ collection }: { collection: Collection }) => {
const workspace = useService(Workspace);
const collectionService = useCollectionManager(useService(CollectionService));
const { node, open } = useEditCollection(useAllPageListConfig());
const { jumpToCollections } = useNavigateHelper();
const openPageEdit = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'page');
updateCollection(ret);
}, [open, collection, updateCollection]);
collectionService.updateCollection(ret);
}, [open, collection, collectionService]);
const openRuleEdit = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'rule');
updateCollection(ret);
}, [collection, open, updateCollection]);
collectionService.updateCollection(ret);
}, [collection, open, collectionService]);
const [showTips, setShowTips] = useState(false);
useEffect(() => {
setShowTips(!localStorage.getItem('hide-empty-collection-help-info'));
@@ -127,8 +118,8 @@ const Placeholder = ({
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspaceId);
}, [jumpToCollections, workspaceId]);
jumpToCollections(workspace.id);
}, [jumpToCollections, workspace]);
return (
<div

View File

@@ -11,9 +11,9 @@ import { JournalTodayButton } from '@affine/core/components/blocksuite/block-sui
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import type { Workspace } from '@affine/workspace';
import { RightSidebarIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useRef } from 'react';

View File

@@ -1,18 +1,24 @@
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { ResizePanel } from '@affine/component/resize-panel';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { globalBlockSuiteSchema, SyncEngineStep } from '@affine/workspace';
import { CollectionService } from '@affine/core/modules/collection';
import {
BookmarkService,
customImageProxyMiddleware,
ImageService,
} from '@blocksuite/blocks';
import type { AffineEditorContainer } from '@blocksuite/presets';
import type { Page, Workspace } from '@blocksuite/store';
import { appSettingAtom } from '@toeverything/infra/atom';
import type { Page as BlockSuitePage } from '@blocksuite/store';
import {
globalBlockSuiteSchema,
Page,
PageListService,
PageManager,
useLiveData,
useServiceOptional,
} from '@toeverything/infra';
import { appSettingAtom, Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import {
memo,
@@ -20,13 +26,12 @@ import {
type ReactNode,
useCallback,
useEffect,
useState,
useMemo,
} from 'react';
import { useParams } from 'react-router-dom';
import type { Map as YMap } from 'yjs';
import { setPageModeAtom } from '../../../atoms';
import { collectionsCRUDAtom } from '../../../atoms/collections';
import { currentModeAtom, currentPageIdAtom } from '../../../atoms/mode';
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
import { HubIsland } from '../../../components/affine/hub-island';
@@ -42,7 +47,8 @@ import { TopTip } from '../../../components/top-tip';
import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands';
import { usePageDocumentTitle } from '../../../hooks/use-global-state';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { performanceRenderLogger } from '../../../shared';
import { CurrentPageService } from '../../../modules/page';
import { performanceRenderLogger, WorkspaceSubPath } from '../../../shared';
import { PageNotFound } from '../../404';
import * as styles from './detail-page.css';
import { DetailPageHeader, RightSidebarHeader } from './detail-page-header';
@@ -104,10 +110,11 @@ const DetailPageLayout = ({
);
};
const DetailPageImpl = memo(function DetailPageImpl({ page }: { page: Page }) {
const DetailPageImpl = memo(function DetailPageImpl() {
const page = useService(Page);
const currentPageId = page.id;
const { openPage, jumpToSubPath } = useNavigateHelper();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
@@ -116,14 +123,15 @@ const DetailPageImpl = memo(function DetailPageImpl({ page }: { page: Page }) {
const isInTrash = pageMeta?.trash;
const { setTemporaryFilter } = useCollectionManager(collectionsCRUDAtom);
const collectionService = useService(CollectionService);
const { setTemporaryFilter } = useCollectionManager(collectionService);
const mode = useAtomValue(currentModeAtom);
const setPageMode = useSetAtom(setPageModeAtom);
useRegisterBlocksuiteEditorCommands(currentPageId, mode);
usePageDocumentTitle(pageMeta);
const onLoad = useCallback(
(page: Page, editor: AffineEditorContainer) => {
(page: BlockSuitePage, editor: AffineEditorContainer) => {
try {
// todo(joooye34): improve the following migration code
const surfaceBlock = page.getBlockByFlavour('affine:surface')[0];
@@ -182,7 +190,7 @@ const DetailPageImpl = memo(function DetailPageImpl({ page }: { page: Page }) {
header={
<>
<DetailPageHeader
page={page}
page={page.blockSuitePage}
workspace={currentWorkspace}
showSidebarSwitch={!isInTrash}
/>
@@ -206,8 +214,14 @@ const DetailPageImpl = memo(function DetailPageImpl({ page }: { page: Page }) {
sidebar={
!isInTrash ? (
<div className={styles.sidebarContainerInner}>
<RightSidebarHeader workspace={currentWorkspace} page={page} />
<EditorSidebar workspace={blockSuiteWorkspace} page={page} />
<RightSidebarHeader
workspace={currentWorkspace}
page={page.blockSuitePage}
/>
<EditorSidebar
workspace={blockSuiteWorkspace}
page={page.blockSuitePage}
/>
</div>
) : null
}
@@ -221,38 +235,44 @@ const DetailPageImpl = memo(function DetailPageImpl({ page }: { page: Page }) {
);
});
const useForceUpdate = () => {
const [, setCount] = useState(0);
return useCallback(() => setCount(count => count + 1), []);
};
const useSafePage = (workspace: Workspace, pageId: string) => {
const forceUpdate = useForceUpdate();
useEffect(() => {
const disposable = workspace.slots.pagesUpdated.on(() => {
forceUpdate();
});
return disposable.dispose;
}, [pageId, workspace.slots.pagesUpdated, forceUpdate]);
return workspace.getPage(pageId);
};
export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentSyncEngineStep = useWorkspaceStatus(
currentWorkspace,
s => s.engine.sync.step
const pageListService = useService(PageListService);
const pageListReady = useLiveData(pageListService.isReady);
const pageMetas = useLiveData(pageListService.pages);
const pageMeta = useMemo(
() => pageMetas.find(page => page.id === pageId),
[pageMetas, pageId]
);
const pageManager = useService(PageManager);
const currentPageService = useService(CurrentPageService);
useEffect(() => {
if (!pageMeta) {
return;
}
const { page, release } = pageManager.open(pageMeta);
currentPageService.openPage(page);
return () => {
currentPageService.closePage();
release();
};
}, [currentPageService, pageManager, pageMeta]);
const page = useServiceOptional(Page);
const currentWorkspace = useService(Workspace);
// set sync engine priority target
useEffect(() => {
currentWorkspace.setPriorityRule(id => id.endsWith(pageId));
}, [pageId, currentWorkspace]);
const page = useSafePage(currentWorkspace?.blockSuiteWorkspace, pageId);
// if sync engine has been synced and the page is null, show 404 page.
if (currentSyncEngineStep === SyncEngineStep.Synced && !page) {
if (pageListReady && !page) {
return <PageNotFound />;
}
@@ -266,7 +286,7 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
});
}
return <DetailPageImpl page={page} />;
return <DetailPageImpl />;
};
export const Component = () => {

View File

@@ -2,8 +2,8 @@ import { IconButton } from '@affine/component';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { useWorkspaceEnabledFeatures } from '@affine/core/hooks/use-workspace-features';
import { FeatureType } from '@affine/graphql';
import type { Workspace } from '@affine/workspace/workspace';
import type { Page } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { useAtom, useAtomValue } from 'jotai';
import { useEffect } from 'react';

View File

@@ -1,18 +1,18 @@
import { WorkspaceFallback } from '@affine/component/workspace';
import { useWorkspace } from '@affine/core/hooks/use-workspace';
import {
currentWorkspaceAtom,
workspaceListAtom,
workspaceListLoadingStatusAtom,
workspaceManagerAtom,
} from '@affine/core/modules/workspace';
import { type Workspace } from '@affine/workspace';
import { useAtom, useAtomValue } from 'jotai';
Workspace,
WorkspaceListService,
WorkspaceManager,
} from '@toeverything/infra';
import { useService, useServiceOptional } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { type ReactElement, Suspense, useEffect, useMemo } from 'react';
import { Outlet, useParams } from 'react-router-dom';
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
import { CurrentWorkspaceService } from '../../modules/workspace/current-workspace';
import { performanceRenderLogger } from '../../shared';
import { PageNotFound } from '../404';
@@ -30,29 +30,27 @@ declare global {
export const Component = (): ReactElement => {
performanceRenderLogger.info('WorkspaceLayout');
const [
_ /* read this atom here to make sure children refresh when currentWorkspace changed */,
setCurrentWorkspace,
] = useAtom(currentWorkspaceAtom);
const currentWorkspaceService = useService(CurrentWorkspaceService);
const params = useParams();
const list = useAtomValue(workspaceListAtom);
const listLoading = useAtomValue(workspaceListLoadingStatusAtom);
const workspaceManager = useAtomValue(workspaceManagerAtom);
const { workspaceList, loading: listLoading } = useLiveData(
useService(WorkspaceListService).status
);
const workspaceManager = useService(WorkspaceManager);
const meta = useMemo(() => {
return list.find(({ id }) => id === params.workspaceId);
}, [list, params.workspaceId]);
return workspaceList.find(({ id }) => id === params.workspaceId);
}, [workspaceList, params.workspaceId]);
const workspace = useWorkspace(meta);
useEffect(() => {
if (!workspace) {
setCurrentWorkspace(null);
currentWorkspaceService.closeWorkspace();
return undefined;
}
setCurrentWorkspace(workspace);
currentWorkspaceService.openWorkspace(workspace ?? null);
// for debug purpose
window.currentWorkspace = workspace;
@@ -65,14 +63,16 @@ export const Component = (): ReactElement => {
);
localStorage.setItem('last_workspace_id', workspace.id);
}, [setCurrentWorkspace, meta, workspaceManager, workspace]);
}, [meta, workspaceManager, workspace, currentWorkspaceService]);
const currentWorkspace = useServiceOptional(Workspace);
// if listLoading is false, we can show 404 page, otherwise we should show loading page.
if (listLoading === false && meta === undefined) {
return <PageNotFound />;
}
if (!workspace) {
if (!currentWorkspace) {
return <WorkspaceFallback key="workspaceLoading" />;
}

View File

@@ -1,7 +1,6 @@
import { TagListHeader, useTagMetas } from '@affine/core/components/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAtomValue } from 'jotai';
import { useService, Workspace } from '@toeverything/infra';
import { useMemo } from 'react';
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
@@ -19,7 +18,7 @@ export const loader: LoaderFunction = async args => {
export const Component = function TagPage() {
const params = useParams();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const { tagUsageCounts } = useTagMetas(
currentWorkspace.blockSuiteWorkspace,

View File

@@ -14,13 +14,13 @@ import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { DeleteIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { Workspace } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import { useService } from '@toeverything/infra/di';
import { useCallback } from 'react';
import { type LoaderFunction } from 'react-router-dom';
import { NIL } from 'uuid';
@@ -61,7 +61,7 @@ export const loader: LoaderFunction = async () => {
};
export const TrashPage = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useService(Workspace);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
assertExists(blockSuiteWorkspace);

View File

@@ -1,26 +1,24 @@
import {
currentWorkspaceAtom,
waitForCurrentWorkspaceAtom,
workspaceListAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { assertExists } from '@blocksuite/global/utils';
import { useAtom, useAtomValue } from 'jotai';
import { WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useAtom } from 'jotai';
import type { ReactElement } from 'react';
import { lazy, Suspense, useCallback } from 'react';
import type { SettingAtom } from '../atoms';
import {
authAtom,
openCreateWorkspaceModalAtom,
openDisableCloudAlertModalAtom,
openSettingModalAtom,
openSignOutModalAtom,
type SettingAtom,
} from '../atoms';
import { PaymentDisableModal } from '../components/affine/payment-disable';
import { useAsyncCallback } from '../hooks/affine-async-hooks';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { CurrentWorkspaceService } from '../modules/workspace/current-workspace';
import { WorkspaceSubPath } from '../shared';
import { signOutCloud } from '../utils/cloud-utils';
const SettingModal = lazy(() =>
@@ -28,6 +26,7 @@ const SettingModal = lazy(() =>
default: module.SettingModal,
}))
);
const Auth = lazy(() =>
import('../components/affine/auth').then(module => ({
default: module.AuthModal,
@@ -80,10 +79,8 @@ const CloudQuotaModal = lazy(() =>
);
export const Setting = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const [{ open, workspaceMetadata, activeTab }, setOpenSettingModalAtom] =
useAtom(openSettingModalAtom);
assertExists(currentWorkspace);
const onSettingClick = useCallback(
({
@@ -162,7 +159,9 @@ export const AuthModal = (): ReactElement => {
};
export function CurrentWorkspaceModals() {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
const [openDisableCloudAlertModal, setOpenDisableCloudAlertModal] = useAtom(
openDisableCloudAlertModalAtom
);
@@ -195,8 +194,12 @@ export function CurrentWorkspaceModals() {
export const SignOutConfirmModal = () => {
const { openPage } = useNavigateHelper();
const [open, setOpen] = useAtom(openSignOutModalAtom);
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const workspaceList = useAtomValue(workspaceListAtom);
const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
const workspaces = useLiveData(
useService(WorkspaceManager).list.workspaceList
);
const onConfirm = useAsyncCallback(async () => {
setOpen(false);
@@ -204,14 +207,14 @@ export const SignOutConfirmModal = () => {
// if current workspace is affine cloud, switch to local workspace
if (currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
const localWorkspace = workspaceList.find(
const localWorkspace = workspaces.find(
w => w.flavour === WorkspaceFlavour.LOCAL
);
if (localWorkspace) {
openPage(localWorkspace.id, WorkspaceSubPath.ALL);
}
}
}, [currentWorkspace?.flavour, openPage, setOpen, workspaceList]);
}, [currentWorkspace?.flavour, openPage, setOpen, workspaces]);
return (
<SignOutModal open={open} onOpenChange={setOpen} onConfirm={onConfirm} />

View File

@@ -0,0 +1,46 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { Page as BlockSuitePage } from '@blocksuite/store';
import {
configureTestingInfraServices,
PageManager,
ServiceCollection,
WorkspaceManager,
} from '@toeverything/infra';
import { CurrentPageService } from './modules/page';
import { CurrentWorkspaceService } from './modules/workspace';
import { configureWebServices } from './web';
export async function configureTestingEnvironment() {
const serviceCollection = new ServiceCollection();
configureWebServices(serviceCollection);
configureTestingInfraServices(serviceCollection);
const rootServices = serviceCollection.provider();
const workspaceManager = rootServices.get(WorkspaceManager);
const { workspace } = workspaceManager.open(
await workspaceManager.createWorkspace(WorkspaceFlavour.LOCAL, async ws => {
const initPage = async (page: BlockSuitePage) => {
await page.load();
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(''),
});
const frameId = page.addBlock('affine:note', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
};
await initPage(ws.createPage({ id: 'page0' }));
})
);
await workspace.engine.sync.waitForSynced();
const { page } = workspace.services.get(PageManager).openByPageId('page0');
rootServices.get(CurrentWorkspaceService).openWorkspace(workspace);
workspace.services.get(CurrentPageService).openPage(page);
return { services: rootServices, workspace, page };
}

View File

@@ -1,18 +0,0 @@
import type { Array as YArray } from 'yjs';
export const updateFirstOfYArray = <T>(
array: YArray<T>,
p: (value: T) => boolean,
update: (value: T) => T
) => {
array.doc?.transact(() => {
for (let i = 0; i < array.length; i++) {
const ele = array.get(i);
if (p(ele)) {
array.delete(i);
array.insert(i, [update(ele)]);
return;
}
}
});
};

View File

@@ -0,0 +1,15 @@
import { configureWorkspaceImplServices } from '@affine/workspace-impl';
import type { ServiceCollection } from '@toeverything/infra';
import { configureInfraServices } from '@toeverything/infra';
import {
configureBusinessServices,
configureWebInfraServices,
} from './modules/services';
export function configureWebServices(services: ServiceCollection) {
configureInfraServices(services);
configureWebInfraServices(services);
configureBusinessServices(services);
configureWorkspaceImplServices(services);
}

View File

@@ -17,9 +17,6 @@
{
"path": "../../frontend/i18n"
},
{
"path": "../../common/workspace"
},
{
"path": "../../frontend/workspace-impl"
},