diff --git a/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts index 8340df9d84..eaa0f3dece 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts @@ -7,7 +7,7 @@ import { copyToClipboard } from "@/lib/common/clipboard"; import type { DataGridTableMeta } from "@/lib/dataGrid/dataGridSql"; import type { CellSelectionMatrix, SelectionData } from "@/lib/dataGrid/gridSelection"; import type { CellValue } from "@/lib/dataGrid/cellValue"; -import { extractDataGridSelection } from "@/lib/backend/api"; +import { extractDataGridSelection, exportQueryResultJson } from "@/lib/backend/api"; import { DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS } from "@/lib/dataGrid/dataGridCopyExtractor"; import { clearDataGridClipboardCopy, parseDataGridClipboard } from "@/lib/dataGrid/dataGridClipboard"; @@ -46,6 +46,7 @@ vi.mock("@/lib/backend/api", async (importOriginal) => { return { ...original, extractDataGridSelection: vi.fn(), + exportQueryResultJson: vi.fn(), }; }); @@ -70,6 +71,7 @@ function createMongoExportState(options: { mongoUpdateTarget?: false; contextColumn?: number; syntheticContext?: boolean; + fullExportResult?: UseDataGridExportOptions["fullExportResult"]; }) { const items = options.items ?? [options.item]; const selectedRowIds = options.selectedRowIds ?? new Set(); @@ -99,6 +101,7 @@ function createMongoExportState(options: { getRowItem: (rowId) => items.find((item) => item.id === rowId), selectedRowIds: ref(selectedRowIds), hasRowSelection: computed(() => selectedRowIds.size > 0), + fullExportResult: options.fullExportResult, }; return useDataGridExport(state); } @@ -1509,6 +1512,33 @@ describe("useDataGridExport prepared row statements", () => { expect(extractDataGridSelection).not.toHaveBeenCalled(); expect(copyToClipboard).not.toHaveBeenCalled(); }); + + it("preserves Mongo Extended JSON objects and dates in JSON exports", async () => { + const columns = ["_id", "valueMap", "createdTime"]; + const mongoCopyDocument = { + _id: { $oid: "6a9fb51db2f0c46b94002f26" }, + valueMap: { field1: "10147E", field2: "0" }, + createdTime: { $date: "2026-09-08T07:11:25.458Z" }, + }; + const item = { ...row(["6a9fb51db2f0c46b94002f26", JSON.stringify(mongoCopyDocument.valueMap), 'ISODate("2026-09-08T07:11:25.458Z")']), sourceIndex: 0 }; + const state = createMongoExportState({ + columns, + item, + mongoDocuments: [mongoCopyDocument], + fullExportResult: async () => ({ + columns, + column_types: ["", "", "datetime"], + rows: [item.data as [string, string, string]], + mongo_copy_documents: [mongoCopyDocument], + affected_rows: 1, + execution_time_ms: 1, + }), + }); + + await state.exportJson(); + + expect(exportQueryResultJson).toHaveBeenCalledWith(expect.any(String), columns, [[mongoCopyDocument._id, mongoCopyDocument.valueMap, mongoCopyDocument.createdTime]]); + }); }); // issue #7471:文本型 MySQL VARBINARY 复制单元格/多选/整行时,外部剪贴板应呈现原始字符串; diff --git a/apps/desktop/src/composables/useDataGridExport.ts b/apps/desktop/src/composables/useDataGridExport.ts index bdd2d73f50..0279a898b2 100644 --- a/apps/desktop/src/composables/useDataGridExport.ts +++ b/apps/desktop/src/composables/useDataGridExport.ts @@ -331,7 +331,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { }); } - function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; columnComments: Array; rows: CellValue[][] } { + function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; columnComments: Array; rows: CellValue[][]; mongoCopyDocuments?: unknown[] } { const hiddenColumnIndexes = new Set(result.hidden_column_indexes ?? []); const exportedColumnIndexes = result.columns.map((_, index) => index).filter((index) => !hiddenColumnIndexes.has(index)); const hasHiddenColumns = exportedColumnIndexes.length !== result.columns.length; @@ -345,15 +345,34 @@ export function useDataGridExport(options: UseDataGridExportOptions) { columnTypes: hasHiddenColumns ? exportedColumnIndexes.map((index) => result.column_types?.[index] ?? "") : (result.column_types ?? []), columnComments: hasHiddenColumns ? exportedColumnIndexes.map((index) => allXlsxColumnComments.value[index]) : [...allXlsxColumnComments.value], rows: hasHiddenColumns ? rows.map((row) => exportedColumnIndexes.map((index) => row[index])) : rows, + mongoCopyDocuments: result.mongo_copy_documents?.slice(0, rows.length), }; } + function mongoDocumentRowsForJson(columnsToExport: string[], rows: CellValue[][], documents: unknown[] | undefined): CellValue[][] { + if (databaseType.value !== "mongodb" || !documents || documents.length !== rows.length) return rows; + return rows.map((row, rowIndex) => { + const document = documents[rowIndex]; + if (!document || typeof document !== "object" || Array.isArray(document)) return row; + const source = document as Record; + return columnsToExport.map((column, columnIndex) => (Object.prototype.hasOwnProperty.call(source, column) ? (source[column] as CellValue) : (row[columnIndex] ?? null))); + }); + } + + function mongoLocalRowsForJson(items: RowItem[]): CellValue[][] { + return items.map((item) => { + const document = rowToJsonObject(item); + return columns.value.map((column) => (document[column] as CellValue) ?? null); + }); + } + async function resultToExport( rowIds?: number[], onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void, useFullExport = true, formatDateTime = true, headerMode: XlsxHeaderMode = "name", + preserveMongoExtendedJson = false, ): Promise<{ columns: string[]; columnTypes: string[]; @@ -367,7 +386,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { if (result) { const columnComments = buildXlsxHeaderOverrides(result.columns, commentsForExportColumns(result.columns), headerMode); return { - ...applyGlobalDateTimeExportFormat({ columns: result.columns, columnTypes: result.column_types ?? [], rows: result.rows }, formatDateTime), + ...applyGlobalDateTimeExportFormat({ columns: result.columns, columnTypes: result.column_types ?? [], rows: preserveMongoExtendedJson ? mongoDocumentRowsForJson(result.columns, result.rows, result.mongo_copy_documents) : result.rows }, formatDateTime && !preserveMongoExtendedJson), columnComments, spatialColumns: result.spatial_columns, spatialValues: result.spatial_values, @@ -383,7 +402,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { const normalized = normalizeCompleteLocalResult(completeLocalResult.value); const columnComments = buildXlsxHeaderOverrides(normalized.columns, normalized.columnComments, headerMode); return { - ...applyGlobalDateTimeExportFormat({ columns: normalized.columns, columnTypes: normalized.columnTypes, rows: normalized.rows }, formatDateTime), + ...applyGlobalDateTimeExportFormat({ columns: normalized.columns, columnTypes: normalized.columnTypes, rows: preserveMongoExtendedJson ? mongoDocumentRowsForJson(normalized.columns, normalized.rows, normalized.mongoCopyDocuments) : normalized.rows }, formatDateTime && !preserveMongoExtendedJson), columnComments, spatialColumns: completeLocalResult.value.spatial_columns, spatialValues: completeLocalResult.value.spatial_values, @@ -399,9 +418,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) { { columns: columns.value, columnTypes: (columnTypes.value ?? []).map((type) => type ?? ""), - rows: exportItems.map((item) => item.data), + rows: preserveMongoExtendedJson && databaseType.value === "mongodb" ? mongoLocalRowsForJson(exportItems) : exportItems.map((item) => item.data), }, - formatDateTime, + formatDateTime && !preserveMongoExtendedJson, ), columnComments: commentHeader, ...(spatialColumns?.length ? { spatialColumns } : {}), @@ -842,7 +861,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { if (!path) return; outputPath = path as string; } - const result = await resultToExport(rowIds); + const result = await resultToExport(rowIds, undefined, true, true, "name", true); await api.exportQueryResultJson(outputPath, result.columns, result.rows); toast(t("grid.exported")); } catch (e: any) { @@ -864,7 +883,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { if (!path) return; outputPath = path as string; } - const result = await resultToExport(undefined, undefined, false); + const result = await resultToExport(undefined, undefined, false, true, "name", true); await api.exportQueryResultJson(outputPath, result.columns, result.rows); toast(t("grid.exported")); } catch (e: any) {