From 74e42eb02644e59819986f367b8fb9d4c1f3b251 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 19 May 2026 14:44:01 +0800 Subject: [PATCH 1/6] fix: preserve filter column order after header drag --- .../__tests__/filter/filter-plugin.test.ts | 94 +++++++++++++++++++ packages/vtable-plugins/src/filter/filter.ts | 20 +++- .../columns/listTable-dragHeader.test.ts | 36 +++++++ packages/vtable/src/ListTable.ts | 9 ++ 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts diff --git a/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts b/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts new file mode 100644 index 000000000..70d49a743 --- /dev/null +++ b/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts @@ -0,0 +1,94 @@ +// @ts-nocheck +let subscribeCallback: ((state: any, action?: any) => void) | undefined; + +jest.mock('@visactor/vtable', () => ({ + TABLE_EVENT_TYPE: { + BEFORE_INIT: 'before_init', + BEFORE_UPDATE_OPTION: 'before_update_option', + ICON_CLICK: 'icon_click', + SCROLL: 'scroll', + CHANGE_CELL_VALUE: 'change_cell_value', + UPDATE_RECORD: 'update_record', + ADD_RECORD: 'add_record', + DELETE_RECORD: 'delete_record', + ADD_COLUMN: 'add_column', + DELETE_COLUMN: 'delete_column' + }, + TYPES: { + IconPosition: { + right: 'right' + } + } +})); + +jest.mock('../../src/filter/filter-engine', () => ({ + FilterEngine: jest.fn().mockImplementation(() => ({})) +})); + +jest.mock('../../src/filter/filter-state-manager', () => ({ + FilterStateManager: jest.fn().mockImplementation(() => ({ + subscribe: (cb: (state: any, action?: any) => void) => { + subscribeCallback = cb; + }, + getFilterState: () => undefined, + getActiveFilterFields: () => [], + reapplyCurrentFilters: jest.fn(), + dispatch: jest.fn() + })) +})); + +jest.mock('../../src/filter/filter-toolbar', () => ({ + FilterToolbar: jest.fn().mockImplementation(() => ({ + render: jest.fn(), + updateStyles: jest.fn(), + isVisible: false, + hide: jest.fn(), + show: jest.fn(), + adjustMenuPosition: jest.fn(), + valueFilter: { + syncSingleStateFromTableData: jest.fn() + } + })) +})); + +const { FilterPlugin } = require('../../src/filter/filter'); + +describe('FilterPlugin', () => { + beforeEach(() => { + subscribeCallback = undefined; + }); + + test('uses current table column order when filter state updates', () => { + const staleColumns = [ + { field: 'a', title: 'A' }, + { field: 'b', title: 'B' }, + { field: 'c', title: 'C' } + ]; + const currentColumns = [ + { field: 'a', title: 'A' }, + { field: 'c', title: 'C' }, + { field: 'b', title: 'B' } + ]; + const table = { + isListTable: () => true, + updateColumns: jest.fn(), + get columns() { + return currentColumns; + } + }; + + const plugin = new FilterPlugin({}); + plugin.table = table; + plugin.initFilterPlugin({ + options: { + columns: staleColumns + } + }); + + subscribeCallback?.({}, { type: 'apply_filters' }); + + expect(table.updateColumns).toHaveBeenCalledWith(currentColumns, { + clearRowHeightCache: false + }); + }); +}); diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index 1c9ad2f53..539073149 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -78,17 +78,20 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { this.filterEngine = new FilterEngine(this.pluginOptions); this.filterStateManager = new FilterStateManager(this.table, this.filterEngine); this.filterToolbar = new FilterToolbar(this.table, this.filterStateManager, this.pluginOptions); + // BEFORE_INIT 阶段 table.columns 可能还不可用,先缓存本次 options.columns 作为 getCurrentColumns 的回退值。 this.columns = eventArgs.options.columns; this.filterToolbar.render(document.body); - this.updateFilterIcons(this.columns); + this.updateFilterIcons(this.getCurrentColumns()); this.filterStateManager.subscribe((_: FilterState, action?: FilterAction) => { // 新增筛选配置时,不需要更新筛选图标以及表格 if (action?.type === FilterActionType.ADD_FILTER) { return; } - this.updateFilterIcons(this.columns); - (this.table as ListTable).updateColumns(this.columns, { + const currentColumns = this.getCurrentColumns(); + this.columns = currentColumns; + this.updateFilterIcons(currentColumns); + (this.table as ListTable).updateColumns(currentColumns, { clearRowHeightCache: false }); }); @@ -176,7 +179,9 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { // 更新筛选器UI样式 this.filterToolbar.updateStyles(this.pluginOptions.styles); // 更新icon - (this.table as ListTable).updateColumns(this.columns, { + const currentColumns = this.getCurrentColumns(); + this.columns = currentColumns; + (this.table as ListTable).updateColumns(currentColumns, { clearRowHeightCache: false }); } @@ -226,6 +231,13 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { this.updateFilterIcons(options.columns); } + private getCurrentColumns(): ColumnsDefine { + if (this.table?.isListTable?.()) { + return (this.table as ListTable).columns; + } + return this.columns ?? []; + } + /** * 重新应用所有激活的筛选状态 * 在 updateOption 后调用,因为 updateOption 会全量更新表格 diff --git a/packages/vtable/__tests__/columns/listTable-dragHeader.test.ts b/packages/vtable/__tests__/columns/listTable-dragHeader.test.ts index b366f9045..b7f736c03 100644 --- a/packages/vtable/__tests__/columns/listTable-dragHeader.test.ts +++ b/packages/vtable/__tests__/columns/listTable-dragHeader.test.ts @@ -125,6 +125,42 @@ describe('listTable-cellType-function init test', () => { ]); listTable2.release(); }); + test('listTable changeHeaderPosition keeps options.columns in sync for follow-up updates', () => { + const containerDom2: HTMLElement = createDiv(); + containerDom2.style.position = 'relative'; + containerDom2.style.width = '1000px'; + containerDom2.style.height = '800px'; + const records2 = [ + { a: 1, b: 2, c: 3 }, + { a: 4, b: 5, c: 6 } + ]; + const columns2 = [ + { field: 'a', title: 'A' }, + { field: 'b', title: 'B' }, + { field: 'c', title: 'C' } + ]; + const listTable2 = new ListTable(containerDom2, { records: records2, columns: columns2, dragHeaderMode: 'all' }); + listTable2.changeHeaderPosition({ + source: { col: 1, row: 0 }, + target: { col: 2, row: 0 }, + movingColumnOrRow: 'column' + }); + expect(listTable2.options.columns).toEqual([ + { field: 'a', title: 'A' }, + { field: 'c', title: 'C' }, + { field: 'b', title: 'B' } + ]); + listTable2.updateOption({ + ...listTable2.options, + widthMode: 'standard' + }); + expect(listTable2.columns).toEqual([ + { field: 'a', title: 'A' }, + { field: 'c', title: 'C' }, + { field: 'b', title: 'B' } + ]); + listTable2.release(); + }); test('listTable dragHeader interaction', () => { option.transpose = true; listTable.updateOption(option); diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 1a79253a3..6aa0355ef 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -1056,10 +1056,19 @@ export class ListTable extends BaseTable implements ListTableAPI { } adjustHeightResizedRowMap(moveContext, this); } + this.syncColumnsStateFromLayoutMap(); return moveContext; } return null; } + private syncColumnsStateFromLayoutMap() { + const currentColumns = this.columns; + this.internalProps.columns = cloneDeepSpec(currentColumns, ['children']); + this.options.columns = currentColumns; + if (this.options.header) { + this.options.header = currentColumns; + } + } changeRecordOrder(sourceIndex: number, targetIndex: number) { if (this.transpose) { sourceIndex = this.getRecordShowIndexByCell(sourceIndex, 0); From e197017532d4ab5d1639cc082da2e70191f98ca4 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 19 May 2026 14:44:16 +0800 Subject: [PATCH 2/6] test: add issue-5137 repro demo --- .../vtable-plugins/demo/filter/issue-5137.ts | 209 ++++++++++++++++++ packages/vtable-plugins/demo/menu.ts | 4 + 2 files changed, 213 insertions(+) create mode 100644 packages/vtable-plugins/demo/filter/issue-5137.ts diff --git a/packages/vtable-plugins/demo/filter/issue-5137.ts b/packages/vtable-plugins/demo/filter/issue-5137.ts new file mode 100644 index 000000000..78e346d2f --- /dev/null +++ b/packages/vtable-plugins/demo/filter/issue-5137.ts @@ -0,0 +1,209 @@ +import * as VTable from '@visactor/vtable'; +import { FilterPlugin } from '../../src/filter'; + +const CONTAINER_ID = 'vTable'; + +type RecordItem = { + id: number; + name: string; + gender: '男' | '女'; + city: string; + department: string; + status: '在职' | '请假' | '离职'; +}; + +function createRecords(): RecordItem[] { + return [ + { id: 1, name: '张三', gender: '男', city: '北京', department: '研发', status: '在职' }, + { id: 2, name: '李四', gender: '女', city: '上海', department: '销售', status: '请假' }, + { id: 3, name: '王五', gender: '男', city: '深圳', department: '研发', status: '在职' }, + { id: 4, name: '赵六', gender: '女', city: '杭州', department: '设计', status: '离职' }, + { id: 5, name: '钱七', gender: '男', city: '广州', department: '运营', status: '在职' }, + { id: 6, name: '孙八', gender: '女', city: '苏州', department: '销售', status: '请假' } + ]; +} + +function createColumns(): VTable.ColumnsDefine { + return [ + { field: 'id', title: 'ID', width: 80, sort: true }, + { field: 'name', title: '姓名', width: 120, sort: true }, + { field: 'gender', title: '性别', width: 100 }, + { field: 'city', title: '城市', width: 120 }, + { field: 'department', title: '部门', width: 120 }, + { field: 'status', title: '状态', width: 120 } + ]; +} + +function getOrderText(table: VTable.ListTable) { + return table.columns.map(col => `${String(col.field)}${col.hide ? '(hide)' : ''}`).join(' | '); +} + +function getOptionOrderText(table: VTable.ListTable) { + return (table.options.columns ?? []).map(col => `${String(col.field)}${col.hide ? '(hide)' : ''}`).join(' | '); +} + +export function createTable() { + const container = document.getElementById(CONTAINER_ID); + if (!container) { + return; + } + + const panel = document.createElement('div'); + panel.style.position = 'fixed'; + panel.style.top = '12px'; + panel.style.right = '12px'; + panel.style.zIndex = '9999'; + panel.style.display = 'flex'; + panel.style.flexDirection = 'column'; + panel.style.gap = '8px'; + panel.style.width = '420px'; + panel.style.padding = '12px'; + panel.style.background = 'rgba(17,24,39,0.88)'; + panel.style.color = '#fff'; + panel.style.fontSize = '12px'; + panel.style.borderRadius = '8px'; + + const title = document.createElement('div'); + title.textContent = 'issue-5137 repro: 隐藏列 -> 拖拽排序 -> Filter 确认'; + title.style.fontWeight = 'bold'; + + const desc = document.createElement('div'); + desc.textContent = + '手动复现:1. 点击“隐藏性别列” 2. 直接拖拽“城市”列到“姓名”后面 3. 点击“状态”列的筛选图标并确认。也可用下面的一键脚本。'; + desc.style.lineHeight = '1.5'; + + const buttonRow = document.createElement('div'); + buttonRow.style.display = 'flex'; + buttonRow.style.flexWrap = 'wrap'; + buttonRow.style.gap = '8px'; + + const status = document.createElement('pre'); + status.style.margin = '0'; + status.style.whiteSpace = 'pre-wrap'; + status.style.lineHeight = '1.5'; + + const records = createRecords(); + const columns = createColumns(); + const filterPlugin = new FilterPlugin({ + defaultEnabled: true + }); + + const table = new VTable.ListTable({ + container, + records, + columns, + widthMode: 'standard', + dragHeaderMode: 'all', + plugins: [filterPlugin] + }); + + function updateStatus(label: string) { + status.textContent = `${label}\n` + `current: ${getOrderText(table)}\n` + `options: ${getOptionOrderText(table)}`; + } + + function hideGenderColumn() { + const nextColumns = table.columns.map(col => { + if (col.field === 'gender') { + return { + ...col, + hide: true + }; + } + return { ...col }; + }); + table.updateColumns(nextColumns, { clearRowHeightCache: false }); + updateStatus('已隐藏 `gender` 列'); + } + + function dragCityAfterName() { + const cityIndex = table.columns.findIndex(col => col.field === 'city'); + const nameIndex = table.columns.findIndex(col => col.field === 'name'); + if (cityIndex < 0 || nameIndex < 0) { + updateStatus('未找到 `city` 或 `name` 列'); + return; + } + table.changeHeaderPosition({ + source: { col: cityIndex, row: 0 }, + target: { col: nameIndex + 1, row: 0 }, + movingColumnOrRow: 'column' + }); + updateStatus('已把 `city` 拖到 `name` 后面'); + } + + function applyStatusFilter() { + filterPlugin.applyFilterSnapshot({ + filters: [ + { + field: 'status', + type: 'byValue', + values: ['在职', '请假'], + enable: true + } + ] + }); + updateStatus('已执行一次 Filter 确认(status in 在职/请假)'); + } + + function runAllSteps() { + hideGenderColumn(); + dragCityAfterName(); + applyStatusFilter(); + updateStatus('已完成完整复现链路'); + } + + function resetTable() { + table.updateOption({ + ...table.options, + records: createRecords(), + columns: createColumns(), + plugins: [filterPlugin] + }); + filterPlugin.applyFilterSnapshot({ filters: [] }); + updateStatus('已重置为初始状态'); + } + + function createButton(text: string, onClick: () => void) { + const button = document.createElement('button'); + button.textContent = text; + button.style.padding = '6px 10px'; + button.style.cursor = 'pointer'; + button.style.borderRadius = '4px'; + button.style.border = '1px solid rgba(255,255,255,0.2)'; + button.style.background = 'rgba(255,255,255,0.12)'; + button.style.color = '#fff'; + button.addEventListener('click', onClick); + return button; + } + + buttonRow.appendChild(createButton('隐藏性别列', hideGenderColumn)); + buttonRow.appendChild(createButton('拖拽 city 到 name 后', dragCityAfterName)); + buttonRow.appendChild(createButton('执行 Filter 确认', applyStatusFilter)); + buttonRow.appendChild(createButton('一键复现', runAllSteps)); + buttonRow.appendChild(createButton('重置', resetTable)); + + panel.appendChild(title); + panel.appendChild(desc); + panel.appendChild(buttonRow); + panel.appendChild(status); + document.body.appendChild(panel); + + const demoWindow = window as Window & + typeof globalThis & { + tableInstance?: VTable.ListTable; + filterPlugin?: FilterPlugin; + issue5137?: Record; + }; + + demoWindow.tableInstance = table; + demoWindow.filterPlugin = filterPlugin; + demoWindow.issue5137 = { + hideGenderColumn, + dragCityAfterName, + applyStatusFilter, + runAllSteps, + resetTable, + getOrderText: () => getOrderText(table) + }; + + updateStatus('初始化完成'); +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index a9718ec90..f5e19298c 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -19,6 +19,10 @@ export const menus = [ path: 'filter', name: 'bug' }, + { + path: 'filter', + name: 'issue-5137' + }, { path: 'filter', name: 'value-filter' From 1b0d8b1db646a88f7b3f7ffb2d078f37e033aee4 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 19 May 2026 15:22:37 +0800 Subject: [PATCH 3/6] fix: guard filter columns before layout init --- .../__tests__/filter/filter-plugin.test.ts | 27 +++++++++++++++++++ packages/vtable-plugins/src/filter/filter.ts | 6 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts b/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts index 70d49a743..a50189feb 100644 --- a/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts +++ b/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts @@ -58,6 +58,33 @@ describe('FilterPlugin', () => { subscribeCallback = undefined; }); + test('falls back to option columns before list table layout is ready', () => { + const optionColumns = [ + { field: 'a', title: 'A' }, + { field: 'b', title: 'B' } + ]; + const table = { + isListTable: () => true, + updateColumns: jest.fn(), + get columns() { + throw new TypeError("Cannot read properties of undefined (reading 'layoutMap')"); + } + }; + + const plugin = new FilterPlugin({}); + plugin.table = table; + + expect(() => + plugin.initFilterPlugin({ + options: { + columns: optionColumns + } + }) + ).not.toThrow(); + + expect(table.updateColumns).not.toHaveBeenCalled(); + }); + test('uses current table column order when filter state updates', () => { const staleColumns = [ { field: 'a', title: 'A' }, diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index 539073149..4ae983cbf 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -233,7 +233,11 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { private getCurrentColumns(): ColumnsDefine { if (this.table?.isListTable?.()) { - return (this.table as ListTable).columns; + try { + return (this.table as ListTable).columns; + } catch (error) { + // BEFORE_INIT 阶段 ListTable 的 layoutMap 可能尚未建立,回退到最近一次缓存的 columns。 + } } return this.columns ?? []; } From d91b99291729817595fca52179c5b246e62cfd2d Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 19 May 2026 18:07:16 +0800 Subject: [PATCH 4/6] fix: preserve hidden column order after filter update --- .../__tests__/filter/filter-plugin.test.ts | 42 +++++++++++++++++++ .../vtable-plugins/demo/filter/issue-5137.ts | 36 ++++++++++------ packages/vtable-plugins/src/filter/filter.ts | 4 ++ .../columns/listTable-dragHeader.test.ts | 42 +++++++++++++++++++ packages/vtable/src/ListTable.ts | 17 ++++++-- 5 files changed, 125 insertions(+), 16 deletions(-) diff --git a/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts b/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts index a50189feb..71cb30bf8 100644 --- a/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts +++ b/packages/vtable-plugins/__tests__/filter/filter-plugin.test.ts @@ -118,4 +118,46 @@ describe('FilterPlugin', () => { clearRowHeightCache: false }); }); + + test('uses synced options.columns when hidden columns exist', () => { + const syncedOptionColumns = [ + { field: 'id', title: 'ID' }, + { field: 'name', title: 'Name' }, + { field: 'gender', title: 'Gender', hide: true }, + { field: 'department', title: 'Department' }, + { field: 'city', title: 'City' }, + { field: 'status', title: 'Status' } + ]; + const table = { + isListTable: () => true, + options: { + columns: syncedOptionColumns + }, + updateColumns: jest.fn(), + get columns() { + return [ + { field: 'id', title: 'ID' }, + { field: 'name', title: 'Name' }, + { field: 'city', title: 'City' }, + { field: 'gender', title: 'Gender', hide: true }, + { field: 'department', title: 'Department' }, + { field: 'status', title: 'Status' } + ]; + } + }; + + const plugin = new FilterPlugin({}); + plugin.table = table; + plugin.initFilterPlugin({ + options: { + columns: syncedOptionColumns + } + }); + + subscribeCallback?.({}, { type: 'apply_filters' }); + + expect(table.updateColumns).toHaveBeenCalledWith(syncedOptionColumns, { + clearRowHeightCache: false + }); + }); }); diff --git a/packages/vtable-plugins/demo/filter/issue-5137.ts b/packages/vtable-plugins/demo/filter/issue-5137.ts index 78e346d2f..aa5161fc7 100644 --- a/packages/vtable-plugins/demo/filter/issue-5137.ts +++ b/packages/vtable-plugins/demo/filter/issue-5137.ts @@ -35,13 +35,26 @@ function createColumns(): VTable.ColumnsDefine { } function getOrderText(table: VTable.ListTable) { - return table.columns.map(col => `${String(col.field)}${col.hide ? '(hide)' : ''}`).join(' | '); + const visibleFields: string[] = []; + for (let col = 0; col < table.colCount; col++) { + visibleFields.push(String(table.getHeaderField(col, 0))); + } + return visibleFields.join(' | '); } function getOptionOrderText(table: VTable.ListTable) { return (table.options.columns ?? []).map(col => `${String(col.field)}${col.hide ? '(hide)' : ''}`).join(' | '); } +function getVisibleHeaderColByField(table: VTable.ListTable, field: string) { + for (let col = 0; col < table.colCount; col++) { + if (table.getHeaderField(col, 0) === field) { + return col; + } + } + return -1; +} + export function createTable() { const container = document.getElementById(CONTAINER_ID); if (!container) { @@ -115,19 +128,19 @@ export function createTable() { updateStatus('已隐藏 `gender` 列'); } - function dragCityAfterName() { - const cityIndex = table.columns.findIndex(col => col.field === 'city'); - const nameIndex = table.columns.findIndex(col => col.field === 'name'); - if (cityIndex < 0 || nameIndex < 0) { - updateStatus('未找到 `city` 或 `name` 列'); + function dragCityAfterDepartment() { + const cityIndex = getVisibleHeaderColByField(table, 'city'); + const departmentIndex = getVisibleHeaderColByField(table, 'department'); + if (cityIndex < 0 || departmentIndex < 0) { + updateStatus('未找到 `city` 或 `department` 列'); return; } table.changeHeaderPosition({ source: { col: cityIndex, row: 0 }, - target: { col: nameIndex + 1, row: 0 }, + target: { col: departmentIndex, row: 0 }, movingColumnOrRow: 'column' }); - updateStatus('已把 `city` 拖到 `name` 后面'); + updateStatus('已把 `city` 拖到 `department` 后面'); } function applyStatusFilter() { @@ -146,7 +159,7 @@ export function createTable() { function runAllSteps() { hideGenderColumn(); - dragCityAfterName(); + dragCityAfterDepartment(); applyStatusFilter(); updateStatus('已完成完整复现链路'); } @@ -176,8 +189,7 @@ export function createTable() { } buttonRow.appendChild(createButton('隐藏性别列', hideGenderColumn)); - buttonRow.appendChild(createButton('拖拽 city 到 name 后', dragCityAfterName)); - buttonRow.appendChild(createButton('执行 Filter 确认', applyStatusFilter)); + buttonRow.appendChild(createButton('拖拽 city 到 department 后', dragCityAfterDepartment)); buttonRow.appendChild(createButton('一键复现', runAllSteps)); buttonRow.appendChild(createButton('重置', resetTable)); @@ -198,7 +210,7 @@ export function createTable() { demoWindow.filterPlugin = filterPlugin; demoWindow.issue5137 = { hideGenderColumn, - dragCityAfterName, + dragCityAfterDepartment, applyStatusFilter, runAllSteps, resetTable, diff --git a/packages/vtable-plugins/src/filter/filter.ts b/packages/vtable-plugins/src/filter/filter.ts index 4ae983cbf..788d4e0a3 100644 --- a/packages/vtable-plugins/src/filter/filter.ts +++ b/packages/vtable-plugins/src/filter/filter.ts @@ -233,6 +233,10 @@ export class FilterPlugin implements pluginsDefinition.IVTablePlugin { private getCurrentColumns(): ColumnsDefine { if (this.table?.isListTable?.()) { + const optionColumns = (this.table as ListTable).options?.columns; + if (optionColumns?.length) { + return optionColumns; + } try { return (this.table as ListTable).columns; } catch (error) { diff --git a/packages/vtable/__tests__/columns/listTable-dragHeader.test.ts b/packages/vtable/__tests__/columns/listTable-dragHeader.test.ts index b7f736c03..5d3bed929 100644 --- a/packages/vtable/__tests__/columns/listTable-dragHeader.test.ts +++ b/packages/vtable/__tests__/columns/listTable-dragHeader.test.ts @@ -161,6 +161,48 @@ describe('listTable-cellType-function init test', () => { ]); listTable2.release(); }); + test('listTable changeHeaderPosition keeps hidden-column order in sync for follow-up updates', () => { + const containerDom2: HTMLElement = createDiv(); + containerDom2.style.position = 'relative'; + containerDom2.style.width = '1000px'; + containerDom2.style.height = '800px'; + const records2 = [ + { id: 1, name: 'A', gender: 'M', city: 'BJ', department: 'RD', status: 'on' }, + { id: 2, name: 'B', gender: 'F', city: 'SH', department: 'Sales', status: 'off' } + ]; + const columns2 = [ + { field: 'id', title: 'ID' }, + { field: 'name', title: 'Name' }, + { field: 'gender', title: 'Gender', hide: true }, + { field: 'city', title: 'City' }, + { field: 'department', title: 'Department' }, + { field: 'status', title: 'Status' } + ]; + const listTable2 = new ListTable(containerDom2, { records: records2, columns: columns2, dragHeaderMode: 'all' }); + listTable2.changeHeaderPosition({ + source: { col: 2, row: 0 }, + target: { col: 3, row: 0 }, + movingColumnOrRow: 'column' + }); + expect(listTable2.options.columns).toEqual([ + { field: 'id', title: 'ID' }, + { field: 'name', title: 'Name' }, + { field: 'gender', title: 'Gender', hide: true }, + { field: 'department', title: 'Department' }, + { field: 'city', title: 'City' }, + { field: 'status', title: 'Status' } + ]); + listTable2.updateOption({ + ...listTable2.options, + widthMode: 'standard' + }); + expect(listTable2.getHeaderField(0, 0)).toBe('id'); + expect(listTable2.getHeaderField(1, 0)).toBe('name'); + expect(listTable2.getHeaderField(2, 0)).toBe('department'); + expect(listTable2.getHeaderField(3, 0)).toBe('city'); + expect(listTable2.getHeaderField(4, 0)).toBe('status'); + listTable2.release(); + }); test('listTable dragHeader interaction', () => { option.transpose = true; listTable.updateOption(option); diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 6aa0355ef..47e7c0d69 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -1062,11 +1062,20 @@ export class ListTable extends BaseTable implements ListTableAPI { return null; } private syncColumnsStateFromLayoutMap() { - const currentColumns = this.columns; - this.internalProps.columns = cloneDeepSpec(currentColumns, ['children']); - this.options.columns = currentColumns; + const visibleColumns = this.internalProps.layoutMap.columnObjects.map(column => column.define); + let visibleIndex = 0; + const nextColumns = this.internalProps.columns.map(column => { + if (column.hide === true) { + return column; + } + const nextVisibleColumn = visibleColumns[visibleIndex]; + visibleIndex += 1; + return nextVisibleColumn ?? column; + }); + this.internalProps.columns = cloneDeepSpec(nextColumns, ['children']); + this.options.columns = nextColumns; if (this.options.header) { - this.options.header = currentColumns; + this.options.header = nextColumns; } } changeRecordOrder(sourceIndex: number, targetIndex: number) { From 8f79ddfb009063d2b9caa93ad452b97ca6fe58bc Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 20 May 2026 10:27:41 +0800 Subject: [PATCH 5/6] docs: update changlog of rush --- .rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc | 23 ++++++++++++++ .../pnpm-10.7.0/last-install.flag | 3 ++ .../pnpm-10.7.0/package-lock.json | 31 +++++++++++++++++++ .../node-v24.11.0/pnpm-10.7.0/package.json | 9 ++++++ ...-filter-column-order_2026-05-20-02-27.json | 11 +++++++ 5 files changed, 77 insertions(+) create mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc create mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag create mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json create mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/package.json create mode 100644 common/changes/@visactor/vtable/fix-issue-5137-filter-column-order_2026-05-20-02-27.json diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc b/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc new file mode 100644 index 000000000..7db0e7b3d --- /dev/null +++ b/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc @@ -0,0 +1,23 @@ +# Rush uses this file to configure the NPM package registry during installation. It is applicable +# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", +# "rush update", and the "install-run.js" scripts. +# +# NOTE: The "rush publish" command uses .npmrc-publish instead. +# +# Before invoking the package manager, Rush will copy this file to the folder where installation +# is performed. The copied file will omit any config lines that reference environment variables +# that are undefined in that session; this avoids problems that would otherwise result due to +# a missing variable being replaced by an empty string. +# +# * * * SECURITY WARNING * * * +# +# It is NOT recommended to store authentication tokens in a text file on a lab machine, because +# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, +# for example if the machine loses power. A safer practice is to pass the token via an +# environment variable, which can be referenced from .npmrc using ${} expansion. For example: +# +# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} +# + +registry=https://registry.npmjs.org/ +always-auth=false diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag b/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag new file mode 100644 index 000000000..0aae51e4a --- /dev/null +++ b/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag @@ -0,0 +1,3 @@ +{ + "node": "24.11.0" +} diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json b/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json new file mode 100644 index 000000000..c810d8aea --- /dev/null +++ b/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "pnpm-local-install", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pnpm-local-install", + "version": "0.0.0", + "dependencies": { + "pnpm": "10.7.0" + } + }, + "node_modules/pnpm": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.7.0.tgz", + "integrity": "sha512-a4Za1LYqHZhCth1nSjk5A7hx2SRJVPZSuIQsK1U8chdrJ49kxGPlLUD/+KujhcI1yMns9cx95P14uLttSWM6tg==", + "license": "MIT", + "bin": { + "pnpm": "bin/pnpm.cjs", + "pnpx": "bin/pnpx.cjs" + }, + "engines": { + "node": ">=18.12" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + } + } +} diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json b/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json new file mode 100644 index 000000000..fe8ad1694 --- /dev/null +++ b/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "pnpm": "10.7.0" + }, + "description": "Temporary file generated by the Rush tool", + "name": "pnpm-local-install", + "private": true, + "version": "0.0.0" +} diff --git a/common/changes/@visactor/vtable/fix-issue-5137-filter-column-order_2026-05-20-02-27.json b/common/changes/@visactor/vtable/fix-issue-5137-filter-column-order_2026-05-20-02-27.json new file mode 100644 index 000000000..2ebebd87e --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-5137-filter-column-order_2026-05-20-02-27.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: preserve hidden column order after filter update\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file From 7dabfc7d14e844bbbf3c5bfd9eb3595440d65a30 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 20 May 2026 10:28:42 +0800 Subject: [PATCH 6/6] chore: remove local rush artifacts --- .rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc | 23 -------------- .../pnpm-10.7.0/last-install.flag | 3 -- .../pnpm-10.7.0/package-lock.json | 31 ------------------- .../node-v24.11.0/pnpm-10.7.0/package.json | 9 ------ 4 files changed, 66 deletions(-) delete mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc delete mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag delete mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json delete mode 100644 .rush-global/node-v24.11.0/pnpm-10.7.0/package.json diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc b/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc deleted file mode 100644 index 7db0e7b3d..000000000 --- a/.rush-global/node-v24.11.0/pnpm-10.7.0/.npmrc +++ /dev/null @@ -1,23 +0,0 @@ -# Rush uses this file to configure the NPM package registry during installation. It is applicable -# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", -# "rush update", and the "install-run.js" scripts. -# -# NOTE: The "rush publish" command uses .npmrc-publish instead. -# -# Before invoking the package manager, Rush will copy this file to the folder where installation -# is performed. The copied file will omit any config lines that reference environment variables -# that are undefined in that session; this avoids problems that would otherwise result due to -# a missing variable being replaced by an empty string. -# -# * * * SECURITY WARNING * * * -# -# It is NOT recommended to store authentication tokens in a text file on a lab machine, because -# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, -# for example if the machine loses power. A safer practice is to pass the token via an -# environment variable, which can be referenced from .npmrc using ${} expansion. For example: -# -# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} -# - -registry=https://registry.npmjs.org/ -always-auth=false diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag b/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag deleted file mode 100644 index 0aae51e4a..000000000 --- a/.rush-global/node-v24.11.0/pnpm-10.7.0/last-install.flag +++ /dev/null @@ -1,3 +0,0 @@ -{ - "node": "24.11.0" -} diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json b/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json deleted file mode 100644 index c810d8aea..000000000 --- a/.rush-global/node-v24.11.0/pnpm-10.7.0/package-lock.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "pnpm-local-install", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "pnpm-local-install", - "version": "0.0.0", - "dependencies": { - "pnpm": "10.7.0" - } - }, - "node_modules/pnpm": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.7.0.tgz", - "integrity": "sha512-a4Za1LYqHZhCth1nSjk5A7hx2SRJVPZSuIQsK1U8chdrJ49kxGPlLUD/+KujhcI1yMns9cx95P14uLttSWM6tg==", - "license": "MIT", - "bin": { - "pnpm": "bin/pnpm.cjs", - "pnpx": "bin/pnpx.cjs" - }, - "engines": { - "node": ">=18.12" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - } - } -} diff --git a/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json b/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json deleted file mode 100644 index fe8ad1694..000000000 --- a/.rush-global/node-v24.11.0/pnpm-10.7.0/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "pnpm": "10.7.0" - }, - "description": "Temporary file generated by the Rush tool", - "name": "pnpm-local-install", - "private": true, - "version": "0.0.0" -}