mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-05 19:40:30 +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:
@@ -0,0 +1,97 @@
|
||||
import type { DocsService } from '@affine/core/modules/doc';
|
||||
import type { WorkspacePropertyFilter } from '@affine/core/modules/workspace-property';
|
||||
import { Service } from '@toeverything/infra';
|
||||
import { map, type Observable } from 'rxjs';
|
||||
|
||||
import type { FilterProvider } from '../../provider';
|
||||
import type { FilterParams } from '../../types';
|
||||
|
||||
export class NumberPropertyFilterProvider
|
||||
extends Service
|
||||
implements FilterProvider
|
||||
{
|
||||
constructor(private readonly docsService: DocsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
filter$(params: FilterParams): Observable<Set<string>> {
|
||||
const method = params.method as WorkspacePropertyFilter<'number'>;
|
||||
const values$ = this.docsService.propertyValues$('custom:' + params.key);
|
||||
const filterValue = Number(params.value);
|
||||
if (method === 'is-not-empty') {
|
||||
return values$.pipe(
|
||||
map(o => {
|
||||
const match = new Set<string>();
|
||||
for (const [id, value] of o) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
match.add(id);
|
||||
}
|
||||
}
|
||||
return match;
|
||||
})
|
||||
);
|
||||
} else if (method === 'is-empty') {
|
||||
return values$.pipe(
|
||||
map(o => {
|
||||
const match = new Set<string>();
|
||||
for (const [id, value] of o) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
match.add(id);
|
||||
}
|
||||
}
|
||||
return match;
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
method === '=' ||
|
||||
method === '≠' ||
|
||||
method === '>' ||
|
||||
method === '<' ||
|
||||
method === '≥' ||
|
||||
method === '≤'
|
||||
) {
|
||||
return values$.pipe(
|
||||
map(o => {
|
||||
const match = new Set<string>();
|
||||
for (const [id, value] of o) {
|
||||
const numValue = Number(value);
|
||||
switch (method) {
|
||||
case '=':
|
||||
if (Math.abs(numValue - filterValue) < Number.EPSILON) {
|
||||
match.add(id);
|
||||
}
|
||||
break;
|
||||
case '≠':
|
||||
if (Math.abs(numValue - filterValue) >= Number.EPSILON) {
|
||||
match.add(id);
|
||||
}
|
||||
break;
|
||||
case '>':
|
||||
if (numValue > filterValue) {
|
||||
match.add(id);
|
||||
}
|
||||
break;
|
||||
case '<':
|
||||
if (numValue < filterValue) {
|
||||
match.add(id);
|
||||
}
|
||||
break;
|
||||
case '≥':
|
||||
if (numValue >= filterValue) {
|
||||
match.add(id);
|
||||
}
|
||||
break;
|
||||
case '≤':
|
||||
if (numValue <= filterValue) {
|
||||
match.add(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
})
|
||||
);
|
||||
}
|
||||
throw new Error(`Unsupported method: ${method}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { DocsService } from '@affine/core/modules/doc';
|
||||
import { Service } from '@toeverything/infra';
|
||||
import { map, type Observable } from 'rxjs';
|
||||
|
||||
import type { GroupByProvider } from '../../provider';
|
||||
import type { GroupByParams } from '../../types';
|
||||
|
||||
export class NumberPropertyGroupByProvider
|
||||
extends Service
|
||||
implements GroupByProvider
|
||||
{
|
||||
constructor(private readonly docsService: DocsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
groupBy$(
|
||||
_items$: Observable<Set<string>>,
|
||||
params: GroupByParams
|
||||
): Observable<Map<string, Set<string>>> {
|
||||
return this.docsService.propertyValues$('custom:' + params.key).pipe(
|
||||
map(o => {
|
||||
const result = new Map<string, Set<string>>();
|
||||
for (const [id, value] of o) {
|
||||
const number = Number(value);
|
||||
if (Number.isNaN(number)) {
|
||||
continue;
|
||||
}
|
||||
// normalize all number to string
|
||||
const strValue = String(number);
|
||||
const set = result.get(strValue) ?? new Set<string>();
|
||||
set.add(id);
|
||||
result.set(strValue, set);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { DocsService } from '@affine/core/modules/doc';
|
||||
import { Service } from '@toeverything/infra';
|
||||
import { map, type Observable } from 'rxjs';
|
||||
|
||||
import type { OrderByProvider } from '../../provider';
|
||||
import type { OrderByParams } from '../../types';
|
||||
|
||||
export class NumberPropertyOrderByProvider
|
||||
extends Service
|
||||
implements OrderByProvider
|
||||
{
|
||||
constructor(private readonly docsService: DocsService) {
|
||||
super();
|
||||
}
|
||||
orderBy$(
|
||||
_items$: Observable<Set<string>>,
|
||||
params: OrderByParams
|
||||
): Observable<string[]> {
|
||||
const isDesc = params.desc;
|
||||
return this.docsService.propertyValues$('custom:' + params.key).pipe(
|
||||
map(o => {
|
||||
return Array.from(o)
|
||||
.map(v => [v[0], Number(v[1])])
|
||||
.filter((i): i is [string, number] => !Number.isNaN(i[1])) // filter NaN value
|
||||
.sort((a, b) => (a[1] - b[1]) * (isDesc ? -1 : 1))
|
||||
.map(i => i[0]);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { EmptyJournalFilterProvider } from './impls/filters/empty-journal';
|
||||
import { FavoriteFilterProvider } from './impls/filters/favorite';
|
||||
import { IntegrationTypeFilterProvider } from './impls/filters/integration-type';
|
||||
import { JournalFilterProvider } from './impls/filters/journal';
|
||||
import { NumberPropertyFilterProvider } from './impls/filters/number';
|
||||
import { PageWidthFilterProvider } from './impls/filters/page-width';
|
||||
import { PropertyFilterProvider } from './impls/filters/property';
|
||||
import { SharedFilterProvider } from './impls/filters/shared';
|
||||
@@ -36,6 +37,7 @@ import { DocPrimaryModeGroupByProvider } from './impls/group-by/doc-primary-mode
|
||||
import { EdgelessThemeGroupByProvider } from './impls/group-by/edgeless-theme';
|
||||
import { IntegrationTypeGroupByProvider } from './impls/group-by/integration-type';
|
||||
import { JournalGroupByProvider } from './impls/group-by/journal';
|
||||
import { NumberPropertyGroupByProvider } from './impls/group-by/number';
|
||||
import { PageWidthGroupByProvider } from './impls/group-by/page-width';
|
||||
import { PropertyGroupByProvider } from './impls/group-by/property';
|
||||
import { SystemGroupByProvider } from './impls/group-by/system';
|
||||
@@ -52,6 +54,7 @@ import { DocPrimaryModeOrderByProvider } from './impls/order-by/doc-primary-mode
|
||||
import { EdgelessThemeOrderByProvider } from './impls/order-by/edgeless-theme';
|
||||
import { IntegrationTypeOrderByProvider } from './impls/order-by/integration-type';
|
||||
import { JournalOrderByProvider } from './impls/order-by/journal';
|
||||
import { NumberPropertyOrderByProvider } from './impls/order-by/number';
|
||||
import { PageWidthOrderByProvider } from './impls/order-by/page-width';
|
||||
import { PropertyOrderByProvider } from './impls/order-by/property';
|
||||
import { SystemOrderByProvider } from './impls/order-by/system';
|
||||
@@ -81,6 +84,9 @@ export function configureCollectionRulesModule(framework: Framework) {
|
||||
.impl(FilterProvider('property:text'), TextPropertyFilterProvider, [
|
||||
DocsService,
|
||||
])
|
||||
.impl(FilterProvider('property:number'), NumberPropertyFilterProvider, [
|
||||
DocsService,
|
||||
])
|
||||
.impl(FilterProvider('property:tags'), TagsFilterProvider, [
|
||||
TagService,
|
||||
DocsService,
|
||||
@@ -196,6 +202,9 @@ export function configureCollectionRulesModule(framework: Framework) {
|
||||
.impl(GroupByProvider('property:text'), TextPropertyGroupByProvider, [
|
||||
DocsService,
|
||||
])
|
||||
.impl(GroupByProvider('property:number'), NumberPropertyGroupByProvider, [
|
||||
DocsService,
|
||||
])
|
||||
.impl(
|
||||
GroupByProvider('property:docPrimaryMode'),
|
||||
DocPrimaryModeGroupByProvider,
|
||||
@@ -293,6 +302,9 @@ export function configureCollectionRulesModule(framework: Framework) {
|
||||
.impl(OrderByProvider('property:text'), TextPropertyOrderByProvider, [
|
||||
DocsService,
|
||||
])
|
||||
.impl(OrderByProvider('property:number'), NumberPropertyOrderByProvider, [
|
||||
DocsService,
|
||||
])
|
||||
.impl(OrderByProvider('property:date'), DatePropertyOrderByProvider, [
|
||||
DocsService,
|
||||
])
|
||||
|
||||
@@ -25,7 +25,7 @@ export type WorkspacePropertyTypes = {
|
||||
filter: 'is' | 'is-not' | 'is-not-empty' | 'is-empty';
|
||||
};
|
||||
number: {
|
||||
filter: 'is' | 'is-not' | 'is-not-empty' | 'is-empty';
|
||||
filter: '=' | '≠' | '>' | '<' | '≥' | '≤' | 'is-not-empty' | 'is-empty';
|
||||
};
|
||||
checkbox: {
|
||||
filter: 'is' | 'is-not';
|
||||
|
||||
Reference in New Issue
Block a user