Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@visactor/vtable-plugins",
"comment": "fix: keep master detail expandable after list table setRecords",
"type": "patch"
}
],
"packageName": "@visactor/vtable-plugins",
"email": "892739385@qq.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as VTable from '@visactor/vtable';
import { MasterDetailPlugin } from '../../src';

const CONTAINER_ID = 'vTable';

const columns: VTable.ColumnsDefine = [
{ field: 'name', title: 'Name', width: 180 },
{ field: 'department', title: 'Department', width: 160 },
{ field: 'status', title: 'Status', width: 120 }
];

const detailColumns: VTable.ColumnsDefine = [
{ field: 'project', title: 'Project', width: 180 },
{ field: 'role', title: 'Role', width: 140 }
];

const createRecords = (prefix: string) => [
{
id: `${prefix}-1`,
name: `${prefix} Employee 1`,
department: 'Engineering',
status: 'Active',
children: [
{ project: `${prefix} Project A`, role: 'Owner' },
{ project: `${prefix} Project B`, role: 'Reviewer' }
]
},
{
id: `${prefix}-2`,
name: `${prefix} Employee 2`,
department: 'Design',
status: 'Active',
children: [{ project: `${prefix} Project C`, role: 'Designer' }]
}
];

const createStatusBar = () => {
const container = document.getElementById(CONTAINER_ID)!;
const status = document.createElement('div');
status.id = 'issue5185Status';
status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;';
status.textContent = 'Click "Check setRecords expand" to verify issue #5185.';

const button = document.createElement('button');
button.textContent = 'Check setRecords expand';
button.style.cssText = 'margin: 0 0 8px 8px;';
button.onclick = () => checkSetRecordsExpand();

container.parentElement?.insertBefore(status, container);
status.appendChild(button);
};

const getSubTableCount = (tableInstance: VTable.ListTable) =>
((tableInstance as any).internalProps.subTableInstances as Map<number, VTable.ListTable>)?.size ?? 0;

const checkSetRecordsExpand = () => {
const tableInstance = (window as any).tableInstance as VTable.ListTable;
const status = document.getElementById('issue5185Status')!;

tableInstance.setRecords(createRecords('After'));
tableInstance.toggleHierarchyState(0, tableInstance.columnHeaderLevelCount);

const subTableCount = getSubTableCount(tableInstance);
const firstRecord = tableInstance.records?.[0] as any;
const pass = subTableCount > 0 && firstRecord?.hierarchyState === VTable.TYPES.HierarchyState.expand;

status.textContent = `${pass ? 'PASS' : 'FAIL'} | subTableCount=${subTableCount}, hierarchyState=${
firstRecord?.hierarchyState
}`;
return status.textContent;
};

