feat: page view persistence (#2581)

This commit is contained in:
Himself65
2023-05-30 00:21:21 +08:00
committed by GitHub
parent e7eb13e966
commit befae6bc9b
5 changed files with 185 additions and 65 deletions
+12 -11
View File
@@ -15,8 +15,10 @@ import {
SettingsIcon, SettingsIcon,
ShareIcon, ShareIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { RESET } from 'jotai/utils';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import React from 'react'; import React from 'react';
import { NIL } from 'uuid';
import { BlockSuiteEditorHeader } from './blocksuite/workspace-header'; import { BlockSuiteEditorHeader } from './blocksuite/workspace-header';
import { WorkspaceTitle } from './pure/workspace-title'; import { WorkspaceTitle } from './pure/workspace-title';
@@ -38,25 +40,24 @@ export function WorkspaceHeader({
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<FilterList <FilterList
value={setting.currentView.filterList} value={setting.currentView.filterList}
onChange={filterList => onChange={filterList => {
setting.changeView( setting.setCurrentView(view => ({
{ ...view,
...setting.currentView, filterList,
filterList, }));
}, }}
setting.currentViewIndex
)
}
/> />
</div> </div>
<div> <div>
{setting.currentViewIndex == null ? ( {setting.currentView.id !== NIL ||
(setting.currentView.id === NIL &&
setting.currentView.filterList.length > 0) ? (
<SaveViewButton <SaveViewButton
init={setting.currentView.filterList} init={setting.currentView.filterList}
onConfirm={setting.createView} onConfirm={setting.createView}
></SaveViewButton> ></SaveViewButton>
) : ( ) : (
<Button onClick={() => setting.selectView()}> <Button onClick={() => setting.setCurrentView(RESET)}>
Back to all Back to all
</Button> </Button>
)} )}
@@ -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');
});
@@ -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 { evalFilterList } from './filter';
import type { Filter, VariableMap } from './filter/vars'; import type { Filter, VariableMap } from './filter/vars';
import type { Literal } from './filter/vars';
export type View = { export type View = {
id: string;
name: string; name: string;
filterList: Filter[]; filterList: Filter[];
}; };
export type AllPageSetting = {
mainView: View;
currentView?: number;
savedViews: View[];
};
export const useAllPageSetting = () => {
const [setting, changeSetting] = useState<AllPageSetting>({
mainView: {
name: 'default',
filterList: [],
},
savedViews: [],
});
const changeView = (view: View, i?: number) => type PersistenceView = Omit<View, 'filterList'> & {
changeSetting(setting => { filterList: (Omit<Filter, 'args'> & {
if (i != null) { args: (Omit<Literal, 'value'> & {
return { value: string;
...setting, })[];
savedViews: setting.savedViews.map((v, index) => })[];
i === index ? view : v };
),
}; export interface PageViewDBV1 extends DBSchema {
} else { view: {
return { key: PersistenceView['id'];
...setting, value: PersistenceView;
mainView: view, };
}; }
const pageViewDBPromise: Promise<IDBPDatabase<PageViewDBV1>> =
environment.isServer
? // never resolve in SSR
new Promise<any>(() => {})
: openDB<PageViewDBV1>('page-view', 1, {
upgrade(database) {
database.createObjectStore('view', {
keyPath: 'id',
});
},
});
const currentViewAtom = atomWithReset<View>({
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 db = await pageViewDBPromise;
const createView = (view: View) => const t = db.transaction('view', 'readwrite').objectStore('view');
changeSetting(setting => ({ const persistenceView: PersistenceView = {
...setting, ...view,
currentView: setting.savedViews.length, filterList: view.filterList.map(filter => {
savedViews: [...setting.savedViews, view], return {
})); ...filter,
const selectView = (i?: number) => args: filter.args.map(arg => {
changeSetting(setting => ({ ...setting, currentView: i })); return {
const currentView = type: arg.type,
setting.currentView != null // @ts-expect-error
? setting.savedViews[setting.currentView] value: arg.value.toString(),
: setting.mainView; };
}),
};
}),
};
await t.put(persistenceView);
await mutate();
},
[mutate]
);
return { return {
currentView, currentView,
currentViewIndex: setting.currentView, savedViews: savedViews as View[],
viewList: setting.savedViews,
selectView, // actions
createView, createView,
changeView, setCurrentView,
}; };
}; };
export const filterByView = (view: View, varMap: VariableMap) => export const filterByView = (view: View, varMap: VariableMap) =>
@@ -1,4 +1,5 @@
import { Button, Input, Modal, ModalWrapper } from '@affine/component'; import { Button, Input, Modal, ModalWrapper } from '@affine/component';
import { uuidv4 } from '@blocksuite/store';
import { useState } from 'react'; import { useState } from 'react';
import { FilterList } from '../filter'; import { FilterList } from '../filter';
@@ -9,12 +10,17 @@ type CreateViewProps = {
init: Filter[]; init: Filter[];
onConfirm: (view: View) => void; onConfirm: (view: View) => void;
}; };
const CreateView = ({ const CreateView = ({
init, init,
onConfirm, onConfirm,
onCancel, onCancel,
}: CreateViewProps & { onCancel: () => void }) => { }: CreateViewProps & { onCancel: () => void }) => {
const [value, onChange] = useState<View>({ name: '', filterList: init }); const [value, onChange] = useState<View>({
name: '',
filterList: init,
id: uuidv4(),
});
return ( return (
<div> <div>
@@ -7,6 +7,7 @@ import type { useAllPageSetting } from '../use-all-page-setting';
const NoDragDiv = styled('div')` const NoDragDiv = styled('div')`
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
`; `;
export const ViewList = ({ export const ViewList = ({
setting, setting,
}: { }: {
@@ -14,15 +15,18 @@ export const ViewList = ({
}) => { }) => {
return ( return (
<div style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}> <div style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}>
{setting.currentViewIndex != null && ( {setting.savedViews.length > 0 && (
<Menu <Menu
trigger="click" trigger="click"
content={ content={
<div> <div>
{setting.viewList.map((v, i) => { {setting.savedViews.map(view => {
return ( return (
<MenuItem onClick={() => setting.selectView(i)} key={i}> <MenuItem
{v.name} onClick={() => setting.setCurrentView(view)}
key={view.id}
>
{view.name}
</MenuItem> </MenuItem>
); );
})} })}
@@ -40,10 +44,10 @@ export const ViewList = ({
<CreateFilterMenu <CreateFilterMenu
value={setting.currentView.filterList} value={setting.currentView.filterList}
onChange={filterList => { onChange={filterList => {
setting.changeView( setting.setCurrentView(view => ({
{ ...setting.currentView, filterList }, ...view,
setting.currentViewIndex filterList,
); }));
}} }}
/> />
} }