mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-26 10:45:57 +08:00
fix(data-view): preserve filtering on hidden properties (#14500)
Fixes issue #14036 where hiding a column used in filters caused empty table/kanban results. Root cause: filter evaluation built the row map from visible properties only. Change: evaluate filters using full property set (propertiesRaw$) so hidden filtered columns still participate. Added unit regressions for both table and kanban hidden-column filtering behavior. Verified this does fix the filtering issue for hidden columns: <img width="3440" height="1440" alt="Screenshot of before and after views of a database with hidden columns and filtering on said column" src="https://github.com/user-attachments/assets/c1e2674f-06be-44e9-97bd-63593172f05b" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed filtering in Kanban and Table views so filters evaluate against all properties (including hidden/raw columns), ensuring consistent results regardless of column visibility. * **Tests** * Added tests covering filtering behavior with hidden and filtered columns to prevent regressions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import type { GroupBy } from '../core/common/types.js';
|
||||
import type { DataSource } from '../core/data-source/base.js';
|
||||
import { DetailSelection } from '../core/detail/selection.js';
|
||||
import type { FilterGroup } from '../core/filter/types.js';
|
||||
import { groupByMatchers } from '../core/group-by/define.js';
|
||||
import { t } from '../core/logical/type-presets.js';
|
||||
import type { DataViewCellLifeCycle } from '../core/property/index.js';
|
||||
@@ -17,7 +18,10 @@ import {
|
||||
pickKanbanGroupColumn,
|
||||
resolveKanbanGroupBy,
|
||||
} from '../view-presets/kanban/group-by-utils.js';
|
||||
import { materializeKanbanColumns } from '../view-presets/kanban/kanban-view-manager.js';
|
||||
import {
|
||||
KanbanSingleView,
|
||||
materializeKanbanColumns,
|
||||
} from '../view-presets/kanban/kanban-view-manager.js';
|
||||
import type { KanbanCard } from '../view-presets/kanban/pc/card.js';
|
||||
import { KanbanDragController } from '../view-presets/kanban/pc/controller/drag.js';
|
||||
import type { KanbanGroup } from '../view-presets/kanban/pc/group.js';
|
||||
@@ -270,6 +274,73 @@ describe('kanban', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('filtering', () => {
|
||||
const sharedFilter: FilterGroup = {
|
||||
type: 'group',
|
||||
op: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'filter',
|
||||
left: {
|
||||
type: 'ref',
|
||||
name: 'status',
|
||||
},
|
||||
function: 'is',
|
||||
args: [{ type: 'literal', value: 'Done' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const sharedTitleProperty = {
|
||||
id: 'title',
|
||||
cellGetOrCreate: () => ({
|
||||
jsonValue$: {
|
||||
value: 'Task 1',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
it('evaluates filters with hidden columns', () => {
|
||||
const statusProperty = {
|
||||
id: 'status',
|
||||
cellGetOrCreate: () => ({
|
||||
jsonValue$: {
|
||||
value: 'Done',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const view = {
|
||||
filter$: { value: sharedFilter },
|
||||
// Simulate status being hidden in current view.
|
||||
properties$: { value: [sharedTitleProperty] },
|
||||
propertiesRaw$: { value: [sharedTitleProperty, statusProperty] },
|
||||
} as unknown as KanbanSingleView;
|
||||
|
||||
expect(KanbanSingleView.prototype.isShow.call(view, 'row-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when hidden filtered column does not match', () => {
|
||||
const statusProperty = {
|
||||
id: 'status',
|
||||
cellGetOrCreate: () => ({
|
||||
jsonValue$: {
|
||||
value: 'In Progress',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const view = {
|
||||
filter$: { value: sharedFilter },
|
||||
// Simulate status being hidden in current view.
|
||||
properties$: { value: [sharedTitleProperty] },
|
||||
propertiesRaw$: { value: [sharedTitleProperty, statusProperty] },
|
||||
} as unknown as KanbanSingleView;
|
||||
|
||||
expect(KanbanSingleView.prototype.isShow.call(view, 'row-1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('drag indicator', () => {
|
||||
it('shows drop preview when insert position exists', () => {
|
||||
const controller = createDragController();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { FilterGroup } from '../core/filter/types.js';
|
||||
import { numberFormats } from '../property-presets/number/utils/formats.js';
|
||||
import {
|
||||
formatNumber,
|
||||
@@ -11,7 +12,10 @@ import { mobileEffects } from '../view-presets/table/mobile/effect.js';
|
||||
import type { MobileTableGroup } from '../view-presets/table/mobile/group.js';
|
||||
import { pcEffects } from '../view-presets/table/pc/effect.js';
|
||||
import type { TableGroup } from '../view-presets/table/pc/group.js';
|
||||
import { materializeTableColumns } from '../view-presets/table/table-view-manager.js';
|
||||
import {
|
||||
materializeTableColumns,
|
||||
TableSingleView,
|
||||
} from '../view-presets/table/table-view-manager.js';
|
||||
|
||||
/** @vitest-environment happy-dom */
|
||||
|
||||
@@ -93,6 +97,96 @@ describe('table column materialization', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('table filtering', () => {
|
||||
test('evaluates filters with hidden columns', () => {
|
||||
const filter: FilterGroup = {
|
||||
type: 'group',
|
||||
op: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'filter',
|
||||
left: {
|
||||
type: 'ref',
|
||||
name: 'status',
|
||||
},
|
||||
function: 'is',
|
||||
args: [{ type: 'literal', value: 'Done' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const titleProperty = {
|
||||
id: 'title',
|
||||
cellGetOrCreate: () => ({
|
||||
jsonValue$: {
|
||||
value: 'Task 1',
|
||||
},
|
||||
}),
|
||||
};
|
||||
const statusProperty = {
|
||||
id: 'status',
|
||||
cellGetOrCreate: () => ({
|
||||
jsonValue$: {
|
||||
value: 'Done',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const view = {
|
||||
filter$: { value: filter },
|
||||
// Simulate status being hidden in current view.
|
||||
properties$: { value: [titleProperty] },
|
||||
propertiesRaw$: { value: [titleProperty, statusProperty] },
|
||||
} as unknown as TableSingleView;
|
||||
|
||||
expect(TableSingleView.prototype.isShow.call(view, 'row-1')).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when hidden filtered column does not match', () => {
|
||||
const filter: FilterGroup = {
|
||||
type: 'group',
|
||||
op: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'filter',
|
||||
left: {
|
||||
type: 'ref',
|
||||
name: 'status',
|
||||
},
|
||||
function: 'is',
|
||||
args: [{ type: 'literal', value: 'Done' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const titleProperty = {
|
||||
id: 'title',
|
||||
cellGetOrCreate: () => ({
|
||||
jsonValue$: {
|
||||
value: 'Task 1',
|
||||
},
|
||||
}),
|
||||
};
|
||||
const statusProperty = {
|
||||
id: 'status',
|
||||
cellGetOrCreate: () => ({
|
||||
jsonValue$: {
|
||||
value: 'In Progress',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const view = {
|
||||
filter$: { value: filter },
|
||||
// Simulate status being hidden in current view.
|
||||
properties$: { value: [titleProperty] },
|
||||
propertiesRaw$: { value: [titleProperty, statusProperty] },
|
||||
} as unknown as TableSingleView;
|
||||
|
||||
expect(TableSingleView.prototype.isShow.call(view, 'row-1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('number formatter', () => {
|
||||
test('number format menu should expose all schema formats', () => {
|
||||
const menuFormats = numberFormats.map(format => format.type);
|
||||
|
||||
@@ -349,7 +349,7 @@ export class KanbanSingleView extends SingleViewBase<KanbanViewData> {
|
||||
isShow(rowId: string): boolean {
|
||||
if (this.filter$.value?.conditions.length) {
|
||||
const rowMap = Object.fromEntries(
|
||||
this.properties$.value.map(column => [
|
||||
this.propertiesRaw$.value.map(column => [
|
||||
column.id,
|
||||
column.cellGetOrCreate(rowId).jsonValue$.value,
|
||||
])
|
||||
|
||||
@@ -269,7 +269,7 @@ export class TableSingleView extends SingleViewBase<TableViewData> {
|
||||
isShow(rowId: string): boolean {
|
||||
if (this.filter$.value?.conditions.length) {
|
||||
const rowMap = Object.fromEntries(
|
||||
this.properties$.value.map(column => [
|
||||
this.propertiesRaw$.value.map(column => [
|
||||
column.id,
|
||||
column.cellGetOrCreate(rowId).jsonValue$.value,
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user