feat: page view storage with cloud support (#4238)

This commit is contained in:
Alex Yang 2023-09-08 15:02:22 -07:00 committed by GitHub
parent 58a935b31d
commit 5f0605a5d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 338 additions and 94 deletions

View File

@ -0,0 +1,4 @@
import { atom } from 'jotai';
import type { SessionContextValue } from 'next-auth/react';
export const sessionAtom = atom<SessionContextValue<true> | null>(null);

View File

@ -19,6 +19,7 @@ import { useGetPageInfoById } from '../../../hooks/use-get-page-info';
import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils';
import { filterPage } from '../../../utils/filter';
import { currentCollectionsAtom } from '../../../utils/user-setting';
import { emptyDescButton, emptyDescKbd, pageListEmptyStyle } from './index.css';
import { usePageHelper } from './utils';
@ -277,7 +278,7 @@ export const BlockSuitePageList = ({
return (
<PageList
workspaceId={blockSuiteWorkspace.id}
collectionsAtom={currentCollectionsAtom}
propertiesMeta={blockSuiteWorkspace.meta.properties}
getPageInfo={getPageInfo}
onCreateNewPage={createPage}

View File

@ -11,6 +11,7 @@ import { IconButton } from '@toeverything/components/button';
import { useCallback, useState } from 'react';
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
import { currentCollectionsAtom } from '../../../../utils/user-setting';
type AddCollectionButtonProps = {
workspace: Workspace;
@ -20,7 +21,7 @@ export const AddCollectionButton = ({
workspace,
}: AddCollectionButtonProps) => {
const getPageInfo = useGetPageInfoById(workspace);
const setting = useCollectionManager(workspace.id);
const setting = useCollectionManager(currentCollectionsAtom);
const t = useAFFiNEI18N();
const [show, showUpdateCollection] = useState(false);
const [defaultCollection, setDefaultCollection] = useState<Collection>();

View File

@ -29,11 +29,12 @@ import {
} from '@toeverything/components/menu';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import type { ReactElement } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
import { filterPage } from '../../../../utils/filter';
import { currentCollectionsAtom } from '../../../../utils/user-setting';
import type { CollectionsListProps } from '../index';
import { Page } from './page';
import * as styles from './styles.css';
@ -148,8 +149,8 @@ const CollectionRenderer = ({
workspace: Workspace;
getPageInfo: GetPageInfoById;
}) => {
const [collapsed, setCollapsed] = React.useState(true);
const setting = useCollectionManager(workspace.id);
const [collapsed, setCollapsed] = useState(true);
const setting = useCollectionManager(currentCollectionsAtom);
const { jumpToSubPath } = useNavigateHelper();
const clickCollection = useCallback(() => {
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
@ -272,7 +273,7 @@ const CollectionRenderer = ({
};
export const CollectionsList = ({ workspace }: CollectionsListProps) => {
const metas = useBlockSuitePageMeta(workspace);
const { savedCollections } = useSavedCollections(workspace.id);
const { savedCollections } = useSavedCollections(currentCollectionsAtom);
const getPageInfo = useGetPageInfoById(workspace);
const pinedCollections = useMemo(
() => savedCollections.filter(v => v.pinned),

View File

@ -28,6 +28,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import { useHistoryAtom } from '../../atoms/history';
import { useAppSetting } from '../../atoms/settings';
import type { AllWorkspace } from '../../shared';
import { currentCollectionsAtom } from '../../utils/user-setting';
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button';
@ -98,7 +99,7 @@ export const RootAppSidebar = ({
}: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace.id;
const [appSettings] = useAppSetting();
const { backToAll } = useCollectionManager(currentWorkspace.id);
const { backToAll } = useCollectionManager(currentCollectionsAtom);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const t = useAFFiNEI18N();
const onClickNewPage = useCallback(async () => {

View File

@ -18,6 +18,7 @@ import { useCallback } from 'react';
import { appHeaderAtom, mainContainerAtom } from '../atoms/element';
import { useGetPageInfoById } from '../hooks/use-get-page-info';
import { useWorkspace } from '../hooks/use-workspace';
import { currentCollectionsAtom } from '../utils/user-setting';
import { SharePageModal } from './affine/share-page-modal';
import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
import { filterContainerStyle } from './filter-container.css';
@ -27,7 +28,7 @@ import { WorkspaceModeFilterTab } from './pure/workspace-mode-filter-tab';
const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
const currentWorkspace = useWorkspace(workspaceId);
const setting = useCollectionManager(workspaceId);
const setting = useCollectionManager(currentCollectionsAtom);
const saveToCollection = useCallback(
async (collection: Collection) => {
await setting.saveCollection(collection);
@ -78,10 +79,10 @@ export function WorkspaceHeader({
currentWorkspaceId,
currentEntry,
}: WorkspaceHeaderProps<WorkspaceFlavour>) {
const setting = useCollectionManager(currentWorkspaceId);
const setAppHeader = useSetAtom(appHeaderAtom);
const currentWorkspace = useWorkspace(currentWorkspaceId);
const setting = useCollectionManager(currentCollectionsAtom);
const getPageInfoById = useGetPageInfoById(
currentWorkspace.blockSuiteWorkspace
);

View File

@ -11,6 +11,7 @@ import { redirect } from 'react-router-dom';
import { getUIAdapter } from '../../adapters/workspace';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { currentCollectionsAtom } from '../../utils/user-setting';
export const loader: LoaderFunction = async args => {
const rootStore = getCurrentStore();
@ -35,7 +36,7 @@ export const loader: LoaderFunction = async args => {
export const AllPage = () => {
const { jumpToPage } = useNavigateHelper();
const [currentWorkspace] = useCurrentWorkspace();
const setting = useCollectionManager(currentWorkspace.id);
const setting = useCollectionManager(currentCollectionsAtom);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@ -26,6 +26,7 @@ import { setPageModeAtom } from '../../atoms';
import { currentModeAtom } from '../../atoms/mode';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { currentCollectionsAtom } from '../../utils/user-setting';
const DetailPageImpl = (): ReactElement => {
const { openPage, jumpToSubPath } = useNavigateHelper();
@ -34,7 +35,7 @@ const DetailPageImpl = (): ReactElement => {
assertExists(currentWorkspace);
assertExists(currentPageId);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const collectionManager = useCollectionManager(currentWorkspace.id);
const collectionManager = useCollectionManager(currentCollectionsAtom);
const mode = useAtomValue(currentModeAtom);
const setPageMode = useSetAtom(setPageModeAtom);

View File

@ -4,21 +4,27 @@ import { pushNotificationAtom } from '@affine/component/notification-center';
import { isDesktop } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { refreshRootMetadataAtom } from '@affine/workspace/atom';
import { useSetAtom } from 'jotai';
import { useAtom, useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { SessionProvider, useSession } from 'next-auth/react';
import { type PropsWithChildren, startTransition, useRef } from 'react';
import { sessionAtom } from '../atoms/cloud-user';
import { useOnceSignedInEvents } from '../atoms/event';
const SessionReporter = () => {
const SessionDefence = (props: PropsWithChildren) => {
const session = useSession();
const prevSession = useRef<ReturnType<typeof useSession>>();
const [sessionInAtom, setSession] = useAtom(sessionAtom);
const pushNotification = useSetAtom(pushNotificationAtom);
const refreshMetadata = useSetAtom(refreshRootMetadataAtom);
const onceSignedInEvents = useOnceSignedInEvents();
const t = useAFFiNEI18N();
if (sessionInAtom !== session && session.status === 'authenticated') {
setSession(session);
}
if (prevSession.current !== session && session.status !== 'loading') {
// unauthenticated -> authenticated
if (
@ -42,14 +48,13 @@ const SessionReporter = () => {
}
prevSession.current = session;
}
return null;
return props.children;
};
export const CloudSessionProvider = ({ children }: PropsWithChildren) => {
return (
<SessionProvider refetchOnWindowFocus>
<SessionReporter />
{children}
<SessionDefence>{children}</SessionDefence>
</SessionProvider>
);
};

View File

@ -0,0 +1,206 @@
import type { CollectionsAtom } from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { DisposableGroup } from '@blocksuite/global/utils';
import { currentWorkspaceAtom } from '@toeverything/infra/atom';
import { type DBSchema, openDB } from 'idb';
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { Observable } from 'rxjs';
import type { Map as YMap } from 'yjs';
import { Doc as YDoc } from 'yjs';
import { sessionAtom } from '../atoms/cloud-user';
export interface PageCollectionDBV1 extends DBSchema {
view: {
key: Collection['id'];
value: Collection;
};
}
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[]>;
}
type Subscribe = () => void;
const collectionDBAtom = atom(
openDB<PageCollectionDBV1>('page-view', 1, {
upgrade(database) {
database.createObjectStore('view', {
keyPath: 'id',
});
},
})
);
const callbackSet = new Set<Subscribe>();
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: Collection) => {
const db = await get(collectionDBAtom);
const t = db.transaction('view', 'readwrite').objectStore('view');
await t.put(value);
callbackSet.forEach(cb => cb());
return key;
},
delete: async (key: string) => {
const db = await get(collectionDBAtom);
const t = db.transaction('view', 'readwrite').objectStore('view');
callbackSet.forEach(cb => cb());
await t.delete(key);
},
list: async () => {
const db = await get(collectionDBAtom);
const t = db.transaction('view').objectStore('view');
return t.getAllKeys();
},
}));
const getCollections = async (
storage: StorageCRUD<Collection>
): Promise<Collection[]> => {
return storage
.list()
.then(async keys => {
return await Promise.all(keys.map(key => storage.get(key))).then(v =>
v.filter((v): v is Collection => v !== null)
);
})
.catch(error => {
console.error('Failed to load collections', error);
return [];
});
};
const pageCollectionBaseAtom = atomWithObservable<Collection[]>(get => {
const currentWorkspacePromise = get(currentWorkspaceAtom);
const session = get(sessionAtom);
const localCRUD = get(localCollectionCRUDAtom);
const userId = session?.data?.user.id ?? null;
const useLocalStorage = userId === null;
return new Observable<Collection[]>(subscriber => {
// initial value
subscriber.next([]);
if (useLocalStorage) {
getCollections(localCRUD).then(collections => {
subscriber.next(collections);
});
const fn = () => {
getCollections(localCRUD).then(collections => {
subscriber.next(collections);
});
};
callbackSet.add(fn);
return () => {
callbackSet.delete(fn);
};
} else {
const group = new DisposableGroup();
currentWorkspacePromise.then(async currentWorkspace => {
const collectionsFromLocal = await getCollections(localCRUD);
const rootDoc = currentWorkspace.doc;
const settingMap = rootDoc.getMap('settings') as YMap<YDoc>;
if (!settingMap.has(userId)) {
settingMap.set(
userId,
new YDoc({
guid: `${rootDoc.guid}:settings:${userId}`,
})
);
}
const settingDoc = settingMap.get(userId) as YDoc;
if (!settingDoc.isLoaded) {
settingDoc.load();
await settingDoc.whenLoaded;
}
const viewMap = settingDoc.getMap('view') as YMap<Collection>;
// sync local storage to doc
collectionsFromLocal.map(v => viewMap.set(v.id, v));
// delete from indexeddb
Promise.all(
collectionsFromLocal.map(async v => {
await localCRUD.delete(v.id);
})
).catch(error => {
console.error('Failed to delete collections from indexeddb', error);
});
const collectionsFromDoc: Collection[] = Array.from(viewMap.keys())
.map(key => viewMap.get(key))
.filter((v): v is Collection => !!v);
const collections = [...collectionsFromDoc];
subscriber.next(collections);
if (group.disposed) {
return;
}
const fn = () => {
const collectionsFromDoc: Collection[] = Array.from(viewMap.keys())
.map(key => viewMap.get(key))
.filter((v): v is Collection => !!v);
const collections = [...collectionsFromLocal, ...collectionsFromDoc];
subscriber.next(collections);
};
viewMap.observe(fn);
group.add(() => {
viewMap.unobserve(fn);
});
});
return () => {
group.dispose();
};
}
});
});
export const currentCollectionsAtom: CollectionsAtom = atom(
get => get(pageCollectionBaseAtom),
async (get, set, apply) => {
const collections = await get(pageCollectionBaseAtom);
let newCollections: Collection[];
if (typeof apply === 'function') {
newCollections = apply(collections);
} else {
newCollections = apply;
}
const session = get(sessionAtom);
const userId = session?.data?.user.id ?? null;
const useLocalStorage = userId === null;
const added = newCollections.filter(v => !collections.includes(v));
const removed = collections.filter(v => !newCollections.includes(v));
if (useLocalStorage) {
const localCRUD = get(localCollectionCRUDAtom);
await Promise.all([
...added.map(async v => {
await localCRUD.set(v.id, v);
}),
...removed.map(async v => {
await localCRUD.delete(v.id);
}),
]);
} else {
const currentWorkspace = await get(currentWorkspaceAtom);
const rootDoc = currentWorkspace.doc;
const settingMap = rootDoc.getMap('settings') as YMap<YDoc>;
const settingDoc = settingMap.get(userId) as YDoc;
const viewMap = settingDoc.getMap('view') as YMap<Collection>;
await Promise.all([
...added.map(async v => {
viewMap.set(v.id, v);
}),
...removed.map(async v => {
viewMap.delete(v.id);
}),
]);
}
}
);

View File

@ -3,16 +3,30 @@
*/
import 'fake-indexeddb/auto';
import type { Collection } from '@affine/env/filter';
import { renderHook } from '@testing-library/react';
import { atom } from 'jotai';
import { expect, test } from 'vitest';
import { createDefaultFilter, vars } from '../filter/vars';
import { useCollectionManager } from '../use-collection-manager';
import {
type CollectionsAtom,
useCollectionManager,
} from '../use-collection-manager';
const defaultMeta = { tags: { options: [] } };
const baseAtom = atom<Collection[]>([]);
const mockAtom: CollectionsAtom = atom(
get => get(baseAtom),
async (get, set, update) => {
set(baseAtom, update);
}
);
test('useAllPageSetting', async () => {
const settingHook = renderHook(() => useCollectionManager('test'));
const settingHook = renderHook(() => useCollectionManager(mockAtom));
const prevCollection = settingHook.result.current.currentCollection;
expect(settingHook.result.current.savedCollections).toEqual([]);
await settingHook.result.current.updateCollection({

View File

@ -1,4 +1,7 @@
import { CollectionBar } from '@affine/component/page-list';
import {
CollectionBar,
type CollectionsAtom,
} from '@affine/component/page-list';
import { DEFAULT_SORT_KEY } from '@affine/env/constant';
import type { PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
@ -36,7 +39,7 @@ interface AllPagesHeadProps {
importFile: () => void;
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
workspaceId: string;
collectionsAtom: CollectionsAtom;
}
const AllPagesHead = ({
@ -47,7 +50,7 @@ const AllPagesHead = ({
importFile,
getPageInfo,
propertiesMeta,
workspaceId,
collectionsAtom,
}: AllPagesHeadProps) => {
const t = useAFFiNEI18N();
const titleList = useMemo(
@ -147,10 +150,10 @@ const AllPagesHead = ({
<TableHead>
<TableHeadRow>{tableItem}</TableHeadRow>
<CollectionBar
workspaceId={workspaceId}
columnsCount={titleList.length}
getPageInfo={getPageInfo}
propertiesMeta={propertiesMeta}
collectionsAtom={collectionsAtom}
/>
</TableHead>
);
@ -158,7 +161,7 @@ const AllPagesHead = ({
export const PageList = ({
isPublicWorkspace = false,
workspaceId,
collectionsAtom,
list,
onCreateNewPage,
onCreateNewEdgeless,
@ -203,7 +206,7 @@ export const PageList = ({
<StyledTableContainer ref={ref}>
<Table showBorder={hasScrollTop} style={{ maxHeight: '100%' }}>
<AllPagesHead
workspaceId={workspaceId}
collectionsAtom={collectionsAtom}
propertiesMeta={propertiesMeta}
isPublicWorkspace={isPublicWorkspace}
sorter={sorter}

View File

@ -1,6 +1,8 @@
import type { CollectionsAtom } from '@affine/component/page-list/use-collection-manager';
import type { Tag } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import type { ReactElement, ReactNode } from 'react';
/**
* Get the keys of an object type whose values are of a given type
@ -15,7 +17,7 @@ export type ListData = {
pageId: string;
icon: JSX.Element;
title: string;
preview?: React.ReactNode;
preview?: ReactNode;
tags: Tag[];
favorite: boolean;
createDate: Date;
@ -34,7 +36,7 @@ export type TrashListData = {
pageId: string;
icon: JSX.Element;
title: string;
preview?: React.ReactNode;
preview?: ReactNode;
createDate: Date;
// TODO remove optional after assert that trashDate is always set
trashDate?: Date;
@ -45,9 +47,9 @@ export type TrashListData = {
export type PageListProps = {
isPublicWorkspace?: boolean;
workspaceId: string;
collectionsAtom: CollectionsAtom;
list: ListData[];
fallback?: React.ReactNode;
fallback?: ReactNode;
onCreateNewPage: () => void;
onCreateNewEdgeless: () => void;
onImportFile: () => void;
@ -59,5 +61,5 @@ export type DraggableTitleCellData = {
pageId: string;
pageTitle: string;
pagePreview?: string;
icon: React.ReactElement;
icon: ReactElement;
};

View File

@ -1,42 +1,19 @@
import type { Collection, Filter, VariableMap } from '@affine/env/filter';
import type { DBSchema } from 'idb';
import type { IDBPDatabase } from 'idb';
import { openDB } from 'idb';
import { useAtom } from 'jotai';
import { atomWithReset, RESET } from 'jotai/utils';
import type { WritableAtom } from 'jotai/vanilla';
import { useCallback } from 'react';
import useSWRImmutable from 'swr/immutable';
import { NIL } from 'uuid';
import { evalFilterList } from './filter';
type PersistenceCollection = Collection;
export interface PageCollectionDBV1 extends DBSchema {
view: {
key: PersistenceCollection['id'];
value: PersistenceCollection;
};
}
const pageCollectionDBPromise: Promise<IDBPDatabase<PageCollectionDBV1>> =
typeof window === 'undefined'
? // never resolve in SSR
new Promise<any>(() => {})
: openDB<PageCollectionDBV1>('page-view', 1, {
upgrade(database) {
database.createObjectStore('view', {
keyPath: 'id',
});
},
});
const defaultCollection = {
id: NIL,
name: 'All',
filterList: [],
workspaceId: 'temporary',
};
const collectionAtom = atomWithReset<{
currentId: string;
defaultCollection: Collection;
@ -45,69 +22,62 @@ const collectionAtom = atomWithReset<{
defaultCollection: defaultCollection,
});
export const useSavedCollections = (workspaceId: string) => {
const { data: savedCollections, mutate } = useSWRImmutable<Collection[]>(
['affine', 'page-collection', workspaceId],
{
fetcher: async () => {
const db = await pageCollectionDBPromise;
const t = db.transaction('view').objectStore('view');
const all = await t.getAll();
return all.filter(v => v.workspaceId === workspaceId);
},
suspense: true,
fallbackData: [],
revalidateOnMount: true,
}
);
export type CollectionsAtom = WritableAtom<
Collection[] | Promise<Collection[]>,
[Collection[] | ((collection: Collection[]) => Collection[])],
Promise<void>
>;
export const useSavedCollections = (collectionAtom: CollectionsAtom) => {
const [savedCollections, setCollections] = useAtom(collectionAtom);
const saveCollection = useCallback(
async (collection: Collection) => {
if (collection.id === NIL) {
return;
}
const db = await pageCollectionDBPromise;
const t = db.transaction('view', 'readwrite').objectStore('view');
await t.put(collection);
await mutate();
await setCollections(old => [...old, collection]);
},
[mutate]
[setCollections]
);
const deleteCollection = useCallback(
async (id: string) => {
if (id === NIL) {
return;
}
const db = await pageCollectionDBPromise;
const t = db.transaction('view', 'readwrite').objectStore('view');
await t.delete(id);
await mutate();
await setCollections(old => old.filter(v => v.id !== id));
},
[mutate]
[setCollections]
);
const addPage = useCallback(
async (collectionId: string, pageId: string) => {
const collection = savedCollections?.find(v => v.id === collectionId);
if (!collection) {
return;
}
await saveCollection({
...collection,
allowList: [pageId, ...(collection.allowList ?? [])],
await setCollections(old => {
const collection = old.find(v => v.id === collectionId);
if (!collection) {
return old;
}
return [
...old.filter(v => v.id !== collectionId),
{
...collection,
allowList: [pageId, ...(collection.allowList ?? [])],
},
];
});
},
[saveCollection, savedCollections]
[setCollections]
);
return {
savedCollections: savedCollections ?? [],
savedCollections,
saveCollection,
deleteCollection,
addPage,
};
};
export const useCollectionManager = (workspaceId: string) => {
export const useCollectionManager = (collectionsAtom: CollectionsAtom) => {
const { savedCollections, saveCollection, deleteCollection, addPage } =
useSavedCollections(workspaceId);
useSavedCollections(collectionsAtom);
const [collectionData, setCollectionData] = useAtom(collectionAtom);
const updateCollection = useCallback(

View File

@ -1,4 +1,7 @@
import { EditCollectionModel } from '@affine/component/page-list';
import {
type CollectionsAtom,
EditCollectionModel,
} from '@affine/component/page-list';
import type { PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@ -15,14 +18,14 @@ import { useActions } from './use-action';
interface CollectionBarProps {
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
collectionsAtom: CollectionsAtom;
columnsCount: number;
workspaceId: string;
}
export const CollectionBar = (props: CollectionBarProps) => {
const { getPageInfo, propertiesMeta, columnsCount, workspaceId } = props;
const { getPageInfo, propertiesMeta, columnsCount, collectionsAtom } = props;
const t = useAFFiNEI18N();
const setting = useCollectionManager(workspaceId);
const setting = useCollectionManager(collectionsAtom);
const collection = setting.currentCollection;
const [open, setOpen] = useState(false);
const actions = useActions({

View File

@ -139,6 +139,36 @@ test.describe('collaboration', () => {
}
});
test('can sync collections between different browser', async ({
page,
browser,
}) => {
await page.reload();
await waitForEditorLoad(page);
await createLocalWorkspace(
{
name: 'test',
},
page
);
await enableCloudWorkspace(page);
await page.getByTestId('slider-bar-add-collection-button').click();
const title = page.getByTestId('input-collection-title');
await title.isVisible();
await title.fill('test collection');
await page.getByTestId('save-collection').click();
{
const context = await browser.newContext();
const page2 = await context.newPage();
await loginUser(page2, user.email);
await page2.goto(page.url());
waitForEditorLoad(page2);
const collections = page2.getByTestId('collections');
await expect(collections.getByText('test collection')).toBeVisible();
}
});
test('exit successfully and re-login', async ({ page }) => {
await page.reload();
await clickSideBarAllPageButton(page);