feat(export): configurable CSV field quoting mode
This commit is contained in:
32 files changed
+442
-56
No files matched your search
@@ -56,3 +56,11 @@ test("round-trips completion selection behavior through the editor settings draf
|
||||
assert.deepEqual(patch, { selectFirstCompletionOnOpen: false });
|
||||
assert.equal(editorSettingsDraftFromSettings({ ...DEFAULT_EDITOR_SETTINGS, ...patch }).selectFirstCompletionOnOpen, false);
|
||||
});
|
||||
|
||||
test("round-trips the persisted CSV quote mode through the editor settings draft", () => {
|
||||
const base = editorSettingsDraftFromSettings(DEFAULT_EDITOR_SETTINGS);
|
||||
const patch = editorSettingsPatchFromDraft({ ...base, csvQuoteMode: "necessary" }, base);
|
||||
|
||||
assert.deepEqual(patch, { csvQuoteMode: "necessary" });
|
||||
assert.equal(editorSettingsDraftFromSettings({ ...DEFAULT_EDITOR_SETTINGS, ...patch }).csvQuoteMode, "necessary");
|
||||
});
|
||||
+14
@@ -15,3 +15,17 @@ test("formatCsv writes database null as an empty cell and preserves literal NULL
|
||||
'"id","note"\n"1",\n"2",""\n"3","NULL"',
|
||||
);
|
||||
});
|
||||
|
||||
test("formatCsv only quotes fields with CSV special characters in necessary mode", () => {
|
||||
assert.equal(
|
||||
formatCsv(
|
||||
["id", "district,name", "note"],
|
||||
[
|
||||
[2085252644, "延庆县", "plain"],
|
||||
[2085252645, "门头沟区", 'line 1\n"line 2"'],
|
||||
],
|
||||
"necessary",
|
||||
),
|
||||
'id,"district,name",note\n2085252644,延庆县,plain\n2085252645,门头沟区,"line 1\n""line 2"""',
|
||||
);
|
||||
});
|
||||
+22
@@ -186,6 +186,21 @@ test("updateEditorSettings persists numericColumnRightAlign toggles", async () =
|
||||
});
|
||||
});
|
||||
|
||||
test("updateEditorSettings persists the CSV quote mode", async () => {
|
||||
await withMockLocalStorage({}, async () => {
|
||||
setActivePinia(createPinia());
|
||||
const store = useSettingsStore();
|
||||
await store.initEditorSettings();
|
||||
|
||||
store.updateEditorSettings({ csvQuoteMode: "necessary" });
|
||||
assert.equal(store.editorSettings.csvQuoteMode, "necessary");
|
||||
await vi.waitFor(() => {
|
||||
const saved = saveEditorSettingsMock.mock.calls.at(-1)?.[0] as { csvQuoteMode?: string } | undefined;
|
||||
assert.equal(saved?.csvQuoteMode, "necessary");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("migrates legacy execute-all settings to current once and preserves later explicit choices", async () => {
|
||||
await withMockLocalStorage({ "dbx-app-state:editor_settings": JSON.stringify({ executeMode: "all" }) }, async () => {
|
||||
setActivePinia(createPinia());
|
||||
@@ -229,6 +244,13 @@ test("defaults export batch size to 2000 rows", () => {
|
||||
assert.equal(normalizeEditorSettings({ exportBatchSize: 2000 }).exportBatchSize, 2000);
|
||||
});
|
||||
|
||||
test("CSV quote mode defaults to all fields and preserves a saved necessary mode", () => {
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.csvQuoteMode, "all");
|
||||
assert.equal(normalizeEditorSettings({}).csvQuoteMode, "all");
|
||||
assert.equal(normalizeEditorSettings({ csvQuoteMode: "necessary" }).csvQuoteMode, "necessary");
|
||||
assert.equal(normalizeEditorSettings({ csvQuoteMode: "invalid" as any }).csvQuoteMode, "all");
|
||||
});
|
||||
|
||||
test("migrates the legacy saved export batch default to 2000 once", async () => {
|
||||
await withMockLocalStorage({ "dbx-editor-settings": JSON.stringify({ exportBatchSize: 10000 }) }, async () => {
|
||||
setActivePinia(createPinia());
|
||||
|
||||
+5
-1
@@ -382,7 +382,7 @@ test("default data grid export file names use sanitized base names and compact l
|
||||
});
|
||||
|
||||
test("full query result CSV export streams through the backend without loading all rows", async () => {
|
||||
useSettingsStore().updateEditorSettings({ globalDateTimeExportFormat: "YYYY/M/D HH:mm:ss" });
|
||||
useSettingsStore().updateEditorSettings({ globalDateTimeExportFormat: "YYYY/M/D HH:mm:ss", csvQuoteMode: "necessary" });
|
||||
const { composable, fullExportResult, queryResultExportRequest, exportProgressDialog, exportProgressState } = buildExportHarness();
|
||||
|
||||
await composable.exportCsv();
|
||||
@@ -391,6 +391,7 @@ test("full query result CSV export streams through the backend without loading a
|
||||
assert.equal(queryResultExportRequest.mock.calls.length, 1);
|
||||
assert.equal(apiMock.startQueryResultExport.mock.calls.length, 1);
|
||||
assert.equal(apiMock.startQueryResultExport.mock.calls[0][0].dateTimeFormat, "YYYY/M/D HH:mm:ss");
|
||||
assert.equal(apiMock.startQueryResultExport.mock.calls[0][0].csvQuoteMode, "necessary");
|
||||
assert.equal(apiMock.exportQueryResultCsv.mock.calls.length, 0);
|
||||
assert.equal(exportProgressDialog.value, true);
|
||||
assert.equal(exportProgressState.value.status, "Done");
|
||||
@@ -687,6 +688,7 @@ test("missing query result export request does not fall back to the in-memory pa
|
||||
});
|
||||
|
||||
test("selected query result CSV export keeps the existing in-memory path", async () => {
|
||||
useSettingsStore().updateEditorSettings({ csvQuoteMode: "necessary" });
|
||||
const { composable, queryResultExportRequest } = buildExportHarness();
|
||||
|
||||
await composable.exportCsv([1]);
|
||||
@@ -696,6 +698,7 @@ test("selected query result CSV export keeps the existing in-memory path", async
|
||||
assert.equal(apiMock.exportQueryResultCsv.mock.calls.length, 1);
|
||||
assert.deepEqual(apiMock.exportQueryResultCsv.mock.calls[0][1], ["id", "name"]);
|
||||
assert.deepEqual(apiMock.exportQueryResultCsv.mock.calls[0][2], [[1, "Ada"]]);
|
||||
assert.equal(apiMock.exportQueryResultCsv.mock.calls[0][3], "necessary");
|
||||
});
|
||||
|
||||
test("selected query result CSV export formats only typed temporal columns", async () => {
|
||||
@@ -852,6 +855,7 @@ test("table data export leaves row limit unset by default", async () => {
|
||||
|
||||
assert.equal(apiMock.startTableExport.mock.calls.length, 1);
|
||||
assert.equal(apiMock.startTableExport.mock.calls[0][0].rowLimit, null);
|
||||
assert.equal(apiMock.startTableExport.mock.calls[0][0].csvQuoteMode, "all");
|
||||
} finally {
|
||||
restoreStorage();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user