mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-09 08:13:29 +08:00
✨ feat(data-grid): 支持表格多格式复制
- 新增 DataGrid 剪贴板 payload 构建工具,统一生成 plain text、HTML、CSV、Markdown、JSON 等格式 - 将单元格选择复制、行复制、列复制从纯文本复制升级为富格式复制 - Ctrl/Cmd+C 改为通过 copy 事件写入多种 MIME 类型,兼容 Excel 等表格软件粘贴 - 为多格式 payload 构建、事件写入、选区复制补充 Vitest 覆盖 Validation: - npm --prefix frontend test -- dataGridClipboardPayload.test.ts dataGridSelectionCopy.test.ts DataGrid.ddl.test.tsx -t "copies row and column data|copies loaded column data|dataGridClipboardPayload|dataGridSelectionCopy" - cd frontend && npx patch-package --verbose - npm --prefix frontend run build
This commit is contained in:
@@ -71,7 +71,7 @@ import {
|
||||
type CopySqlError,
|
||||
} from './dataGridCopyInsert';
|
||||
import { calculateAutoFitColumnWidth } from './dataGridAutoWidth';
|
||||
import { buildSelectedCellClipboardText } from './dataGridSelectionCopy';
|
||||
import { buildSelectedCellClipboardPayload } from './dataGridSelectionCopy';
|
||||
import { buildCopiedRowsForPaste, buildPastedRowsFromCopiedRows } from './dataGridRowClipboard';
|
||||
import {
|
||||
buildDataGridSelectBaseSql,
|
||||
@@ -84,6 +84,11 @@ import {
|
||||
buildClipboardMarkdown,
|
||||
pickRowsForClipboard,
|
||||
} from './dataGridClipboardExport';
|
||||
import {
|
||||
buildTabularClipboardPayloadFromTsv,
|
||||
writeClipboardPayload,
|
||||
type DataGridClipboardPayload,
|
||||
} from './dataGridClipboardPayload';
|
||||
import { applyNoAutoCapAttributesWithin, noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import {
|
||||
DEFAULT_SHORTCUT_OPTIONS,
|
||||
@@ -3748,8 +3753,9 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
useEffect(() => clearAutoCommitTimer, [clearAutoCommitTimer]);
|
||||
|
||||
const copyToClipboard = useCallback((text: string) => {
|
||||
navigator.clipboard.writeText(text).catch(console.error);
|
||||
const copyToClipboard = useCallback((value: string | DataGridClipboardPayload) => {
|
||||
const payload = typeof value === 'string' ? { plainText: value } : value;
|
||||
writeClipboardPayload(payload).catch(console.error);
|
||||
void message.success(translateDataGrid('data_grid.message.copied_to_clipboard'));
|
||||
}, [translateDataGrid]);
|
||||
|
||||
@@ -3778,7 +3784,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const text = mergedDisplayData
|
||||
.map((row) => normalizeClipboardTsvCell(formatClipboardCellText(row?.[normalizedColumnName], columnType, currentConnConfig)))
|
||||
.join('\n');
|
||||
copyToClipboard(text);
|
||||
copyToClipboard(buildTabularClipboardPayloadFromTsv(text));
|
||||
}, [columnMetaMap, columnMetaMapByLowerName, copyToClipboard, currentConnConfig, displayOutputColumnNames, mergedDisplayData, translateDataGrid]);
|
||||
|
||||
const {
|
||||
@@ -3819,7 +3825,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
buildOrderBySQL,
|
||||
buildPaginatedSelectSQL,
|
||||
buildRpcConnectionConfig,
|
||||
buildSelectedCellClipboardText,
|
||||
buildSelectedCellClipboardPayload,
|
||||
buildTableExportTab,
|
||||
buildWhereSQL,
|
||||
cellContextMenu,
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
} from './dataGridCopyInsert';
|
||||
import { calculateAutoFitColumnWidth } from './dataGridAutoWidth';
|
||||
import { buildSelectedCellClipboardText } from './dataGridSelectionCopy';
|
||||
import type { DataGridClipboardPayload } from './dataGridClipboardPayload';
|
||||
import { buildCopiedRowsForPaste, buildPastedRowsFromCopiedRows } from './dataGridRowClipboard';
|
||||
import {
|
||||
buildDataGridSelectBaseSql,
|
||||
@@ -876,7 +877,7 @@ const DataContext = React.createContext<{
|
||||
handleCopyJson: (r: any) => void;
|
||||
handleCopyCsv: (r: any) => void;
|
||||
handleExportSelected: (options: DataExportFileOptions, r: any) => Promise<void>;
|
||||
copyToClipboard: (t: string) => void;
|
||||
copyToClipboard: (t: string | DataGridClipboardPayload) => void;
|
||||
tableName?: string;
|
||||
enableRowContextMenu: boolean;
|
||||
supportsCopyInsert: boolean;
|
||||
|
||||
68
frontend/src/components/dataGridClipboardPayload.test.ts
Normal file
68
frontend/src/components/dataGridClipboardPayload.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
buildTabularClipboardPayload,
|
||||
buildTabularClipboardPayloadFromTsv,
|
||||
writeClipboardPayloadToEvent,
|
||||
} from './dataGridClipboardPayload';
|
||||
|
||||
describe('dataGridClipboardPayload', () => {
|
||||
it('builds plain text, HTML and CSV from one table payload', () => {
|
||||
const payload = buildTabularClipboardPayload({
|
||||
columns: ['id', 'name'],
|
||||
rows: [
|
||||
['1', 'Alice & Bob'],
|
||||
['2', '<Admin>'],
|
||||
],
|
||||
jsonRows: [
|
||||
{ id: 1, name: 'Alice & Bob' },
|
||||
{ id: 2, name: '<Admin>' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(payload.plainText).toBe('id\tname\n1\tAlice & Bob\n2\t<Admin>');
|
||||
expect(payload.csv).toBe('"id","name"\n"1","Alice & Bob"\n"2","<Admin>"');
|
||||
expect(payload.html).toContain('<th>name</th>');
|
||||
expect(payload.html).toContain('<td>Alice & Bob</td>');
|
||||
expect(payload.html).toContain('<td><Admin></td>');
|
||||
expect(payload.markdown).toBe('| id | name |\n| --- | --- |\n| 1 | Alice & Bob |\n| 2 | <Admin> |');
|
||||
expect(payload.json).toBe('[\n {\n "id": 1,\n "name": "Alice & Bob"\n },\n {\n "id": 2,\n "name": "<Admin>"\n }\n]');
|
||||
});
|
||||
|
||||
it('keeps the original TSV plain text when deriving rich formats', () => {
|
||||
const payload = buildTabularClipboardPayloadFromTsv('id\tname\n1\talpha', { firstRowIsHeader: true });
|
||||
|
||||
expect(payload.plainText).toBe('id\tname\n1\talpha');
|
||||
expect(payload.html).toContain('<thead><tr><th>id</th><th>name</th></tr></thead>');
|
||||
expect(payload.csv).toBe('"id","name"\n"1","alpha"');
|
||||
});
|
||||
|
||||
it('sets multiple clipboard MIME types without overwriting different formats', () => {
|
||||
const values: Record<string, string> = {};
|
||||
const event = {
|
||||
clipboardData: {
|
||||
clearData: vi.fn(() => {
|
||||
Object.keys(values).forEach((key) => delete values[key]);
|
||||
}),
|
||||
setData: vi.fn((type: string, value: string) => {
|
||||
values[type] = value;
|
||||
}),
|
||||
},
|
||||
preventDefault: vi.fn(),
|
||||
};
|
||||
|
||||
const payload = buildTabularClipboardPayload({
|
||||
columns: ['id'],
|
||||
rows: [['1']],
|
||||
jsonRows: [{ id: 1 }],
|
||||
});
|
||||
|
||||
expect(writeClipboardPayloadToEvent(event, payload)).toBe(true);
|
||||
expect(values['text/plain']).toBe('id\n1');
|
||||
expect(values['text/html']).toContain('<table>');
|
||||
expect(values['text/csv']).toBe('"id"\n"1"');
|
||||
expect(values['text/markdown']).toBe('| id |\n| --- |\n| 1 |');
|
||||
expect(values['application/json']).toBe('[\n {\n "id": 1\n }\n]');
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
160
frontend/src/components/dataGridClipboardPayload.ts
Normal file
160
frontend/src/components/dataGridClipboardPayload.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
export interface DataGridClipboardPayload {
|
||||
plainText: string;
|
||||
html?: string;
|
||||
csv?: string;
|
||||
markdown?: string;
|
||||
json?: string;
|
||||
}
|
||||
|
||||
export interface BuildTabularClipboardPayloadInput {
|
||||
columns?: string[];
|
||||
rows: string[][];
|
||||
jsonRows?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
type ClipboardWriter = Pick<Clipboard, 'write' | 'writeText'>;
|
||||
|
||||
type ClipboardDataWriter = Pick<DataTransfer, 'clearData' | 'setData'>;
|
||||
|
||||
export interface ClipboardEventLike {
|
||||
clipboardData?: ClipboardDataWriter | null;
|
||||
preventDefault?: () => void;
|
||||
}
|
||||
|
||||
const normalizeClipboardMatrixCell = (value: unknown): string => (
|
||||
value === null || value === undefined ? '' : String(value)
|
||||
);
|
||||
|
||||
const escapeHtml = (value: string): string => (
|
||||
value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
);
|
||||
|
||||
const escapeCsvCell = (value: string): string => `"${value.replace(/"/g, '""')}"`;
|
||||
|
||||
const buildDelimitedText = (rows: string[][], delimiter: string, columns?: string[]): string => {
|
||||
const matrix = columns ? [columns, ...rows] : rows;
|
||||
return matrix.map((row) => row.map(normalizeClipboardMatrixCell).join(delimiter)).join('\n');
|
||||
};
|
||||
|
||||
const buildCsvText = (rows: string[][], columns?: string[]): string => {
|
||||
const matrix = columns ? [columns, ...rows] : rows;
|
||||
return matrix.map((row) => row.map((cell) => escapeCsvCell(normalizeClipboardMatrixCell(cell))).join(',')).join('\n');
|
||||
};
|
||||
|
||||
const buildHtmlTable = (rows: string[][], columns?: string[]): string => {
|
||||
const header = columns && columns.length > 0
|
||||
? `<thead><tr>${columns.map((column) => `<th>${escapeHtml(normalizeClipboardMatrixCell(column))}</th>`).join('')}</tr></thead>`
|
||||
: '';
|
||||
const body = rows.map((row) => (
|
||||
`<tr>${row.map((cell) => `<td>${escapeHtml(normalizeClipboardMatrixCell(cell))}</td>`).join('')}</tr>`
|
||||
)).join('');
|
||||
return `<meta charset="utf-8"><table>${header}<tbody>${body}</tbody></table>`;
|
||||
};
|
||||
|
||||
const buildMarkdownText = (rows: string[][], columns?: string[]): string => {
|
||||
if (!columns || columns.length === 0) return '';
|
||||
const escapeMarkdownCell = (value: string): string => (
|
||||
normalizeClipboardMatrixCell(value)
|
||||
.replace(/\|/g, '\\|')
|
||||
.replace(/\r?\n/g, ' ')
|
||||
);
|
||||
const header = `| ${columns.map(escapeMarkdownCell).join(' | ')} |`;
|
||||
const separator = `| ${columns.map(() => '---').join(' | ')} |`;
|
||||
const lines = rows.map((row) => `| ${row.map(escapeMarkdownCell).join(' | ')} |`);
|
||||
return [header, separator, ...lines].join('\n');
|
||||
};
|
||||
|
||||
export const buildTabularClipboardPayload = ({
|
||||
columns,
|
||||
rows,
|
||||
jsonRows,
|
||||
}: BuildTabularClipboardPayloadInput): DataGridClipboardPayload => ({
|
||||
plainText: buildDelimitedText(rows, '\t', columns),
|
||||
html: buildHtmlTable(rows, columns),
|
||||
csv: buildCsvText(rows, columns),
|
||||
markdown: buildMarkdownText(rows, columns) || undefined,
|
||||
json: jsonRows ? JSON.stringify(jsonRows, null, 2) : undefined,
|
||||
});
|
||||
|
||||
export const buildTabularClipboardPayloadFromTsv = (
|
||||
text: string,
|
||||
options: { firstRowIsHeader?: boolean } = {},
|
||||
): DataGridClipboardPayload => {
|
||||
const matrix = text.split('\n').map((line) => line.split('\t'));
|
||||
const columns = options.firstRowIsHeader ? matrix[0] || [] : undefined;
|
||||
const rows = options.firstRowIsHeader ? matrix.slice(1) : matrix;
|
||||
return {
|
||||
...buildTabularClipboardPayload({ columns, rows }),
|
||||
plainText: text,
|
||||
};
|
||||
};
|
||||
|
||||
export const writeClipboardPayloadToEvent = (
|
||||
event: ClipboardEventLike,
|
||||
payload: DataGridClipboardPayload,
|
||||
): boolean => {
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData || !payload.plainText) return false;
|
||||
|
||||
clipboardData.clearData();
|
||||
clipboardData.setData('text/plain', payload.plainText);
|
||||
if (payload.html) clipboardData.setData('text/html', payload.html);
|
||||
if (payload.csv) clipboardData.setData('text/csv', payload.csv);
|
||||
if (payload.markdown) clipboardData.setData('text/markdown', payload.markdown);
|
||||
if (payload.json) clipboardData.setData('application/json', payload.json);
|
||||
event.preventDefault?.();
|
||||
return true;
|
||||
};
|
||||
|
||||
const appendClipboardItemPart = (
|
||||
parts: Record<string, Blob>,
|
||||
type: string,
|
||||
value: string | undefined,
|
||||
supportsType?: (type: string) => boolean,
|
||||
) => {
|
||||
if (!value) return;
|
||||
if (type !== 'text/plain') {
|
||||
if (supportsType && !supportsType(type)) return;
|
||||
if (!supportsType && type !== 'text/html') return;
|
||||
}
|
||||
parts[type] = new Blob([value], { type });
|
||||
};
|
||||
|
||||
export const writeClipboardPayload = async (
|
||||
payload: DataGridClipboardPayload,
|
||||
clipboard: ClipboardWriter | undefined = globalThis.navigator?.clipboard,
|
||||
): Promise<void> => {
|
||||
if (!payload.plainText) return;
|
||||
|
||||
const ClipboardItemConstructor = typeof ClipboardItem === 'undefined' ? null : ClipboardItem;
|
||||
if (clipboard?.write && ClipboardItemConstructor) {
|
||||
const supportsType = typeof ClipboardItemConstructor.supports === 'function'
|
||||
? ClipboardItemConstructor.supports.bind(ClipboardItemConstructor)
|
||||
: undefined;
|
||||
const parts: Record<string, Blob> = {};
|
||||
appendClipboardItemPart(parts, 'text/plain', payload.plainText, supportsType);
|
||||
appendClipboardItemPart(parts, 'text/html', payload.html, supportsType);
|
||||
appendClipboardItemPart(parts, 'text/csv', payload.csv, supportsType);
|
||||
appendClipboardItemPart(parts, 'text/markdown', payload.markdown, supportsType);
|
||||
appendClipboardItemPart(parts, 'application/json', payload.json, supportsType);
|
||||
|
||||
try {
|
||||
await clipboard.write([new ClipboardItemConstructor(parts)]);
|
||||
return;
|
||||
} catch {
|
||||
// Fall back to writeText below when WebView support is partial.
|
||||
}
|
||||
}
|
||||
|
||||
if (clipboard?.writeText) {
|
||||
await clipboard.writeText(payload.plainText);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Clipboard write is not supported');
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildSelectedCellClipboardText, canSelectGridCellForClipboard } from './dataGridSelectionCopy';
|
||||
import {
|
||||
buildSelectedCellClipboardPayload,
|
||||
buildSelectedCellClipboardText,
|
||||
canSelectGridCellForClipboard,
|
||||
} from './dataGridSelectionCopy';
|
||||
|
||||
describe('dataGridSelectionCopy helpers', () => {
|
||||
it('allows displayed read-only cells while keeping editable expressions out of batch selection', () => {
|
||||
@@ -58,4 +62,23 @@ describe('dataGridSelectionCopy helpers', () => {
|
||||
|
||||
expect(text).toBe('NULL\t{"a":1}\nline1 line2 value\t[1,2]');
|
||||
});
|
||||
|
||||
it('builds a multi-format payload for selected cells', () => {
|
||||
const payload = buildSelectedCellClipboardPayload({
|
||||
selectedCells: [
|
||||
{ rowKey: 'row-1', colName: 'name' },
|
||||
{ rowKey: 'row-1', colName: 'note' },
|
||||
],
|
||||
rows: [
|
||||
{ __rowKey: 'row-1', name: 'A&B', note: '<owner>' },
|
||||
],
|
||||
columnOrder: ['name', 'note'],
|
||||
rowKeyField: '__rowKey',
|
||||
});
|
||||
|
||||
expect(payload.plainText).toBe('A&B\t<owner>');
|
||||
expect(payload.csv).toBe('"A&B","<owner>"');
|
||||
expect(payload.html).toContain('<td>A&B</td>');
|
||||
expect(payload.html).toContain('<td><owner></td>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { buildTabularClipboardPayload, type DataGridClipboardPayload } from './dataGridClipboardPayload';
|
||||
|
||||
export interface SelectedGridCell {
|
||||
rowKey: string;
|
||||
colName: string;
|
||||
@@ -44,8 +46,29 @@ export const buildSelectedCellClipboardText = ({
|
||||
columnOrder: string[];
|
||||
rowKeyField: string;
|
||||
}): string => {
|
||||
const matrix = buildSelectedCellClipboardMatrix({
|
||||
selectedCells,
|
||||
rows,
|
||||
columnOrder,
|
||||
rowKeyField,
|
||||
});
|
||||
|
||||
return matrix.map((row) => row.join('\t')).join('\n');
|
||||
};
|
||||
|
||||
const buildSelectedCellClipboardMatrix = ({
|
||||
selectedCells,
|
||||
rows,
|
||||
columnOrder,
|
||||
rowKeyField,
|
||||
}: {
|
||||
selectedCells: SelectedGridCell[];
|
||||
rows: Array<Record<string, any>>;
|
||||
columnOrder: string[];
|
||||
rowKeyField: string;
|
||||
}): string[][] => {
|
||||
if (!selectedCells.length || !rows.length || !columnOrder.length || !rowKeyField) {
|
||||
return '';
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedRowKeys = new Set(selectedCells.map((cell) => cell.rowKey));
|
||||
@@ -54,7 +77,7 @@ export const buildSelectedCellClipboardText = ({
|
||||
const orderedColumns = columnOrder.filter((columnName) => selectedColumnKeys.has(columnName));
|
||||
|
||||
if (!orderedRows.length || !orderedColumns.length) {
|
||||
return '';
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedCellKeySet = new Set(selectedCells.map((cell) => `${cell.rowKey}::${cell.colName}`));
|
||||
@@ -68,8 +91,26 @@ export const buildSelectedCellClipboardText = ({
|
||||
return '';
|
||||
}
|
||||
return normalizeClipboardCellValue(row?.[columnName]);
|
||||
})
|
||||
.join('\t');
|
||||
})
|
||||
.join('\n');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const buildSelectedCellClipboardPayload = ({
|
||||
selectedCells,
|
||||
rows,
|
||||
columnOrder,
|
||||
rowKeyField,
|
||||
}: {
|
||||
selectedCells: SelectedGridCell[];
|
||||
rows: Array<Record<string, any>>;
|
||||
columnOrder: string[];
|
||||
rowKeyField: string;
|
||||
}): DataGridClipboardPayload => {
|
||||
const matrix = buildSelectedCellClipboardMatrix({
|
||||
selectedCells,
|
||||
rows,
|
||||
columnOrder,
|
||||
rowKeyField,
|
||||
});
|
||||
return buildTabularClipboardPayload({ rows: matrix });
|
||||
};
|
||||
|
||||
@@ -3,6 +3,11 @@ import { message } from 'antd';
|
||||
import { ExportQueryWithOptions } from '../../wailsjs/go/app/App';
|
||||
import type { CopySqlError } from './dataGridCopyInsert';
|
||||
import type { V2CellContextMenuActionKey, V2ColumnHeaderContextMenuActionKey } from './V2TableContextMenu';
|
||||
import {
|
||||
buildTabularClipboardPayloadFromTsv,
|
||||
writeClipboardPayloadToEvent,
|
||||
type DataGridClipboardPayload,
|
||||
} from './dataGridClipboardPayload';
|
||||
import {
|
||||
DEFAULT_DATA_EXPORT_FORMAT,
|
||||
DEFAULT_XLSX_ROWS_PER_SHEET,
|
||||
@@ -35,7 +40,7 @@ export const useDataGridV2Actions = (ctx: DataGridV2ActionsContext) => {
|
||||
buildOrderBySQL,
|
||||
buildPaginatedSelectSQL,
|
||||
buildRpcConnectionConfig,
|
||||
buildSelectedCellClipboardText,
|
||||
buildSelectedCellClipboardPayload,
|
||||
buildTableExportTab,
|
||||
buildWhereSQL,
|
||||
cellContextMenu,
|
||||
@@ -270,11 +275,11 @@ const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeade
|
||||
.catch(() => message.error(translateDataGrid('data_grid.message.ddl_copy_failed')));
|
||||
}, [ddlText, translateDataGrid]);
|
||||
|
||||
const handleCopySelectedCellsToClipboard = useCallback(() => {
|
||||
const buildSelectedCellsClipboardPayload = useCallback((): DataGridClipboardPayload | null => {
|
||||
const activeSelection = currentSelectionRef.current.size > 0 ? currentSelectionRef.current : selectedCells;
|
||||
if (activeSelection.size === 0) {
|
||||
void message.info(translateDataGrid('data_grid.message.drag_select_cells_to_copy'));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Array.from(activeSelection)
|
||||
@@ -282,22 +287,37 @@ const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeade
|
||||
.filter((item): item is { rowKey: string; colName: string } => !!item);
|
||||
if (parsed.length === 0) {
|
||||
void message.info(translateDataGrid('data_grid.message.no_copyable_cells'));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = buildSelectedCellClipboardText({
|
||||
const payload = buildSelectedCellClipboardPayload({
|
||||
selectedCells: parsed,
|
||||
rows: mergedDisplayData as Array<Record<string, any>>,
|
||||
columnOrder: displayColumnNames,
|
||||
rowKeyField: GONAVI_ROW_KEY,
|
||||
});
|
||||
if (!text) {
|
||||
if (!payload.plainText) {
|
||||
void message.info(translateDataGrid('data_grid.message.selection_no_copyable_content'));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
copyToClipboard(text);
|
||||
}, [selectedCells, mergedDisplayData, displayColumnNames, copyToClipboard, translateDataGrid]);
|
||||
return payload;
|
||||
}, [
|
||||
GONAVI_ROW_KEY,
|
||||
buildSelectedCellClipboardPayload,
|
||||
currentSelectionRef,
|
||||
displayColumnNames,
|
||||
mergedDisplayData,
|
||||
selectedCells,
|
||||
splitCellKey,
|
||||
translateDataGrid,
|
||||
]);
|
||||
|
||||
const handleCopySelectedCellsToClipboard = useCallback(() => {
|
||||
const payload = buildSelectedCellsClipboardPayload();
|
||||
if (!payload) return;
|
||||
copyToClipboard(payload);
|
||||
}, [buildSelectedCellsClipboardPayload, copyToClipboard]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || !isTableSurfaceActive || (!cellEditMode && selectedCells.size === 0)) return;
|
||||
@@ -325,21 +345,35 @@ const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeade
|
||||
return;
|
||||
}
|
||||
|
||||
const isCopy = (event.ctrlKey || event.metaKey) && !event.altKey && String(event.key || '').toLowerCase() === 'c';
|
||||
if (!isCopy) return;
|
||||
|
||||
if (document.getSelection?.()?.toString()) return;
|
||||
|
||||
const activeSelection = currentSelectionRef.current.size > 0 ? currentSelectionRef.current : selectedCells;
|
||||
if (activeSelection.size === 0) return;
|
||||
|
||||
event.preventDefault();
|
||||
handleCopySelectedCellsToClipboard();
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [cellEditMode, selectedCells, handleCopySelectedCellsToClipboard, resetCellSelection, closeCellEditMode, isActive, isTableSurfaceActive]);
|
||||
}, [cellEditMode, selectedCells, resetCellSelection, closeCellEditMode, isActive, isTableSurfaceActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || !isTableSurfaceActive || selectedCells.size === 0) return;
|
||||
|
||||
const onCopy = (event: ClipboardEvent) => {
|
||||
if (event.defaultPrevented) return;
|
||||
const activeElement = document.activeElement as HTMLElement | null;
|
||||
const eventTarget = event.target instanceof HTMLElement ? event.target : null;
|
||||
const nativeShortcutGuard = 'input, textarea, select, [contenteditable="true"], .ant-modal, .ant-dropdown, .ant-select-dropdown, .ant-picker-dropdown, .ant-popover, [data-gonavi-close-shortcut-guard]';
|
||||
if (activeElement?.closest(nativeShortcutGuard) || eventTarget?.closest(nativeShortcutGuard)) return;
|
||||
if (document.getSelection?.()?.toString()) return;
|
||||
|
||||
const payload = buildSelectedCellsClipboardPayload();
|
||||
if (!payload) return;
|
||||
if (writeClipboardPayloadToEvent(event, payload)) {
|
||||
void message.success(translateDataGrid('data_grid.message.copied_to_clipboard'));
|
||||
return;
|
||||
}
|
||||
copyToClipboard(payload);
|
||||
};
|
||||
|
||||
window.addEventListener('copy', onCopy);
|
||||
return () => window.removeEventListener('copy', onCopy);
|
||||
}, [buildSelectedCellsClipboardPayload, copyToClipboard, isActive, isTableSurfaceActive, selectedCells.size, translateDataGrid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || !isTableSurfaceActive || (!cellEditMode && selectedCells.size === 0)) return;
|
||||
@@ -523,7 +557,7 @@ const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeade
|
||||
void message.info(translateDataGrid('data_grid.message.current_row_no_copyable_content'));
|
||||
return;
|
||||
}
|
||||
copyToClipboard(text);
|
||||
copyToClipboard(buildTabularClipboardPayloadFromTsv(text, { firstRowIsHeader: true }));
|
||||
}, [columnMetaMap, columnMetaMapByLowerName, copyToClipboard, currentConnConfig, displayOutputColumnNames, getContextMenuTargetRows, translateDataGrid]);
|
||||
|
||||
const buildConnConfig = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user