fix(grid): clear identity columns when pasting new rows

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-05 22:19:05 +08:00
1 parent bc2d32f20f
commit 3c3ab96421
2 files changed
+54 -6

No files matched your search

@@ -36,7 +36,15 @@ vi.mock("@/stores/productionSafetyStore", () => ({
useProductionSafetyStore: () => ({}),
}));
function createEditor(sourceColumns?: Array<string | undefined>, confirmDangerousRowDeletion = true, cacheKey?: string, readonlyColumnIndexes?: number[], existingRows: CellValue[][] = [], onCellValueChanged?: (rowId: number, columnIndex: number) => void) {
function createEditor(
sourceColumns?: Array<string | undefined>,
confirmDangerousRowDeletion = true,
cacheKey?: string,
readonlyColumnIndexes?: number[],
existingRows: CellValue[][] = [],
onCellValueChanged?: (rowId: number, columnIndex: number) => void,
tableColumns?: Array<{ name: string; data_type: string; extra?: string; column_default?: string }>,
) {
let editor: ReturnType<typeof useDataGridEditor>;
const result = ref<{ columns: string[]; rows: CellValue[][] }>({
columns: ["first", "hidden", "last"],
@@ -51,7 +59,7 @@ function createEditor(sourceColumns?: Array<string | undefined>, confirmDangerou
database: computed(() => "app"),
tableMeta: computed(() => ({
tableName: "people",
columns: [
columns: tableColumns ?? [
{ name: "first", data_type: "varchar" },
{ name: "hidden", data_type: "varchar" },
{ name: "last", data_type: "varchar" },
@@ -388,6 +396,29 @@ describe("useDataGridEditor appendPastedRowsToNewRow", () => {
expect(editor.hasPendingChanges.value).toBe(true);
});
it("clears generated key columns instead of pasting the copied value", () => {
const editor = createEditor(undefined, true, undefined, undefined, [], undefined, [
{ name: "first", data_type: "integer", extra: "autoincrement" },
{ name: "hidden", data_type: "varchar" },
{ name: "last", data_type: "varchar" },
]);
const result = editor.appendPastedRowsToNewRow(
-1,
[
["1", "Lovelace"],
["2", "Hopper"],
],
[0, 2],
);
expect(result).toEqual({ ok: true, rowCount: 2 });
expect(editor.newRows.value).toEqual([
[null, null, "Lovelace"],
[null, null, "Hopper"],
]);
});
it("keeps explicitly read-only mapped columns out of editing and paste", () => {
const editor = createEditor(["first", "hidden", "last"], true, undefined, [0]);
@@ -1167,10 +1167,16 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
reusableNewRowCount++;
}
}
const shouldClearColumn = clonedColumnClearPredicate();
const mappedRows = pastedRows.map((pastedRow, rowIndex) => {
const nextRow = rowIndex < reusableNewRowCount ? nextRows[targetNewIndex! + rowIndex]! : emptyDraftRow();
for (let columnOffset = 0; columnOffset < Math.min(pastedRow.length, targetColumns.length); columnOffset++) {
const columnIndex = targetColumns[columnOffset]!;
// Pasting a copied row into a new row must not reuse its generated key.
if (shouldClearColumn(columnIndex)) {
nextRow[columnIndex] = null;
continue;
}
const value = pastedRow[columnOffset];
nextRow[columnIndex] = value === null ? null : coerceCellValue(value, nextRow[columnIndex], columnIndex);
}
@@ -1205,15 +1211,26 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
}
function clonedRowData(item: RowItem, resolvedValues?: ReadonlyMap<number, CellValue>): CellValue[] {
const columnInfoByName = new Map((tableMeta.value?.columns ?? []).map((column) => [column.name.toLowerCase(), column]));
const shouldClearColumn = clonedColumnClearPredicate();
return item.data.map((val, i) => {
const columnName = sourceColumns.value?.[i] ?? result.value.columns[i];
const columnInfo = columnInfoByName.get(columnName.toLowerCase());
if (shouldClearClonedColumn(columnName, columnInfo)) return null;
if (shouldClearColumn(i)) return null;
return resolvedValues?.has(i) ? (resolvedValues.get(i) ?? null) : val;
});
}
// Columns generated by the database (auto increment / identity / sequence
// defaults) must not be carried over when a row's values are copied into a
// new row, otherwise saving fails on a duplicate key. Both the clone-row and
// the clipboard-paste paths resolve the columns through this predicate.
function clonedColumnClearPredicate(): (columnIndex: number) => boolean {
const columnInfoByName = new Map((tableMeta.value?.columns ?? []).map((column) => [column.name.toLowerCase(), column]));
return (columnIndex: number) => {
const columnName = sourceColumns.value?.[columnIndex] ?? result.value.columns[columnIndex];
if (columnName === undefined) return false;
return shouldClearClonedColumn(columnName, columnInfoByName.get(columnName.toLowerCase()));
};
}
function shouldClearClonedColumn(columnName: string, columnInfo: ColumnInfo | undefined): boolean {
if (usesSyntheticRowIdKey(resolvedDatabaseType.value, [columnName])) return true;
if (resolvedDatabaseType.value === "neo4j" && columnName === DBX_NEO4J_ELEMENT_ID_COLUMN) return true;