feat(export): configurable CSV field quoting mode
This commit is contained in:
32 files changed
+442
-56
No files matched your search
@@ -90,6 +90,7 @@ import {
|
||||
type TabPlacement,
|
||||
type TabSortMode,
|
||||
type UpdateDownloadSource,
|
||||
type CsvQuoteMode,
|
||||
type CustomThemeColors,
|
||||
type CustomTheme,
|
||||
type McpConnectionPolicy,
|
||||
@@ -548,6 +549,7 @@ const editSidebarShowTooltips = ref(settingsStore.editorSettings.sidebarShowTool
|
||||
const editSidebarIndent = ref(settingsStore.editorSettings.sidebarIndent);
|
||||
const editSidebarFontSize = ref(settingsStore.editorSettings.sidebarFontSize);
|
||||
const editExportBatchSize = ref(settingsStore.editorSettings.exportBatchSize);
|
||||
const editCsvQuoteMode = ref<CsvQuoteMode>(settingsStore.editorSettings.csvQuoteMode);
|
||||
const editGlobalDateTimeDisplayFormat = ref(settingsStore.editorSettings.globalDateTimeDisplayFormat);
|
||||
const editGlobalDateTimeExportFormat = ref(settingsStore.editorSettings.globalDateTimeExportFormat);
|
||||
const editGlobalDateTimeImportFormat = ref(settingsStore.editorSettings.globalDateTimeImportFormat);
|
||||
@@ -698,6 +700,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
sidebarCopyTableNameIncludeSchema: editSidebarCopyTableNameIncludeSchema.value,
|
||||
redisKeyTemplates: normalizeRedisKeyTemplates(editRedisKeyTemplates.value),
|
||||
exportBatchSize: editExportBatchSize.value,
|
||||
csvQuoteMode: editCsvQuoteMode.value,
|
||||
globalDateTimeDisplayFormat: editGlobalDateTimeDisplayFormat.value,
|
||||
globalDateTimeExportFormat: editGlobalDateTimeExportFormat.value,
|
||||
globalDateTimeImportFormat: editGlobalDateTimeImportFormat.value,
|
||||
@@ -1102,6 +1105,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editSidebarIndent.value = settingsStore.editorSettings.sidebarIndent;
|
||||
editSidebarFontSize.value = settingsStore.editorSettings.sidebarFontSize;
|
||||
editExportBatchSize.value = settingsStore.editorSettings.exportBatchSize;
|
||||
editCsvQuoteMode.value = settingsStore.editorSettings.csvQuoteMode;
|
||||
editGlobalDateTimeDisplayFormat.value = settingsStore.editorSettings.globalDateTimeDisplayFormat;
|
||||
editGlobalDateTimeExportFormat.value = settingsStore.editorSettings.globalDateTimeExportFormat;
|
||||
editGlobalDateTimeImportFormat.value = settingsStore.editorSettings.globalDateTimeImportFormat;
|
||||
@@ -1398,6 +1402,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(DEFAULT_EDITOR_SETTINGS.tableColumnTemplateFields);
|
||||
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(DEFAULT_EDITOR_SETTINGS.redisKeyTemplates).join("\n");
|
||||
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
|
||||
editCsvQuoteMode.value = DEFAULT_EDITOR_SETTINGS.csvQuoteMode;
|
||||
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
|
||||
editGlobalDateTimeExportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeExportFormat;
|
||||
editGlobalDateTimeImportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeImportFormat;
|
||||
@@ -1507,6 +1512,7 @@ function resetAllDefaults() {
|
||||
editSidebarCopyTableNameIncludeSchema.value = DEFAULT_EDITOR_SETTINGS.sidebarCopyTableNameIncludeSchema;
|
||||
editRedisKeyTemplates.value = normalizeRedisKeyTemplates(DEFAULT_EDITOR_SETTINGS.redisKeyTemplates).join("\n");
|
||||
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
|
||||
editCsvQuoteMode.value = DEFAULT_EDITOR_SETTINGS.csvQuoteMode;
|
||||
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
|
||||
editGlobalDateTimeExportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeExportFormat;
|
||||
editGlobalDateTimeImportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeImportFormat;
|
||||
@@ -6518,6 +6524,23 @@ onUnmounted(() => {
|
||||
<div class="text-sm font-medium text-muted-foreground">
|
||||
{{ t("settings.exportSection") }}
|
||||
</div>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 space-y-0.5">
|
||||
<Label for="csv-quote-mode">{{ t("settings.csvQuoteMode") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.csvQuoteModeDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Select v-model="editCsvQuoteMode">
|
||||
<SelectTrigger id="csv-quote-mode" class="h-8 w-44 shrink-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{{ t("settings.csvQuoteModeAll") }}</SelectItem>
|
||||
<SelectItem value="necessary">{{ t("settings.csvQuoteModeNecessary") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.exportBatchSize") }}</Label>
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
@@ -2156,7 +2156,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
executePage: (sql) => api.executeQuery(props.connection.id, props.database, sql),
|
||||
});
|
||||
if (format === "csv") {
|
||||
await api.exportQueryResultCsv(filePath, result.columns, result.rows);
|
||||
await api.exportQueryResultCsv(filePath, result.columns, result.rows, settingsStore.editorSettings.csvQuoteMode);
|
||||
} else {
|
||||
const comments = result.columns.map((name) => columnInfos?.find((column) => column.name.toLocaleLowerCase() === name.toLocaleLowerCase())?.comment);
|
||||
const headerOverrides = buildXlsxHeaderOverrides(result.columns, comments, headerMode);
|
||||
@@ -2194,6 +2194,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
tableName: row.name,
|
||||
filePath,
|
||||
format,
|
||||
csvQuoteMode: settingsStore.editorSettings.csvQuoteMode,
|
||||
columns,
|
||||
columnComments: format === "xlsx" ? columnComments : undefined,
|
||||
autoFilter: format === "xlsx" ? autoFilter : undefined,
|
||||
|
||||
@@ -756,7 +756,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
outputPath = path as string;
|
||||
}
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, rows);
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, rows, useSettingsStore().editorSettings.csvQuoteMode);
|
||||
if (needsFullExport && exportProgressState) {
|
||||
exportProgressState.value = {
|
||||
...exportProgressState.value,
|
||||
@@ -795,7 +795,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(undefined, undefined, false);
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, result.rows);
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, result.rows, useSettingsStore().editorSettings.csvQuoteMode);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
@@ -1154,6 +1154,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
tableName: meta.tableName,
|
||||
filePath: outputPath,
|
||||
format,
|
||||
csvQuoteMode: editorSettings.csvQuoteMode,
|
||||
columns: columns.value,
|
||||
columnTypes: columnTypes.value,
|
||||
columnComments: format === "xlsx" ? buildXlsxHeaderOverrides(columns.value, visibleXlsxColumnComments.value, headerMode) : undefined,
|
||||
@@ -1227,7 +1228,14 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
});
|
||||
const columnComments = format === "xlsx" ? buildXlsxHeaderOverrides(allColumns.value, allXlsxColumnComments.value, headerMode) : undefined;
|
||||
const request = baseRequest
|
||||
? { ...baseRequest, dateTimeFormat: useSettingsStore().editorSettings.globalDateTimeExportFormat || undefined, numericColumnRightAlign: useSettingsStore().editorSettings.numericColumnRightAlign ?? true, columnComments, autoFilter: format === "xlsx" ? autoFilter : undefined }
|
||||
? {
|
||||
...baseRequest,
|
||||
csvQuoteMode: useSettingsStore().editorSettings.csvQuoteMode,
|
||||
dateTimeFormat: useSettingsStore().editorSettings.globalDateTimeExportFormat || undefined,
|
||||
numericColumnRightAlign: useSettingsStore().editorSettings.numericColumnRightAlign ?? true,
|
||||
columnComments,
|
||||
autoFilter: format === "xlsx" ? autoFilter : undefined,
|
||||
}
|
||||
: undefined;
|
||||
if (!request) throw new Error("Unable to build query result export request");
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
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";
|
||||
import type { CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
|
||||
type StructureCopyFormat = "tsv" | "markdown";
|
||||
|
||||
@@ -42,6 +43,7 @@ interface SidebarTableExportTarget {
|
||||
identifierQuote?: string;
|
||||
batchSize: number;
|
||||
rowLimit: number | null;
|
||||
csvQuoteMode: CsvQuoteMode;
|
||||
fileNameBase?: string;
|
||||
}
|
||||
|
||||
@@ -360,6 +362,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
identifierQuote: connectionStore.connectionIdentifierQuote(connectionId),
|
||||
batchSize: editorSettings.exportBatchSize,
|
||||
rowLimit: editorSettings.exportRowLimitEnabled ? editorSettings.exportRowLimit : null,
|
||||
csvQuoteMode: editorSettings.csvQuoteMode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -395,7 +398,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
executePage: (sql) => api.executeQuery(connectionId, database, sql),
|
||||
});
|
||||
if (format === "csv") {
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, result.rows);
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, result.rows, 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);
|
||||
@@ -424,6 +427,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
tableName: target.tableName,
|
||||
filePath: outputPath,
|
||||
format,
|
||||
csvQuoteMode: target.csvQuoteMode,
|
||||
columns: queryColumns,
|
||||
columnComments,
|
||||
autoFilter: format === "xlsx" ? autoFilter : undefined,
|
||||
|
||||
@@ -6855,6 +6855,10 @@ export default {
|
||||
queryExportKeysetOptimizationEnabled: "Try keyset optimization",
|
||||
queryExportKeysetOptimizationEnabledDescription: "Only applies to safely recognized single-table queries; complex queries automatically fall back.",
|
||||
exportSection: "Export",
|
||||
csvQuoteMode: "CSV field quotes",
|
||||
csvQuoteModeDescription: "Choose how CSV fields use double quotes.",
|
||||
csvQuoteModeAll: "All fields",
|
||||
csvQuoteModeNecessary: "Only when needed",
|
||||
performanceSection: "Performance",
|
||||
shortcutExecuteSql: "Execute SQL",
|
||||
shortcutExecuteSqlInNewResultTab: "Execute SQL in new result tab",
|
||||
|
||||
@@ -6472,6 +6472,10 @@ export default withEnglishFallback({
|
||||
queryExportKeysetOptimizationEnabled: "Intentar optimización keyset",
|
||||
queryExportKeysetOptimizationEnabledDescription: "Solo aplica a consultas de una sola tabla reconocidas de forma segura; las consultas complejas usan el método estándar automáticamente.",
|
||||
exportSection: "Exportar",
|
||||
csvQuoteMode: "Comillas de campos CSV",
|
||||
csvQuoteModeDescription: "Elige cómo usar comillas dobles en los campos CSV.",
|
||||
csvQuoteModeAll: "Todos los campos",
|
||||
csvQuoteModeNecessary: "Solo cuando sea necesario",
|
||||
performanceSection: "Rendimiento",
|
||||
shortcutExecuteSql: "Ejecutar SQL",
|
||||
shortcutExecuteSqlInNewResultTab: "Ejecutar SQL en una nueva pestaña de resultados",
|
||||
|
||||
@@ -6472,6 +6472,10 @@ export default withEnglishFallback({
|
||||
queryExportKeysetOptimizationEnabled: "Prova ottimizzazione keyset",
|
||||
queryExportKeysetOptimizationEnabledDescription: "Si applica solo a query a tabella singola riconosciute in sicurezza; le query complesse ricadono automaticamente.",
|
||||
exportSection: "Esportazione",
|
||||
csvQuoteMode: "Virgolette campi CSV",
|
||||
csvQuoteModeDescription: "Scegli come usare le virgolette doppie nei campi CSV.",
|
||||
csvQuoteModeAll: "Tutti i campi",
|
||||
csvQuoteModeNecessary: "Solo quando necessario",
|
||||
performanceSection: "Prestazioni",
|
||||
shortcutExecuteSql: "Esegui SQL",
|
||||
shortcutExecuteSqlInNewResultTab: "Esegui SQL in una nuova scheda dei risultati",
|
||||
|
||||
@@ -6480,6 +6480,10 @@ export default withEnglishFallback({
|
||||
exportBatchSize: "エクスポートバッチサイズ",
|
||||
exportBatchSizeDescription: "テーブルデータエクスポート時のバッチあたりの取得行数(100〜100000)。",
|
||||
exportSection: "エクスポート",
|
||||
csvQuoteMode: "CSV フィールドの引用符",
|
||||
csvQuoteModeDescription: "CSV フィールドを二重引用符で囲む方法を選択します。",
|
||||
csvQuoteModeAll: "すべてのフィールド",
|
||||
csvQuoteModeNecessary: "必要な場合のみ",
|
||||
performanceSection: "パフォーマンス",
|
||||
shortcutExecuteSql: "SQLを実行",
|
||||
shortcutExecuteSqlInNewResultTab: "新しい結果タブでSQLを実行",
|
||||
|
||||
@@ -6631,6 +6631,10 @@ export default withEnglishFallback({
|
||||
shortcutUppercaseSelection: "선택 영역을 대문자로 변환",
|
||||
shortcutLowercaseSelection: "선택 영역을 소문자로 변환",
|
||||
shortcutExPasteSqlInCondition: "ExPaste: IN 조건으로 붙여넣기",
|
||||
csvQuoteMode: "CSV 필드 따옴표",
|
||||
csvQuoteModeDescription: "CSV 내보내기 시 필드에 큰따옴표를 사용하는 방식을 선택합니다.",
|
||||
csvQuoteModeAll: "모든 필드",
|
||||
csvQuoteModeNecessary: "필수 필드만",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "이전 JRE 디렉터리 제거 실패: {path} (원래 오류: {error})",
|
||||
|
||||
@@ -6474,6 +6474,10 @@ export default withEnglishFallback({
|
||||
queryExportKeysetOptimizationEnabled: "Tentar otimização keyset",
|
||||
queryExportKeysetOptimizationEnabledDescription: "Aplica-se apenas a consultas de tabela única reconhecidas com segurança; consultas complexas recuam automaticamente.",
|
||||
exportSection: "Exportar",
|
||||
csvQuoteMode: "Aspas dos campos CSV",
|
||||
csvQuoteModeDescription: "Escolha como usar aspas duplas nos campos CSV.",
|
||||
csvQuoteModeAll: "Todos os campos",
|
||||
csvQuoteModeNecessary: "Somente quando necessário",
|
||||
performanceSection: "Desempenho",
|
||||
shortcutExecuteSql: "Executar SQL",
|
||||
shortcutExecuteSqlInNewResultTab: "Executar SQL em uma nova aba de resultados",
|
||||
|
||||
@@ -6836,6 +6836,10 @@ export default withEnglishFallback({
|
||||
queryExportKeysetOptimizationEnabled: "尝试 keyset 优化",
|
||||
queryExportKeysetOptimizationEnabledDescription: "仅对可安全识别的单表查询生效,复杂查询会自动回退。",
|
||||
exportSection: "导出",
|
||||
csvQuoteMode: "CSV 字段引号",
|
||||
csvQuoteModeDescription: "选择 CSV 导出时字段使用双引号的方式。",
|
||||
csvQuoteModeAll: "全部字段",
|
||||
csvQuoteModeNecessary: "仅必要字段",
|
||||
performanceSection: "性能",
|
||||
shortcutExecuteSql: "执行 SQL",
|
||||
shortcutExecuteSqlInNewResultTab: "在新结果标签页中执行 SQL",
|
||||
|
||||
@@ -6190,6 +6190,10 @@ export default withEnglishFallback({
|
||||
updateDownloadSourceOfficial: "官方來源(建議)",
|
||||
updateDownloadSourceCnb: "CNB",
|
||||
exportSection: "匯出",
|
||||
csvQuoteMode: "CSV 欄位引號",
|
||||
csvQuoteModeDescription: "選擇 CSV 匯出時欄位使用雙引號的方式。",
|
||||
csvQuoteModeAll: "全部欄位",
|
||||
csvQuoteModeNecessary: "僅必要欄位",
|
||||
performanceSection: "效能",
|
||||
exportBatchSize: "匯出批次大小",
|
||||
dateTimeSection: "日期與時間",
|
||||
|
||||
@@ -153,6 +153,7 @@ import type {
|
||||
ElasticsearchDeleteByQueryResult,
|
||||
} from "@/lib/backend/tauri";
|
||||
import type { QueryEditability } from "@/lib/sql/sqlAnalysis";
|
||||
import type { CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
import { isTerminalTransferProgress } from "@/lib/backend/transferProgress";
|
||||
import type {
|
||||
DataGridColumnDistinctValuesSqlOptions,
|
||||
@@ -2595,9 +2596,9 @@ export async function cancelQueryResultExport(exportId: string, executionId?: st
|
||||
});
|
||||
}
|
||||
|
||||
export async function exportQueryResultCsv(filePath: string, columns: string[], rows: readonly (readonly XlsxCellValue[])[]): Promise<void> {
|
||||
export async function exportQueryResultCsv(filePath: string, columns: string[], rows: readonly (readonly XlsxCellValue[])[], csvQuoteMode: CsvQuoteMode = "all"): Promise<void> {
|
||||
const { formatCsv } = await import("@/lib/export/exportFormats");
|
||||
const content = formatCsv(columns, rows as (string | number | boolean | null)[][]);
|
||||
const content = formatCsv(columns, rows as (string | number | boolean | null)[][], csvQuoteMode);
|
||||
const fileName = filePath.split(/[\\/]/).pop() || "export.csv";
|
||||
const blob = new Blob(["\uFEFF", content], {
|
||||
type: "text/csv;charset=utf-8",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
|
||||
import { decodeMeilisearchDocumentPage, decodeMeilisearchSearchResult, type MeilisearchDocumentPage, type MeilisearchDocumentPageWire, type MeilisearchSearchResult, type MeilisearchSearchWireResult } from "@/lib/backend/meilisearchTransport";
|
||||
import type { XuguTablespaceInfo } from "@/types/database";
|
||||
import type { CreatedKey, EnqueuedTaskSummary, KeyCreateInput, KeyListItem, KeyPage, KeyUpdateInput, MeilisearchSystemOverview, MeilisearchTask, TaskListInput, TaskPage, TaskSelector } from "@/types/meilisearchManagement";
|
||||
import type { CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
|
||||
/** Normalize Tauri rejections once at the public backend boundary. */
|
||||
async function invokeBackend<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
@@ -4839,6 +4840,7 @@ export interface TableExportRequest {
|
||||
tableName: string;
|
||||
filePath: string;
|
||||
format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt";
|
||||
csvQuoteMode?: CsvQuoteMode;
|
||||
columns?: string[];
|
||||
columnTypes?: Array<string | null | undefined>;
|
||||
columnComments?: Array<string | null> | null;
|
||||
@@ -4862,6 +4864,7 @@ export interface TableCsvExportOptions {
|
||||
columns?: string[];
|
||||
pageSize?: number;
|
||||
timeoutSecs?: number;
|
||||
csvQuoteMode?: CsvQuoteMode;
|
||||
}
|
||||
|
||||
export interface TableExportProgress {
|
||||
@@ -4886,6 +4889,7 @@ export interface QueryResultExportRequest {
|
||||
useAgentCursor: boolean;
|
||||
filePath: string;
|
||||
format: "csv" | "xlsx" | "txt" | "sql";
|
||||
csvQuoteMode?: CsvQuoteMode;
|
||||
includeSqlSheet?: boolean;
|
||||
pageSize: number;
|
||||
rowLimit?: number | null;
|
||||
@@ -5029,12 +5033,13 @@ export async function recordDatabaseExportDestination(directory: string): Promis
|
||||
await invoke("record_database_export_destination", { directory });
|
||||
}
|
||||
|
||||
export async function exportQueryResultCsv(filePath: string, columns: string[], rows: readonly (readonly XlsxCellValue[])[]): Promise<void> {
|
||||
export async function exportQueryResultCsv(filePath: string, columns: string[], rows: readonly (readonly XlsxCellValue[])[], csvQuoteMode: CsvQuoteMode = "all"): Promise<void> {
|
||||
return invoke("export_query_result_csv", {
|
||||
request: {
|
||||
filePath,
|
||||
columns,
|
||||
rows,
|
||||
csvQuoteMode,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export type CsvQuoteMode = "all" | "necessary";
|
||||
|
||||
export const DEFAULT_CSV_QUOTE_MODE: CsvQuoteMode = "all";
|
||||
|
||||
export function normalizeCsvQuoteMode(value: unknown): CsvQuoteMode {
|
||||
return value === "necessary" ? "necessary" : DEFAULT_CSV_QUOTE_MODE;
|
||||
}
|
||||
|
||||
export function csvFieldNeedsQuotes(value: string): boolean {
|
||||
return /[",\r\n]/.test(value);
|
||||
}
|
||||
|
||||
export function escapeCsvField(value: string, quoteMode: CsvQuoteMode): string {
|
||||
if (quoteMode === "necessary" && !csvFieldNeedsQuotes(value)) return value;
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { DatabaseType, QueryResult } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { escapeCsvField, type CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
|
||||
export type ExportCellValue = string | number | boolean | null;
|
||||
|
||||
export function formatCsv(columns: string[], rows: ExportCellValue[][]): string {
|
||||
const esc = (v: string) => `"${v.replace(/"/g, '""')}"`;
|
||||
const header = columns.map(esc).join(",");
|
||||
const body = rows.map((row) => row.map((c) => (c === null ? "" : esc(String(c)))).join(",")).join("\n");
|
||||
export function formatCsv(columns: string[], rows: ExportCellValue[][], quoteMode: CsvQuoteMode = "all"): string {
|
||||
const header = columns.map((column) => escapeCsvField(column, quoteMode)).join(",");
|
||||
const body = rows.map((row) => row.map((cell) => (cell === null ? "" : escapeCsvField(String(cell), quoteMode))).join(",")).join("\n");
|
||||
return `${header}\n${body}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"sidebarCopyTableNameIncludeSchema",
|
||||
"redisKeyTemplates",
|
||||
"exportBatchSize",
|
||||
"csvQuoteMode",
|
||||
"exportRowLimitEnabled",
|
||||
"exportRowLimit",
|
||||
"queryExportKeysetOptimizationEnabled",
|
||||
|
||||
@@ -6975,6 +6975,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
totalRows,
|
||||
timeoutSecs: queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs),
|
||||
keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled,
|
||||
csvQuoteMode: settings.csvQuoteMode,
|
||||
clientSessionId,
|
||||
executionId: uuid(),
|
||||
exportTableName: options.exportTableName,
|
||||
@@ -7012,6 +7013,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
totalRows: null,
|
||||
timeoutSecs: queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs),
|
||||
keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled,
|
||||
csvQuoteMode: settings.csvQuoteMode,
|
||||
clientSessionId: `${tabClientSessionId(tab, "export")}:${exportId}`,
|
||||
executionId: uuid(),
|
||||
numericColumnRightAlign: settings.numericColumnRightAlign,
|
||||
|
||||
@@ -26,11 +26,12 @@ import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides }
|
||||
import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates";
|
||||
import { type DataTabReuseMode, DEFAULT_DATA_TAB_REUSE_MODE, normalizeDataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
|
||||
import { normalizeCompletionTriggerMode, type SqlCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
import { DEFAULT_CSV_QUOTE_MODE, normalizeCsvQuoteMode, type CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
import { configureMetadataRuntimeCache, METADATA_CACHE_DEFAULT_MEMORY_MB, normalizeMetadataCacheMemoryMb } from "@/lib/metadata/metadataRuntimeCache";
|
||||
import type { AiApiStyle, AiAssistantMode, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiModelEffortPreference, AiProvider, AiReasoningLevel, AiTestConnectionResult } from "@/types/ai";
|
||||
import type { SqlShortcutAction, SqlSnippet, TableInfoTab } from "@/types/database";
|
||||
|
||||
export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, DataTabReuseMode, SavedSqlOpenTargetMode, SqlCompletionTriggerMode };
|
||||
export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, CsvQuoteMode, DataTabReuseMode, SavedSqlOpenTargetMode, SqlCompletionTriggerMode };
|
||||
|
||||
export interface DesktopSettings {
|
||||
show_tray_icon: boolean;
|
||||
@@ -789,6 +790,7 @@ export interface EditorSettings {
|
||||
sqlShortcuts: SqlShortcutAction[];
|
||||
tableColumnTemplateFields: string[];
|
||||
exportBatchSize: number;
|
||||
csvQuoteMode: CsvQuoteMode;
|
||||
/** Global Redis key-search templates; overridden by non-empty connection templates. */
|
||||
redisKeyTemplates: string[];
|
||||
exportRowLimitEnabled: boolean;
|
||||
@@ -1014,6 +1016,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
sqlShortcuts: [],
|
||||
tableColumnTemplateFields: [...DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS],
|
||||
exportBatchSize: 2000,
|
||||
csvQuoteMode: DEFAULT_CSV_QUOTE_MODE,
|
||||
redisKeyTemplates: [],
|
||||
exportRowLimitEnabled: false,
|
||||
exportRowLimit: 100000,
|
||||
@@ -1484,6 +1487,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
sqlShortcuts: normalizeSqlShortcuts(settings.sqlShortcuts, existing?.sqlShortcuts),
|
||||
tableColumnTemplateFields: normalizeTableColumnTemplateFields(settings.tableColumnTemplateFields),
|
||||
exportBatchSize: typeof settings.exportBatchSize === "number" && settings.exportBatchSize >= 100 && settings.exportBatchSize <= 100000 ? Math.round(settings.exportBatchSize) : DEFAULT_EDITOR_SETTINGS.exportBatchSize,
|
||||
csvQuoteMode: normalizeCsvQuoteMode(settings.csvQuoteMode),
|
||||
redisKeyTemplates: normalizeRedisKeyTemplates(settings.redisKeyTemplates),
|
||||
exportRowLimitEnabled: typeof settings.exportRowLimitEnabled === "boolean" ? settings.exportRowLimitEnabled : DEFAULT_EDITOR_SETTINGS.exportRowLimitEnabled,
|
||||
exportRowLimit: typeof settings.exportRowLimit === "number" && settings.exportRowLimit >= 100 && settings.exportRowLimit <= 2147483647 ? Math.round(settings.exportRowLimit) : DEFAULT_EDITOR_SETTINGS.exportRowLimit,
|
||||
@@ -2151,6 +2155,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.sqlShortcuts !== undefined) editorSettings.value.sqlShortcuts = normalizeSqlShortcuts(partial.sqlShortcuts);
|
||||
if (partial.tableColumnTemplateFields !== undefined) editorSettings.value.tableColumnTemplateFields = normalizeTableColumnTemplateFields(partial.tableColumnTemplateFields);
|
||||
if (partial.exportBatchSize !== undefined) editorSettings.value.exportBatchSize = Math.min(100000, Math.max(100, Math.round(partial.exportBatchSize)));
|
||||
if (partial.csvQuoteMode !== undefined) editorSettings.value.csvQuoteMode = normalizeCsvQuoteMode(partial.csvQuoteMode);
|
||||
if (partial.redisKeyTemplates !== undefined) editorSettings.value.redisKeyTemplates = normalizeRedisKeyTemplates(partial.redisKeyTemplates);
|
||||
if (partial.exportRowLimitEnabled !== undefined) editorSettings.value.exportRowLimitEnabled = partial.exportRowLimitEnabled;
|
||||
if (partial.exportRowLimit !== undefined) editorSettings.value.exportRowLimit = Math.min(2147483647, Math.max(100, Math.round(partial.exportRowLimit)));
|
||||
|
||||
Reference in new issue
Block a user