diff --git a/frontend/src/components/dataGridRowClipboard.test.ts b/frontend/src/components/dataGridRowClipboard.test.ts new file mode 100644 index 0000000..232f4ff --- /dev/null +++ b/frontend/src/components/dataGridRowClipboard.test.ts @@ -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' }, + ]); + }); +}); diff --git a/frontend/src/components/dataGridRowClipboard.ts b/frontend/src/components/dataGridRowClipboard.ts new file mode 100644 index 0000000..e3688ea --- /dev/null +++ b/frontend/src/components/dataGridRowClipboard.ts @@ -0,0 +1,66 @@ +export interface BuildCopiedRowsForPasteInput { + rows: Array>; + selectedRowKeys: any[]; + columnNames: string[]; + rowKeyField: string; + rowKeyToString?: (key: any) => string; +} + +export interface BuildPastedRowsFromCopiedRowsInput { + rows: Array>; + 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, + columnNames: string[], + rowKeyField: string, +): Record => { + const next: Record = {}; + getCopyableColumnNames(columnNames, rowKeyField).forEach((columnName) => { + next[columnName] = row?.[columnName]; + }); + return next; +}; + +export const buildCopiedRowsForPaste = ({ + rows, + selectedRowKeys, + columnNames, + rowKeyField, + rowKeyToString = defaultRowKeyToString, +}: BuildCopiedRowsForPasteInput): Array> => { + if (!Array.isArray(rows) || !Array.isArray(selectedRowKeys) || selectedRowKeys.length === 0) { + return []; + } + + const rowsByKey = new Map>(); + 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 => Boolean(row)) + .map((row) => pickCopyableRowValues(row, columnNames, rowKeyField)); +}; + +export const buildPastedRowsFromCopiedRows = ({ + rows, + columnNames, + rowKeyField, + createRowKey, +}: BuildPastedRowsFromCopiedRowsInput): Array> => + rows.map((row, index) => ({ + [rowKeyField]: createRowKey(index), + ...pickCopyableRowValues(row, columnNames, rowKeyField), + }));