feat(data-grid): 增加复制行和粘贴行操作

- 表数据工具栏新增复制行、粘贴行按钮
- 支持将选中行复制为新增行草稿,提交前可继续检查编辑
- 抽离行复制粘贴数据构造逻辑并补充回归测试
Refs #332
This commit is contained in:
Syngnat
2026-04-26 19:09:25 +08:00
parent 9eb06f6f96
commit 2b340f3136
2 changed files with 107 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { buildCopiedRowsForPaste, buildPastedRowsFromCopiedRows } from './dataGridRowClipboard';
const rowKeyField = '__gonavi_row_key__';
describe('dataGridRowClipboard', () => {
it('copies selected rows in selection order without the internal row key', () => {
const copiedRows = buildCopiedRowsForPaste({
rows: [
{ [rowKeyField]: 'row-1', id: 1, name: 'alpha', hidden_note: 'A' },
{ [rowKeyField]: 'row-2', id: 2, name: 'beta', hidden_note: 'B' },
],
selectedRowKeys: ['row-2', 'row-1'],
columnNames: ['id', 'name', 'hidden_note'],
rowKeyField,
});
expect(copiedRows).toEqual([
{ id: 2, name: 'beta', hidden_note: 'B' },
{ id: 1, name: 'alpha', hidden_note: 'A' },
]);
});
it('builds pasted rows as new rows with fresh internal keys', () => {
const pastedRows = buildPastedRowsFromCopiedRows({
rows: [
{ id: 2, name: 'beta' },
{ id: 1, name: 'alpha' },
],
columnNames: ['id', 'name'],
rowKeyField,
createRowKey: (index) => `paste-${index}`,
});
expect(pastedRows).toEqual([
{ [rowKeyField]: 'paste-0', id: 2, name: 'beta' },
{ [rowKeyField]: 'paste-1', id: 1, name: 'alpha' },
]);
});
});

View File

@@ -0,0 +1,66 @@
export interface BuildCopiedRowsForPasteInput {
rows: Array<Record<string, any>>;
selectedRowKeys: any[];
columnNames: string[];
rowKeyField: string;
rowKeyToString?: (key: any) => string;
}
export interface BuildPastedRowsFromCopiedRowsInput {
rows: Array<Record<string, any>>;
columnNames: string[];
rowKeyField: string;
createRowKey: (index: number) => string;
}
const defaultRowKeyToString = (key: any): string => String(key);
const getCopyableColumnNames = (columnNames: string[], rowKeyField: string): string[] =>
columnNames.filter((columnName) => columnName !== rowKeyField);
const pickCopyableRowValues = (
row: Record<string, any>,
columnNames: string[],
rowKeyField: string,
): Record<string, any> => {
const next: Record<string, any> = {};
getCopyableColumnNames(columnNames, rowKeyField).forEach((columnName) => {
next[columnName] = row?.[columnName];
});
return next;
};
export const buildCopiedRowsForPaste = ({
rows,
selectedRowKeys,
columnNames,
rowKeyField,
rowKeyToString = defaultRowKeyToString,
}: BuildCopiedRowsForPasteInput): Array<Record<string, any>> => {
if (!Array.isArray(rows) || !Array.isArray(selectedRowKeys) || selectedRowKeys.length === 0) {
return [];
}
const rowsByKey = new Map<string, Record<string, any>>();
rows.forEach((row) => {
const rowKey = row?.[rowKeyField];
if (rowKey === undefined || rowKey === null) return;
rowsByKey.set(rowKeyToString(rowKey), row);
});
return selectedRowKeys
.map((selectedKey) => rowsByKey.get(rowKeyToString(selectedKey)))
.filter((row): row is Record<string, any> => Boolean(row))
.map((row) => pickCopyableRowValues(row, columnNames, rowKeyField));
};
export const buildPastedRowsFromCopiedRows = ({
rows,
columnNames,
rowKeyField,
createRowKey,
}: BuildPastedRowsFromCopiedRowsInput): Array<Record<string, any>> =>
rows.map((row, index) => ({
[rowKeyField]: createRowKey(index),
...pickCopyableRowValues(row, columnNames, rowKeyField),
}));