fix(grid): copy cells containing double quotes verbatim

This commit is contained in:
弈秋 authored and GitHub committed 2026-09-23 14:43:06 +08:00
1 parent bd32919542
commit 6471e37e5f
6 files changed
+97 -19

No files matched your search

@@ -726,7 +726,7 @@ describe("useDataGridExport prepared row statements", () => {
]);
});
it("uses escaped TSV for a multi-cell smart copy without relying on clipboard metadata", async () => {
it("uses TSV (quotes only for separator/newline) for a multi-cell smart copy without relying on clipboard metadata", async () => {
const rows = [
[1, '{"msg":"success"}'],
[2, "inside\ttab\nnext line"],
@@ -737,7 +737,9 @@ describe("useDataGridExport prepared row statements", () => {
columns: ["id", "name"],
rows,
};
const text = '1\t"{""msg"":""success""}"\n2\t"inside\ttab\nnext line"';
// A value containing only double quotes is copied verbatim (#10087); a value
// containing a tab/newline is still quoted so the TSV shape survives.
const text = '1\t{"msg":"success"}\n2\t"inside\ttab\nnext line"';
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text, mimeType: "text/tab-separated-values", fileExtension: "tsv", rowCount: 2, columnCount: 2 });
const state = createExportState(editableTable, ["id", "name"], matrix, undefined, undefined, rows);
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { formatTsv } from "@/lib/export/exportFormats";
describe("formatTsv", () => {
it("copies a value that only contains double quotes verbatim (#10087)", () => {
const output = formatTsv(["id", "mag"], [[1, '"9932b4d2ad1c4ff88b5bfe1690ee1bb8"']]);
expect(output).toBe('id\tmag\n1\t"9932b4d2ad1c4ff88b5bfe1690ee1bb8"');
});
it("still quotes values containing a tab or newline so the TSV shape survives", () => {
const output = formatTsv(["v"], [["a\tb"], ["c\nd"]]);
expect(output).toBe('v\n"a\tb"\n"c\nd"');
});
it("renders NULL as an empty field without adding quotes", () => {
const output = formatTsv(["id", "name"], [[1, null]]);
expect(output).toBe("id\tname\n1\t");
});
});
+7 -4
View File
@@ -12,13 +12,16 @@ export function formatCsv(columns: string[], rows: ExportCellValue[][], quoteMod
}
// Tab-separated values with a header row, mirroring Navicat's "Text File (*.txt)"
// export: fields are joined by a tab, NULL becomes empty, and a field is only
// wrapped in double quotes (with " doubled) when it contains a tab, newline,
// or quote - i.e. the minimum needed to round-trip the value.
// export: fields are joined by a tab and NULL becomes empty. A field is wrapped
// in double quotes only when it contains a tab or a line break - the characters
// that would otherwise corrupt the TSV row/column shape. A field that merely
// contains a double quote is emitted verbatim: TSV is parsed by splitting on the
// tab alone (there is no quote state machine on paste-back), so quoting such a
// value would corrupt it (e.g. a value of `"abc"` must not become `"""abc"""`).
export function formatTsv(columns: string[], rows: ExportCellValue[][]): string {
const esc = (value: ExportCellValue) => {
const text = value === null ? "" : String(value);
if (text.includes("\t") || text.includes("\n") || text.includes("\r") || text.includes('"')) {
if (text.includes("\t") || text.includes("\n") || text.includes("\r")) {
return `"${text.replace(/"/g, '""')}"`;
}
return text;