From 34f061f56b340973486b7590e5cc0e42b1376068 Mon Sep 17 00:00:00 2001 From: zgq Date: Mon, 14 Sep 2026 13:56:42 +0800 Subject: [PATCH] fix(clipboard): use Windows line endings for result copies --- chat2db-community-client/package.json | 2 +- .../src/utils/clipboardText.test.ts | 34 +++++++++++++++++++ .../src/utils/clipboardText.ts | 4 +++ chat2db-community-client/src/utils/index.ts | 3 +- .../src/utils/internalClipboard.ts | 4 +-- 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 chat2db-community-client/src/utils/clipboardText.test.ts create mode 100644 chat2db-community-client/src/utils/clipboardText.ts diff --git a/chat2db-community-client/package.json b/chat2db-community-client/package.json index c70ccffdb1..0002af4cd7 100644 --- a/chat2db-community-client/package.json +++ b/chat2db-community-client/package.json @@ -86,7 +86,7 @@ "test:monaco-lifecycle": "tsx src/utils/monaco.test.ts && tsx src/components/SingleFileMonacoEditor/refAdapter.test.ts", "test:mcp-lifecycle": "tsx src/blocks/Setting/McpSetting/mcpLifecycle.test.ts", "test:sql-completion-context": "tsx src/components/SQLEditor/core/sqlCompletionContext.test.ts && tsx src/components/SQLEditor/core/sqlCompletionRequestError.test.ts", - "test:sql-in-clipboard": "tsx src/utils/sqlInClipboard.test.ts && tsx src/components/SQLEditor/helper/sqlInsertValueDefaults.test.ts", + "test:sql-in-clipboard": "tsx src/utils/clipboardText.test.ts && tsx src/utils/internalClipboard.test.ts && tsx src/utils/sqlInClipboard.test.ts && tsx src/components/SQLEditor/helper/sqlInsertValueDefaults.test.ts", "test:console-tab-name": "tsx src/store/workspace/utils/consoleTabName.test.ts && tsx src/utils/workspaceObjectTabTitle.test.ts", "test:workspace-tab-scroll": "tsx src/store/workspace/utils/workspaceTabScrollRequest.test.ts && tsx src/components/Tabs/activeTabScroll.test.ts && tsx src/components/Tabs/wheelScroll.test.ts && tsx src/components/Tabs/tabAccentColor.test.ts", "test:workspace-split-lifecycle": "tsx src/pages/main/workspace/components/WorkspaceTabs/workspaceTabContentLayout.test.ts && tsx src/pages/main/workspace/components/WorkspaceTabs/workspaceTabLayout.test.ts", diff --git a/chat2db-community-client/src/utils/clipboardText.test.ts b/chat2db-community-client/src/utils/clipboardText.test.ts new file mode 100644 index 0000000000..6fac40ed02 --- /dev/null +++ b/chat2db-community-client/src/utils/clipboardText.test.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import { normalizeClipboardText } from './clipboardText'; +import { getInternalResultGridClipboard, setInternalResultGridClipboard, clearInternalClipboard } from './internalClipboard'; + +const windowsAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + 'Mozilla/5.0 (Windows NT 6.1)', + 'Win32', +]; + +for (const userAgent of windowsAgents) { + assert.equal(normalizeClipboardText('1001\n1002\n1003', userAgent), '1001\r\n1002\r\n1003'); + assert.equal(normalizeClipboardText('a\r\nb\nc\rd\n', userAgent), 'a\r\nb\r\nc\r\nd\r\n'); + assert.equal(normalizeClipboardText('a\r\nb', userAgent), 'a\r\nb', 'existing CRLF must not acquire extra CR'); + assert.equal(normalizeClipboardText('中文\t001\n\t002', userAgent), '中文\t001\r\n\t002'); + assert.equal(normalizeClipboardText('single value', userAgent), 'single value'); + assert.equal(normalizeClipboardText('', userAgent), ''); +} + +for (const userAgent of ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', 'Mozilla/5.0 (X11; Linux x86_64)']) { + const text = '中文\t001\r\nvalue\nnext\rlast'; + assert.equal(normalizeClipboardText(text, userAgent), text, 'non-Windows copy preserves the original text'); +} + +const rows = [['first\nsecond', 'value\twith tab'], ['CRLF\r\nvalue', 'CR\rvalue']]; +setInternalResultGridClipboard(rows); +const windowsText = normalizeClipboardText(rows.map((row) => row.join('\t')).join('\n'), windowsAgents[0]); +assert.deepEqual(getInternalResultGridClipboard(windowsText), rows, 'Windows paste retains original cell boundaries and values'); +assert.deepEqual(getInternalResultGridClipboard(windowsText.replace(/\r\n/g, '\n')), rows, 'browser LF normalization retains grid metadata'); +assert.equal(getInternalResultGridClipboard(`${windowsText}x`), null, 'different text must not reuse grid metadata'); +clearInternalClipboard(); +assert.equal(getInternalResultGridClipboard(windowsText), null); + +console.log('Clipboard text and Windows result-grid round-trip tests passed'); diff --git a/chat2db-community-client/src/utils/clipboardText.ts b/chat2db-community-client/src/utils/clipboardText.ts new file mode 100644 index 0000000000..ce66fd7591 --- /dev/null +++ b/chat2db-community-client/src/utils/clipboardText.ts @@ -0,0 +1,4 @@ +export function normalizeClipboardText(text: string, userAgent: string) { + // Windows text clipboard formats require CRLF, including for legacy native applications. + return /Windows|Win32|Win64/i.test(userAgent) ? text.replace(/\r\n|\r|\n/g, '\r\n') : text; +} diff --git a/chat2db-community-client/src/utils/index.ts b/chat2db-community-client/src/utils/index.ts index 781915d3f2..e31777a5b2 100644 --- a/chat2db-community-client/src/utils/index.ts +++ b/chat2db-community-client/src/utils/index.ts @@ -7,6 +7,7 @@ import queryString from 'query-string'; import React from 'react'; import { v4 as uuid } from 'uuid'; import { isDesktop } from './env'; +import { normalizeClipboardText } from './clipboardText'; import { clearInternalClipboard } from './internalClipboard'; import { findNode } from './treeNodeLookup'; @@ -321,7 +322,7 @@ export function copyToClipboard( _copyToClipboard(' ', { format: 'text/plain' }); return _copyToClipboard('', { format: 'text/plain' }); } - return _copyToClipboard(text, { format: 'text/plain' }); + return _copyToClipboard(normalizeClipboardText(text, navigator.userAgent), { format: 'text/plain' }); // staticMessage.success('Copied to clipboard'); } catch { diff --git a/chat2db-community-client/src/utils/internalClipboard.ts b/chat2db-community-client/src/utils/internalClipboard.ts index 7236ec2f9c..391af7ee3a 100644 --- a/chat2db-community-client/src/utils/internalClipboard.ts +++ b/chat2db-community-client/src/utils/internalClipboard.ts @@ -15,13 +15,13 @@ export function clearInternalClipboard() { export function setInternalResultGridClipboard(rows: string[][]) { internalResultGridClipboard = { - text: serializeGrid(rows), + text: serializeGrid(rows).replace(/\r\n?/g, '\n'), rows: rows.map((row) => [...row]), }; } export function getInternalResultGridClipboard(text: string) { - if (internalResultGridClipboard?.text !== text) { + if (internalResultGridClipboard?.text !== text.replace(/\r\n?/g, '\n')) { return null; } return internalResultGridClipboard.rows.map((row) => [...row]);