export function createTable() {
const option: VTable.ListTableConstructorOptions = {
records: createRecords('Initial'),
columns,
widthMode: 'standard',
defaultRowHeight: 36,
plugins: [
new MasterDetailPlugin({
detailTableOptions: {
columns: detailColumns,
heightMode: 'autoHeight',
defaultRowHeight: 30,
style: {
height: 90
}
}
})
]
};

createStatusBar();
const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID)!, option);
(window as any).tableInstance = tableInstance;
(window as any).issue5185Run = checkSetRecordsExpand;
}
4 changes: 4 additions & 0 deletions packages/vtable-plugins/demo/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ export const menus = [
path: 'master-detail-plugin',
name: 'master-detail-plugin9'
},
{
path: 'master-detail-plugin',
name: 'issue-5185-set-records-expand'
},
{
menu: 'pivot-plugin',
children: [
Expand Down
20 changes: 15 additions & 5 deletions packages/vtable-plugins/src/master-detail-plugin/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { DetailTableOptions, MasterDetailPluginOptions } from './types';
export class ConfigManager {
private expandRowCallback?: (rowIndex: number) => void;
private childrenKey: string;
private expansionVersion = 0;

constructor(private pluginOptions: MasterDetailPluginOptions, private table: VTable.ListTable) {
this.childrenKey = pluginOptions.childrenKey || 'children';
Expand Down Expand Up @@ -161,7 +162,8 @@ export class ConfigManager {
/**
* 处理记录的层级状态
*/
private processRecordsHierarchyStates(records: unknown[]): void {
processRecordsHierarchyStates(records: unknown[], expandInitialRows: boolean = true): void {
const expansionVersion = ++this.expansionVersion;
const HierarchyState = VTable.TYPES.HierarchyState;
// 兼容处理headerExpandLevel
const hierarchyExpandLevel = this.table.options.hierarchyExpandLevel || this.table.options.headerExpandLevel;
Expand Down Expand Up @@ -193,22 +195,24 @@ export class ConfigManager {
});
};
processRecords(records);
this.performInitialExpansion();
if (expandInitialRows) {
this.performInitialExpansion(expansionVersion);
}
}

/**
* 遍历所有记录,根据 hierarchyState 状态执行初始展开
* 与VTable的异步CellGroup创建过程同步,在每个CellGroup创建后检查是否需要展开
*/
private performInitialExpansion(): void {
private performInitialExpansion(expansionVersion: number): void {
// 获取需要展开的记录索引列表
const expandableRecords = this.getExpandableRecords();
if (expandableRecords.length === 0) {
return;
}

// 开始异步展开过程,与VTable的渲染频率同步
this.startAsyncExpansion(expandableRecords);
this.startAsyncExpansion(expandableRecords, expansionVersion);
}

/**
Expand Down Expand Up @@ -268,11 +272,16 @@ export class ConfigManager {
* 开始异步展开过程,与VTable的异步渲染同步
*/
private startAsyncExpansion(
expandableRecords: Array<{ recordIndex: number; actualRowIndex: number; record: unknown }>
expandableRecords: Array<{ recordIndex: number; actualRowIndex: number; record: unknown }>,
expansionVersion: number
): void {
let currentIndex = 0;

const processNextExpansion = (): void => {
if (expansionVersion !== this.expansionVersion) {
return;
}

if (currentIndex >= expandableRecords.length) {
return; // 所有展开操作完成
}
Expand Down Expand Up @@ -337,6 +346,7 @@ export class ConfigManager {
* 释放所有资源和引用
*/
release(): void {
this.expansionVersion++;
this.isRowExpanded = () => false;
// 清理对表格的引用
(this as unknown as { table: VTable.ListTable | null }).table = null;
Expand Down
31 changes: 27 additions & 4 deletions packages/vtable-plugins/src/master-detail-plugin/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,39 @@ export class MasterDetailPlugin implements pluginsDefinition.IVTablePlugin {
collapseRow: (rowIndex: number) => this.collapseRow(rowIndex),
updateSubTablePositions: () => this.subTableManager.recalculateAllSubTablePositions(),
updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) =>
this.updateRowHeightForExpand(rowIndex, deltaHeight)
this.updateRowHeightForExpand(rowIndex, deltaHeight),
resetMasterDetailStateBeforeSetRecords: () => this.resetMasterDetailStateBeforeSetRecords()
});

// 执行API扩展
this.tableAPIExtensions.extendTableAPI();
}

/**
* setRecords 前清理旧主从表状态,避免新数据复用旧展开行和子表实例
*/
private resetMasterDetailStateBeforeSetRecords(): void {
const internalProps = getInternalProps(this.table);
const expandedRows = [...this.eventManager.getExpandedRows()];
expandedRows.forEach(rowIndex => {
try {
this.collapseRowToNoRealRecordIndex(rowIndex);
} catch (error) {
console.warn(`Failed to collapse master detail row ${rowIndex} before setRecords:`, error);
}
});

const subTableRowIndices = Array.from(internalProps.subTableInstances?.keys() ?? []);
subTableRowIndices.forEach(bodyRowIndex => {
this.subTableManager.removeSubTable(bodyRowIndex);
});

internalProps.expandedRecordIndices?.splice(0);
internalProps.originalRowHeights?.clear();
internalProps.subTableCheckboxStates?.clear();
this.eventManager.setExpandedRows([]);
}

/**
* 在 adaptive 处理后更新原始高度缓存
*/
Expand Down Expand Up @@ -266,9 +292,6 @@ export class MasterDetailPlugin implements pluginsDefinition.IVTablePlugin {
this.updateRowHeightForExpand(rowIndex, deltaHeight);
this.table.scenegraph.updateContainerHeight(rowIndex, deltaHeight);
internalProps._heightResizedRowMap.add(rowIndex);
if (rowIndex === 96) {
console.log('wokk');
}
this.subTableManager.renderSubTable(bodyRowIndex, childrenData, (record, bodyRowIndex) =>
this.configManager.getDetailConfigForRecord(record, bodyRowIndex)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class TableAPIExtensions {
private originalUpdateChartSizeForResizeColWidth?: (col: number) => void;
private originalUpdateChartSizeForResizeRowHeight?: (row: number) => void;
private originalUpdateRowHeight?: (row: number, detaY: number, skipTableHeightMap?: boolean) => void;
private originalSetRecords?: (records: Array<any>, option?: Parameters<VTable.ListTable['setRecords']>[1]) => void;
private originalGetResizeColAt?: (
abstractX: number,
abstractY: number,
Expand Down Expand Up @@ -50,6 +51,7 @@ export class TableAPIExtensions {
collapseRow: (rowIndex: number) => void;
updateSubTablePositions: () => void;
updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) => void;
resetMasterDetailStateBeforeSetRecords: () => void;
};

constructor(
Expand All @@ -67,6 +69,7 @@ export class TableAPIExtensions {
collapseRow: (rowIndex: number) => void;
updateSubTablePositions: () => void;
updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) => void;
resetMasterDetailStateBeforeSetRecords: () => void;
}
) {
this.table = table;
Expand Down Expand Up @@ -140,6 +143,8 @@ export class TableAPIExtensions {
this.extendUpdateRowHeight();
// 处理展开行的列宽调整检测
this.extendGetResizeColAt();
// 处理 setRecords 后主从表层级状态重建
this.extendSetRecords();
}

/**
Expand Down Expand Up @@ -197,6 +202,24 @@ export class TableAPIExtensions {
};
}

/**
* 扩展 setRecords 方法
*/
private extendSetRecords(): void {
const table = this.table;
this.originalSetRecords = table.setRecords.bind(table);
table.setRecords = (records: Array<any>, option?: Parameters<VTable.ListTable['setRecords']>[1]) => {
this.callbacks.resetMasterDetailStateBeforeSetRecords();
if (Array.isArray(records)) {
this.configManager.processRecordsHierarchyStates(records, false);
}
this.originalSetRecords?.(records, option);
if (Array.isArray(records)) {
this.configManager.processRecordsHierarchyStates(records);
}
};
}

/**
* 扩展 updateResizeRow 方法
*/
Expand Down
Loading