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
16 changed files with 338 additions and 94 deletions

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({