mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-03 02:20:19 +08:00
feat: page view persistence (#2581)
This commit is contained in:
@@ -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,
|
||||||
);
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user