From befae6bc9b39bc46b950eb7101798d6bfef8984f Mon Sep 17 00:00:00 2001 From: Himself65 Date: Tue, 30 May 2023 00:21:21 +0800 Subject: [PATCH] feat: page view persistence (#2581) --- apps/web/src/components/workspace-header.tsx | 23 +-- .../__tests__/use-all-page-setting.spec.ts | 52 +++++++ .../page-list/use-all-page-setting.ts | 147 ++++++++++++------ .../components/page-list/view/create-view.tsx | 8 +- .../components/page-list/view/view-list.tsx | 20 ++- 5 files changed, 185 insertions(+), 65 deletions(-) create mode 100644 packages/component/src/components/page-list/__tests__/use-all-page-setting.spec.ts diff --git a/apps/web/src/components/workspace-header.tsx b/apps/web/src/components/workspace-header.tsx index b1d1d0caaa..2e3487524b 100644 --- a/apps/web/src/components/workspace-header.tsx +++ b/apps/web/src/components/workspace-header.tsx @@ -15,8 +15,10 @@ import { SettingsIcon, ShareIcon, } from '@blocksuite/icons'; +import { RESET } from 'jotai/utils'; import type { ReactElement } from 'react'; import React from 'react'; +import { NIL } from 'uuid'; import { BlockSuiteEditorHeader } from './blocksuite/workspace-header'; import { WorkspaceTitle } from './pure/workspace-title'; @@ -38,25 +40,24 @@ export function WorkspaceHeader({
- setting.changeView( - { - ...setting.currentView, - filterList, - }, - setting.currentViewIndex - ) - } + onChange={filterList => { + setting.setCurrentView(view => ({ + ...view, + filterList, + })); + }} />
- {setting.currentViewIndex == null ? ( + {setting.currentView.id !== NIL || + (setting.currentView.id === NIL && + setting.currentView.filterList.length > 0) ? ( ) : ( - )} diff --git a/packages/component/src/components/page-list/__tests__/use-all-page-setting.spec.ts b/packages/component/src/components/page-list/__tests__/use-all-page-setting.spec.ts new file mode 100644 index 0000000000..65b0e6b94f --- /dev/null +++ b/packages/component/src/components/page-list/__tests__/use-all-page-setting.spec.ts @@ -0,0 +1,52 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import { renderHook } from '@testing-library/react'; +import { RESET } from 'jotai/utils'; +import { expect, test } from 'vitest'; + +import { useAllPageSetting } from '../use-all-page-setting'; + +test('useAllPageSetting', async () => { + const settingHook = renderHook(() => useAllPageSetting()); + const prevView = settingHook.result.current.currentView; + expect(settingHook.result.current.savedViews).toEqual([]); + settingHook.result.current.setCurrentView(view => ({ + ...view, + filterList: [ + { + type: 'filter', + left: { + type: 'ref', + name: 'Created', + }, + funcName: 'Create', + args: [], + }, + ], + })); + settingHook.rerender(); + const nextView = settingHook.result.current.currentView; + expect(nextView).not.toBe(prevView); + expect(nextView.filterList).toEqual([ + { + type: 'filter', + left: { + type: 'ref', + name: 'Created', + }, + funcName: 'Create', + args: [], + }, + ]); + settingHook.result.current.setCurrentView(RESET); + await settingHook.result.current.createView({ + ...settingHook.result.current.currentView, + id: '1', + }); + settingHook.rerender(); + expect(settingHook.result.current.savedViews.length).toBe(1); + expect(settingHook.result.current.savedViews[0].id).toBe('1'); +}); diff --git a/packages/component/src/components/page-list/use-all-page-setting.ts b/packages/component/src/components/page-list/use-all-page-setting.ts index 176df30ebd..bea49ba2fe 100644 --- a/packages/component/src/components/page-list/use-all-page-setting.ts +++ b/packages/component/src/components/page-list/use-all-page-setting.ts @@ -1,61 +1,118 @@ -import { useState } from 'react'; +import dayjs from 'dayjs'; +import type { DBSchema } from 'idb'; +import { openDB } from 'idb'; +import type { IDBPDatabase } from 'idb/build/entry'; +import { useAtom } from 'jotai'; +import { atomWithReset } from 'jotai/utils'; +import { useCallback } from 'react'; +import useSWRImmutable from 'swr/immutable'; +import { NIL } from 'uuid'; import { evalFilterList } from './filter'; import type { Filter, VariableMap } from './filter/vars'; +import type { Literal } from './filter/vars'; export type View = { + id: string; name: string; filterList: Filter[]; }; -export type AllPageSetting = { - mainView: View; - currentView?: number; - savedViews: View[]; -}; -export const useAllPageSetting = () => { - const [setting, changeSetting] = useState({ - mainView: { - name: 'default', - filterList: [], - }, - savedViews: [], - }); - const changeView = (view: View, i?: number) => - changeSetting(setting => { - if (i != null) { - return { - ...setting, - savedViews: setting.savedViews.map((v, index) => - i === index ? view : v - ), - }; - } else { - return { - ...setting, - mainView: view, - }; +type PersistenceView = Omit & { + filterList: (Omit & { + args: (Omit & { + value: string; + })[]; + })[]; +}; + +export interface PageViewDBV1 extends DBSchema { + view: { + key: PersistenceView['id']; + value: PersistenceView; + }; +} + +const pageViewDBPromise: Promise> = + environment.isServer + ? // never resolve in SSR + new Promise(() => {}) + : openDB('page-view', 1, { + upgrade(database) { + database.createObjectStore('view', { + keyPath: 'id', + }); + }, + }); + +const currentViewAtom = atomWithReset({ + name: 'default', + id: NIL, + filterList: [], +}); + +export const useAllPageSetting = () => { + const { data: savedViews, mutate } = useSWRImmutable( + ['affine', 'page-view'], + { + fetcher: async () => { + const db = await pageViewDBPromise; + const t = db.transaction('view').objectStore('view'); + const all = await t.getAll(); + return all.map(view => ({ + ...view, + filterList: view.filterList.map(filter => ({ + ...filter, + args: filter.args.map(arg => ({ + ...arg, + value: dayjs(arg.value), + })), + })), + })); + }, + suspense: true, + fallbackData: [], + revalidateOnMount: true, + } + ); + + const [currentView, setCurrentView] = useAtom(currentViewAtom); + + const createView = useCallback( + async (view: View) => { + if (view.id === NIL) { + return; } - }); - const createView = (view: View) => - changeSetting(setting => ({ - ...setting, - currentView: setting.savedViews.length, - savedViews: [...setting.savedViews, view], - })); - const selectView = (i?: number) => - changeSetting(setting => ({ ...setting, currentView: i })); - const currentView = - setting.currentView != null - ? setting.savedViews[setting.currentView] - : setting.mainView; + const db = await pageViewDBPromise; + const t = db.transaction('view', 'readwrite').objectStore('view'); + const persistenceView: PersistenceView = { + ...view, + filterList: view.filterList.map(filter => { + return { + ...filter, + args: filter.args.map(arg => { + return { + type: arg.type, + // @ts-expect-error + value: arg.value.toString(), + }; + }), + }; + }), + }; + await t.put(persistenceView); + await mutate(); + }, + [mutate] + ); + return { currentView, - currentViewIndex: setting.currentView, - viewList: setting.savedViews, - selectView, + savedViews: savedViews as View[], + + // actions createView, - changeView, + setCurrentView, }; }; export const filterByView = (view: View, varMap: VariableMap) => diff --git a/packages/component/src/components/page-list/view/create-view.tsx b/packages/component/src/components/page-list/view/create-view.tsx index 4b2c3b3aca..1299dcdd89 100644 --- a/packages/component/src/components/page-list/view/create-view.tsx +++ b/packages/component/src/components/page-list/view/create-view.tsx @@ -1,4 +1,5 @@ import { Button, Input, Modal, ModalWrapper } from '@affine/component'; +import { uuidv4 } from '@blocksuite/store'; import { useState } from 'react'; import { FilterList } from '../filter'; @@ -9,12 +10,17 @@ type CreateViewProps = { init: Filter[]; onConfirm: (view: View) => void; }; + const CreateView = ({ init, onConfirm, onCancel, }: CreateViewProps & { onCancel: () => void }) => { - const [value, onChange] = useState({ name: '', filterList: init }); + const [value, onChange] = useState({ + name: '', + filterList: init, + id: uuidv4(), + }); return (
diff --git a/packages/component/src/components/page-list/view/view-list.tsx b/packages/component/src/components/page-list/view/view-list.tsx index dbdfd04d5f..9445a0bf00 100644 --- a/packages/component/src/components/page-list/view/view-list.tsx +++ b/packages/component/src/components/page-list/view/view-list.tsx @@ -7,6 +7,7 @@ import type { useAllPageSetting } from '../use-all-page-setting'; const NoDragDiv = styled('div')` -webkit-app-region: no-drag; `; + export const ViewList = ({ setting, }: { @@ -14,15 +15,18 @@ export const ViewList = ({ }) => { return (
- {setting.currentViewIndex != null && ( + {setting.savedViews.length > 0 && ( - {setting.viewList.map((v, i) => { + {setting.savedViews.map(view => { return ( - setting.selectView(i)} key={i}> - {v.name} + setting.setCurrentView(view)} + key={view.id} + > + {view.name} ); })} @@ -40,10 +44,10 @@ export const ViewList = ({ { - setting.changeView( - { ...setting.currentView, filterList }, - setting.currentViewIndex - ); + setting.setCurrentView(view => ({ + ...view, + filterList, + })); }} /> }