From b60f04ea616c4dd593621efbc289ccd21366e654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=AD=90=E5=87=A1?= <113765024+xiaou61@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:29:15 +0800 Subject: [PATCH] fix(grid): keep cloned rows next to their source --- .../__tests__/useDataGridEditor.spec.ts | 65 ++++++++++++++++++- .../src/composables/useDataGridEditor.ts | 8 ++- packages/app-tests/dataGridEditor.test.ts | 2 +- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts index cb85f2aca8..740068109f 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts @@ -1,4 +1,4 @@ -import { computed, ref, type Ref } from "vue"; +import { computed, nextTick, ref, type Ref } from "vue"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { clearDataGridPendingSnapshot, DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID, useDataGridEditor } from "@/composables/useDataGridEditor"; import type { CellValue } from "@/lib/dataGrid/cellValue"; @@ -514,6 +514,69 @@ describe("useDataGridEditor appendPastedRowsToNewRow", () => { expect(editor.newRows.value[1]).toEqual(["Ada", "full payload", "Lovelace"]); }); + + it("places a cloned source row below its source row", () => { + const editor = createEditor(undefined, true, undefined, undefined, [ + ["Ada", null, "Lovelace"], + ["Grace", null, "Hopper"], + ]); + editor.newRows.value = []; + editor.newRowMeta.value = []; + + editor.cloneRow(1); + + expect(editor.newRowMeta.value[0]?.placement).toEqual({ anchorId: 1, position: "below" }); + }); + + it("keeps the scroll position when the cloned row anchors below its source row", async () => { + class ScrollerStub { + scrollTop = 0; + scrollHeight = 4_000; + } + vi.stubGlobal("HTMLElement", ScrollerStub); + const editor = createEditor(undefined, true, undefined, undefined, [ + ["Ada", null, "Lovelace"], + ["Grace", null, "Hopper"], + ]); + editor.newRows.value = []; + editor.newRowMeta.value = []; + const scroller = new ScrollerStub(); + editor.scrollerRef.value = scroller as unknown as NonNullable; + + editor.cloneRow(1); + await nextTick(); + vi.unstubAllGlobals(); + + expect(editor.newRowMeta.value[0]?.placement).toEqual({ anchorId: 1, position: "below" }); + expect(scroller.scrollTop).toBe(0); + }); + + it("places a cloned pending row below the pending source row", () => { + const editor = createEditor(); + editor.newRows.value = []; + editor.newRowMeta.value = []; + editor.addRows(1); + + editor.cloneRow(-1); + + expect(editor.newRowMeta.value[1]?.placement).toEqual({ anchorId: -1, position: "below" }); + }); + + it("places each multi-row clone below its corresponding source row", () => { + const editor = createEditor(undefined, true, undefined, undefined, [ + ["Ada", null, "Lovelace"], + ["Grace", null, "Hopper"], + ]); + editor.newRows.value = []; + editor.newRowMeta.value = []; + + editor.cloneRows([1, 0]); + + expect(editor.newRowMeta.value.map((meta) => meta.placement)).toEqual([ + { anchorId: 1, position: "below" }, + { anchorId: 0, position: "below" }, + ]); + }); }); describe("useDataGridEditor saveChanges reload", () => { diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index d7fac098c5..a291e52337 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -272,7 +272,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { if (value !== baseline[column]) edited.add(column); }); } - return allocateNewRowMeta(null, sourceIndex, [...edited]); + const placement = item.isNew && item.newIndex !== undefined ? (inherited ? { anchorId: -inherited.token, position: "below" as const } : null) : sourceIndex === undefined ? null : { anchorId: sourceIndex, position: "below" as const }; + return allocateNewRowMeta(placement, sourceIndex, [...edited]); } // Restore a metadata snapshot and resume token allocation past its maximum so // newly created rows never collide with tokens held by restored rows (a fresh @@ -1218,7 +1219,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { pushUndoSnapshot(); rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value); newRows.value.push(clonedData); - newRowMeta.value.push(clonedRowMeta(item, clonedData)); + const clonedMeta = clonedRowMeta(item, clonedData); + newRowMeta.value.push(clonedMeta); newRows.value = [...newRows.value]; newRowMeta.value = [...newRowMeta.value]; touchPendingChanges(); @@ -1228,7 +1230,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { const newRowId = -newRows.value.length; nextTick(() => { const el = getScrollerElement(); - if (el) el.scrollTop = el.scrollHeight; + if (clonedMeta.placement === null && el) el.scrollTop = el.scrollHeight; startEdit(newRowId, initialEditColumn?.value ?? 0); }); } diff --git a/packages/app-tests/dataGridEditor.test.ts b/packages/app-tests/dataGridEditor.test.ts index ed6f6008f9..1ceb0a8e31 100644 --- a/packages/app-tests/dataGridEditor.test.ts +++ b/packages/app-tests/dataGridEditor.test.ts @@ -431,7 +431,7 @@ test("cloning a row preserves its source and edited columns for custom saves", a assert.deepEqual(savedMeta, [ { token: 1, - placement: null, + placement: { anchorId: 0, position: "below" }, sourceIndex: 0, editedColumns: [1], },