fix(export): preserve datetime precision across CSV and XLSX exports
This commit is contained in:
1 parent
ffe1e58fbb
commit
ab6a7645f6
14 files changed
+438
-43
No files matched your search
@@ -129,6 +129,7 @@ describe("useDataGridExport XLSX headers", () => {
|
||||
expect.any(String),
|
||||
expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Identifier)"], autoFilter: false }), expect.objectContaining({ sheetName: "Names", columnComments: ["name (Display name)"], autoFilter: false })]),
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -141,6 +142,6 @@ describe("useDataGridExport XLSX headers", () => {
|
||||
|
||||
await state.exportAllResultsXlsx();
|
||||
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Joined identifier)"], autoFilter: false })]), false);
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Joined identifier)"], autoFilter: false })]), false, undefined);
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ import type { DatabaseType, QueryResult } from "@/types/database";
|
||||
import type { QueryResultExportRequest } from "@/lib/backend/api";
|
||||
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { buildXlsxSqlWorksheet } from "@/lib/export/xlsxSqlSheet";
|
||||
import { formatTemporalRowsForExport } from "@/lib/dataGrid/columnFormatter";
|
||||
import { forceCsvTextForTemporalColumns, formatTemporalRowsForExport } from "@/lib/dataGrid/columnFormatter";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
|
||||
import i18n from "@/i18n";
|
||||
@@ -407,8 +407,12 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
async function writeXlsxResult(outputPath: string, result: { columns: string[]; columnTypes: string[]; columnComments?: (string | null)[]; rows: CellValue[][] }, includeSqlSheet: boolean, autoFilter: boolean) {
|
||||
const sqlWorksheet = includeSqlSheet ? buildXlsxSqlWorksheet([{ sql: currentExportSql() || "" }]) : undefined;
|
||||
const rightAlign = useSettingsStore().editorSettings.numericColumnRightAlign;
|
||||
// The rows are already rendered with the global export pattern, but the
|
||||
// workbook still needs the pattern itself so its numFmt matches; without it
|
||||
// a `SSS` pattern silently displays as `yyyy-mm-dd hh:mm:ss`.
|
||||
const dateTimeFormat = useSettingsStore().editorSettings.globalDateTimeExportFormat || undefined;
|
||||
if (!sqlWorksheet) {
|
||||
await api.exportQueryResultXlsx(outputPath, currentXlsxSheetName(), result.columns, result.columnTypes, result.columnComments, result.rows, rightAlign, autoFilter);
|
||||
await api.exportQueryResultXlsx(outputPath, currentXlsxSheetName(), result.columns, result.columnTypes, result.columnComments, result.rows, rightAlign, autoFilter, dateTimeFormat);
|
||||
return;
|
||||
}
|
||||
await api.exportQueryResultsXlsx(
|
||||
@@ -426,6 +430,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
{ ...sqlWorksheet, autoFilter },
|
||||
],
|
||||
autoFilter,
|
||||
dateTimeFormat,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -733,8 +738,11 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
});
|
||||
// Hand the raw rows straight to the Rust command. Formatting (NULL→"",
|
||||
// bool/number→text, etc.) happens there on a spawn_blocking thread, so
|
||||
// we avoid mapping every cell synchronously on the UI thread.
|
||||
const rows = result.rows;
|
||||
// we avoid mapping every cell synchronously on the UI thread. Temporal
|
||||
// columns are the one exception: force-text them here (see
|
||||
// forceCsvTextForTemporalColumns) so WPS/Excel-style CSV import can't
|
||||
// re-guess and truncate/reformat a date-looking string.
|
||||
const rows = forceCsvTextForTemporalColumns(result.rows, result.columnTypes);
|
||||
if (needsFullExport && exportProgressState) {
|
||||
exportProgressState.value = {
|
||||
...exportProgressState.value,
|
||||
@@ -795,7 +803,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(undefined, undefined, false);
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, result.rows, useSettingsStore().editorSettings.csvQuoteMode);
|
||||
const rows = forceCsvTextForTemporalColumns(result.rows, result.columnTypes);
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, rows, useSettingsStore().editorSettings.csvQuoteMode);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
@@ -1079,7 +1088,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
autoFilter: exportOptions.autoFilter,
|
||||
}));
|
||||
const sqlWorksheet = includeSqlSheet ? buildXlsxSqlWorksheet(sheets.map((sheet) => ({ resultName: sheet.sheetName, sql: sheet.sql || sheet.result.sourceStatement || "" }))) : undefined;
|
||||
await api.exportQueryResultsXlsx(outputPath, sqlWorksheet ? [...worksheets, { ...sqlWorksheet, autoFilter: exportOptions.autoFilter }] : worksheets, exportOptions.autoFilter);
|
||||
await api.exportQueryResultsXlsx(outputPath, sqlWorksheet ? [...worksheets, { ...sqlWorksheet, autoFilter: exportOptions.autoFilter }] : worksheets, exportOptions.autoFilter, exportPattern || undefined);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { joinExportedDdls } from "@/lib/export/ddlExport";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { sidebarStructureExportTargets, sidebarTableDataExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
import { forceCsvTextForTemporalColumns } from "@/lib/dataGrid/columnFormatter";
|
||||
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxExportOptions, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
import { isLoadingStructurePreview, showStructureDocCopyDialog, showStructurePreviewDialog, structureDocCopyText, structureDocCopyTitle, structurePreviewDefaultFileName, structurePreviewError, structurePreviewSql, structurePreviewTitle } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
@@ -398,11 +399,11 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
executePage: (sql) => api.executeQuery(connectionId, database, sql),
|
||||
});
|
||||
if (format === "csv") {
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, result.rows, target.csvQuoteMode);
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, forceCsvTextForTemporalColumns(result.rows, result.column_types ?? []), target.csvQuoteMode);
|
||||
} else {
|
||||
const comments = result.columns.map((name) => exportColumnInfos?.find((column) => column.name.toLocaleLowerCase() === name.toLocaleLowerCase())?.comment);
|
||||
const headerOverrides = buildXlsxHeaderOverrides(result.columns, comments, headerMode);
|
||||
await api.exportQueryResultXlsx(outputPath, target.tableName, result.columns, result.column_types ?? result.columns.map(() => ""), headerOverrides, result.rows, undefined, autoFilter);
|
||||
await api.exportQueryResultXlsx(outputPath, target.tableName, result.columns, result.column_types ?? result.columns.map(() => ""), headerOverrides, result.rows, undefined, autoFilter, settingsStore.editorSettings.globalDateTimeExportFormat || undefined);
|
||||
}
|
||||
currentTask.status = "Done";
|
||||
currentTask.rowsExported = result.rows.length;
|
||||
|
||||
Reference in new issue
Block a user