feat(export): configurable query result export file name

This commit is contained in:
lxk955
2026-10-17 20:12:35 +08:00
committed by GitHub
parent 50f4ec9f6b
commit b7cddbb253
25 changed files with 165 additions and 8 deletions
@@ -109,6 +109,7 @@ import {
type UpdateDownloadSource,
type CsvQuoteMode,
type CsvNullMode,
type QueryResultExportFileNameSource,
type CustomThemeColors,
type CustomTheme,
DEFAULT_CUSTOM_THEMES,
@@ -990,6 +991,7 @@ const editAutoOpenExportFolder = ref(settingsStore.editorSettings.autoOpenExport
const editApplyColumnFormattersOnExport = ref(settingsStore.editorSettings.applyColumnFormattersOnExport);
const editCsvQuoteMode = ref<CsvQuoteMode>(settingsStore.editorSettings.csvQuoteMode);
const editCsvNullMode = ref<CsvNullMode>(settingsStore.editorSettings.csvNullMode);
const editQueryResultExportFileNameSource = ref<QueryResultExportFileNameSource>(settingsStore.editorSettings.queryResultExportFileNameSource);
const editGlobalDateTimeDisplayFormat = ref(settingsStore.editorSettings.globalDateTimeDisplayFormat);
const editGlobalDateTimeExportFormat = ref(settingsStore.editorSettings.globalDateTimeExportFormat);
const editGlobalDateTimeImportFormat = ref(settingsStore.editorSettings.globalDateTimeImportFormat);
@@ -1283,6 +1285,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
applyColumnFormattersOnExport: editApplyColumnFormattersOnExport.value,
csvQuoteMode: editCsvQuoteMode.value,
csvNullMode: editCsvNullMode.value,
queryResultExportFileNameSource: editQueryResultExportFileNameSource.value,
globalDateTimeDisplayFormat: editGlobalDateTimeDisplayFormat.value,
globalDateTimeExportFormat: editGlobalDateTimeExportFormat.value,
globalDateTimeImportFormat: editGlobalDateTimeImportFormat.value,
@@ -1976,6 +1979,7 @@ function syncEditorSettingsDraftFromStore() {
editApplyColumnFormattersOnExport.value = settingsStore.editorSettings.applyColumnFormattersOnExport;
editCsvQuoteMode.value = settingsStore.editorSettings.csvQuoteMode;
editCsvNullMode.value = settingsStore.editorSettings.csvNullMode;
editQueryResultExportFileNameSource.value = settingsStore.editorSettings.queryResultExportFileNameSource;
editGlobalDateTimeDisplayFormat.value = settingsStore.editorSettings.globalDateTimeDisplayFormat;
editGlobalDateTimeExportFormat.value = settingsStore.editorSettings.globalDateTimeExportFormat;
editGlobalDateTimeImportFormat.value = settingsStore.editorSettings.globalDateTimeImportFormat;
@@ -2141,6 +2145,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
applyColumnFormattersOnExport: editApplyColumnFormattersOnExport,
csvQuoteMode: editCsvQuoteMode,
csvNullMode: editCsvNullMode,
queryResultExportFileNameSource: editQueryResultExportFileNameSource,
exportRowLimitEnabled: editExportRowLimitEnabled,
exportRowLimit: editExportRowLimit,
queryExportKeysetOptimizationEnabled: editQueryExportKeysetOptimizationEnabled,
@@ -2747,6 +2752,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editApplyColumnFormattersOnExport.value = DEFAULT_EDITOR_SETTINGS.applyColumnFormattersOnExport;
editCsvQuoteMode.value = DEFAULT_EDITOR_SETTINGS.csvQuoteMode;
editCsvNullMode.value = DEFAULT_EDITOR_SETTINGS.csvNullMode;
editQueryResultExportFileNameSource.value = DEFAULT_EDITOR_SETTINGS.queryResultExportFileNameSource;
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
editGlobalDateTimeExportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeExportFormat;
editGlobalDateTimeImportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeImportFormat;
@@ -2916,6 +2922,7 @@ function resetAllDefaults() {
editApplyColumnFormattersOnExport.value = DEFAULT_EDITOR_SETTINGS.applyColumnFormattersOnExport;
editCsvQuoteMode.value = DEFAULT_EDITOR_SETTINGS.csvQuoteMode;
editCsvNullMode.value = DEFAULT_EDITOR_SETTINGS.csvNullMode;
editQueryResultExportFileNameSource.value = DEFAULT_EDITOR_SETTINGS.queryResultExportFileNameSource;
editGlobalDateTimeDisplayFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeDisplayFormat;
editGlobalDateTimeExportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeExportFormat;
editGlobalDateTimeImportFormat.value = DEFAULT_EDITOR_SETTINGS.globalDateTimeImportFormat;
@@ -10149,6 +10156,23 @@ onUnmounted(() => {
</SelectContent>
</Select>
</div>
<div data-settings-search-id="data-export-file-name-source" :class="['flex items-start justify-between gap-4', settingsSearchTargetClass('data-export-file-name-source')]">
<div class="min-w-0 space-y-0.5">
<Label for="query-result-export-file-name-source">{{ t("settings.queryResultExportFileNameSource") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.queryResultExportFileNameSourceDescription") }}
</p>
</div>
<Select v-model="editQueryResultExportFileNameSource">
<SelectTrigger id="query-result-export-file-name-source" class="h-8 w-56 shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="result-label">{{ t("settings.queryResultExportFileNameSourceResultLabel") }}</SelectItem>
<SelectItem value="tab-title">{{ t("settings.queryResultExportFileNameSourceTabTitle") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div data-settings-search-id="data-export-preferred-path" :class="['space-y-2', settingsSearchTargetClass('data-export-preferred-path')]">
<Label for="export-preferred-path">{{ t("settings.preferredExportPath") }}</Label>
<div class="flex items-center gap-2">
@@ -471,9 +471,8 @@ function increaseTableFontSize() {
setTableFontSize(tableFontSize.value + 1);
}
/** Export file base name for the query result grid: the result's label (nearby
* comment or `schema.table`) names exports, matching the result tab (#9894). */
const activeQueryResultExportBaseName = computed(() => queryResultExportBaseName(props.activeTab.result?.sourceLabel, props.activeTab.title));
/** Export file base name for the query result grid: prefers result label or tab title based on settings (#9894, #12092). */
const activeQueryResultExportBaseName = computed(() => queryResultExportBaseName(props.activeTab.result?.sourceLabel, props.activeTab.title, settingsStore.editorSettings.queryResultExportFileNameSource));
const activeTabDimension = computed(() => {
const tab = props.activeTab;
+4
View File
@@ -8149,6 +8149,10 @@ export default withEnglishFallback({
"CSV ixracında NULL-un yazılma üsulu. «\\N» seçildikdə NULL \\N kimi, boş sətir isə boş sahə kimi yazılır; beləliklə onları fərqləndirmək mümkündür və yenidən idxalda boş sətir NULL olmur. «Boş» seçildikdə həm NULL, həm də boş sətir boş sahə kimi yazılır və fərqləndirilə bilmir.",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "Boş",
queryResultExportFileNameSource: "Sorğu nəticəsi ixrac fayl adı",
queryResultExportFileNameSourceDescription: "Sorğu nəticələrini ixrac edərkən defolt fayl adı mənbəyini seçin. Nəticə etiketi SQL şərhini və ya cədvəli üstün tutur; vərəq başlığı sorğu vərəq başlığını (saxlanılan SQL fayl adı) üstün tutur.",
queryResultExportFileNameSourceResultLabel: "Nəticə etiketi (şərh və ya cədvəl)",
queryResultExportFileNameSourceTabTitle: "Vərəq başlığı (SQL fayl adı)",
performanceSection: "Məhsuldarlıq",
shortcutExecuteSql: "SQL-i icra et",
shortcutExecuteSqlInNewResultTab: "SQL-i yeni nəticə vərəqində icra et",
+4
View File
@@ -9664,6 +9664,10 @@ export default {
'How NULL is written when exporting CSV. With "\\N", NULL is written as \\N and empty strings as empty fields, so the two stay distinguishable and re-importing does not turn empty strings into NULL. With "Empty", NULL and empty strings are both written as empty fields and cannot be told apart.',
csvNullModeMarker: "\\N",
csvNullModeEmpty: "Empty",
queryResultExportFileNameSource: "Query result export file name",
queryResultExportFileNameSourceDescription: "Choose the default source for query result export file names. Result label prefers the SQL comment or table name; query tab title prefers the query tab title (saved SQL file name).",
queryResultExportFileNameSourceResultLabel: "Result label (comment or table)",
queryResultExportFileNameSourceTabTitle: "Query tab title (SQL file name)",
performanceSection: "Performance",
shortcutExecuteSql: "Execute SQL",
shortcutExecuteSqlInNewResultTab: "Execute SQL in new result tab",
+4
View File
@@ -8709,6 +8709,10 @@ export default withEnglishFallback({
"Cómo se escribe NULL al exportar CSV. Con «\\N», NULL se escribe como \\N y las cadenas vacías como campos vacíos, así que se distinguen y al reimportar las cadenas vacías no se convierten en NULL. Con «Vacío», tanto NULL como las cadenas vacías se escriben como campos vacíos y no se pueden distinguir.",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "Vacío",
queryResultExportFileNameSource: "Nombre de archivo de exportación de resultados",
queryResultExportFileNameSourceDescription: "Elige la fuente predeterminada para el nombre del archivo al exportar resultados. La etiqueta de resultado prefiere el comentario SQL o la tabla; el título de la pestaña prefiere el nombre de la pestaña (nombre del archivo SQL guardado).",
queryResultExportFileNameSourceResultLabel: "Etiqueta de resultado (comentario o tabla)",
queryResultExportFileNameSourceTabTitle: "Título de la pestaña (archivo SQL)",
performanceSection: "Rendimiento",
shortcutExecuteSql: "Ejecutar SQL",
shortcutExecuteSqlInNewResultTab: "Ejecutar SQL en una nueva pestaña de resultados",
+4
View File
@@ -8960,6 +8960,10 @@ export default withEnglishFallback({
'Bagaimana NULL ditulis saat mengekspor CSV. Dengan "\\N", NULL ditulis sebagai \\N dan string kosong sebagai bidang kosong, sehingga keduanya tetap dapat dibedakan dan impor ulang tidak mengubah string kosong menjadi NULL. Dengan "Kosong", NULL dan string kosong sama-sama ditulis sebagai bidang kosong dan tidak dapat dibedakan.',
csvNullModeMarker: "\\N",
csvNullModeEmpty: "Kosong",
queryResultExportFileNameSource: "Nama file ekspor hasil kueri",
queryResultExportFileNameSourceDescription: "Pilih sumber default untuk nama file saat mengekspor hasil kueri. Label hasil memprioritaskan komentar SQL atau tabel; judul tab memprioritaskan judul tab kueri (nama file SQL tersimpan).",
queryResultExportFileNameSourceResultLabel: "Label hasil (komentar atau tabel)",
queryResultExportFileNameSourceTabTitle: "Judul tab kueri (nama file SQL)",
performanceSection: "Performa",
shortcutExecuteSql: "Eksekusi SQL",
shortcutExecuteSqlInNewResultTab: "Eksekusi SQL di tab hasil baru",
+4
View File
@@ -8472,6 +8472,10 @@ export default withEnglishFallback({
"Come viene scritto NULL durante l'esportazione CSV. Con «\\N», NULL viene scritto come \\N e le stringhe vuote come campi vuoti, quindi restano distinguibili e al reimport le stringhe vuote non diventano NULL. Con «Vuoto», sia NULL sia le stringhe vuote vengono scritti come campi vuoti e non sono distinguibili.",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "Vuoto",
queryResultExportFileNameSource: "Nome file esportazione risultati query",
queryResultExportFileNameSourceDescription: "Scegli l'origine predefinita per il nome file di esportazione dei risultati. L'etichetta risultato preferisce il commento SQL o la tabella; il titolo della scheda preferisce il nome della scheda (file SQL salvato).",
queryResultExportFileNameSourceResultLabel: "Etichetta risultato (commento o tabella)",
queryResultExportFileNameSourceTabTitle: "Titolo della scheda (file SQL)",
performanceSection: "Prestazioni",
shortcutExecuteSql: "Esegui SQL",
shortcutExecuteSqlInNewResultTab: "Esegui SQL in una nuova scheda dei risultati",
+4
View File
@@ -8461,6 +8461,10 @@ export default withEnglishFallback({
csvNullModeDescription: "CSV エクスポート時に NULL を書く方式です。「\\N」では NULL を \\N、空文字列を空フィールドとして書き出すため両者を区別でき、再インポートしても空文字列は NULL になりません。「空」では NULL も空文字列も空フィールドとして書き出すため、区別できません。",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "空",
queryResultExportFileNameSource: "クエリ結果のエクスポートファイル名",
queryResultExportFileNameSourceDescription: "クエリ結果をエクスポートする際のデフォルトのファイル名ソースを選択します。結果ラベルは SQL コメントまたはテーブル名を優先し、クエリタブ名はクエリタブ名(保存された SQL ファイル名)を優先します。",
queryResultExportFileNameSourceResultLabel: "結果ラベル(コメントまたはテーブル名)",
queryResultExportFileNameSourceTabTitle: "クエリタブ名(SQL ファイル名)",
performanceSection: "パフォーマンス",
shortcutExecuteSql: "SQLを実行",
shortcutExecuteSqlInNewResultTab: "新しい結果タブでSQLを実行",
+4
View File
@@ -8939,6 +8939,10 @@ export default withEnglishFallback({
csvNullModeDescription: 'CSV 내보내기 시 NULL을 쓰는 방식입니다. "\\N"은 NULL을 \\N으로, 빈 문자열을 빈 필드로 쓰므로 둘을 구분할 수 있고 다시 가져와도 빈 문자열이 NULL이 되지 않습니다. "비움"은 NULL과 빈 문자열을 모두 빈 필드로 쓰므로 구분할 수 없습니다.',
csvNullModeMarker: "\\N",
csvNullModeEmpty: "비움",
queryResultExportFileNameSource: "쿼리 결과 내보내기 파일 이름",
queryResultExportFileNameSourceDescription: "쿼리 결과를 내보낼 때 사용할 기본 파일 이름 소스를 선택합니다. 결과 레이블은 SQL 주석이나 테이블 이름을 우선하고, 쿼리 탭 제목은 쿼리 탭 제목(저장된 SQL 파일 이름)을 우선합니다.",
queryResultExportFileNameSourceResultLabel: "결과 레이블 (주석 또는 테이블)",
queryResultExportFileNameSourceTabTitle: "쿼리 탭 제목 (SQL 파일 이름)",
shortcutConvertNamingStyle: "네이밍 스타일 전환",
dataGridKeepFilterEditorExpanded: "필터 편집기 기본 펼치기",
dataGridKeepFilterEditorExpandedDescription: "조건 및 텍스트 보기에서 편집기를 처음에 펼칩니다. 수동으로 접을 수도 있습니다.",
+4
View File
@@ -8470,6 +8470,10 @@ export default withEnglishFallback({
"Como NULL é escrito ao exportar CSV. Com «\\N», NULL é escrito como \\N e strings vazias como campos vazios, então os dois permanecem distinguíveis e, ao reimportar, strings vazias não viram NULL. Com «Vazio», tanto NULL quanto strings vazias são escritos como campos vazios e não podem ser distinguidos.",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "Vazio",
queryResultExportFileNameSource: "Nome do arquivo de exportação de resultados",
queryResultExportFileNameSourceDescription: "Escolha a fonte padrão para o nome do arquivo ao exportar resultados. O rótulo do resultado prefere o comentário SQL ou tabela; o título da aba prefere o nome da aba (nome do arquivo SQL salvo).",
queryResultExportFileNameSourceResultLabel: "Rótulo do resultado (comentário ou tabela)",
queryResultExportFileNameSourceTabTitle: "Título da aba (arquivo SQL)",
performanceSection: "Desempenho",
shortcutExecuteSql: "Executar SQL",
shortcutExecuteSqlInNewResultTab: "Executar SQL em uma nova aba de resultados",
+4
View File
@@ -9856,6 +9856,10 @@ export default withEnglishFallback({
"Как записывается NULL при экспорте CSV. С «\\N» NULL записывается как \\N, а пустая строка — как пустое поле, поэтому они различимы и при повторном импорте пустая строка не станет NULL. С «Пусто» и NULL, и пустая строка записываются как пустое поле, и различить их нельзя.",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "Пусто",
queryResultExportFileNameSource: "Имя файла экспорта результатов запроса",
queryResultExportFileNameSourceDescription: "Выберите источник имени файла по умолчанию для экспорта результатов. Метка результата отдает предпочтение комментарию SQL или таблице; заголовок вкладки отдает предпочтение имени вкладки (сохраненный файл SQL).",
queryResultExportFileNameSourceResultLabel: "Метка результата (комментарий или таблица)",
queryResultExportFileNameSourceTabTitle: "Заголовок вкладки (файл SQL)",
performanceSection: "Производительность",
shortcutExecuteSql: "Выполнить SQL",
shortcutExecuteSqlInNewResultTab: "Выполнить SQL в новой вкладке результатов",
+4
View File
@@ -10294,6 +10294,10 @@ export default withEnglishFallback({
csvNullModeDescription: 'วิธีเขียน NULL เมื่อส่งออก CSV เมื่อใช้ "\\N" ระบบจะเขียน NULL เป็น \\N และสตริงว่างเป็นฟิลด์ว่าง ทำให้แยกกันได้และการนำเข้ากลับไม่เปลี่ยนสตริงว่างเป็น NULL เมื่อใช้ "ว่าง" NULL และสตริงว่างจะเขียนเป็นฟิลด์ว่างเหมือนกันและแยกไม่ออก',
csvNullModeMarker: "\\N",
csvNullModeEmpty: "ว่าง",
queryResultExportFileNameSource: "ชื่อไฟล์ส่งออกผลลัพธ์แบบสอบถาม",
queryResultExportFileNameSourceDescription: "เลือกแหล่งที่มาของชื่อไฟล์เริ่มต้นเมื่อส่งออกผลลัพธ์แบบสอบถาม ป้ายกำกับผลลัพธ์จะเลือกใช้ความคิดเห็น SQL หรือตารางก่อน ชื่อแท็บจะเลือกใช้ชื่อแท็บแบบสอบถาม (ชื่อไฟล์ SQL ที่บันทึกไว้) ก่อน",
queryResultExportFileNameSourceResultLabel: "ป้ายกำกับผลลัพธ์ (ความคิดเห็นหรือตาราง)",
queryResultExportFileNameSourceTabTitle: "ชื่อแท็บแบบสอบถาม (ชื่อไฟล์ SQL)",
performanceSection: "ประสิทธิภาพ",
shortcutExecuteSql: "เรียกใช้ SQL",
shortcutExecuteSqlInNewResultTab: "เรียกใช้ SQL ในแท็บผลลัพธ์ใหม่",
+4
View File
@@ -8052,6 +8052,10 @@ export default withEnglishFallback({
csvNullModeDescription: "CSV dışa aktarılırken NULL nasıl yazılır. «\\N» ile NULL \\N olarak, boş dize ise boş alan olarak yazılır; böylece ikisi ayırt edilebilir ve yeniden içe aktarımda boş dize NULL olmaz. «Boş» ile hem NULL hem de boş dize boş alan olarak yazılır ve ayırt edilemez.",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "Boş",
queryResultExportFileNameSource: "Sorgu sonucu dışa aktarma dosya adı",
queryResultExportFileNameSourceDescription: "Sorgu sonuçlarını dışa aktarırken varsayılan dosya adı kaynağını seçin. Sonuç etiketi SQL yorumunu veya tabloyu tercih eder; sekme başlığı sorgu sekmesi başlığını (kaydedilmiş SQL dosya adı) tercih eder.",
queryResultExportFileNameSourceResultLabel: "Sonuç etiketi (yorum veya tablo)",
queryResultExportFileNameSourceTabTitle: "Sekme başlığı (SQL dosya adı)",
performanceSection: "Başarım",
shortcutExecuteSql: "SQL'i çalıştır",
shortcutExecuteSqlInNewResultTab: "SQL'i yeni sonuç sekmesinde çalıştır",
+4
View File
@@ -9641,6 +9641,10 @@ export default withEnglishFallback({
csvNullModeDescription: "导出 CSV 时 NULL 的写法。选「\\N」时 NULL 写成 \\N、空字符串写为空字段,两者可以区分,重新导入不会把空字符串变成 NULL;选「留空」时 NULL 与空字符串都写为空字段,在文件里无法区分。",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "留空",
queryResultExportFileNameSource: "查询结果导出文件名",
queryResultExportFileNameSourceDescription: "导出查询结果时默认使用的文件名来源。优先结果标识时,优先使用 SQL 旁边的注释名称或表名;优先查询页签名时,优先使用查询页签名(即保存的 SQL 文件名)。",
queryResultExportFileNameSourceResultLabel: "结果标识(注释或表名)",
queryResultExportFileNameSourceTabTitle: "查询页签名(SQL 文件名)",
performanceSection: "性能",
shortcutExecuteSql: "执行 SQL",
shortcutExecuteSqlInNewResultTab: "在新结果标签页中执行 SQL",
+4
View File
@@ -8356,6 +8356,10 @@ export default withEnglishFallback({
csvNullModeDescription: "匯出 CSV 時 NULL 的寫法。選「\\N」時 NULL 寫成 \\N、空字串寫成空欄位,兩者可以區分,重新匯入不會把空字串變成 NULL;選「留空」時 NULL 與空字串都寫成空欄位,在檔案裡無法區分。",
csvNullModeMarker: "\\N",
csvNullModeEmpty: "留空",
queryResultExportFileNameSource: "查詢結果匯出檔案名稱",
queryResultExportFileNameSourceDescription: "匯出查詢結果時預設使用的檔案名稱來源。優先結果標識時,優先使用 SQL 旁邊的註釋名稱或表名;優先查詢分頁名稱時,優先使用查詢分頁名稱(即儲存的 SQL 檔案名稱)。",
queryResultExportFileNameSourceResultLabel: "結果標識(註釋或表名)",
queryResultExportFileNameSourceTabTitle: "查詢分頁名稱(SQL 檔案名稱)",
performanceSection: "效能",
exportBatchSize: "匯出批次大小",
dateTimeSection: "日期與時間",
+30 -3
View File
@@ -1,12 +1,24 @@
import { describe, expect, it } from "vitest";
import { queryResultExportBaseName, sanitizeExportBaseName } from "@/lib/export/saveTextFile";
import { DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE, normalizeQueryResultExportFileNameSource, queryResultExportBaseName, sanitizeExportBaseName } from "@/lib/export/saveTextFile";
describe("queryResultExportBaseName", () => {
it("prefers the result label derived from the SQL's nearby comment", () => {
it("prefers the result label derived from the SQL's nearby comment by default (#9894)", () => {
expect(queryResultExportBaseName("检查结果", "query 5")).toBe("检查结果");
expect(queryResultExportBaseName("检查结果", "query 5", "result-label")).toBe("检查结果");
});
it("falls back to the tab title when the result carries no label", () => {
it("prefers the tab title when configured to prefer tab title (#12092)", () => {
expect(queryResultExportBaseName("orders", "sales_report.sql", "tab-title")).toBe("sales_report.sql");
expect(sanitizeExportBaseName(queryResultExportBaseName("orders", "sales_report.sql", "tab-title")!)).toBe("sales_report");
});
it("falls back to the result label in tab-title mode when tab title is missing (#12092)", () => {
expect(queryResultExportBaseName("orders", undefined, "tab-title")).toBe("orders");
expect(queryResultExportBaseName("orders", "", "tab-title")).toBe("orders");
expect(queryResultExportBaseName("orders", " ", "tab-title")).toBe("orders");
});
it("falls back to the tab title when the result carries no label in result-label mode", () => {
expect(queryResultExportBaseName(undefined, "query 5")).toBe("query 5");
expect(queryResultExportBaseName("", "query 5")).toBe("query 5");
expect(queryResultExportBaseName(" ", "query 5")).toBe("query 5");
@@ -19,9 +31,24 @@ describe("queryResultExportBaseName", () => {
it("returns undefined when neither source provides a name", () => {
expect(queryResultExportBaseName(undefined, undefined)).toBeUndefined();
expect(queryResultExportBaseName("", "")).toBeUndefined();
expect(queryResultExportBaseName(undefined, undefined, "tab-title")).toBeUndefined();
expect(queryResultExportBaseName("", "", "tab-title")).toBeUndefined();
});
it("keeps the sanitized base name free of path separators", () => {
expect(sanitizeExportBaseName(queryResultExportBaseName("检查结果/2026", "query 5")!)).toBe("检查结果_2026");
});
});
describe("normalizeQueryResultExportFileNameSource", () => {
it("accepts valid export filename source values", () => {
expect(normalizeQueryResultExportFileNameSource("result-label")).toBe("result-label");
expect(normalizeQueryResultExportFileNameSource("tab-title")).toBe("tab-title");
});
it("falls back to default on invalid or missing values", () => {
expect(normalizeQueryResultExportFileNameSource(undefined)).toBe(DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE);
expect(normalizeQueryResultExportFileNameSource("invalid")).toBe(DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE);
expect(normalizeQueryResultExportFileNameSource(123)).toBe(DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE);
});
});
+18 -2
View File
@@ -42,16 +42,32 @@ export async function saveTextFile(content: string, defaultFileName: string, fil
return true;
}
export type QueryResultExportFileNameSource = "result-label" | "tab-title";
export const DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE: QueryResultExportFileNameSource = "result-label";
export function normalizeQueryResultExportFileNameSource(value: unknown, fallback: QueryResultExportFileNameSource = DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE): QueryResultExportFileNameSource {
return value === "tab-title" || value === "result-label" ? value : fallback;
}
/**
* Export file base name for a query result (#9894).
* Export file base name for a query result (#9894, #12092).
*
* When sourceMode is "result-label" (default):
* The result tab is already named after the SQL's nearby comment (`-- name: x`
* or a plain leading comment) or after the `schema.table` that produced it, so
* an export of that result should reuse the same name instead of the generic
* query tab title ("query 3", "查询 3"). Table data tabs keep passing their
* table name, which is why the caller decides the fallback.
*
* When sourceMode is "tab-title":
* The export file name prefers the query tab's title (e.g. the saved SQL file
* name like `sales_report.sql`), falling back to the result label if the tab
* title is not available.
*/
export function queryResultExportBaseName(resultLabel: string | undefined, tabTitle: string | undefined): string | undefined {
export function queryResultExportBaseName(resultLabel: string | undefined, tabTitle: string | undefined, sourceMode: QueryResultExportFileNameSource = DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE): string | undefined {
if (sourceMode === "tab-title") {
return tabTitle?.trim() || resultLabel?.trim() || undefined;
}
return resultLabel?.trim() || tabTitle?.trim() || undefined;
}
@@ -62,6 +62,12 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
expect(draft.caretBrowsingEnabled).toBe(true);
});
it("includes queryResultExportFileNameSource (#12092)", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("queryResultExportFileNameSource");
const draft = editorSettingsDraftFromSettings(makeSettings({ queryResultExportFileNameSource: "tab-title" } as Partial<EditorSettings>));
expect(draft.queryResultExportFileNameSource).toBe("tab-title");
});
it("includes continueOnErrorOnBatch", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("continueOnErrorOnBatch");
});
@@ -159,6 +159,16 @@ describe("settings search", () => {
});
});
it("indexes the query result export file name source setting (#12092)", () => {
expect(SETTINGS_SEARCH_DEFINITIONS).toContainEqual({
id: "data-export-file-name-source",
category: "data",
titleKey: "settings.queryResultExportFileNameSource",
descriptionKey: "settings.queryResultExportFileNameSourceDescription",
targetId: "data",
});
});
it("places SQL file limits in their owning settings categories", () => {
expect(SETTINGS_SEARCH_DEFINITIONS).toContainEqual({
id: "sql-file-editor-max-mb",
@@ -565,6 +565,11 @@ describe("settingsTransfer", () => {
expect(collectTransferCategories(["csvNullMode", "csvQuoteMode"])).toEqual(["data"]);
});
it("maps queryResultExportFileNameSource into the data category (#12092)", () => {
expect(transferCategoryForKey("queryResultExportFileNameSource")).toBe("data");
expect(collectTransferCategories(["queryResultExportFileNameSource", "pageSize"])).toEqual(["data"]);
});
it("round-trips zebra row background in data category", () => {
expect(transferCategoryForKey("dataGridZebraRowBg")).toBe("data");
@@ -151,6 +151,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"applyColumnFormattersOnExport",
"csvQuoteMode",
"csvNullMode",
"queryResultExportFileNameSource",
"exportRowLimitEnabled",
"exportRowLimit",
"queryExportKeysetOptimizationEnabled",
@@ -271,6 +271,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "data-datetime-import-format", category: "data", titleKey: "settings.globalDateTimeImportFormat", descriptionKey: "settings.globalDateTimeImportFormatDescription", targetId: "data" },
{ id: "data-export", category: "data", titleKey: "settings.exportSection", targetId: "data" },
{ id: "data-export-preferred-path", category: "data", titleKey: "settings.preferredExportPath", descriptionKey: "settings.preferredExportPathDescription", targetId: "data" },
{ id: "data-export-file-name-source", category: "data", titleKey: "settings.queryResultExportFileNameSource", descriptionKey: "settings.queryResultExportFileNameSourceDescription", targetId: "data" },
{ id: "data-dameng-client-bin-dir", category: "data", titleKey: "settings.damengClientBinDir", descriptionKey: "settings.damengClientBinDirDescription", targetId: "data" },
{ id: "data-export-auto-open-folder", category: "data", titleKey: "settings.autoOpenExportFolder", descriptionKey: "settings.autoOpenExportFolderDescription", targetId: "data", visible: desktopOnly },
{ id: "data-export-apply-column-formatters", category: "data", titleKey: "settings.applyColumnFormattersOnExport", descriptionKey: "settings.applyColumnFormattersOnExportDescription", targetId: "data" },
@@ -206,6 +206,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"applyColumnFormattersOnExport",
"csvQuoteMode",
"csvNullMode",
"queryResultExportFileNameSource",
"exportRowLimitEnabled",
"exportRowLimit",
"queryExportKeysetOptimizationEnabled",
@@ -66,6 +66,15 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ webLogoPosition: undefined }).webLogoPosition).toBe("left");
});
it("defaults queryResultExportFileNameSource to result-label and validates options (#12092)", () => {
expect(DEFAULT_EDITOR_SETTINGS.queryResultExportFileNameSource).toBe("result-label");
expect(normalizeEditorSettings({}).queryResultExportFileNameSource).toBe("result-label");
expect(normalizeEditorSettings({ queryResultExportFileNameSource: "tab-title" }).queryResultExportFileNameSource).toBe("tab-title");
expect(normalizeEditorSettings({ queryResultExportFileNameSource: "result-label" }).queryResultExportFileNameSource).toBe("result-label");
expect(normalizeEditorSettings({ queryResultExportFileNameSource: "invalid" as any }).queryResultExportFileNameSource).toBe("result-label");
expect(normalizeEditorSettings({ queryResultExportFileNameSource: undefined }).queryResultExportFileNameSource).toBe("result-label");
});
it("defaults caretBrowsingEnabled to false and normalizes boolean values (#11827)", () => {
expect(DEFAULT_EDITOR_SETTINGS.caretBrowsingEnabled).toBe(false);
expect(normalizeEditorSettings({}).caretBrowsingEnabled).toBe(false);
+6
View File
@@ -43,6 +43,7 @@ import { normalizeExcludedCompletionItems } from "@/lib/editor/excludedCompletio
import { DEFAULT_SQL_TABLE_COMPLETION_SCHEMA_QUALIFICATION, normalizeSqlTableCompletionSchemaQualification, type SqlTableCompletionSchemaQualification } from "@/lib/sql/sqlCompletionSchemaQualification";
import { DEFAULT_CSV_QUOTE_MODE, normalizeCsvQuoteMode, type CsvQuoteMode } from "@/lib/export/csvQuoteMode";
import { DEFAULT_CSV_NULL_MODE, normalizeCsvNullMode, type CsvNullMode } from "@/lib/export/csvNullMode";
import { DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE, normalizeQueryResultExportFileNameSource, type QueryResultExportFileNameSource } from "@/lib/export/saveTextFile";
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";
@@ -62,6 +63,7 @@ export type {
CsvQuoteMode,
CsvNullMode,
DataTabReuseMode,
QueryResultExportFileNameSource,
SavedSqlOpenTargetMode,
SqlCompletionTriggerMode,
SqlTableCompletionSchemaQualification,
@@ -1189,6 +1191,7 @@ export interface EditorSettings {
applyColumnFormattersOnExport: boolean;
csvQuoteMode: CsvQuoteMode;
csvNullMode: CsvNullMode;
queryResultExportFileNameSource: QueryResultExportFileNameSource;
/** Global Redis key-search templates; overridden by non-empty connection templates. */
redisKeyTemplates: string[];
/** Sidebar database-list cap for Redis connections; the rest are revealed via "load more". */
@@ -1530,6 +1533,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
applyColumnFormattersOnExport: true,
csvQuoteMode: DEFAULT_CSV_QUOTE_MODE,
csvNullMode: DEFAULT_CSV_NULL_MODE,
queryResultExportFileNameSource: DEFAULT_QUERY_RESULT_EXPORT_FILE_NAME_SOURCE,
redisKeyTemplates: [],
redisDatabaseDisplayLimit: REDIS_DATABASE_DISPLAY_LIMIT_DEFAULT,
redisValuePreviewLimitKiB: REDIS_VALUE_PREVIEW_LIMIT_KIB_DEFAULT,
@@ -2239,6 +2243,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
applyColumnFormattersOnExport: typeof settings.applyColumnFormattersOnExport === "boolean" ? settings.applyColumnFormattersOnExport : DEFAULT_EDITOR_SETTINGS.applyColumnFormattersOnExport,
csvQuoteMode: normalizeCsvQuoteMode(settings.csvQuoteMode),
csvNullMode: normalizeCsvNullMode(settings.csvNullMode),
queryResultExportFileNameSource: normalizeQueryResultExportFileNameSource(settings.queryResultExportFileNameSource),
redisKeyTemplates: normalizeRedisKeyTemplates(settings.redisKeyTemplates),
redisValuePreviewLimitKiB: normalizeRedisValuePreviewLimitKiB(settings.redisValuePreviewLimitKiB),
redisDatabaseDisplayLimit:
@@ -3105,6 +3110,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.applyColumnFormattersOnExport !== undefined) editorSettings.value.applyColumnFormattersOnExport = partial.applyColumnFormattersOnExport === true;
if (partial.csvQuoteMode !== undefined) editorSettings.value.csvQuoteMode = normalizeCsvQuoteMode(partial.csvQuoteMode);
if (partial.csvNullMode !== undefined) editorSettings.value.csvNullMode = normalizeCsvNullMode(partial.csvNullMode);
if (partial.queryResultExportFileNameSource !== undefined) editorSettings.value.queryResultExportFileNameSource = normalizeQueryResultExportFileNameSource(partial.queryResultExportFileNameSource);
if (partial.redisValuePreviewLimitKiB !== undefined) editorSettings.value.redisValuePreviewLimitKiB = normalizeRedisValuePreviewLimitKiB(partial.redisValuePreviewLimitKiB);
if (partial.redisKeyTemplates !== undefined) editorSettings.value.redisKeyTemplates = normalizeRedisKeyTemplates(partial.redisKeyTemplates);
if (partial.redisDatabaseDisplayLimit !== undefined) editorSettings.value.redisDatabaseDisplayLimit = Math.min(REDIS_DATABASE_DISPLAY_LIMIT_MAX, Math.max(REDIS_DATABASE_DISPLAY_LIMIT_MIN, Math.round(partial.redisDatabaseDisplayLimit)));