feat: add tags support (#2988)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
3720
2023-07-04 15:32:11 +08:00
committed by Alex Yang
parent 65c6479ee4
commit 34ad5cdaef
34 changed files with 706 additions and 78 deletions

View File

@@ -6,6 +6,7 @@ import 'fake-indexeddb/auto';
import type {
Filter,
LiteralValue,
PropertiesMeta,
Ref,
VariableMap,
} from '@affine/env/filter';
@@ -21,7 +22,7 @@ import { tBoolean, tDate } from '../filter/logical/custom-type';
import { toLiteral } from '../filter/shared-types';
import type { FilterMatcherDataType } from '../filter/vars';
import { filterMatcher } from '../filter/vars';
import { filterByFilterList } from '../use-all-page-setting';
import { filterByFilterList } from '../use-collection-manager';
const ref = (name: keyof VariableMap): Ref => {
return {
type: 'ref',
@@ -33,9 +34,18 @@ const mockVariableMap = (vars: Partial<VariableMap>): VariableMap => {
Created: 0,
Updated: 0,
'Is Favourited': false,
Tags: [],
...vars,
};
};
const mockPropertiesMeta = (meta: Partial<PropertiesMeta>): PropertiesMeta => {
return {
tags: {
options: [],
},
...meta,
};
};
const filter = (
matcherData: FilterMatcherDataType,
left: Ref,
@@ -127,7 +137,11 @@ describe('render filter', () => {
return (
<I18nextProvider i18n={i18n}>
<Condition value={value} onChange={onChange} />
<Condition
propertiesMeta={mockPropertiesMeta({})}
value={value}
onChange={onChange}
/>
</I18nextProvider>
);
};
@@ -143,7 +157,13 @@ describe('render filter', () => {
const [value, onChange] = useState(
filter(fn, ref('Created'), [new Date(2023, 5, 29).getTime()])
);
return <Condition value={value} onChange={onChange} />;
return (
<Condition
propertiesMeta={mockPropertiesMeta({})}
value={value}
onChange={onChange}
/>
);
};
test('date condition function change', async () => {

View File

@@ -7,20 +7,24 @@ import { renderHook } from '@testing-library/react';
import { expect, test } from 'vitest';
import { createDefaultFilter, vars } from '../filter/vars';
import { useAllPageSetting } from '../use-all-page-setting';
import { useCollectionManager } from '../use-collection-manager';
const defaultMeta = { tags: { options: [] } };
test('useAllPageSetting', async () => {
const settingHook = renderHook(() => useAllPageSetting());
const settingHook = renderHook(() => useCollectionManager());
const prevCollection = settingHook.result.current.currentCollection;
expect(settingHook.result.current.savedCollections).toEqual([]);
await settingHook.result.current.updateCollection({
...settingHook.result.current.currentCollection,
filterList: [createDefaultFilter(vars[0])],
filterList: [createDefaultFilter(vars[0], defaultMeta)],
});
settingHook.rerender();
const nextCollection = settingHook.result.current.currentCollection;
expect(nextCollection).not.toBe(prevCollection);
expect(nextCollection.filterList).toEqual([createDefaultFilter(vars[0])]);
expect(nextCollection.filterList).toEqual([
createDefaultFilter(vars[0], defaultMeta),
]);
settingHook.result.current.backToAll();
await settingHook.result.current.saveCollection({
...settingHook.result.current.currentCollection,

View File

@@ -1,5 +1,6 @@
import { CollectionBar } from '@affine/component/page-list';
import { DEFAULT_SORT_KEY } from '@affine/env/constant';
import type { PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
@@ -34,6 +35,7 @@ const AllPagesHead = ({
createNewEdgeless,
importFile,
getPageInfo,
propertiesMeta,
}: {
isPublicWorkspace: boolean;
sorter: ReturnType<typeof useSorter<ListData>>;
@@ -41,6 +43,7 @@ const AllPagesHead = ({
createNewEdgeless: () => void;
importFile: () => void;
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
}) => {
const t = useAFFiNEI18N();
const titleList = [
@@ -49,17 +52,21 @@ const AllPagesHead = ({
content: t['Title'](),
proportion: 0.5,
},
{
key: 'tags',
content: t['Tags'](),
proportion: 0.2,
},
{
key: 'createDate',
content: t['Created'](),
proportion: 0.2,
proportion: 0.1,
},
{
key: 'updatedDate',
content: t['Updated'](),
proportion: 0.2,
proportion: 0.1,
},
{
key: 'unsortable_action',
content: (
@@ -110,7 +117,11 @@ const AllPagesHead = ({
</TableCell>
))}
</TableHeadRow>
<CollectionBar getPageInfo={getPageInfo} />
<CollectionBar
columnsCount={titleList.length}
getPageInfo={getPageInfo}
propertiesMeta={propertiesMeta}
/>
</TableHead>
);
};
@@ -123,6 +134,7 @@ export const PageList = ({
onImportFile,
fallback,
getPageInfo,
propertiesMeta,
}: PageListProps) => {
const sorter = useSorter<ListData>({
data: list,
@@ -160,6 +172,7 @@ export const PageList = ({
<StyledTableContainer ref={ref}>
<Table showBorder={hasScrollTop} style={{ maxHeight: '100%' }}>
<AllPagesHead
propertiesMeta={propertiesMeta}
isPublicWorkspace={isPublicWorkspace}
sorter={sorter}
createNewPage={onCreateNewPage}

View File

@@ -6,6 +6,7 @@ import { Fragment } from 'react';
import { styled } from '../../styles';
import { TableBody, TableCell } from '../../ui/table';
import { FavoriteTag } from './components/favorite-tag';
import { Tags } from './components/tags';
import { TitleCell } from './components/title-cell';
import { OperationCell } from './operation-cell';
import { StyledTableBodyRow } from './styles';
@@ -51,6 +52,7 @@ export const AllPagesBody = ({
pageId,
title,
preview,
tags,
icon,
isPublicPage,
favorite,
@@ -86,6 +88,14 @@ export const AllPagesBody = ({
data-testid="title"
onClick={onClickPage}
/>
<TableCell
data-testid="tags"
hidden={isSmallDevices}
onClick={onClickPage}
style={{ fontSize: 'var(--affine-font-xs)' }}
>
<Tags value={tags}></Tags>
</TableCell>
<TableCell
data-testid="created-date"
ellipsis={true}

View File

@@ -0,0 +1,30 @@
import { style } from '@vanilla-extract/css';
export const tagList = style({
display: 'flex',
flexWrap: 'nowrap',
gap: 10,
overflow: 'hidden',
});
export const tagListFull = style({
display: 'flex',
flexWrap: 'wrap',
gap: 10,
maxWidth: 300,
padding: 10,
overflow: 'hidden',
});
export const tag = style({
flexShrink: 0,
padding: '2px 10px',
borderRadius: 6,
fontSize: 12,
lineHeight: '16px',
fontWeight: 400,
maxWidth: '100%',
color: 'var(--affine-text-primary-color)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});

View File

@@ -0,0 +1,26 @@
import type { Tag } from '@affine/env/filter';
import Menu from '../../../ui/menu/menu';
import * as styles from './tags.css';
export const Tags = ({ value }: { value: Tag[] }) => {
const list = value.map(tag => {
return (
<div
key={tag.id}
className={styles.tag}
style={{ backgroundColor: tag.color }}
>
{tag.value}
</div>
);
});
return (
<Menu
pointerEnterDelay={500}
content={<div className={styles.tagListFull}>{list}</div>}
>
<div className={styles.tagList}>{list}</div>
</Menu>
);
};

View File

@@ -1,4 +1,5 @@
import type { Filter, Literal } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter';
import type { ReactNode } from 'react';
import { useMemo } from 'react';
@@ -6,26 +7,42 @@ import { Menu, MenuItem } from '../../../ui/menu';
import { FilterTag } from './filter-tag-translation';
import * as styles from './index.css';
import { literalMatcher } from './literal-matcher';
import { tBoolean } from './logical/custom-type';
import type { TFunction, TType } from './logical/typesystem';
import { typesystem } from './logical/typesystem';
import { variableDefineMap } from './shared-types';
import { filterMatcher, VariableSelect, vars } from './vars';
export const Condition = ({
value,
onChange,
propertiesMeta,
}: {
value: Filter;
onChange: (filter: Filter) => void;
propertiesMeta: PropertiesMeta;
}) => {
const data = useMemo(
() => filterMatcher.find(v => v.data.name === value.funcName),
[value.funcName]
);
const data = useMemo(() => {
const data = filterMatcher.find(v => v.data.name === value.funcName);
if (!data) {
return;
}
const instance = typesystem.instance(
{},
[variableDefineMap[value.left.name].type(propertiesMeta)],
tBoolean.create(),
data.type
);
return {
render: data.data.render,
type: instance,
};
}, [propertiesMeta, value.funcName, value.left.name]);
if (!data) {
return null;
}
const render =
data.data.render ??
data.render ??
(({ ast }) => {
const args = renderArgs(value, onChange, data.type);
return (
@@ -34,7 +51,13 @@ export const Condition = ({
>
<Menu
trigger="click"
content={<VariableSelect selected={[]} onSelect={onChange} />}
content={
<VariableSelect
propertiesMeta={propertiesMeta}
selected={[]}
onSelect={onChange}
/>
}
>
<div data-testid="variable-name" className={styles.filterTypeStyle}>
<div className={styles.filterTypeIconStyle}>
@@ -47,7 +70,13 @@ export const Condition = ({
</Menu>
<Menu
trigger="click"
content={<FunctionSelect value={value} onChange={onChange} />}
content={
<FunctionSelect
propertiesMeta={propertiesMeta}
value={value}
onChange={onChange}
/>
}
>
<div className={styles.switchStyle} data-testid="filter-name">
<FilterTag name={ast.funcName} />
@@ -63,17 +92,19 @@ export const Condition = ({
const FunctionSelect = ({
value,
onChange,
propertiesMeta,
}: {
value: Filter;
onChange: (value: Filter) => void;
propertiesMeta: PropertiesMeta;
}) => {
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 filterMatcher.allMatchedData(type(propertiesMeta));
}, [propertiesMeta, value.left.name]);
return (
<div data-testid="filter-name-select">
{list.map(v => (
@@ -109,7 +140,11 @@ export const Arg = ({
}
return (
<div data-testid="filter-arg" style={{ marginLeft: 4, fontWeight: 600 }}>
{data.render({ type, value, onChange })}
{data.render({
type,
value: value?.value,
onChange: v => onChange({ type: 'literal', value: v }),
})}
</div>
);
};
@@ -119,15 +154,17 @@ export const renderArgs = (
type: TFunction
): ReactNode => {
const rest = type.args.slice(1);
return rest.map((type, i) => {
return rest.map((argType, i) => {
const value = filter.args[i];
return (
<Arg
key={i}
type={type}
type={argType}
value={value}
onChange={value => {
const args = filter.args.map((v, index) => (i === index ? value : v));
const args = type.args.map((_, index) =>
i === index ? value : filter.args[index]
);
onChange({
...filter,
args,

View File

@@ -5,8 +5,8 @@ import { filterMatcher } from './vars';
const evalRef = (ref: Ref, variableMap: VariableMap) => {
return variableMap[ref.name];
};
const evalLiteral = (lit: Literal) => {
return lit.value;
const evalLiteral = (lit?: Literal) => {
return lit?.value;
};
const evalFilter = (filter: Filter, variableMap: VariableMap): boolean => {
const impl = filterMatcher.findData(v => v.name === filter.funcName)?.impl;

View File

@@ -1,4 +1,5 @@
import type { Filter } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter';
import { CloseIcon, PlusIcon } from '@blocksuite/icons';
import { Menu } from '../../..';
@@ -9,9 +10,11 @@ import { CreateFilterMenu } from './vars';
export const FilterList = ({
value,
onChange,
propertiesMeta,
}: {
value: Filter[];
onChange: (value: Filter[]) => void;
propertiesMeta: PropertiesMeta;
}) => {
return (
<div
@@ -25,6 +28,7 @@ export const FilterList = ({
return (
<div className={styles.filterItemStyle} key={i}>
<Condition
propertiesMeta={propertiesMeta}
value={filter}
onChange={filter => {
onChange(
@@ -45,7 +49,13 @@ export const FilterList = ({
})}
<Menu
trigger={'click'}
content={<CreateFilterMenu value={value} onChange={onChange} />}
content={
<CreateFilterMenu
value={value}
onChange={onChange}
propertiesMeta={propertiesMeta}
/>
}
>
<div
style={{

View File

@@ -1,2 +1,3 @@
export * from './eval';
export * from './filter-list';
export * from './utils';

View File

@@ -1,20 +1,21 @@
import type { Literal } from '@affine/env/filter';
import type { LiteralValue, Tag } from '@affine/env/filter';
import dayjs from 'dayjs';
import type { ReactNode } from 'react';
import { AFFiNEDatePicker } from '../../date-picker';
import { FilterTag } from './filter-tag-translation';
import { inputStyle } from './index.css';
import { tBoolean, tDate } from './logical/custom-type';
import { tBoolean, tDate, tTag } from './logical/custom-type';
import { Matcher } from './logical/matcher';
import type { TType } from './logical/typesystem';
import { typesystem } from './logical/typesystem';
import { tArray, typesystem } from './logical/typesystem';
import { MultiSelect } from './multi-select';
export const literalMatcher = new Matcher<{
render: (props: {
type: TType;
value: Literal;
onChange: (lit: Literal) => void;
value: LiteralValue;
onChange: (lit: LiteralValue) => void;
}) => ReactNode;
}>((type, target) => {
return typesystem.isSubtype(type, target);
@@ -26,23 +27,44 @@ literalMatcher.register(tBoolean.create(), {
className={inputStyle}
style={{ cursor: 'pointer' }}
onClick={() => {
onChange({ type: 'literal', value: !value.value });
onChange(!value);
}}
>
<FilterTag name={value.value?.toString()} />
<FilterTag name={value?.toString()} />
</div>
),
});
literalMatcher.register(tDate.create(), {
render: ({ value, onChange }) => (
<AFFiNEDatePicker
value={dayjs(value.value as number).format('YYYY-MM-DD')}
value={dayjs(value as number).format('YYYY-MM-DD')}
onChange={e => {
onChange({
type: 'literal',
value: dayjs(e, 'YYYY-MM-DD').valueOf(),
});
onChange(dayjs(e, 'YYYY-MM-DD').valueOf());
}}
/>
),
});
const getTagsOfArrayTag = (type: TType): Tag[] => {
if (type.type === 'array') {
if (tTag.is(type.ele)) {
return type.ele.data?.tags ?? [];
}
return [];
} else {
return [];
}
};
literalMatcher.register(tArray(tTag.create()), {
render: ({ type, value, onChange }) => {
return (
<MultiSelect
value={(value ?? []) as string[]}
onChange={value => onChange(value)}
options={getTagsOfArrayTag(type).map(v => ({
label: v.value,
value: v.id,
}))}
></MultiSelect>
);
},
});

View File

@@ -1,3 +1,5 @@
import type { Tag } from '@affine/env/filter';
import { DataHelper, typesystem } from './typesystem';
export const tNumber = typesystem.defineData(
@@ -12,3 +14,8 @@ export const tBoolean = typesystem.defineData(
export const tDate = typesystem.defineData(
DataHelper.create<{ value: number }>('Date')
);
export const tTag = typesystem.defineData<{ tags: Tag[] }>({
name: 'Tag',
supers: [],
});

View File

@@ -0,0 +1,50 @@
import { style } from '@vanilla-extract/css';
export const content = style({
fontSize: 12,
color: 'var(--affine-text-primary-color)',
borderRadius: 8,
padding: '3px 4px',
cursor: 'pointer',
':hover': {
backgroundColor: 'var(--affine-hover-color)',
},
});
export const text = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: 350,
});
export const optionList = style({
display: 'flex',
flexDirection: 'column',
gap: 4,
padding: '0 4px',
});
export const selectOption = style({
display: 'flex',
alignItems: 'center',
fontSize: 14,
height: 26,
borderRadius: 5,
maxWidth: 240,
minWidth: 100,
padding: '0 12px',
cursor: 'pointer',
':hover': {
backgroundColor: 'var(--affine-hover-color)',
},
});
export const optionLabel = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
});
export const done = style({
display: 'flex',
alignItems: 'center',
color: 'var(--affine-primary-color)',
marginLeft: 8,
});

View File

@@ -0,0 +1,77 @@
import { DoneIcon } from '@blocksuite/icons';
import type { MouseEvent } from 'react';
import { useMemo } from 'react';
import Menu from '../../../ui/menu/menu';
import * as styles from './multi-select.css';
export const MultiSelect = ({
value,
onChange,
options,
}: {
value: string[];
onChange: (value: string[]) => void;
options: {
label: string;
value: string;
}[];
}) => {
const optionMap = useMemo(
() => Object.fromEntries(options.map(v => [v.value, v])),
[options]
);
return (
<Menu
trigger="click"
content={
<div data-testid="multi-select" className={styles.optionList}>
{options.map(option => {
const selected = value.includes(option.value);
const click = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
if (selected) {
onChange(value.filter(v => v !== option.value));
} else {
onChange([...value, option.value]);
}
};
return (
<div
className={styles.selectOption}
data-testid="select-option"
style={{
backgroundColor: selected
? 'var(--affine-hover-color)'
: undefined,
}}
onClick={click}
key={option.value}
>
<div className={styles.optionLabel}>{option.label}</div>
<div
style={{ opacity: selected ? 1 : 0 }}
className={styles.done}
>
<DoneIcon />
</div>
</div>
);
})}
</div>
}
>
<div className={styles.content}>
{value.length ? (
<div className={styles.text}>
{value.map(id => optionMap[id]?.label).join(', ')}
</div>
) : (
<div style={{ color: 'var(--affine-text-secondary-color)' }}>
Empty
</div>
)}
</div>
</Menu>
);
};

View File

@@ -1,8 +1,19 @@
import type { Literal, LiteralValue, VariableMap } from '@affine/env/filter';
import { DateTimeIcon, FavoritedIcon } from '@blocksuite/icons';
import type {
Literal,
LiteralValue,
PropertiesMeta,
VariableMap,
} from '@affine/env/filter';
import {
DateTimeIcon,
FavoritedIcon,
MultiSelectIcon,
} from '@blocksuite/icons';
import type { ReactElement } from 'react';
import { tBoolean, tDate } from './logical/custom-type';
import { tBoolean, tDate, tTag } from './logical/custom-type';
import type { TType } from './logical/typesystem';
import { tArray } from './logical/typesystem';
export const toLiteral = (value: LiteralValue): Literal => ({
type: 'literal',
@@ -11,29 +22,34 @@ export const toLiteral = (value: LiteralValue): Literal => ({
export type FilterVariable = {
name: keyof VariableMap;
type: TType;
type: (propertiesMeta: PropertiesMeta) => TType;
icon: ReactElement;
};
export const variableDefineMap = {
Created: {
type: tDate.create(),
type: () => tDate.create(),
icon: <DateTimeIcon />,
},
Updated: {
type: tDate.create(),
type: () => tDate.create(),
icon: <DateTimeIcon />,
},
'Is Favourited': {
type: tBoolean.create(),
type: () => tBoolean.create(),
icon: <FavoritedIcon />,
},
Tags: {
type: meta => tArray(tTag.create({ tags: meta.tags.options })),
icon: <MultiSelectIcon />,
},
// Imported: {
// type: tBoolean.create(),
// },
// 'Daily Note': {
// type: tBoolean.create(),
// },
} as const;
} satisfies Record<string, Omit<FilterVariable, 'name'>>;
export type InternalVariableMap = {
[K in keyof typeof variableDefineMap]: LiteralValue;

View File

@@ -0,0 +1,10 @@
import type { Filter } from '@affine/env/filter';
export const createTagFilter = (id: string): Filter => {
return {
type: 'filter',
left: { type: 'ref', name: 'Tags' },
funcName: 'contains all',
args: [{ type: 'literal', value: [id] }],
};
};

View File

@@ -1,4 +1,9 @@
import type { Filter, LiteralValue, VariableMap } from '@affine/env/filter';
import type {
Filter,
LiteralValue,
PropertiesMeta,
VariableMap,
} from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import dayjs from 'dayjs';
import type { ReactNode } from 'react';
@@ -6,10 +11,16 @@ import type { ReactNode } from 'react';
import { MenuItem } from '../../../ui/menu';
import { FilterTag } from './filter-tag-translation';
import * as styles from './index.css';
import { tBoolean, tDate } from './logical/custom-type';
import { tBoolean, tDate, tTag } from './logical/custom-type';
import { Matcher } from './logical/matcher';
import type { TFunction } from './logical/typesystem';
import { tFunction, typesystem } from './logical/typesystem';
import {
tArray,
tFunction,
tTypeRef,
tTypeVar,
typesystem,
} from './logical/typesystem';
import type { FilterVariable } from './shared-types';
import { variableDefineMap } from './shared-types';
@@ -21,8 +32,11 @@ export const vars: FilterVariable[] = Object.entries(variableDefineMap).map(
})
);
export const createDefaultFilter = (variable: FilterVariable): Filter => {
const data = filterMatcher.match(variable.type);
export const createDefaultFilter = (
variable: FilterVariable,
propertiesMeta: PropertiesMeta
): Filter => {
const data = filterMatcher.match(variable.type(propertiesMeta));
if (!data) {
throw new Error('No matching function found');
}
@@ -37,12 +51,15 @@ export const createDefaultFilter = (variable: FilterVariable): Filter => {
export const CreateFilterMenu = ({
value,
onChange,
propertiesMeta,
}: {
value: Filter[];
onChange: (value: Filter[]) => void;
propertiesMeta: PropertiesMeta;
}) => {
return (
<VariableSelect
propertiesMeta={propertiesMeta}
selected={value}
onSelect={filter => {
onChange([...value, filter]);
@@ -52,9 +69,11 @@ export const CreateFilterMenu = ({
};
export const VariableSelect = ({
onSelect,
propertiesMeta,
}: {
selected: Filter[];
onSelect: (value: Filter) => void;
propertiesMeta: PropertiesMeta;
}) => {
const t = useAFFiNEI18N();
return (
@@ -70,7 +89,7 @@ export const VariableSelect = ({
icon={variableDefineMap[v.name].icon}
key={v.name}
onClick={() => {
onSelect(createDefaultFilter(v));
onSelect(createDefaultFilter(v, propertiesMeta));
}}
className={styles.menuItemStyle}
>
@@ -90,7 +109,7 @@ export type FilterMatcherDataType = {
name: string;
defaultArgs: () => LiteralValue[];
render?: (props: { ast: Filter }) => ReactNode;
impl: (...args: LiteralValue[]) => boolean;
impl: (...args: (LiteralValue | undefined)[]) => boolean;
};
export const filterMatcher = new Matcher<FilterMatcherDataType, TFunction>(
(type, target) => {
@@ -146,3 +165,103 @@ filterMatcher.register(
},
}
);
filterMatcher.register(
tFunction({ args: [tArray(tTag.create())], rt: tBoolean.create() }),
{
name: 'is not empty',
defaultArgs: () => [],
impl: tags => {
if (Array.isArray(tags)) {
return tags.length > 0;
}
return true;
},
}
);
filterMatcher.register(
tFunction({ args: [tArray(tTag.create())], rt: tBoolean.create() }),
{
name: 'is empty',
defaultArgs: () => [],
impl: tags => {
if (Array.isArray(tags)) {
return tags.length == 0;
}
return true;
},
}
);
filterMatcher.register(
tFunction({
typeVars: [tTypeVar('T', tTag.create())],
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
rt: tBoolean.create(),
}),
{
name: 'contains all',
defaultArgs: () => [],
impl: (tags, target) => {
if (Array.isArray(tags) && Array.isArray(target)) {
return target.every(id => tags.includes(id));
}
return true;
},
}
);
filterMatcher.register(
tFunction({
typeVars: [tTypeVar('T', tTag.create())],
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
rt: tBoolean.create(),
}),
{
name: 'contains one of',
defaultArgs: () => [],
impl: (tags, target) => {
if (Array.isArray(tags) && Array.isArray(target)) {
return target.some(id => tags.includes(id));
}
return true;
},
}
);
filterMatcher.register(
tFunction({
typeVars: [tTypeVar('T', tTag.create())],
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
rt: tBoolean.create(),
}),
{
name: 'does not contains all',
defaultArgs: () => [],
impl: (tags, target) => {
if (Array.isArray(tags) && Array.isArray(target)) {
return !target.every(id => tags.includes(id));
}
return true;
},
}
);
filterMatcher.register(
tFunction({
typeVars: [tTypeVar('T', tTag.create())],
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
rt: tBoolean.create(),
}),
{
name: 'does not contains one of',
defaultArgs: () => [],
impl: (tags, target) => {
if (Array.isArray(tags) && Array.isArray(target)) {
return !target.some(id => tags.includes(id));
}
return true;
},
}
);

View File

@@ -7,5 +7,5 @@ export * from './operation-cell';
export * from './operation-menu-items';
export * from './styles';
export * from './type';
export * from './use-all-page-setting';
export * from './use-collection-manager';
export * from './view';

View File

@@ -1,3 +1,5 @@
import type { Tag } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
/**
@@ -14,6 +16,7 @@ export type ListData = {
icon: JSX.Element;
title: string;
preview?: string;
tags: Tag[];
favorite: boolean;
createDate: Date;
updatedDate: Date;
@@ -48,6 +51,7 @@ export type PageListProps = {
onCreateNewEdgeless: () => void;
onImportFile: () => void;
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
};
export type DraggableTitleCellData = {

View File

@@ -31,16 +31,17 @@ const pageCollectionDBPromise: Promise<IDBPDatabase<PageCollectionDBV1>> =
},
});
const defaultCollection = {
id: NIL,
name: 'All',
filterList: [],
};
const collectionAtom = atomWithReset<{
currentId: string;
defaultCollection: Collection;
}>({
currentId: NIL,
defaultCollection: {
id: NIL,
name: 'All',
filterList: [],
},
defaultCollection: defaultCollection,
});
export const useSavedCollections = () => {
@@ -102,7 +103,7 @@ export const useSavedCollections = () => {
};
};
export const useAllPageSetting = () => {
export const useCollectionManager = () => {
const { savedCollections, saveCollection, deleteCollection, addPage } =
useSavedCollections();
const [collectionData, setCollectionData] = useAtom(collectionAtom);
@@ -132,6 +133,18 @@ export const useAllPageSetting = () => {
const backToAll = useCallback(() => {
setCollectionData(RESET);
}, [setCollectionData]);
const setTemporaryFilter = useCallback(
(filterList: Filter[]) => {
setCollectionData({
currentId: NIL,
defaultCollection: {
...defaultCollection,
filterList: filterList,
},
});
},
[setCollectionData]
);
const currentCollection =
collectionData.currentId === NIL
? collectionData.defaultCollection
@@ -149,6 +162,7 @@ export const useAllPageSetting = () => {
backToAll,
deleteCollection,
addPage,
setTemporaryFilter,
};
};
export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>

View File

@@ -1,4 +1,5 @@
import { EditCollectionModel } from '@affine/component/page-list';
import type { PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import {
DeleteIcon,
@@ -13,15 +14,19 @@ import type { ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { Button } from '../../../ui/button/button';
import { useAllPageSetting } from '../use-all-page-setting';
import { useCollectionManager } from '../use-collection-manager';
import * as styles from './collection-bar.css';
export const CollectionBar = ({
getPageInfo,
propertiesMeta,
columnsCount,
}: {
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
columnsCount: number;
}) => {
const setting = useAllPageSetting();
const setting = useCollectionManager();
const collection = setting.currentCollection;
const [open, setOpen] = useState(false);
const actions: {
@@ -80,6 +85,7 @@ export const CollectionBar = ({
<td>
<div className={styles.view}>
<EditCollectionModel
propertiesMeta={propertiesMeta}
getPageInfo={getPageInfo}
init={collection}
open={open}
@@ -109,8 +115,9 @@ export const CollectionBar = ({
})}
</div>
</td>
<td></td>
<td></td>
{Array.from({ length: columnsCount - 2 }).map((_, i) => (
<td key={i}></td>
))}
<td
style={{
display: 'flex',

View File

@@ -1,5 +1,6 @@
import { EditCollectionModel } from '@affine/component/page-list';
import type { Collection, Filter } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
@@ -19,7 +20,7 @@ import { Button, MenuItem } from '../../..';
import Menu from '../../../ui/menu/menu';
import { appSidebarOpenAtom } from '../../app-sidebar';
import { CreateFilterMenu } from '../filter/vars';
import type { useAllPageSetting } from '../use-all-page-setting';
import type { useCollectionManager } from '../use-collection-manager';
import * as styles from './collection-list.css';
const CollectionOption = ({
@@ -28,7 +29,7 @@ const CollectionOption = ({
updateCollection,
}: {
collection: Collection;
setting: ReturnType<typeof useAllPageSetting>;
setting: ReturnType<typeof useCollectionManager>;
updateCollection: (view: Collection) => void;
}) => {
const actions: {
@@ -118,9 +119,11 @@ const CollectionOption = ({
export const CollectionList = ({
setting,
getPageInfo,
propertiesMeta,
}: {
setting: ReturnType<typeof useAllPageSetting>;
setting: ReturnType<typeof useCollectionManager>;
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
}) => {
const t = useAFFiNEI18N();
const [open] = useAtom(appSidebarOpenAtom);
@@ -205,6 +208,7 @@ export const CollectionList = ({
placement="bottom-start"
content={
<CreateFilterMenu
propertiesMeta={propertiesMeta}
value={setting.currentCollection.filterList}
onChange={onChange}
/>
@@ -221,6 +225,7 @@ export const CollectionList = ({
</Button>
</Menu>
<EditCollectionModel
propertiesMeta={propertiesMeta}
getPageInfo={getPageInfo}
init={collection}
open={!!collection}

View File

@@ -1,4 +1,5 @@
import type { Collection } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import {
EdgelessIcon,
@@ -25,6 +26,7 @@ type CreateCollectionProps = {
onConfirm: (collection: Collection) => void;
onConfirmText?: string;
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
};
export const EditCollectionModel = ({
init,
@@ -32,12 +34,14 @@ export const EditCollectionModel = ({
open,
onClose,
getPageInfo,
propertiesMeta,
}: {
init?: Collection;
onConfirm: (view: Collection) => void;
open: boolean;
onClose: () => void;
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
}) => {
return (
<Modal open={open} onClose={onClose}>
@@ -56,6 +60,7 @@ export const EditCollectionModel = ({
/>
{init ? (
<EditCollection
propertiesMeta={propertiesMeta}
title="Update Collection"
onConfirmText="Save"
init={init}
@@ -120,6 +125,7 @@ export const EditCollection = ({
onCancel,
onConfirmText,
getPageInfo,
propertiesMeta,
}: CreateCollectionProps & {
onCancel: () => void;
}) => {
@@ -189,6 +195,7 @@ export const EditCollection = ({
>
<div className={styles.filterTitle}>Filters</div>
<FilterList
propertiesMeta={propertiesMeta}
value={value.filterList}
onChange={list =>
onChange({
@@ -262,6 +269,7 @@ export const SaveCollectionButton = ({
init,
onConfirm,
getPageInfo,
propertiesMeta,
}: CreateCollectionProps) => {
const [show, changeShow] = useState(false);
return (
@@ -280,6 +288,7 @@ export const SaveCollectionButton = ({
</div>
</Button>
<EditCollectionModel
propertiesMeta={propertiesMeta}
init={init}
onConfirm={onConfirm}
open={show}

View File

@@ -1,3 +1,5 @@
import type { Workspace } from '@blocksuite/store';
export type LiteralValue =
| number
| string
@@ -33,3 +35,11 @@ export type Collection = {
allowList?: string[];
excludeList?: string[];
};
export type Tag = {
id: string;
value: string;
color: string;
parentId?: string;
};
export type PropertiesMeta = Workspace['meta']['properties'];

View File

@@ -72,6 +72,7 @@
"Delete": "Delete",
"Title": "Title",
"Untitled": "Untitled",
"Tags": "Tags",
"Created": "Created",
"Updated": "Updated",
"Open in new tab": "Open in new tab",