mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat: page view storage with cloud support (#4238)
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user