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

View File

@@ -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');
});

View File

@@ -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<AllPageSetting>({
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<View, 'filterList'> & {
filterList: (Omit<Filter, 'args'> & {
args: (Omit<Literal, 'value'> & {
value: string;
})[];
})[];
};
export interface PageViewDBV1 extends DBSchema {
view: {
key: PersistenceView['id'];
value: PersistenceView;
};
}
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 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) =>

View File

@@ -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<View>({ name: '', filterList: init });
const [value, onChange] = useState<View>({
name: '',
filterList: init,
id: uuidv4(),
});
return (
<div>

View File

@@ -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 (
<div style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}>
{setting.currentViewIndex != null && (
{setting.savedViews.length > 0 && (
<Menu
trigger="click"
content={
<div>
{setting.viewList.map((v, i) => {
{setting.savedViews.map(view => {
return (
<MenuItem onClick={() => setting.selectView(i)} key={i}>
{v.name}
<MenuItem
onClick={() => setting.setCurrentView(view)}
key={view.id}
>
{view.name}
</MenuItem>
);
})}
@@ -40,10 +44,10 @@ export const ViewList = ({
<CreateFilterMenu
value={setting.currentView.filterList}
onChange={filterList => {
setting.changeView(
{ ...setting.currentView, filterList },
setting.currentViewIndex
);
setting.setCurrentView(view => ({
...view,
filterList,
}));
}}
/>
}