From 1e6ac0ccff97702c3f29857cb20c3ce9bd5e2b01 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 9 Jul 2026 21:17:37 +0800 Subject: [PATCH] fix(grid): batch row delete updates --- apps/desktop/src/components/grid/DataGrid.vue | 11 ++- .../src/composables/useDataGridEditor.ts | 59 +++++++++---- packages/app-tests/dataGridEditor.test.ts | 86 +++++++++++++++++++ 3 files changed, 139 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 020fe54327..03ecef0080 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -4293,6 +4293,14 @@ const displayRowRefs = computed(() => { const displayRowCount = computed(() => displayRowRefs.value.length); +const displayRowIndexByIdLookup = computed(() => { + const lookup = new Map(); + displayRowRefs.value.forEach((ref, index) => { + lookup.set(ref.id, index); + }); + return lookup; +}); + function rowItemFromDisplayRef(ref: DisplayRowRef): RowItem { if (ref.isNew) { return { @@ -4324,7 +4332,8 @@ function displayItemAt(rowIndex: number): RowItem | undefined { } function displayRowIndexById(rowId: number): number { - return displayRowRefs.value.findIndex((ref) => ref.id === rowId); + // Multi-row actions call getRowItem for each selected row; keep that lookup O(1). + return displayRowIndexByIdLookup.value.get(rowId) ?? -1; } const displayItems = computed(() => displayRowRefs.value.map(rowItemFromDisplayRef)); diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index d85e031105..04bbf31260 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -900,29 +900,57 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { } } - function applyDeleteRow(rowId: number) { - const item = getRowItem(rowId); - if (!item) return; - if (item.isNew && item.newIndex !== undefined) { - if (isSavingNewRow(item)) return; - pushUndoSnapshot(); - newRows.value.splice(item.newIndex, 1); + function applyDeleteRows(rowIds: number[]) { + const items = rowIds.map((rowId) => getRowItem(rowId)).filter((item): item is RowItem => !!item); + if (items.length === 0) return; + + const newIndexes = new Set(); + const sourceIndexes = new Set(); + const deletedRowIds = new Set(); + + for (const item of items) { + if (item.isNew && item.newIndex !== undefined) { + if (isSavingNewRow(item)) continue; + newIndexes.add(item.newIndex); + deletedRowIds.add(item.id); + } else if (item.sourceIndex !== undefined && canEditExistingRows.value) { + sourceIndexes.add(item.sourceIndex); + deletedRowIds.add(item.id); + } + } + + if (newIndexes.size === 0 && sourceIndexes.size === 0) return; + + // Batch row deletion into one reactive update so multi-row deletes do not + // rebuild the entire grid and undo history once per selected row. + pushUndoSnapshot(); + if (newIndexes.size > 0) { + [...newIndexes] + .sort((a, b) => b - a) + .forEach((newIndex) => { + newRows.value.splice(newIndex, 1); + }); newRows.value = [...newRows.value]; - } else if (item.sourceIndex !== undefined) { - if (!canEditExistingRows.value) return; - pushUndoSnapshot(); - dirtyRows.value.delete(item.sourceIndex); - deletedRows.value.add(item.sourceIndex); + } + if (sourceIndexes.size > 0) { + for (const sourceIndex of sourceIndexes) { + dirtyRows.value.delete(sourceIndex); + deletedRows.value.add(sourceIndex); + } dirtyRows.value = new Map(dirtyRows.value); deletedRows.value = new Set(deletedRows.value); } touchPendingChanges(); - if (editingCell.value?.rowId === rowId) editingCell.value = null; + if (editingCell.value && deletedRowIds.has(editingCell.value.rowId)) editingCell.value = null; if (useTransaction.value && !transactionActive.value) { enterTransaction(); } } + function applyDeleteRow(rowId: number) { + applyDeleteRows([rowId]); + } + const showDeleteRowConfirm = ref(false); const pendingDeleteRowId = ref(null); const pendingDeleteRowIds = ref([]); @@ -939,9 +967,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { function confirmDeleteRow() { if (pendingDeleteRowIds.value.length > 0) { - for (const rowId of pendingDeleteRowIds.value) { - applyDeleteRow(rowId); - } + applyDeleteRows(pendingDeleteRowIds.value); pendingDeleteRowIds.value = []; return; } @@ -1404,6 +1430,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { addRow, cloneRow, cloneRows, + applyDeleteRows, applyDeleteRow, showDeleteRowConfirm, pendingDeleteRowId, diff --git a/packages/app-tests/dataGridEditor.test.ts b/packages/app-tests/dataGridEditor.test.ts index 8ac6ffb30a..07dc86f5f0 100644 --- a/packages/app-tests/dataGridEditor.test.ts +++ b/packages/app-tests/dataGridEditor.test.ts @@ -662,6 +662,92 @@ test("undo and redo cover row add and delete operations", () => { assert.deepEqual([...editor.deletedRows.value], [0]); }); +test("batch row delete records a single undo snapshot", () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + + const result = computed(() => ({ + columns: ["id", "name"], + rows: [ + [1, "Ada"] as CellValue[], + [2, "Linus"] as CellValue[], + [3, "Grace"] as CellValue[], + ], + })); + const rowStatusFilter = ref<"all" | "changed" | "edited" | "new" | "deleted">("all"); + let editor: ReturnType; + + editor = useDataGridEditor({ + result, + editable: computed(() => true), + databaseType: computed(() => "postgres"), + connectionId: computed(() => undefined), + database: computed(() => undefined), + tableMeta: computed(() => ({ + tableName: "people", + columns: [column("id", true), column("name")], + primaryKeys: ["id"], + })), + onExecuteSql: computed(() => undefined), + customSaveHandler: computed(() => undefined), + sql: computed(() => "SELECT id, name FROM people"), + searchText: ref(""), + whereFilterInput: ref(""), + orderByInput: ref(""), + currentWhereInput: computed(() => undefined), + rowStatusFilter, + pageSize: ref(50), + currentPage: ref(1), + getRowItem: (rowId) => { + if (rowId >= 0 && result.value.rows[rowId]) { + return { + id: rowId, + sourceIndex: rowId, + data: result.value.rows[rowId], + isNew: false, + isDeleted: editor.deletedRows.value.has(rowId), + isDirtyCol: [false, editor.dirtyRows.value.get(rowId)?.has(1) ?? false], + status: editor.deletedRows.value.has(rowId) ? "deleted" : editor.dirtyRows.value.has(rowId) ? "edited" : "clean", + }; + } + if (rowId < 0) { + const newIndex = -rowId - 1; + const row = editor.newRows.value[newIndex]; + if (!row) return undefined; + return { + id: rowId, + newIndex, + data: row, + isNew: true, + isDeleted: false, + isDirtyCol: [false, false], + status: "new", + }; + } + return undefined; + }, + emit: () => {}, + }); + + editor.applyDeleteRows([0, 1]); + assert.deepEqual([...editor.deletedRows.value], [0, 1]); + + editor.undoPendingChange(); + assert.deepEqual([...editor.deletedRows.value], []); + assert.equal(editor.canUndoPendingChange.value, false); + + editor.redoPendingChange(); + assert.deepEqual([...editor.deletedRows.value], [0, 1]); + + editor.newRows.value = [ + [4, "Katherine"], + [5, "Margaret"], + [6, "Donald"], + ]; + editor.applyDeleteRows([-1, -2]); + assert.deepEqual(editor.newRows.value, [[6, "Donald"]]); +}); + test("undo and redo restore pending cell edits before save", () => { setActivePinia(createPinia()); installBrowserTestGlobals();