mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-11 14:08:55 +08:00
feat(core): add number property filter and group by (#12483)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced filtering options for number properties with multiple comparison operators and empty/not empty checks. - Added support for grouping and ordering by number properties. - Introduced new UI components for number property filtering and group headers. - Improved draft completion behavior for text and tags filters using empty/not empty methods. - **Bug Fixes** - Improved consistency in draft completion behavior when filtering by text or tags using empty/not empty methods. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -70,7 +70,12 @@ import {
|
||||
JournalGroupHeader,
|
||||
JournalValue,
|
||||
} from './journal';
|
||||
import { NumberDocListProperty, NumberValue } from './number';
|
||||
import {
|
||||
NumberDocListProperty,
|
||||
NumberFilterValue,
|
||||
NumberGroupHeader,
|
||||
NumberValue,
|
||||
} from './number';
|
||||
import {
|
||||
PageWidthDocListProperty,
|
||||
PageWidthFilterValue,
|
||||
@@ -158,8 +163,23 @@ export const WorkspacePropertyTypes = {
|
||||
value: NumberValue,
|
||||
name: 'com.affine.page-properties.property.number',
|
||||
description: 'com.affine.page-properties.property.number.tooltips',
|
||||
filterMethod: {
|
||||
'<': '<',
|
||||
'=': '=',
|
||||
'≠': '≠',
|
||||
'≥': '≥',
|
||||
'≤': '≤',
|
||||
'>': '>',
|
||||
'is-not-empty': 'com.affine.filter.is not empty',
|
||||
'is-empty': 'com.affine.filter.is empty',
|
||||
},
|
||||
allowInGroupBy: true,
|
||||
allowInOrderBy: true,
|
||||
filterValue: NumberFilterValue,
|
||||
defaultFilter: { method: 'is-not-empty' },
|
||||
showInDocList: 'stack',
|
||||
docListProperty: NumberDocListProperty,
|
||||
groupHeader: NumberGroupHeader,
|
||||
},
|
||||
checkbox: {
|
||||
icon: CheckBoxCheckLinearIcon,
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { PropertyValue } from '@affine/component';
|
||||
import { Input, Menu, type MenuRef, PropertyValue } from '@affine/component';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { NumberIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import {
|
||||
type ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||
import type { GroupHeaderProps } from '../explorer/types';
|
||||
import type { PropertyValueProps } from '../properties/types';
|
||||
import * as styles from './number.css';
|
||||
|
||||
@@ -58,6 +64,109 @@ export const NumberValue = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const NumberFilterValue = ({
|
||||
filter,
|
||||
isDraft,
|
||||
onDraftCompleted,
|
||||
onChange,
|
||||
}: {
|
||||
filter: FilterParams;
|
||||
isDraft?: boolean;
|
||||
onDraftCompleted?: () => void;
|
||||
onChange?: (filter: FilterParams) => void;
|
||||
}) => {
|
||||
const [tempValue, setTempValue] = useState(filter.value || '');
|
||||
const [valueMenuOpen, setValueMenuOpen] = useState(false);
|
||||
const menuRef = useRef<MenuRef>(null);
|
||||
const t = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
if (isDraft) {
|
||||
menuRef.current?.changeOpen(true);
|
||||
}
|
||||
}, [isDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
// update temp value with new filter value
|
||||
setTempValue(filter.value || '');
|
||||
}, [filter.value]);
|
||||
|
||||
const submitTempValue = useCallback(() => {
|
||||
if (tempValue !== (filter.value || '')) {
|
||||
onChange?.({
|
||||
...filter,
|
||||
value: tempValue,
|
||||
});
|
||||
}
|
||||
}, [filter, onChange, tempValue]);
|
||||
|
||||
const handleInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
submitTempValue();
|
||||
setValueMenuOpen(false);
|
||||
onDraftCompleted?.();
|
||||
},
|
||||
[submitTempValue, onDraftCompleted]
|
||||
);
|
||||
|
||||
const handleInputEnter = useCallback(() => {
|
||||
submitTempValue();
|
||||
setValueMenuOpen(false);
|
||||
onDraftCompleted?.();
|
||||
}, [submitTempValue, onDraftCompleted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDraft &&
|
||||
(filter.method === 'is-not-empty' || filter.method === 'is-empty')
|
||||
) {
|
||||
onDraftCompleted?.();
|
||||
}
|
||||
}, [isDraft, filter.method, onDraftCompleted]);
|
||||
|
||||
return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
rootOptions={{
|
||||
open: valueMenuOpen,
|
||||
onOpenChange: setValueMenuOpen,
|
||||
onClose: onDraftCompleted,
|
||||
}}
|
||||
contentOptions={{
|
||||
onPointerDownOutside: submitTempValue,
|
||||
sideOffset: -28,
|
||||
}}
|
||||
items={
|
||||
<Input
|
||||
inputStyle={{
|
||||
fontSize: cssVar('fontBase'),
|
||||
}}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
autoFocus
|
||||
autoSelect
|
||||
value={tempValue}
|
||||
onChange={value => {
|
||||
setTempValue(value);
|
||||
}}
|
||||
onEnter={handleInputEnter}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
style={{ height: 34, borderRadius: 4 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{filter.value ? (
|
||||
<span>{filter.value}</span>
|
||||
) : (
|
||||
<span style={{ color: cssVarV2('text/placeholder') }}>
|
||||
{t['com.affine.filter.empty']()}
|
||||
</span>
|
||||
)}
|
||||
</Menu>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const NumberDocListProperty = ({ value }: { value: number }) => {
|
||||
if (value !== 0 && !value) {
|
||||
return null;
|
||||
@@ -65,3 +174,13 @@ export const NumberDocListProperty = ({ value }: { value: number }) => {
|
||||
|
||||
return <StackProperty icon={<NumberIcon />}>{value}</StackProperty>;
|
||||
};
|
||||
|
||||
export const NumberGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
|
||||
const t = useI18n();
|
||||
const number = groupId || t['com.affine.filter.empty']();
|
||||
return (
|
||||
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||
{number}
|
||||
</PlainTextDocGroupHeader>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -98,6 +98,16 @@ export const TagsFilterValue = ({
|
||||
},
|
||||
[filter, onChange, selectedTags]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDraft &&
|
||||
(filter.method === 'is-not-empty' || filter.method === 'is-empty')
|
||||
) {
|
||||
onDraftCompleted?.();
|
||||
}
|
||||
}, [isDraft, filter.method, onDraftCompleted]);
|
||||
|
||||
return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? (
|
||||
<WorkspaceTagsInlineEditor
|
||||
placeholder={
|
||||
|
||||
@@ -216,14 +216,25 @@ export const TextFilterValue = ({
|
||||
if (e.key !== 'Escape') return;
|
||||
submitTempValue();
|
||||
setValueMenuOpen(false);
|
||||
onDraftCompleted?.();
|
||||
},
|
||||
[submitTempValue]
|
||||
[submitTempValue, onDraftCompleted]
|
||||
);
|
||||
|
||||
const handleInputEnter = useCallback(() => {
|
||||
submitTempValue();
|
||||
setValueMenuOpen(false);
|
||||
}, [submitTempValue]);
|
||||
onDraftCompleted?.();
|
||||
}, [submitTempValue, onDraftCompleted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDraft &&
|
||||
(filter.method === 'is-not-empty' || filter.method === 'is-empty')
|
||||
) {
|
||||
onDraftCompleted?.();
|
||||
}
|
||||
}, [isDraft, filter.method, onDraftCompleted]);
|
||||
|
||||
return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? (
|
||||
<Menu
|
||||
|
||||
Reference in New Issue
Block a user