feat: headless filter in all pages tab (#2566)
Co-authored-by: himself65 <himself65@outlook.com>
This commit is contained in:
@@ -18,9 +18,11 @@ export const blockSuiteFeatureFlags = {
|
|||||||
* @type {import('@affine/env').BuildFlags}
|
* @type {import('@affine/env').BuildFlags}
|
||||||
*/
|
*/
|
||||||
export const buildFlags = {
|
export const buildFlags = {
|
||||||
enableAllPageFilter: process.env.ENABLE_ALL_PAGE_FILTER
|
enableAllPageFilter:
|
||||||
? process.env.ENABLE_ALL_PAGE_FILTER === 'true'
|
!!process.env.VERCEL ||
|
||||||
: false,
|
(process.env.ENABLE_ALL_PAGE_FILTER
|
||||||
|
? process.env.ENABLE_ALL_PAGE_FILTER === 'true'
|
||||||
|
: false),
|
||||||
enableImagePreviewModal: process.env.ENABLE_IMAGE_PREVIEW_MODAL
|
enableImagePreviewModal: process.env.ENABLE_IMAGE_PREVIEW_MODAL
|
||||||
? process.env.ENABLE_IMAGE_PREVIEW_MODAL === 'true'
|
? process.env.ENABLE_IMAGE_PREVIEW_MODAL === 'true'
|
||||||
: true,
|
: true,
|
||||||
|
|||||||
@@ -330,9 +330,10 @@ export const AffinePlugin: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
PageList: ({ blockSuiteWorkspace, onOpenPage }) => {
|
PageList: ({ blockSuiteWorkspace, onOpenPage, view }) => {
|
||||||
return (
|
return (
|
||||||
<BlockSuitePageList
|
<BlockSuitePageList
|
||||||
|
view={view}
|
||||||
listType="all"
|
listType="all"
|
||||||
onOpenPage={onOpenPage}
|
onOpenPage={onOpenPage}
|
||||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||||
|
|||||||
@@ -100,10 +100,11 @@ export const LocalPlugin: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
PageList: ({ blockSuiteWorkspace, onOpenPage }) => {
|
PageList: ({ blockSuiteWorkspace, onOpenPage, view }) => {
|
||||||
return (
|
return (
|
||||||
<BlockSuitePageList
|
<BlockSuitePageList
|
||||||
listType="all"
|
listType="all"
|
||||||
|
view={view}
|
||||||
onOpenPage={onOpenPage}
|
onOpenPage={onOpenPage}
|
||||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { Empty } from '@affine/component';
|
import { Empty } from '@affine/component';
|
||||||
import type { ListData, TrashListData } from '@affine/component/page-list';
|
import type {
|
||||||
import { PageList, PageListTrashView } from '@affine/component/page-list';
|
ListData,
|
||||||
|
TrashListData,
|
||||||
|
View,
|
||||||
|
} from '@affine/component/page-list';
|
||||||
|
import {
|
||||||
|
filterByView,
|
||||||
|
PageList,
|
||||||
|
PageListTrashView,
|
||||||
|
} from '@affine/component/page-list';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
@@ -19,6 +27,7 @@ export type BlockSuitePageListProps = {
|
|||||||
listType: 'all' | 'trash' | 'shared' | 'public';
|
listType: 'all' | 'trash' | 'shared' | 'public';
|
||||||
isPublic?: true;
|
isPublic?: true;
|
||||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||||
|
view?: View;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
@@ -61,6 +70,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
|||||||
onOpenPage,
|
onOpenPage,
|
||||||
listType,
|
listType,
|
||||||
isPublic = false,
|
isPublic = false,
|
||||||
|
view,
|
||||||
}) => {
|
}) => {
|
||||||
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||||
const {
|
const {
|
||||||
@@ -74,8 +84,22 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
|||||||
usePageHelper(blockSuiteWorkspace);
|
usePageHelper(blockSuiteWorkspace);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const list = useMemo(
|
const list = useMemo(
|
||||||
() => pageMetas.filter(pageMeta => filter[listType](pageMeta, pageMetas)),
|
() =>
|
||||||
[pageMetas, listType]
|
pageMetas.filter(pageMeta => {
|
||||||
|
if (!filter[listType](pageMeta, pageMetas)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(view);
|
||||||
|
if (!view) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return filterByView(view, {
|
||||||
|
Favorite: !!pageMeta.favorite,
|
||||||
|
Created: pageMeta.createDate,
|
||||||
|
Updated: pageMeta.updatedDate,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
[pageMetas, listType, view]
|
||||||
);
|
);
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
return <PageListEmpty listType={listType} />;
|
return <PageListEmpty listType={listType} />;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
|
|||||||
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
|
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
lazy,
|
lazy,
|
||||||
@@ -44,6 +44,7 @@ export type BaseHeaderProps<
|
|||||||
currentPage: Page | null;
|
currentPage: Page | null;
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
isPreview: boolean;
|
isPreview: boolean;
|
||||||
|
leftSlot?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enum HeaderRightItemName {
|
export const enum HeaderRightItemName {
|
||||||
@@ -198,11 +199,14 @@ export const Header = forwardRef<
|
|||||||
data-is-edgeless={mode === 'edgeless'}
|
data-is-edgeless={mode === 'edgeless'}
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<SidebarSwitch
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
visible={!open}
|
<SidebarSwitch
|
||||||
tooltipContent={t['Expand sidebar']()}
|
visible={!open}
|
||||||
data-testid="sliderBar-arrowButton-expand"
|
tooltipContent={t['Expand sidebar']()}
|
||||||
/>
|
data-testid="sliderBar-arrowButton-expand"
|
||||||
|
/>
|
||||||
|
{props.leftSlot}
|
||||||
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
import { Button } from '@affine/component';
|
||||||
|
import {
|
||||||
|
FilterList,
|
||||||
|
SaveViewButton,
|
||||||
|
useAllPageSetting,
|
||||||
|
ViewList,
|
||||||
|
} from '@affine/component/page-list';
|
||||||
|
import { config } from '@affine/env';
|
||||||
import { QueryParamError, Unreachable } from '@affine/env/constant';
|
import { QueryParamError, Unreachable } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
@@ -17,6 +25,7 @@ import type { NextPageWithLayout } from '../../../shared';
|
|||||||
|
|
||||||
const AllPage: NextPageWithLayout = () => {
|
const AllPage: NextPageWithLayout = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const setting = useAllPageSetting();
|
||||||
const { jumpToPage } = useRouterHelper(router);
|
const { jumpToPage } = useRouterHelper(router);
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
@@ -37,6 +46,38 @@ const AllPage: NextPageWithLayout = () => {
|
|||||||
if (typeof router.query.workspaceId !== 'string') {
|
if (typeof router.query.workspaceId !== 'string') {
|
||||||
throw new QueryParamError('workspaceId', router.query.workspaceId);
|
throw new QueryParamError('workspaceId', router.query.workspaceId);
|
||||||
}
|
}
|
||||||
|
const leftSlot = config.enableAllPageFilter && (
|
||||||
|
<ViewList setting={setting}></ViewList>
|
||||||
|
);
|
||||||
|
const filterContainer = config.enableAllPageFilter &&
|
||||||
|
setting.currentView.filterList.length > 0 && (
|
||||||
|
<div style={{ padding: 12, display: 'flex' }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<FilterList
|
||||||
|
value={setting.currentView.filterList}
|
||||||
|
onChange={filterList =>
|
||||||
|
setting.changeView(
|
||||||
|
{
|
||||||
|
...setting.currentView,
|
||||||
|
filterList,
|
||||||
|
},
|
||||||
|
setting.currentViewIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{setting.currentViewIndex == null ? (
|
||||||
|
<SaveViewButton
|
||||||
|
init={setting.currentView.filterList}
|
||||||
|
onConfirm={setting.createView}
|
||||||
|
></SaveViewButton>
|
||||||
|
) : (
|
||||||
|
<Button onClick={() => setting.selectView()}>Back to all</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
|
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||||
const PageList = WorkspaceAdapters[currentWorkspace.flavour].UI.PageList;
|
const PageList = WorkspaceAdapters[currentWorkspace.flavour].UI.PageList;
|
||||||
return (
|
return (
|
||||||
@@ -50,10 +91,13 @@ const AllPage: NextPageWithLayout = () => {
|
|||||||
isPreview={false}
|
isPreview={false}
|
||||||
isPublic={false}
|
isPublic={false}
|
||||||
icon={<FolderIcon />}
|
icon={<FolderIcon />}
|
||||||
|
leftSlot={leftSlot}
|
||||||
>
|
>
|
||||||
{t['All pages']()}
|
{t['All pages']()}
|
||||||
</WorkspaceTitle>
|
</WorkspaceTitle>
|
||||||
|
{filterContainer}
|
||||||
<PageList
|
<PageList
|
||||||
|
view={setting.currentView}
|
||||||
onOpenPage={onClickPage}
|
onOpenPage={onClickPage}
|
||||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||||
/>
|
/>
|
||||||
@@ -72,10 +116,13 @@ const AllPage: NextPageWithLayout = () => {
|
|||||||
isPreview={false}
|
isPreview={false}
|
||||||
isPublic={false}
|
isPublic={false}
|
||||||
icon={<FolderIcon />}
|
icon={<FolderIcon />}
|
||||||
|
leftSlot={leftSlot}
|
||||||
>
|
>
|
||||||
{t['All pages']()}
|
{t['All pages']()}
|
||||||
</WorkspaceTitle>
|
</WorkspaceTitle>
|
||||||
|
{filterContainer}
|
||||||
<PageList
|
<PageList
|
||||||
|
view={setting.currentView}
|
||||||
onOpenPage={onClickPage}
|
onOpenPage={onClickPage}
|
||||||
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"@toeverything/theme": "^0.5.9",
|
"@toeverything/theme": "^0.5.9",
|
||||||
"@vanilla-extract/dynamic": "^2.0.3",
|
"@vanilla-extract/dynamic": "^2.0.3",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
"jotai": "^2.1.0",
|
"jotai": "^2.1.0",
|
||||||
"lit": "^2.7.4",
|
"lit": "^2.7.4",
|
||||||
"lottie-web": "^5.11.0",
|
"lottie-web": "^5.11.0",
|
||||||
|
|||||||
127
packages/component/src/components/page-list/filter/condition.tsx
Normal file
127
packages/component/src/components/page-list/filter/condition.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { Menu, MenuItem } from '../../../ui/menu';
|
||||||
|
import { literalMatcher } from './literal-matcher';
|
||||||
|
import type { TFunction, TType } from './logical/typesystem';
|
||||||
|
import type { Filter, Literal } from './vars';
|
||||||
|
import { ChangeFilterMenu, filterMatcher, vars } from './vars';
|
||||||
|
|
||||||
|
export const Condition = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: Filter;
|
||||||
|
onChange: (filter: Filter) => void;
|
||||||
|
}) => {
|
||||||
|
const data = useMemo(
|
||||||
|
() => filterMatcher.find(v => v.data.name === value.funcName),
|
||||||
|
[value.funcName]
|
||||||
|
);
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const render =
|
||||||
|
data.data.render ??
|
||||||
|
(({ ast }) => {
|
||||||
|
const args = renderArgs(value, onChange, data.type);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', userSelect: 'none', alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
trigger="click"
|
||||||
|
content={<ChangeFilterMenu selected={[]} onSelect={onChange} />}
|
||||||
|
>
|
||||||
|
<div>{ast.left.name}</div>
|
||||||
|
</Menu>
|
||||||
|
<Menu
|
||||||
|
trigger="click"
|
||||||
|
content={<FunctionSelect value={value} onChange={onChange} />}
|
||||||
|
>
|
||||||
|
<div style={{ marginLeft: 4, color: 'gray' }}>{ast.funcName}</div>
|
||||||
|
</Menu>
|
||||||
|
{args}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return <>{render({ ast: value })}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FunctionSelect = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: Filter;
|
||||||
|
onChange: (value: Filter) => void;
|
||||||
|
}) => {
|
||||||
|
const list = useMemo(() => {
|
||||||
|
const type = vars.find(v => v.name === value.left.name)?.type;
|
||||||
|
if (!type) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return filterMatcher.allMatchedData(type);
|
||||||
|
}, [value.left.name]);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{list.map(v => (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
funcName: v.name,
|
||||||
|
args: v.defaultArgs().map(v => ({ type: 'literal', value: v })),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
key={v.name}
|
||||||
|
>
|
||||||
|
{v.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Arg = ({
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
type: TType;
|
||||||
|
value: Literal;
|
||||||
|
onChange: (lit: Literal) => void;
|
||||||
|
}) => {
|
||||||
|
const data = useMemo(() => literalMatcher.match(type), [type]);
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ marginLeft: 4 }}>
|
||||||
|
{data.render({ type, value, onChange })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const renderArgs = (
|
||||||
|
filter: Filter,
|
||||||
|
onChange: (value: Filter) => void,
|
||||||
|
type: TFunction
|
||||||
|
): ReactNode => {
|
||||||
|
const rest = type.args.slice(1);
|
||||||
|
return rest.map((type, i) => {
|
||||||
|
const value = filter.args[i];
|
||||||
|
return (
|
||||||
|
<Arg
|
||||||
|
key={i}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={value => {
|
||||||
|
const args = filter.args.map((v, index) => (i === index ? value : v));
|
||||||
|
onChange({
|
||||||
|
...filter,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
></Arg>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
25
packages/component/src/components/page-list/filter/eval.ts
Normal file
25
packages/component/src/components/page-list/filter/eval.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Filter, Literal, Ref } from './vars';
|
||||||
|
import type { VariableMap } from './vars';
|
||||||
|
import { filterMatcher } from './vars';
|
||||||
|
|
||||||
|
const evalRef = (ref: Ref, variableMap: VariableMap) => {
|
||||||
|
return variableMap[ref.name];
|
||||||
|
};
|
||||||
|
const evalLiteral = (lit: Literal) => {
|
||||||
|
return lit.value;
|
||||||
|
};
|
||||||
|
const evalFilter = (filter: Filter, variableMap: VariableMap): boolean => {
|
||||||
|
const impl = filterMatcher.findData(v => v.name === filter.funcName)?.impl;
|
||||||
|
if (!impl) {
|
||||||
|
throw new Error('No function implementation found');
|
||||||
|
}
|
||||||
|
const leftValue = evalRef(filter.left, variableMap);
|
||||||
|
const args = filter.args.map(evalLiteral);
|
||||||
|
return impl(leftValue, ...args);
|
||||||
|
};
|
||||||
|
export const evalFilterList = (
|
||||||
|
filterList: Filter[],
|
||||||
|
variableMap: VariableMap
|
||||||
|
) => {
|
||||||
|
return filterList.every(filter => evalFilter(filter, variableMap));
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Menu } from '@affine/component';
|
||||||
|
|
||||||
|
import { Condition } from './condition';
|
||||||
|
import type { Filter } from './vars';
|
||||||
|
import { CreateFilterMenu } from './vars';
|
||||||
|
|
||||||
|
export const FilterList = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: Filter[];
|
||||||
|
onChange: (value: Filter[]) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||||
|
{value.map((filter, i) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
border: '1px solid gray',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: '2px 6px',
|
||||||
|
margin: 4,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
}}
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<Condition
|
||||||
|
value={filter}
|
||||||
|
onChange={filter => {
|
||||||
|
onChange(
|
||||||
|
value.map((old, oldIndex) => (oldIndex === i ? filter : old))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{ marginLeft: 8, cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(value.filter((_, index) => i !== index));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
x
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Menu
|
||||||
|
trigger={'click'}
|
||||||
|
content={<CreateFilterMenu value={value} onChange={onChange} />}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: 4,
|
||||||
|
marginLeft: 4,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './eval';
|
||||||
|
export * from './filter-list';
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { tBoolean, tDate } from './logical/custom-type';
|
||||||
|
import { Matcher } from './logical/matcher';
|
||||||
|
import type { TType } from './logical/typesystem';
|
||||||
|
import { typesystem } from './logical/typesystem';
|
||||||
|
import type { Literal } from './vars';
|
||||||
|
|
||||||
|
export const literalMatcher = new Matcher<{
|
||||||
|
render: (props: {
|
||||||
|
type: TType;
|
||||||
|
value: Literal;
|
||||||
|
onChange: (lit: Literal) => void;
|
||||||
|
}) => ReactNode;
|
||||||
|
}>((type, target) => {
|
||||||
|
return typesystem.isSubtype(type, target);
|
||||||
|
});
|
||||||
|
|
||||||
|
literalMatcher.register(tBoolean.create(), {
|
||||||
|
render: ({ value, onChange }) => (
|
||||||
|
<div
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
onChange({ type: 'literal', value: !value.value });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value.value?.toString()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
literalMatcher.register(tDate.create(), {
|
||||||
|
render: ({ value, onChange }) => (
|
||||||
|
<input
|
||||||
|
value={dayjs(value.value as number).format('YYYY-MM-DD')}
|
||||||
|
type="date"
|
||||||
|
onChange={e => {
|
||||||
|
onChange({
|
||||||
|
type: 'literal',
|
||||||
|
value: dayjs(e.target.value, 'YYYY-MM-DD'),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { DataHelper, typesystem } from './typesystem';
|
||||||
|
|
||||||
|
export const tNumber = typesystem.defineData(
|
||||||
|
DataHelper.create<{ value: number }>('Number')
|
||||||
|
);
|
||||||
|
export const tString = typesystem.defineData(
|
||||||
|
DataHelper.create<{ value: string }>('String')
|
||||||
|
);
|
||||||
|
export const tBoolean = typesystem.defineData(
|
||||||
|
DataHelper.create<{ value: boolean }>('Boolean')
|
||||||
|
);
|
||||||
|
export const tDate = typesystem.defineData(
|
||||||
|
DataHelper.create<{ value: number }>('Date')
|
||||||
|
);
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { TType } from './typesystem';
|
||||||
|
import { typesystem } from './typesystem';
|
||||||
|
|
||||||
|
type MatcherData<Data, Type extends TType = TType> = { type: Type; data: Data };
|
||||||
|
|
||||||
|
export class Matcher<Data, Type extends TType = TType> {
|
||||||
|
private list: MatcherData<Data, Type>[] = [];
|
||||||
|
|
||||||
|
constructor(private _match?: (type: Type, target: TType) => boolean) {}
|
||||||
|
|
||||||
|
register(type: Type, data: Data) {
|
||||||
|
this.list.push({ type, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
match(type: TType) {
|
||||||
|
const match = this._match ?? typesystem.isSubtype.bind(typesystem);
|
||||||
|
for (const t of this.list) {
|
||||||
|
if (match(t.type, type)) {
|
||||||
|
return t.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allMatched(type: TType): MatcherData<Data>[] {
|
||||||
|
const match = this._match ?? typesystem.isSubtype.bind(typesystem);
|
||||||
|
const result: MatcherData<Data>[] = [];
|
||||||
|
for (const t of this.list) {
|
||||||
|
if (match(t.type, type)) {
|
||||||
|
result.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
allMatchedData(type: TType): Data[] {
|
||||||
|
const match = this._match ?? typesystem.isSubtype.bind(typesystem);
|
||||||
|
const result: Data[] = [];
|
||||||
|
for (const t of this.list) {
|
||||||
|
if (match(t.type, type)) {
|
||||||
|
result.push(t.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
findData(f: (data: Data) => boolean): Data | undefined {
|
||||||
|
return this.list.find(data => f(data.data))?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
find(
|
||||||
|
f: (data: MatcherData<Data, Type>) => boolean
|
||||||
|
): MatcherData<Data, Type> | undefined {
|
||||||
|
return this.list.find(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
all(): MatcherData<Data, Type>[] {
|
||||||
|
return this.list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
/**
|
||||||
|
* This file will be moved to a separate package soon.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TUnion {
|
||||||
|
type: 'union';
|
||||||
|
title: 'union';
|
||||||
|
list: TType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tUnion = (list: TType[]): TUnion => ({
|
||||||
|
type: 'union',
|
||||||
|
title: 'union',
|
||||||
|
list,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO treat as data type
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export interface TArray<Ele extends TType = TType> {
|
||||||
|
type: 'array';
|
||||||
|
ele: Ele;
|
||||||
|
title: 'array';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tArray = <const T extends TType>(ele: T): TArray<T> => {
|
||||||
|
return {
|
||||||
|
type: 'array',
|
||||||
|
title: 'array',
|
||||||
|
ele,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type TTypeVar = {
|
||||||
|
type: 'typeVar';
|
||||||
|
title: 'typeVar';
|
||||||
|
name: string;
|
||||||
|
bound: TType;
|
||||||
|
};
|
||||||
|
export const tTypeVar = (name: string, bound: TType): TTypeVar => {
|
||||||
|
return {
|
||||||
|
type: 'typeVar',
|
||||||
|
title: 'typeVar',
|
||||||
|
name,
|
||||||
|
bound,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type TTypeRef = {
|
||||||
|
type: 'typeRef';
|
||||||
|
title: 'typeRef';
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
export const tTypeRef = (name: string): TTypeRef => {
|
||||||
|
return {
|
||||||
|
type: 'typeRef',
|
||||||
|
title: 'typeRef',
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFunction = {
|
||||||
|
type: 'function';
|
||||||
|
title: 'function';
|
||||||
|
typeVars: TTypeVar[];
|
||||||
|
args: TType[];
|
||||||
|
rt: TType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tFunction = (fn: {
|
||||||
|
typeVars?: TTypeVar[];
|
||||||
|
args: TType[];
|
||||||
|
rt: TType;
|
||||||
|
}): TFunction => {
|
||||||
|
return {
|
||||||
|
type: 'function',
|
||||||
|
title: 'function',
|
||||||
|
typeVars: fn.typeVars ?? [],
|
||||||
|
args: fn.args,
|
||||||
|
rt: fn.rt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TType = TDataType | TArray | TUnion | TTypeRef | TFunction;
|
||||||
|
|
||||||
|
export type DataTypeShape = Record<string, unknown>;
|
||||||
|
export type TDataType<Data extends DataTypeShape = Record<string, unknown>> = {
|
||||||
|
type: 'data';
|
||||||
|
name: string;
|
||||||
|
data?: Data;
|
||||||
|
};
|
||||||
|
export type ValueOfData<T extends DataDefine> = T extends DataDefine<infer R>
|
||||||
|
? R
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export class DataDefine<Data extends DataTypeShape = Record<string, unknown>> {
|
||||||
|
constructor(
|
||||||
|
private config: DataDefineConfig<Data>,
|
||||||
|
private dataMap: Map<string, DataDefine>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(data?: Data): TDataType<Data> {
|
||||||
|
return {
|
||||||
|
type: 'data',
|
||||||
|
name: this.config.name,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
is(data: TType): data is TDataType<Data> {
|
||||||
|
if (data.type !== 'data') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return data.name === this.config.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isByName(name: string): boolean {
|
||||||
|
return name === this.config.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubOf(superType: TDataType): boolean {
|
||||||
|
if (this.is(superType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.config.supers.some(sup => sup.isSubOf(superType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSubOfByName(superType: string): boolean {
|
||||||
|
if (this.isByName(superType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.config.supers.some(sup => sup.isSubOfByName(superType));
|
||||||
|
}
|
||||||
|
|
||||||
|
isSuperOf(subType: TDataType): boolean {
|
||||||
|
const dataDefine = this.dataMap.get(subType.name);
|
||||||
|
if (!dataDefine) {
|
||||||
|
throw new Error('bug');
|
||||||
|
}
|
||||||
|
return dataDefine.isSubOfByName(this.config.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// type DataTypeVar = {};
|
||||||
|
|
||||||
|
// TODO support generic data type
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
interface DataDefineConfig<T extends DataTypeShape> {
|
||||||
|
name: string;
|
||||||
|
supers: DataDefine[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataHelper<T extends DataTypeShape> {
|
||||||
|
create<V = Record<string, unknown>>(name: string): DataDefineConfig<T & V>;
|
||||||
|
|
||||||
|
extends<V extends DataTypeShape>(
|
||||||
|
dataDefine: DataDefine<V>
|
||||||
|
): DataHelper<T & V>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDataHelper = <T extends DataTypeShape = Record<string, unknown>>(
|
||||||
|
...supers: DataDefine[]
|
||||||
|
): DataHelper<T> => {
|
||||||
|
return {
|
||||||
|
create(name: string) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
supers,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
extends(dataDefine) {
|
||||||
|
return createDataHelper(...supers, dataDefine);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const DataHelper = createDataHelper();
|
||||||
|
|
||||||
|
export class Typesystem {
|
||||||
|
dataMap = new Map<string, DataDefine<any>>();
|
||||||
|
|
||||||
|
defineData<T extends DataTypeShape>(
|
||||||
|
config: DataDefineConfig<T>
|
||||||
|
): DataDefine<T> {
|
||||||
|
const result = new DataDefine(config, this.dataMap);
|
||||||
|
this.dataMap.set(config.name, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDataType(t: TType): t is TDataType {
|
||||||
|
return t.type === 'data';
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubtype(
|
||||||
|
superType: TType,
|
||||||
|
sub: TType,
|
||||||
|
context?: Record<string, TType>
|
||||||
|
): boolean {
|
||||||
|
if (superType.type === 'typeRef') {
|
||||||
|
// TODO both are ref
|
||||||
|
if (context && sub.type != 'typeRef') {
|
||||||
|
context[superType.name] = sub;
|
||||||
|
}
|
||||||
|
// TODO bound
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (sub.type === 'typeRef') {
|
||||||
|
// TODO both are ref
|
||||||
|
if (context) {
|
||||||
|
context[sub.name] = superType;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tUnknown.is(superType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (superType.type === 'union') {
|
||||||
|
return superType.list.some(type => this.isSubtype(type, sub, context));
|
||||||
|
}
|
||||||
|
if (sub.type === 'union') {
|
||||||
|
return sub.list.every(type => this.isSubtype(superType, type, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDataType(sub)) {
|
||||||
|
const dataDefine = this.dataMap.get(sub.name);
|
||||||
|
if (!dataDefine) {
|
||||||
|
throw new Error('bug');
|
||||||
|
}
|
||||||
|
if (!this.isDataType(superType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return dataDefine.isSubOf(superType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (superType.type === 'array' || sub.type === 'array') {
|
||||||
|
if (superType.type !== 'array' || sub.type !== 'array') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.isSubtype(superType.ele, sub.ele, context);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
subst(context: Record<string, TType>, template: TFunction): TFunction {
|
||||||
|
const subst = (type: TType): TType => {
|
||||||
|
if (this.isDataType(type)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
switch (type.type) {
|
||||||
|
case 'typeRef':
|
||||||
|
return { ...context[type.name] };
|
||||||
|
case 'union':
|
||||||
|
return tUnion(type.list.map(type => subst(type)));
|
||||||
|
case 'array':
|
||||||
|
return tArray(subst(type.ele));
|
||||||
|
case 'function':
|
||||||
|
throw new Error('TODO');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = tFunction({
|
||||||
|
args: template.args.map(type => subst(type)),
|
||||||
|
rt: subst(template.rt),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance(
|
||||||
|
context: Record<string, TType>,
|
||||||
|
realArgs: TType[],
|
||||||
|
realRt: TType,
|
||||||
|
template: TFunction
|
||||||
|
): TFunction {
|
||||||
|
const ctx = { ...context };
|
||||||
|
console.log(template);
|
||||||
|
template.args.forEach((arg, i) => {
|
||||||
|
const realArg = realArgs[i];
|
||||||
|
if (realArg) {
|
||||||
|
this.isSubtype(arg, realArg, ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.isSubtype(realRt, template.rt);
|
||||||
|
return this.subst(ctx, template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const typesystem = new Typesystem();
|
||||||
|
export const tUnknown = typesystem.defineData(DataHelper.create('Unknown'));
|
||||||
169
packages/component/src/components/page-list/filter/vars.tsx
Normal file
169
packages/component/src/components/page-list/filter/vars.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { MenuItem } from '../../../ui/menu';
|
||||||
|
import { tBoolean, tDate } from './logical/custom-type';
|
||||||
|
import { Matcher } from './logical/matcher';
|
||||||
|
import type { TFunction, TType } from './logical/typesystem';
|
||||||
|
import { tFunction, typesystem } from './logical/typesystem';
|
||||||
|
|
||||||
|
export type Ref = {
|
||||||
|
type: 'ref';
|
||||||
|
name: keyof VariableMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Filter = {
|
||||||
|
type: 'filter';
|
||||||
|
left: Ref;
|
||||||
|
funcName: string;
|
||||||
|
args: Literal[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Literal = {
|
||||||
|
type: 'literal';
|
||||||
|
value: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterVariable = {
|
||||||
|
name: keyof VariableMap;
|
||||||
|
type: TType;
|
||||||
|
};
|
||||||
|
export const variableDefineMap = {
|
||||||
|
Created: {
|
||||||
|
type: tDate.create(),
|
||||||
|
},
|
||||||
|
Updated: {
|
||||||
|
type: tDate.create(),
|
||||||
|
},
|
||||||
|
Favorite: {
|
||||||
|
type: tBoolean.create(),
|
||||||
|
},
|
||||||
|
// Imported: {
|
||||||
|
// type: tBoolean.create(),
|
||||||
|
// },
|
||||||
|
// 'Daily Note': {
|
||||||
|
// type: tBoolean.create(),
|
||||||
|
// },
|
||||||
|
} as const;
|
||||||
|
export type VariableMap = { [K in keyof typeof variableDefineMap]: unknown };
|
||||||
|
export const vars: FilterVariable[] = Object.entries(variableDefineMap).map(
|
||||||
|
([key, value]) => ({
|
||||||
|
name: key as keyof VariableMap,
|
||||||
|
type: value.type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const createDefaultFilter = (variable: FilterVariable): Filter => {
|
||||||
|
const data = filterMatcher.match(variable.type);
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('No matching function found');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'filter',
|
||||||
|
left: { type: 'ref', name: variable.name },
|
||||||
|
funcName: data.name,
|
||||||
|
args: data.defaultArgs().map(value => ({ type: 'literal', value })),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateFilterMenu = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: Filter[];
|
||||||
|
onChange: (value: Filter[]) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ChangeFilterMenu
|
||||||
|
selected={value}
|
||||||
|
onSelect={filter => {
|
||||||
|
onChange([...value, filter]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChangeFilterMenu = ({
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
selected: Filter[];
|
||||||
|
onSelect: (value: Filter) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{vars
|
||||||
|
// .filter(v => !selected.find(filter => filter.left.name === v.name))
|
||||||
|
.map(v => (
|
||||||
|
<MenuItem
|
||||||
|
key={v.name}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(createDefaultFilter(v));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{v.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterMatcher = new Matcher<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
defaultArgs: () => unknown[];
|
||||||
|
render?: (props: { ast: Filter }) => ReactNode;
|
||||||
|
impl: (...args: unknown[]) => boolean;
|
||||||
|
},
|
||||||
|
TFunction
|
||||||
|
>((type, target) => {
|
||||||
|
const staticType = typesystem.subst(
|
||||||
|
Object.fromEntries(type.typeVars?.map(v => [v.name, v.bound]) ?? []),
|
||||||
|
type
|
||||||
|
);
|
||||||
|
const firstArg = staticType.args[0];
|
||||||
|
return firstArg && typesystem.isSubtype(firstArg, target);
|
||||||
|
});
|
||||||
|
|
||||||
|
filterMatcher.register(
|
||||||
|
tFunction({
|
||||||
|
args: [tBoolean.create(), tBoolean.create()],
|
||||||
|
rt: tBoolean.create(),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'is',
|
||||||
|
defaultArgs: () => [true],
|
||||||
|
impl: (value, target) => {
|
||||||
|
return value == target;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
filterMatcher.register(
|
||||||
|
tFunction({ args: [tDate.create(), tDate.create()], rt: tBoolean.create() }),
|
||||||
|
{
|
||||||
|
name: 'after',
|
||||||
|
defaultArgs: () => {
|
||||||
|
return [dayjs().subtract(1, 'day').endOf('day')];
|
||||||
|
},
|
||||||
|
impl: (date, target) => {
|
||||||
|
if (typeof date !== 'number' || !dayjs.isDayjs(target)) {
|
||||||
|
throw new Error('argument type error');
|
||||||
|
}
|
||||||
|
return dayjs(date).isAfter(target.endOf('day'));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
filterMatcher.register(
|
||||||
|
tFunction({ args: [tDate.create(), tDate.create()], rt: tBoolean.create() }),
|
||||||
|
{
|
||||||
|
name: 'before',
|
||||||
|
defaultArgs: () => [dayjs().endOf('day')],
|
||||||
|
impl: (date, target) => {
|
||||||
|
if (typeof date !== 'number' || !dayjs.isDayjs(target)) {
|
||||||
|
throw new Error('argument type error');
|
||||||
|
}
|
||||||
|
return dayjs(date).isBefore(target.startOf('day'));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
export * from './all-page';
|
export * from './all-page';
|
||||||
|
export * from './filter';
|
||||||
export * from './operation-cell';
|
export * from './operation-cell';
|
||||||
export * from './operation-menu-items';
|
export * from './operation-menu-items';
|
||||||
export * from './styles';
|
export * from './styles';
|
||||||
export * from './type';
|
export * from './type';
|
||||||
|
export * from './use-all-page-setting';
|
||||||
|
export * from './view';
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { evalFilterList } from './filter';
|
||||||
|
import type { Filter, VariableMap } from './filter/vars';
|
||||||
|
|
||||||
|
export type View = {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
return {
|
||||||
|
currentView,
|
||||||
|
currentViewIndex: setting.currentView,
|
||||||
|
viewList: setting.savedViews,
|
||||||
|
selectView,
|
||||||
|
createView,
|
||||||
|
changeView,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const filterByView = (view: View, varMap: VariableMap) =>
|
||||||
|
evalFilterList(view.filterList, varMap);
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Button, Input, Modal, ModalWrapper } from '@affine/component';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { FilterList } from '../filter';
|
||||||
|
import type { Filter } from '../filter/vars';
|
||||||
|
import type { View } from '../use-all-page-setting';
|
||||||
|
|
||||||
|
type CreateViewProps = {
|
||||||
|
init: Filter[];
|
||||||
|
onConfirm: (view: View) => void;
|
||||||
|
};
|
||||||
|
const CreateView = ({
|
||||||
|
init,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: CreateViewProps & { onCancel: () => void }) => {
|
||||||
|
const [value, onChange] = useState<View>({ name: '', filterList: init });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Save As New View</h1>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 20,
|
||||||
|
marginTop: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FilterList
|
||||||
|
value={value.filterList}
|
||||||
|
onChange={list => onChange({ ...value, filterList: list })}
|
||||||
|
></FilterList>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<Input
|
||||||
|
placeholder="Untitled View"
|
||||||
|
value={value.name}
|
||||||
|
onChange={text => onChange({ ...value, name: text })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 20 }}
|
||||||
|
>
|
||||||
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
style={{ marginLeft: 20 }}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (value.name.trim().length > 0) {
|
||||||
|
onConfirm(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const SaveViewButton = ({ init, onConfirm }: CreateViewProps) => {
|
||||||
|
const [show, changeShow] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => changeShow(true)}>Save view</Button>
|
||||||
|
<Modal open={show} onClose={() => changeShow(false)}>
|
||||||
|
<ModalWrapper width={560} style={{ padding: '40px' }}>
|
||||||
|
<CreateView
|
||||||
|
init={init}
|
||||||
|
onCancel={() => changeShow(false)}
|
||||||
|
onConfirm={view => {
|
||||||
|
onConfirm(view);
|
||||||
|
changeShow(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalWrapper>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './create-view';
|
||||||
|
export * from './view-list';
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { MenuItem, styled } from '@affine/component';
|
||||||
|
|
||||||
|
import Menu from '../../../ui/menu/menu';
|
||||||
|
import { CreateFilterMenu } from '../filter/vars';
|
||||||
|
import type { useAllPageSetting } from '../use-all-page-setting';
|
||||||
|
|
||||||
|
const NoDragDiv = styled('div')`
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
`;
|
||||||
|
export const ViewList = ({
|
||||||
|
setting,
|
||||||
|
}: {
|
||||||
|
setting: ReturnType<typeof useAllPageSetting>;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}>
|
||||||
|
{setting.currentViewIndex != null && (
|
||||||
|
<Menu
|
||||||
|
trigger="click"
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
{setting.viewList.map((v, i) => {
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={() => setting.selectView(i)} key={i}>
|
||||||
|
{v.name}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NoDragDiv style={{ marginRight: 12, cursor: 'pointer' }}>
|
||||||
|
{setting.currentView.name}
|
||||||
|
</NoDragDiv>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
<Menu
|
||||||
|
trigger="click"
|
||||||
|
content={
|
||||||
|
<CreateFilterMenu
|
||||||
|
value={setting.currentView.filterList}
|
||||||
|
onChange={filterList => {
|
||||||
|
setting.changeView(
|
||||||
|
{ ...setting.currentView, filterList },
|
||||||
|
setting.currentViewIndex
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NoDragDiv style={{ cursor: 'pointer' }}>Filter</NoDragDiv>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
/// <reference path='../../../apps/electron/layers/preload/preload.d.ts' />
|
/// <reference path='../../../apps/electron/layers/preload/preload.d.ts' />
|
||||||
|
import type { View } from '@affine/component/page-list';
|
||||||
import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api';
|
import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api';
|
||||||
import type { EditorContainer } from '@blocksuite/editor';
|
import type { EditorContainer } from '@blocksuite/editor';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
@@ -191,6 +192,7 @@ type PageDetailProps<Flavour extends keyof WorkspaceRegistry> =
|
|||||||
type PageListProps<_Flavour extends keyof WorkspaceRegistry> = {
|
type PageListProps<_Flavour extends keyof WorkspaceRegistry> = {
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||||
|
view: View;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ __metadata:
|
|||||||
"@vitejs/plugin-react": ^4.0.0
|
"@vitejs/plugin-react": ^4.0.0
|
||||||
clsx: ^1.2.1
|
clsx: ^1.2.1
|
||||||
concurrently: ^8.0.1
|
concurrently: ^8.0.1
|
||||||
|
dayjs: ^1.11.7
|
||||||
jest-mock: ^29.5.0
|
jest-mock: ^29.5.0
|
||||||
jotai: ^2.1.0
|
jotai: ^2.1.0
|
||||||
lit: ^2.7.4
|
lit: ^2.7.4
|
||||||
@@ -13232,6 +13233,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dayjs@npm:^1.11.7":
|
||||||
|
version: 1.11.7
|
||||||
|
resolution: "dayjs@npm:1.11.7"
|
||||||
|
checksum: 5003a7c1dd9ed51385beb658231c3548700b82d3548c0cfbe549d85f2d08e90e972510282b7506941452c58d32136d6362f009c77ca55381a09c704e9f177ebb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"debounce@npm:^1.2.0":
|
"debounce@npm:^1.2.0":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "debounce@npm:1.2.1"
|
resolution: "debounce@npm:1.2.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user