fix(grid): auto expand new rows for multi row paste

This commit is contained in:
jearhe
2026-09-01 12:29:22 +08:00
committed by GitHub
parent 9de152cf72
commit d59b86bd57
2 changed files with 117 additions and 13 deletions
@@ -8729,14 +8729,15 @@ function batchAppendPasteError(reason: string): string {
return t(messages[reason] ?? "grid.batchAppendPasteInvalidTarget");
}
function blankCellBatchAppendPasteTarget(pastedRows: readonly (readonly (string | null)[])[]): { rowId: number; columnIndexes: number[] } | null {
function blankSelectionBatchAppendPasteTarget(pastedRows: readonly (readonly (string | null)[])[]): { rowId: number; columnIndexes: number[] } | null {
if (pastedRows.length <= 1) return null;
const range = selectedRange.value;
if (!range || range.startRow !== range.endRow || range.startCol !== range.endCol) return null;
const item = displayItemAt(range.startRow);
if ((!item?.isNew && !item?.isDraft) || item.isDeleted || item.data.some((value) => value !== null && (typeof value !== "string" || value.trim() !== ""))) return null;
if (!range || (range.startRow === range.endRow && range.startCol !== range.endCol)) return null;
const selectedItems = Array.from({ length: range.endRow - range.startRow + 1 }, (_, offset) => displayItemAt(range.startRow + offset));
if (selectedItems.some((item) => (!item?.isNew && !item?.isDraft) || item.isDeleted || item.data.some((value) => value !== null && (typeof value !== "string" || value.trim() !== "")))) return null;
const item = selectedItems[0];
const columnIndex = visibleColumnIndexes.value[range.startCol];
if (columnIndex === undefined || !canEditCellItem(item, columnIndex)) return null;
if (!item || columnIndex === undefined || !canEditCellItem(item, columnIndex)) return null;
return { rowId: item.id, columnIndexes: visibleColumnIndexes.value.slice(range.startCol) };
}
@@ -8760,7 +8761,7 @@ function pasteTextIntoGrid(text: string): boolean {
if (targetRowId !== null) {
return appendParsedRowsToBlankTarget(targetRowId, rows, visibleColumnIndexes.value);
}
const cellTarget = blankCellBatchAppendPasteTarget(rows);
const cellTarget = blankSelectionBatchAppendPasteTarget(rows);
if (cellTarget) return appendParsedRowsToBlankTarget(cellTarget.rowId, rows, cellTarget.columnIndexes);
return pasteRowsIntoSelection(rows);
}
@@ -180,6 +180,90 @@ afterEach(() => {
});
describe("DataGrid multi-row paste from a blank cell", () => {
it("expands beyond pre-added blank rows when pasted rows exceed them", async () => {
const { host } = mountGrid();
await settle();
for (let index = 0; index < 5; index++) await addBlankRow(host);
const blankRows = pendingRows(host);
expect(blankRows).toHaveLength(5);
await selectCell(visibleCells(blankRows[0]!)[0]!);
await selectCell(visibleCells(blankRows[4]!)[0]!, { shiftKey: true });
await paste(host, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
const rows = pendingRows(host);
expect(rows).toHaveLength(10);
expect(rows.map((row) => visibleCellTexts(row)[0])).toEqual(["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]);
expect(visibleCellTexts(rows.at(-1)!)[0]).toBe("10");
});
it("reuses exactly enough pre-added blank rows without adding an extra row", async () => {
const { host } = mountGrid();
await settle();
for (let index = 0; index < 10; index++) await addBlankRow(host);
const blankRows = pendingRows(host);
expect(blankRows).toHaveLength(10);
await selectCell(visibleCells(blankRows[0]!)[0]!);
await selectCell(visibleCells(blankRows[9]!)[0]!, { shiftKey: true });
await paste(host, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
const rows = pendingRows(host);
expect(rows).toHaveLength(10);
expect(rows.map((row) => visibleCellTexts(row)[0])).toEqual(["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]);
expect(visibleCellTexts(rows.at(-1)!)[0]).toBe("10");
});
it("keeps extra pre-added blank rows when the clipboard has fewer rows", async () => {
const { host } = mountGrid();
await settle();
for (let index = 0; index < 10; index++) await addBlankRow(host);
const blankRows = pendingRows(host);
await selectCell(visibleCells(blankRows[0]!)[0]!);
await selectCell(visibleCells(blankRows[9]!)[0]!, { shiftKey: true });
await paste(host, "1\n2\n3\n4\n5");
const rows = pendingRows(host);
expect(rows).toHaveLength(10);
expect(visibleCellTexts(rows[0]!)[0]).toBe("1");
expect(visibleCellTexts(rows[4]!)[0]).toBe("5");
expect(visibleCellTexts(rows[5]!)[0]).toBe("NULL");
expect(visibleCellTexts(rows[9]!)[0]).toBe("NULL");
});
it("keeps single-cell auto expansion with pre-added blank rows", async () => {
const { host } = mountGrid();
await settle();
for (let index = 0; index < 5; index++) await addBlankRow(host);
const blankRows = pendingRows(host);
await selectCell(visibleCells(blankRows[0]!)[0]!);
await paste(host, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
const rows = pendingRows(host);
expect(rows).toHaveLength(10);
expect(rows.map((row) => visibleCellTexts(row)[0])).toEqual(["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]);
expect(visibleCellTexts(rows.at(-1)!)[0]).toBe("10");
});
it("preserves visible column mapping for a multi-row selection", async () => {
const { host } = mountGrid({ hideNullColumns: true });
await settle();
for (let index = 0; index < 5; index++) await addBlankRow(host);
const blankRows = pendingRows(host);
await selectCell(visibleCells(blankRows[0]!)[1]!);
await selectCell(visibleCells(blankRows[4]!)[1]!, { shiftKey: true });
const clipboardRows = Array.from({ length: 10 }, (_, index) => `value-${index + 1}\tnote-${index + 1}`).join("\n");
await paste(host, clipboardRows);
const rows = pendingRows(host);
expect(rows).toHaveLength(10);
expect(visibleCellTexts(rows[0]!)).toEqual(["NULL", "value-1", "note-1"]);
expect(visibleCellTexts(rows.at(-1)!)).toEqual(["NULL", "value-10", "note-10"]);
});
it("starts at the selected visible column, skips hidden columns, and appends rows", async () => {
const { host } = mountGrid({ hideNullColumns: true });
await settle();
@@ -218,17 +302,18 @@ describe("DataGrid multi-row paste from a blank cell", () => {
it("keeps row-number paste priority and starts from the first visible column", async () => {
const { host } = mountGrid({ hideNullColumns: true });
await settle();
await addBlankRow(host);
for (let index = 0; index < 5; index++) await addBlankRow(host);
const [blankRow] = pendingRows(host);
expect(blankRow).toBeDefined();
await selectRowNumber(blankRow!);
await paste(host, "10\t20\n30\t40");
const blankRows = pendingRows(host);
expect(blankRows).toHaveLength(5);
await selectRowNumber(blankRows[0]!);
const clipboardRows = Array.from({ length: 10 }, (_, index) => `${index + 10}\t${index + 20}`).join("\n");
await paste(host, clipboardRows);
const rows = pendingRows(host);
expect(rows).toHaveLength(2);
expect(rows).toHaveLength(10);
expect(visibleCellTexts(rows[0]!)).toEqual(["10", "20", "NULL"]);
expect(visibleCellTexts(rows[1]!)).toEqual(["30", "40", "NULL"]);
expect(visibleCellTexts(rows.at(-1)!)).toEqual(["19", "29", "NULL"]);
});
it("keeps nonblank new rows on the ordinary bounded paste path", async () => {
@@ -248,6 +333,24 @@ describe("DataGrid multi-row paste from a blank cell", () => {
expect(visibleCellTexts(rows[0]!)[2]).toBe("first");
});
it("does not expand a multi-row selection containing a nonblank new row", async () => {
const { host } = mountGrid();
await settle();
for (let index = 0; index < 5; index++) await addBlankRow(host);
let rows = pendingRows(host);
await selectCell(visibleCells(rows[0]!)[2]!);
await paste(host, "occupied");
rows = pendingRows(host);
await selectCell(visibleCells(rows[0]!)[2]!);
await selectCell(visibleCells(rows[4]!)[2]!, { shiftKey: true });
await paste(host, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
rows = pendingRows(host);
expect(rows).toHaveLength(5);
expect(visibleCellTexts(rows.at(-1)!)[2]).toBe("5");
});
it("keeps existing rows on the ordinary bounded paste path", async () => {
const { host } = mountGrid();
await settle();