From fb9f49b9489002d04cb5a163dd9cedfa2b696858 Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Mon, 23 Feb 2026 04:45:12 -0800 Subject: [PATCH] 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: Screenshot of before and after
views of a database with hidden columns and filtering on said column ## 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. --- .../src/__tests__/kanban.unit.spec.ts | 73 +++++++++++++- .../src/__tests__/table.unit.spec.ts | 96 ++++++++++++++++++- .../kanban/kanban-view-manager.ts | 2 +- .../view-presets/table/table-view-manager.ts | 2 +- 4 files changed, 169 insertions(+), 4 deletions(-) diff --git a/blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts b/blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts index 30447b898..c43a4d92e 100644 --- a/blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts +++ b/blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts @@ -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(); diff --git a/blocksuite/affine/data-view/src/__tests__/table.unit.spec.ts b/blocksuite/affine/data-view/src/__tests__/table.unit.spec.ts index 18b112125..1a436773b 100644 --- a/blocksuite/affine/data-view/src/__tests__/table.unit.spec.ts +++ b/blocksuite/affine/data-view/src/__tests__/table.unit.spec.ts @@ -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); diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts b/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts index d7cd704a1..4758fb281 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts @@ -349,7 +349,7 @@ export class KanbanSingleView extends SingleViewBase { 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, ]) diff --git a/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts b/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts index fa7cc35d8..462e86f03 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts @@ -269,7 +269,7 @@ export class TableSingleView extends SingleViewBase { 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, ])