fix(mysql): copy varbinary text values as raw strings

This commit is contained in:
jearhe authored and GitHub committed 2026-09-01 01:47:14 +08:00
1 parent cd008b2d56
commit 14ad5ef9bf
8 files changed
+299 -35

No files matched your search

+16 -8
View File
@@ -144,6 +144,7 @@ import {
binaryCellDisplayText,
binaryCellDownloadFileName,
binaryCellDownloadPayload,
binaryCellClipboardText,
canImportBinaryCellFile,
canDownloadBinaryCellValue,
downloadBinaryCellPayload,
@@ -9736,13 +9737,20 @@ async function onGridKeydown(event: KeyboardEvent) {
}
}
function detailClipboardText(detail: DataGridCellDetail): string {
if (detail.value === null) return "";
const binaryText = binaryCellClipboardText(detail.value, detail.type, resolvedDatabaseType.value);
if (binaryText !== null) return binaryText;
if (isBlobCellColumnType(detail.type)) return binaryCellUtf8Text(detail.value, detail.type, resolvedDatabaseType.value) ?? displayCellValue(detail.value);
return displayCellValue(detail.value);
}
async function copyDetailValue() {
const initialDetail = activeCellDetail.value;
if (!initialDetail || !(await hydrateLargeValueCell(initialDetail.rowId, initialDetail.colIndex))) return;
const detail = activeCellDetail.value;
if (!detail) return;
const text = detail.value === null ? "" : isBlobCellColumnType(detail.type) ? (binaryCellUtf8Text(detail.value, detail.type, resolvedDatabaseType.value) ?? displayCellValue(detail.value)) : displayCellValue(detail.value);
copyText(text);
copyText(detailClipboardText(detail));
}
async function copyDetailFormattedJson() {
@@ -9929,7 +9937,7 @@ async function copyRowDetailJson() {
try {
const detail = await resolvedRowDetailForCopy();
if (!detail) return;
copyText(dataGridRowDetailJson(detail));
copyText(dataGridRowDetailJson(detail, undefined, resolvedDatabaseType.value));
} catch (error) {
reportLargeValueLoadError(error);
}
@@ -9939,7 +9947,7 @@ async function copyRowDetailTsv() {
try {
const detail = await resolvedRowDetailForCopy();
if (!detail) return;
copyText(dataGridRowDetailTsv(detail));
copyText(dataGridRowDetailTsv(detail, resolvedDatabaseType.value));
} catch (error) {
reportLargeValueLoadError(error);
}
@@ -9948,14 +9956,14 @@ async function copyRowDetailTsv() {
async function copyRowDetailFieldValue(field: DataGridCellDetail) {
if (!(await hydrateLargeValueCell(field.rowId, field.colIndex))) return;
const resolved = cellDetailFor(field.rowNumber - 1, field.colIndex);
if (resolved) copyText(resolved.value === null ? "" : displayCellValue(resolved.value));
if (resolved) copyText(detailClipboardText(resolved));
}
async function copyColumnDetailJson() {
try {
const detail = await resolvedColumnDetailForCopy();
if (!detail) return;
copyText(dataGridColumnDetailJson(detail));
copyText(dataGridColumnDetailJson(detail, resolvedDatabaseType.value));
} catch (error) {
reportLargeValueLoadError(error);
}
@@ -9965,7 +9973,7 @@ async function copyColumnDetailTsv() {
try {
const detail = await resolvedColumnDetailForCopy();
if (!detail) return;
copyText(dataGridColumnDetailTsv(detail));
copyText(dataGridColumnDetailTsv(detail, resolvedDatabaseType.value));
} catch (error) {
reportLargeValueLoadError(error);
}
@@ -9980,7 +9988,7 @@ function copyColumnDetailColumnName() {
async function copyColumnDetailFieldValue(field: DataGridCellDetail) {
if (!(await hydrateLargeValueCell(field.rowId, field.colIndex))) return;
const resolved = cellDetailFor(field.rowNumber - 1, field.colIndex);
if (resolved) copyText(resolved.value === null ? "" : displayCellValue(resolved.value));
if (resolved) copyText(detailClipboardText(resolved));
}
const transposeRecordWidths = ref<number[]>([]);
@@ -1466,3 +1466,122 @@ describe("useDataGridExport prepared row statements", () => {
expect(copyToClipboard).not.toHaveBeenCalled();
});
});
// issue #7471:文本型 MySQL VARBINARY 复制单元格/多选/整行时,外部剪贴板应呈现原始字符串;
// SQL 路径与 DBX 内部网格回粘仍保留 hex 以保证 byte-for-byte round-trip,非文本二进制也始终保持 hex。
describe("useDataGridExport VARBINARY 文本复制 (#7471)", () => {
const varbinTable: DataGridTableMeta = {
tableName: "test_varbin",
primaryKeys: ["id"],
columns: [
{ name: "id", data_type: "int", is_nullable: false, is_primary_key: true },
{ name: "name", data_type: "varbinary(128)", is_nullable: false },
],
};
beforeEach(() => {
vi.clearAllMocks();
clearDataGridClipboardCopy();
});
it("复制文本型 VARBINARY 单元格时把 0x<hex> 还原为原始字符串", async () => {
const matrix: CellSelectionMatrix = { rowIndexes: [0], columnIndexes: [1], columns: ["name"], rows: [["0x616263"]] };
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "abc", mimeType: "text/plain", fileExtension: "txt", rowCount: 1, columnCount: 1 });
const state = createExportState(varbinTable, ["id", "name"], matrix, [1, "0x616263"]);
await expect(state.copyWithPreference("smart")).resolves.toBe(true);
expect(extractDataGridSelection).toHaveBeenCalledWith(expect.objectContaining({ extractor: "raw", rows: [["abc"]] }));
expect(copyToClipboard).toHaveBeenCalledWith("abc");
// OS 剪贴板是文本,DBX 内部剪贴板仍保留原 hex,粘回 VARBINARY 时不会重编码或丢字节。
expect(parseDataGridClipboard("abc")).toEqual([["0x616263"]]);
});
it("非文本 VARBINARY 保持 0x/hex,避免破坏任意 bytes", async () => {
const matrix: CellSelectionMatrix = { rowIndexes: [0], columnIndexes: [1], columns: ["name"], rows: [["0xdeadbeef"]] };
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "0xdeadbeef", mimeType: "text/plain", fileExtension: "txt", rowCount: 1, columnCount: 1 });
const state = createExportState(varbinTable, ["id", "name"], matrix, [1, "0xdeadbeef"]);
await expect(state.copyWithPreference("smart")).resolves.toBe(true);
expect(extractDataGridSelection).toHaveBeenCalledWith(expect.objectContaining({ extractor: "raw", rows: [["0xdeadbeef"]] }));
});
it("多选 TSV 复制同样对文本型 VARBINARY 解码", async () => {
const rows = [["0x616263"], ["0x31"]];
const matrix: CellSelectionMatrix = { rowIndexes: [0, 1], columnIndexes: [1], columns: ["name"], rows };
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "abc\n1", mimeType: "text/tab-separated-values", fileExtension: "tsv", rowCount: 2, columnCount: 1 });
const state = createExportState(varbinTable, ["id", "name"], matrix, undefined, undefined, [
[1, "0x616263"],
[2, "0x31"],
]);
await expect(state.copyWithPreference("smart")).resolves.toBe(true);
expect(extractDataGridSelection).toHaveBeenCalledWith(expect.objectContaining({ extractor: "tsv", rows: [["abc"], ["1"]] }));
expect(parseDataGridClipboard("abc\n1")).toEqual([["0x616263"], ["0x31"]]);
});
it("JSON 复制格式同样呈现文本型 VARBINARY", async () => {
const matrix: CellSelectionMatrix = { rowIndexes: [0], columnIndexes: [1], columns: ["name"], rows: [["0x616263"]] };
const text = JSON.stringify({ name: "abc" }, null, 2);
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text, mimeType: "application/json", fileExtension: "json", rowCount: 1, columnCount: 1 });
const state = createExportState(varbinTable, ["id", "name"], matrix, [1, "0x616263"]);
await expect(state.copyWithExtractor("json")).resolves.toBe(true);
expect(extractDataGridSelection).toHaveBeenCalledWith(expect.objectContaining({ extractor: "json", rows: [["abc"]] }));
expect(copyToClipboard).toHaveBeenCalledWith(text);
});
it("SQL 提取器保持 hex 字面量以保证 round-trip", async () => {
const matrix: CellSelectionMatrix = { rowIndexes: [0], columnIndexes: [1], columns: ["name"], rows: [["0x616263"]] };
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "INSERT INTO `test_varbin` (`name`) VALUES (0x616263);", mimeType: "application/sql", fileExtension: "sql", rowCount: 1, columnCount: 1 });
const state = createExportState(varbinTable, ["id", "name"], matrix, [1, "0x616263"]);
await expect(state.copyWithExtractor("sql-inserts")).resolves.toBe(true);
// SQL 路径不解码:请求里仍是 0x<hex>,交由后端 data_grid_sql 生成安全的 hex 字面量。
expect(extractDataGridSelection).toHaveBeenCalledWith(expect.objectContaining({ extractor: "sql-inserts", rows: [["0x616263"]] }));
});
it("右键「复制单元格」对文本型 VARBINARY 直接写入原始字符串", async () => {
const state = createExportState(varbinTable, ["id", "name"], undefined, [1, "0x616263"], undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, false, 1, 1);
await state.copyCell();
expect(copyToClipboard).toHaveBeenCalledWith("abc");
// 剪贴板走本地 copyCell 路径,不应呼叫后端提取器。
expect(extractDataGridSelection).not.toHaveBeenCalled();
});
it("右键「复制单元格」对非文本 VARBINARY 保持 0x/hex", async () => {
const state = createExportState(varbinTable, ["id", "name"], undefined, [1, "0xdeadbeef"], undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, false, 1, 1);
await state.copyCell();
expect(copyToClipboard).toHaveBeenCalledWith("0xdeadbeef");
});
it("复制整行 JSON 时呈现文本型 VARBINARY", async () => {
const state = createExportState(varbinTable, ["id", "name"], undefined, [1, "0x616263"], undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, false, 1, 1);
await state.copyRow();
const copied = vi.mocked(copyToClipboard).mock.calls[0]?.[0] ?? "";
expect(JSON.parse(copied)).toEqual({ id: 1, name: "abc" });
});
it("复制全部时外部文本呈现 VARBINARY,内部网格副本保留 hex", async () => {
const state = createExportState(varbinTable, ["id", "name"], undefined, [1, "0x616263"]);
await state.copyAll();
const text = "id\tname\n1\tabc";
expect(copyToClipboard).toHaveBeenCalledWith(text);
expect(parseDataGridClipboard(text)).toEqual([
["id", "name"],
["1", "0x616263"],
]);
});
});
@@ -9,6 +9,7 @@ import type { DataGridExtractRequest, DataGridExtractorOptions } from "@/lib/dat
import { useToast } from "@/composables/useToast";
import { useExportTracker } from "@/composables/useExportTracker";
import { clipboardCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
import { binaryCellClipboardText } from "@/lib/dataGrid/binaryCellDownload";
import { tryStartExclusiveActivation, type ActionActivationGuard } from "@/lib/connection/actionActivation";
import { copyToClipboard } from "@/lib/common/clipboard";
import { clearDataGridClipboardCopy, rememberDataGridClipboardCopy } from "@/lib/dataGrid/dataGridClipboard";
@@ -492,6 +493,10 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
});
}
function binaryClipboardCellValue(value: CellValue, columnIndex: number): CellValue {
return binaryCellClipboardText(value, columnTypes.value?.[columnIndex], databaseType.value) ?? value;
}
function rowToJsonObject(item: RowItem): Record<string, unknown> {
if (options.databaseType.value === "mongodb" && item.sourceIndex !== undefined) {
const original = options.mongoDocuments?.value?.[item.sourceIndex];
@@ -500,12 +505,14 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
}
const obj: Record<string, unknown> = {};
columns.value.forEach((col, i) => {
const value = item.data[i];
const value = binaryClipboardCellValue(item.data[i], i);
if (typeof value === "string" && columnTypes.value?.[i]?.trim().toLowerCase() === "json") {
try {
obj[col] = JSON.parse(value);
return;
} catch {}
} catch {
// Keep malformed JSON cells as their original text.
}
}
obj[col] = value;
});
@@ -529,7 +536,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
const sourceIndex = visibleColumnIndexes.value[contextCell.value.col] ?? contextCell.value.col;
const [resolvedItem] = await resolveVisibleRowValues([item], [sourceIndex]);
const val = resolvedItem?.data[contextCell.value.col] ?? null;
await copyText(clipboardCellValue(val), { rows: [[val]] });
// 外部剪贴板呈现文本型 MySQL VARBINARY(NULL 也按空串输出);内部网格副本仍保留原 hex,保证回粘无损。
await copyText(clipboardCellValue(binaryClipboardCellValue(val, contextCell.value.col)), { rows: [[val]] });
}
async function copyRow() {
@@ -672,7 +680,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
async function copyAll() {
const rows = (await resolveVisibleRowValues(displayItems.value.filter((item) => !item.isDraft))).map((item) => item.data);
await copyText(formatTsv(columns.value, rows), { rows, header: columns.value });
// formatTsv 引用处理解码后文本中可能出现的制表符/换行;内部副本仍用原始 rows(hex)保证回粘无损。
const decodedRows = rows.map((row) => row.map((cell, index) => binaryClipboardCellValue(cell, index)));
await copyText(formatTsv(columns.value, decodedRows), { rows, header: columns.value });
}
// --- Export functions ---
@@ -19,6 +19,7 @@ import {
type DataGridExtractWarningCode,
} from "@/lib/dataGrid/dataGridCopyExtractor";
import type { DataGridTableMeta } from "@/lib/dataGrid/dataGridSql";
import { binaryCellClipboardText } from "@/lib/dataGrid/binaryCellDownload";
import { formatError } from "@/lib/backend/errorUtils";
import type { DatabaseType } from "@/types/database";
@@ -28,6 +29,15 @@ interface ExtractorRowItem {
isDraft?: boolean;
}
interface ExtractorRequestSource {
rowIds: number[];
sourceColumnIndexes: number[];
columnTypes: Array<string | undefined>;
normalizeValues: boolean;
presentBinaryText: boolean;
rawRows: unknown[][];
}
interface UseDataGridExtractorOptions {
columns: ComputedRef<string[]>;
displayItems: ComputedRef<ExtractorRowItem[]>;
@@ -60,7 +70,7 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
const { t } = useI18n();
const { toast } = useToast();
const hasUnsupportedDiscreteSelection = computed(() => options.hasCellSelection.value && options.selectedCellMatrix.value === null);
const requestSources = new WeakMap<DataGridExtractRequest, { rowIds: number[]; sourceColumnIndexes: number[]; columnTypes: Array<string | undefined>; normalizeValues: boolean }>();
const requestSources = new WeakMap<DataGridExtractRequest, ExtractorRequestSource>();
function normalizeCellValue(value: unknown, columnType: string | undefined): unknown {
if (typeof value !== "string" || columnType?.trim().toLowerCase() !== "json") return value;
@@ -71,6 +81,16 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
}
}
// 除 SQL 外的剪贴板展示格式都可把文本型 MySQL VARBINARY 从 `0x<hex>` 还原为原始字符串。
// SQL 必须继续持有 hex 才能保证 round-trip;rawRows 则始终保存原值,供 DBX 内部网格回粘使用。
function extractorCellValue(value: unknown, columnType: string | undefined, normalizeValues: boolean, presentBinaryText: boolean): unknown {
if (presentBinaryText) {
const text = binaryCellClipboardText(value, columnType, options.databaseType.value);
if (text !== null) return text;
}
return normalizeValues ? normalizeCellValue(value, columnType) : value;
}
function selectionData(): SelectionData | null {
if (options.hasRowSelection.value && options.selectedRowIds.value.size > 0) {
const rows = options.displayItems.value.filter((item) => options.selectedRowIds.value.has(item.id) && !item.isDraft).map((item) => item.data);
@@ -186,7 +206,11 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
}));
const descriptor = DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor];
const selectedColumnIndexes = selectedSourceIndexes.map((sourceIndex) => compactIndexBySource.get(sourceIndex)).filter((index): index is number => index !== undefined);
const rows = sourceRows.map((row) => requiredSourceIndexes.map((sourceIndex) => (descriptor.category === "json" || descriptor.category === "sql" ? normalizeCellValue(row[sourceIndex], columnTypesBySource.get(sourceIndex)) : row[sourceIndex])));
const columnTypes = requiredSourceIndexes.map((sourceIndex) => columnTypesBySource.get(sourceIndex));
const normalizeValues = descriptor.category === "json" || descriptor.category === "sql";
const presentBinaryText = descriptor.category !== "sql";
const rawRows = sourceRows.map((row) => requiredSourceIndexes.map((sourceIndex) => row[sourceIndex]));
const rows = rawRows.map((row) => row.map((value, index) => extractorCellValue(value, columnTypes[index], normalizeValues, presentBinaryText)));
const tableMeta =
descriptor.category === "sql"
? compactTableMeta(
@@ -209,28 +233,46 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
requestSources.set(request, {
rowIds: sourceRowIds,
sourceColumnIndexes: requiredSourceIndexes,
columnTypes: requiredSourceIndexes.map((sourceIndex) => columnTypesBySource.get(sourceIndex)),
normalizeValues: descriptor.category === "json" || descriptor.category === "sql",
columnTypes,
normalizeValues,
presentBinaryText,
rawRows,
});
return request;
}
async function resolveRequestSourceValues(request: DataGridExtractRequest, rowLimit?: number): Promise<DataGridExtractRequest> {
const source = requestSources.get(request);
if (!source || !options.resolveSourceValues) return rowLimit === undefined ? request : { ...request, rows: request.rows.slice(0, rowLimit) };
const rowIds = rowLimit === undefined ? source.rowIds : source.rowIds.slice(0, rowLimit);
const overlays = await options.resolveSourceValues(rowIds, source.sourceColumnIndexes);
const rows = request.rows.slice(0, rowIds.length).map((row, rowIndex) => {
const values = overlays.get(rowIds[rowIndex]!);
if (!source) return rowLimit === undefined ? request : { ...request, rows: request.rows.slice(0, rowLimit) };
const limitedSource: ExtractorRequestSource =
rowLimit === undefined
? source
: {
...source,
rowIds: source.rowIds.slice(0, rowLimit),
rawRows: source.rawRows.slice(0, rowLimit),
};
if (!options.resolveSourceValues) {
if (rowLimit === undefined) return request;
const limitedRequest = { ...request, rows: request.rows.slice(0, rowLimit) };
requestSources.set(limitedRequest, limitedSource);
return limitedRequest;
}
const overlays = await options.resolveSourceValues(limitedSource.rowIds, limitedSource.sourceColumnIndexes);
const rawRows = limitedSource.rawRows.map((row, rowIndex) => {
const values = overlays.get(limitedSource.rowIds[rowIndex]!);
if (!values) return row;
return row.map((value, compactIndex) => {
const sourceIndex = source.sourceColumnIndexes[compactIndex];
if (sourceIndex === undefined || !values.has(sourceIndex)) return value;
const resolved = values.get(sourceIndex);
return source.normalizeValues ? normalizeCellValue(resolved, source.columnTypes[compactIndex]) : resolved;
const sourceIndex = limitedSource.sourceColumnIndexes[compactIndex];
return sourceIndex !== undefined && values.has(sourceIndex) ? values.get(sourceIndex) : value;
});
});
return { ...request, rows };
const rows = rawRows.map((row) => row.map((value, index) => extractorCellValue(value, limitedSource.columnTypes[index], limitedSource.normalizeValues, limitedSource.presentBinaryText)));
const resolvedRequest = { ...request, rows };
requestSources.set(resolvedRequest, { ...limitedSource, rawRows });
return resolvedRequest;
}
function canBuildSqlUpdateRequest(): boolean {
@@ -337,7 +379,8 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) {
const gridCopy = isGridTabularCopy
? (() => {
const includeRowHeader = extractor !== "raw" && request.options.dsv.includeRowHeader;
const rows = request.rows.map((row, rowIndex) => {
const rawRows = requestSources.get(request)?.rawRows ?? request.rows;
const rows = rawRows.map((row, rowIndex) => {
const selectedRow = request.selectedColumnIndexes.map((index) => row[index]);
return includeRowHeader ? [rowIndex + 1, ...selectedRow] : selectedRow;
});
@@ -114,6 +114,7 @@ const HEX_ESCAPE_RE = /^(?:\\x[0-9a-fA-F]{2}|\s)+$/;
const BINARY_TYPE_RE = /^(?:blob|tinyblob|mediumblob|longblob|bytea|bytes|binary|varbinary|image|raw|long\s+raw)(?:\b|\()/i;
const FIXED_BINARY_TYPE_RE = /^binary(?:\b|\()/i;
const BINARY_STRING_TYPE_RE = /^(?:binary|varbinary)(?:\b|\()/i;
const VARBINARY_TYPE_RE = /^varbinary(?:\b|\()/i;
const BLOB_TYPE_RE = /^(?:blob|tinyblob|mediumblob|longblob)(?:\b|\()/i;
const MYSQL_FILE_IMPORT_TYPE_RE = /^(?:blob|tinyblob|mediumblob|longblob|binary|varbinary)(?:\b|\()/i;
@@ -220,6 +221,23 @@ export function binaryCellUtf8Text(value: unknown, columnType?: string, database
return binaryCellUtf8TextBytes(bytes, columnType);
}
// 复制到剪贴板时,把「文本型」MySQL VARBINARY 单元格还原成其原始字符串。
// DBX 后端为保留任意 bytes,把该值统一序列化成 `0x<hex>`;前端只有在严格 UTF-8 解码、
// 无控制字符且重新编码后与原 bytes 完全一致时,才把 payload(如 token)复制为文本。
// 其余情况返回 null,让调用方沿用无损 hex。范围严格限定 MySQL VARBINARY,避免改变
// MySQL BINARY/BLOB、SQL Server VARBINARY、PostgreSQL BYTEA、Oracle RAW 等现有语义。
export function binaryCellClipboardText(value: unknown, columnType?: string, databaseType?: DatabaseType): string | null {
if (databaseType !== "mysql" || !VARBINARY_TYPE_RE.test((columnType ?? "").trim())) return null;
// MySQL QueryResult 的 binary canonical form 是 `0x<hex>`;不要把普通的偶数长度
// 文本(例如 "abcd")猜测成 hex bytes。
const bytes = typeof value === "string" ? parseBinaryCellHexValue(value) : null;
if (!bytes) return null;
const text = binaryCellUtf8TextBytes(bytes, columnType);
if (text === null) return null;
const encoded = new TextEncoder().encode(text);
return encoded.length === bytes.length && encoded.every((byte, index) => byte === bytes[index]) ? text : null;
}
// MySQL BLOB 的文本预览必须与编辑写回路径(coerceMysqlBlobTextValue,仅 mysql)走同一闸门:
// SQLite/DuckDB/H2/Firebird 等库同样暴露 `blob` 列,若一律按文本预览会出现
// “显示是文本、编辑器却是十六进制”的不一致,故 blob 文本预览仅在 mysql 连接开启。
@@ -1,4 +1,5 @@
import { cellImagePreviewUrl } from "@/lib/dataGrid/cellImageUrl";
import { binaryCellClipboardText } from "@/lib/dataGrid/binaryCellDownload";
import { clipboardCellValue, displayCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
import { formatJsonText } from "@/lib/dataGrid/cellDetailPresentation";
import type { DatabaseType } from "@/types/database";
@@ -186,11 +187,11 @@ function detailColumnType(typeByColumn: ReadonlyMap<string, string> | undefined,
return resultColumnTypes?.[columnIndex]?.trim() ?? "";
}
export function dataGridRowDetailJson(detail: DataGridRowDetail, originalDocument?: unknown): string {
export function dataGridRowDetailJson(detail: DataGridRowDetail, originalDocument?: unknown, databaseType?: DatabaseType): string {
if (originalDocument !== undefined) return JSON.stringify(jsonDetailDisplayValue(originalDocument), null, 2);
const row: Record<string, CellValue> = {};
detail.fields.forEach((field) => {
row[field.column] = field.value;
row[field.column] = binaryCellClipboardText(field.value, field.type, databaseType) ?? field.value;
});
return JSON.stringify(jsonDetailDisplayValue(row), null, 2);
}
@@ -214,23 +215,23 @@ export function jsonDetailDisplayValue(value: unknown): unknown {
return value;
}
export function dataGridRowDetailTsv(detail: DataGridRowDetail): string {
return detail.fields.map((field) => clipboardCellValue(field.value)).join("\t");
export function dataGridRowDetailTsv(detail: DataGridRowDetail, databaseType?: DatabaseType): string {
return detail.fields.map((field) => clipboardCellValue(binaryCellClipboardText(field.value, field.type, databaseType) ?? field.value)).join("\t");
}
export function dataGridColumnDetailJson(detail: DataGridColumnDetail): string {
export function dataGridColumnDetailJson(detail: DataGridColumnDetail, databaseType?: DatabaseType): string {
return JSON.stringify(
detail.fields.map((field) => ({
row: field.rowNumber,
value: field.value,
value: binaryCellClipboardText(field.value, field.type, databaseType) ?? field.value,
})),
null,
2,
);
}
export function dataGridColumnDetailTsv(detail: DataGridColumnDetail): string {
return detail.fields.map((field) => clipboardCellValue(field.value)).join("\n");
export function dataGridColumnDetailTsv(detail: DataGridColumnDetail, databaseType?: DatabaseType): string {
return detail.fields.map((field) => clipboardCellValue(binaryCellClipboardText(field.value, field.type, databaseType) ?? field.value)).join("\n");
}
export interface BuildDeleteRowConfirmDetailsOptions<TRow> {