mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 18:41:52 +08:00
fb9f49b948
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 -->
248 lines
7.0 KiB
TypeScript
248 lines
7.0 KiB
TypeScript
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,
|
|
NumberFormatSchema,
|
|
parseNumber,
|
|
} from '../property-presets/number/utils/formatter.js';
|
|
import { DEFAULT_COLUMN_WIDTH } from '../view-presets/table/consts.js';
|
|
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,
|
|
TableSingleView,
|
|
} from '../view-presets/table/table-view-manager.js';
|
|
|
|
/** @vitest-environment happy-dom */
|
|
|
|
describe('TableGroup', () => {
|
|
test('toggle collapse on pc', () => {
|
|
pcEffects();
|
|
const group = document.createElement(
|
|
'affine-data-view-table-group'
|
|
) as TableGroup;
|
|
|
|
expect(group.collapsed$.value).toBe(false);
|
|
(group as any)._toggleCollapse();
|
|
expect(group.collapsed$.value).toBe(true);
|
|
(group as any)._toggleCollapse();
|
|
expect(group.collapsed$.value).toBe(false);
|
|
});
|
|
|
|
test('toggle collapse on mobile', () => {
|
|
mobileEffects();
|
|
const group = document.createElement(
|
|
'mobile-table-group'
|
|
) as MobileTableGroup;
|
|
|
|
expect(group.collapsed$.value).toBe(false);
|
|
(group as any)._toggleCollapse();
|
|
expect(group.collapsed$.value).toBe(true);
|
|
(group as any)._toggleCollapse();
|
|
expect(group.collapsed$.value).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('table column materialization', () => {
|
|
test('appends missing properties while preserving existing order and state', () => {
|
|
const columns = [
|
|
{ id: 'status', width: 240, hide: true },
|
|
{ id: 'title', width: 320 },
|
|
];
|
|
|
|
const next = materializeTableColumns(columns, ['title', 'status', 'date']);
|
|
|
|
expect(next).toEqual([
|
|
{ id: 'status', width: 240, hide: true },
|
|
{ id: 'title', width: 320 },
|
|
{ id: 'date', width: DEFAULT_COLUMN_WIDTH },
|
|
]);
|
|
});
|
|
|
|
test('drops stale columns that no longer exist in data source', () => {
|
|
const columns = [
|
|
{ id: 'title', width: 320 },
|
|
{ id: 'removed', width: 200, hide: true },
|
|
];
|
|
|
|
const next = materializeTableColumns(columns, ['title']);
|
|
|
|
expect(next).toEqual([{ id: 'title', width: 320 }]);
|
|
});
|
|
|
|
test('returns original reference when columns are already materialized', () => {
|
|
const columns = [
|
|
{ id: 'title', width: 320 },
|
|
{ id: 'status', width: 240, hide: true },
|
|
];
|
|
|
|
const next = materializeTableColumns(columns, ['title', 'status']);
|
|
|
|
expect(next).toBe(columns);
|
|
});
|
|
|
|
test('supports type-aware default width when materializing missing columns', () => {
|
|
const next = materializeTableColumns([], ['title', 'status'], id =>
|
|
id === 'title' ? 260 : DEFAULT_COLUMN_WIDTH
|
|
);
|
|
|
|
expect(next).toEqual([
|
|
{ id: 'title', width: 260 },
|
|
{ id: 'status', width: DEFAULT_COLUMN_WIDTH },
|
|
]);
|
|
});
|
|
});
|
|
|
|
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);
|
|
const schemaFormats = NumberFormatSchema.options;
|
|
|
|
expect(new Set(menuFormats)).toEqual(new Set(schemaFormats));
|
|
expect(menuFormats).toHaveLength(schemaFormats.length);
|
|
});
|
|
|
|
test('formats grouped decimal numbers with Intl grouping rules', () => {
|
|
const value = 11451.4;
|
|
const decimals = 1;
|
|
const expected = new Intl.NumberFormat(navigator.language, {
|
|
style: 'decimal',
|
|
useGrouping: true,
|
|
minimumFractionDigits: decimals,
|
|
maximumFractionDigits: decimals,
|
|
}).format(value);
|
|
|
|
expect(formatNumber(value, 'numberWithCommas', decimals)).toBe(expected);
|
|
});
|
|
|
|
test('formats percent values with Intl percent rules', () => {
|
|
const value = 0.1234;
|
|
const decimals = 2;
|
|
const expected = new Intl.NumberFormat(navigator.language, {
|
|
style: 'percent',
|
|
useGrouping: false,
|
|
minimumFractionDigits: decimals,
|
|
maximumFractionDigits: decimals,
|
|
}).format(value);
|
|
|
|
expect(formatNumber(value, 'percent', decimals)).toBe(expected);
|
|
});
|
|
|
|
test('formats currency values with Intl currency rules', () => {
|
|
const value = 11451.4;
|
|
const expected = new Intl.NumberFormat(navigator.language, {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
currencyDisplay: 'symbol',
|
|
}).format(value);
|
|
|
|
expect(formatNumber(value, 'currencyUSD')).toBe(expected);
|
|
});
|
|
|
|
test('parses grouped number string pasted from clipboard', () => {
|
|
expect(parseNumber('11,451.4')).toBe(11451.4);
|
|
});
|
|
|
|
test('keeps regular decimal parsing', () => {
|
|
expect(parseNumber('123.45')).toBe(123.45);
|
|
});
|
|
|
|
test('supports comma as decimal separator in locale-specific input', () => {
|
|
expect(parseNumber('11451,4', ',')).toBe(11451.4);
|
|
});
|
|
});
|