From dc4764e9308b26570012e8c32e2927dc00ee1e1d Mon Sep 17 00:00:00 2001 From: gggaiitx <62124152+gggaiitx@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:14:55 +0800 Subject: [PATCH] feat(export): add HTML export for query result grids --- .cargo/config.toml | 4 + apps/desktop/src/components/grid/DataGrid.vue | 23 ++ .../src/composables/useDataGridExport.ts | 51 ++++ apps/desktop/src/i18n/locales/az.ts | 4 + apps/desktop/src/i18n/locales/en.ts | 4 + apps/desktop/src/i18n/locales/es.ts | 4 + apps/desktop/src/i18n/locales/it.ts | 4 + apps/desktop/src/i18n/locales/ja.ts | 4 + apps/desktop/src/i18n/locales/ko.ts | 4 + apps/desktop/src/i18n/locales/pt-BR.ts | 4 + apps/desktop/src/i18n/locales/tr.ts | 4 + apps/desktop/src/i18n/locales/zh-CN.ts | 4 + apps/desktop/src/i18n/locales/zh-TW.ts | 4 + apps/desktop/src/lib/backend/api.ts | 1 + apps/desktop/src/lib/backend/http.ts | 5 + apps/desktop/src/lib/backend/tauri.ts | 11 + crates/dbx-core/src/text_export.rs | 280 +++++++++++++++++- crates/dbx-web/src/main.rs | 1 + crates/dbx-web/src/routes/text_export.rs | 12 +- src-tauri/src/commands/text_export.rs | 18 +- src-tauri/src/lib.rs | 1 + 21 files changed, 442 insertions(+), 5 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index c7ddd216a..b809288ae 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -19,3 +19,7 @@ LIBSQLITE3_FLAGS = "SQLITE_ENABLE_MATH_FUNCTIONS" [target.x86_64-pc-windows-msvc] linker = "rust-lld" +# Increase main-thread stack from the Windows default of 1 MiB to 8 MiB. +# Large Rust projects in debug mode overflow the 1 MiB stack when DB query +# paths pull in deep serde/driver call stacks. This embeds the size in the PE. +rustflags = ["-C", "link-arg=/STACK:8388608"] diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 1fda58e19..8b039a1ff 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -5565,6 +5565,12 @@ function exportSelectedRowsMarkdown() { return exportMarkdown(rowIds); } +function exportSelectedRowsHtml() { + const rowIds = affectedRowIds(); + if (rowIds.length === 0) return; + return exportHtml(rowIds); +} + function exportSelectedRowsSql() { const rowIds = affectedRowIds(); if (rowIds.length === 0) return; @@ -7467,6 +7473,8 @@ const { exportCurrentPageJson, exportMarkdown, exportCurrentPageMarkdown, + exportHtml, + exportCurrentPageHtml, exportXlsx, exportXlsxWithSql, exportCurrentPageXlsx, @@ -7655,6 +7663,10 @@ const exportMenuItems = computed(() => { value: "selected-markdown", label: t("grid.exportSelectedRowsMarkdown"), }, + { + value: "selected-html", + label: t("grid.exportSelectedRowsHtml"), + }, { value: "selected-sql", label: t("grid.exportSelectedRowsSql") }, { value: "selected-txt", label: t("grid.exportSelectedRowsTxt") }, ] @@ -7667,6 +7679,7 @@ const exportMenuItems = computed(() => { ...(canIncludeSql ? [{ value: "xlsx-with-sql", label: t("grid.exportXlsxWithSql") }] : []), { value: "json", label: t("grid.exportJson") }, { value: "markdown", label: t("grid.exportMarkdown") }, + { value: "html", label: t("grid.exportHtml") }, { value: "sql", label: t("grid.exportSql") }, { value: "txt", label: t("grid.exportTxt") }, ...allResultItems, @@ -7687,6 +7700,7 @@ const exportMenuItems = computed(() => { : []), { value: "page-json", label: t("grid.exportCurrentPageJson") }, { value: "page-markdown", label: t("grid.exportCurrentPageMarkdown") }, + { value: "page-html", label: t("grid.exportCurrentPageHtml") }, { value: "page-sql", label: t("grid.exportCurrentPageSql") }, { value: "page-txt", label: t("grid.exportCurrentPageTxt") }, { @@ -7705,6 +7719,7 @@ const exportMenuItems = computed(() => { : []), { value: "json", label: t("grid.exportCurrentResultJson") }, { value: "markdown", label: t("grid.exportCurrentResultMarkdown") }, + { value: "html", label: t("grid.exportCurrentResultHtml") }, { value: "sql", label: t("grid.exportCurrentResultSql") }, { value: "txt", label: t("grid.exportCurrentResultTxt") }, ...allResultItems, @@ -7723,6 +7738,7 @@ function selectExportMenuItem(value: string) { "page-xlsx-with-sql": exportCurrentPageXlsxWithSql, "page-json": exportCurrentPageJson, "page-markdown": exportCurrentPageMarkdown, + "page-html": exportCurrentPageHtml, "page-sql": exportCurrentPageSql, "page-txt": exportCurrentPageTxt, csv: exportCsv, @@ -7732,6 +7748,7 @@ function selectExportMenuItem(value: string) { "all-results-xlsx-with-sql": exportAllResultsXlsxWithSql, json: exportJson, markdown: exportMarkdown, + html: exportHtml, sql: exportSql, txt: exportTxt, "selected-csv": exportSelectedRowsCsv, @@ -7739,6 +7756,7 @@ function selectExportMenuItem(value: string) { "selected-xlsx-with-sql": exportSelectedRowsXlsxWithSql, "selected-json": exportSelectedRowsJson, "selected-markdown": exportSelectedRowsMarkdown, + "selected-html": exportSelectedRowsHtml, "selected-sql": exportSelectedRowsSql, "selected-txt": exportSelectedRowsTxt, }; @@ -11415,6 +11433,7 @@ function exportSubmenu(): ContextMenuItem { { label: t("grid.exportXlsx"), action: exportXlsx }, { label: t("grid.exportJson"), action: exportJson }, { label: t("grid.exportMarkdown"), action: exportMarkdown }, + { label: t("grid.exportHtml"), action: exportHtml }, { label: t("grid.exportSql"), action: exportSql }, { label: t("grid.exportTxt"), action: exportTxt }, ]; @@ -11448,6 +11467,10 @@ function exportSubmenu(): ContextMenuItem { label: t("grid.exportSelectedRowsMarkdown"), action: exportSelectedRowsMarkdown, }, + { + label: t("grid.exportSelectedRowsHtml"), + action: exportSelectedRowsHtml, + }, { label: t("grid.exportSelectedRowsSql"), action: exportSelectedRowsSql }, { label: t("grid.exportSelectedRowsTxt"), action: exportSelectedRowsTxt }, ); diff --git a/apps/desktop/src/composables/useDataGridExport.ts b/apps/desktop/src/composables/useDataGridExport.ts index d2b144adc..063b65d24 100644 --- a/apps/desktop/src/composables/useDataGridExport.ts +++ b/apps/desktop/src/composables/useDataGridExport.ts @@ -48,6 +48,7 @@ const FORMAT_META: Record = { xlsx: { ext: "xlsx", label: "Excel" }, json: { ext: "json", label: "JSON" }, markdown: { ext: "md", label: "Markdown" }, + html: { ext: "html", label: "HTML" }, sql: { ext: "sql", label: "SQL" }, txt: { ext: "txt", label: "Text" }, }; @@ -453,6 +454,10 @@ export function useDataGridExport(options: UseDataGridExportOptions) { return currentResultLabel?.value || tableMeta.value?.tableName || "Export"; } + function currentExportTitle(): string { + return tableMeta.value?.tableName || currentResultLabel?.value || "Query Result"; + } + function currentExportSql(): string | undefined { return resultExportSql?.value || sql.value; } @@ -985,6 +990,50 @@ export function useDataGridExport(options: UseDataGridExportOptions) { }); } + async function exportHtml(rowIds?: number[]) { + await runExclusiveExport(async () => { + try { + let outputPath = exportFileName("export", "html"); + if (isTauriRuntime()) { + const { save } = await import("@tauri-apps/plugin-dialog"); + const path = await save({ + defaultPath: outputPath, + filters: [{ name: "HTML", extensions: ["html"] }], + }); + if (!path) return; + outputPath = path as string; + } + const result = await resultToExport(rowIds); + await api.exportQueryResultHtml(outputPath, currentExportTitle(), result.columns, result.rows); + toast(t("grid.exported")); + } catch (e: any) { + toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000); + } + }); + } + + async function exportCurrentPageHtml() { + await runExclusiveExport(async () => { + try { + let outputPath = exportFileName("export-page", "html", { page: true }); + if (isTauriRuntime()) { + const { save } = await import("@tauri-apps/plugin-dialog"); + const path = await save({ + defaultPath: outputPath, + filters: [{ name: "HTML", extensions: ["html"] }], + }); + if (!path) return; + outputPath = path as string; + } + const result = await resultToExport(undefined, undefined, false); + await api.exportQueryResultHtml(outputPath, currentExportTitle(), result.columns, result.rows); + toast(t("grid.exported")); + } catch (e: any) { + toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000); + } + }); + } + async function exportCurrentPageTxt() { await runExclusiveExport(async () => { try { @@ -1599,6 +1648,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) { exportCurrentPageJson, exportMarkdown, exportCurrentPageMarkdown, + exportHtml, + exportCurrentPageHtml, exportTxt, exportCurrentPageTxt, exportXlsx, diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index a89dce383..b0ce34482 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -1797,6 +1797,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "Cari səhifəni SQL ilə XLSX kimi ixrac et", exportCurrentPageJson: "Cari səhifəni JSON kimi ixrac et", exportCurrentPageMarkdown: "Cari səhifəni Markdown kimi ixrac et", + exportCurrentPageHtml: "Cari səhifəni HTML kimi ixrac et", exportCurrentPageSql: "Cari səhifəni SQL INSERT kimi ixrac et", exportCurrentPageTxt: "Cari səhifəni TXT kimi ixrac et", exportCurrentResultCsv: "Cari nəticə dəstinin bütün verilənlərini CSV kimi ixrac et", @@ -1804,12 +1805,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "Cari nəticə dəstinin bütün verilənlərini SQL ilə XLSX kimi ixrac et", exportCurrentResultJson: "Cari nəticə dəstinin bütün verilənlərini JSON kimi ixrac et", exportCurrentResultMarkdown: "Cari nəticə dəstinin bütün verilənlərini Markdown kimi ixrac et", + exportCurrentResultHtml: "Cari nəticə dəstinin bütün verilənlərini HTML kimi ixrac et", exportCurrentResultSql: "Cari nəticə dəstinin bütün verilənlərini SQL INSERT kimi ixrac et", exportCurrentResultTxt: "Cari nəticə dəstinin bütün verilənlərini TXT kimi ixrac et", exportAllResultsXlsx: "Bu icranın bütün nəticə dəstlərini XLSX kimi ixrac et", exportAllResultsXlsxWithSql: "Bu icranın bütün nəticə dəstlərini SQL ilə XLSX kimi ixrac et", exportJson: "JSON-a ixrac et", exportMarkdown: "Markdown-a ixrac et", + exportHtml: "HTML-ə ixrac et", exportSql: "SQL INSERT kimi ixrac et", sqlInsertModeTitle: "SQL INSERT ixrac rejimi", sqlInsertModePrompt: "Sətirlərin SQL INSERT faylına necə yazılacağını seçin:", @@ -1823,6 +1826,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "Seçilmiş sətirləri SQL ilə XLSX kimi ixrac et", exportSelectedRowsJson: "Seçilmiş sətirləri JSON kimi ixrac et", exportSelectedRowsMarkdown: "Seçilmiş sətirləri Markdown kimi ixrac et", + exportSelectedRowsHtml: "Seçilmiş sətirləri HTML kimi ixrac et", exportSelectedRowsSql: "Seçilmiş sətirləri SQL INSERT kimi ixrac et", exportSelectedRowsTxt: "Seçilmiş sətirləri TXT kimi ixrac et", exported: "İxrac edildi", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index df4096e54..688e78298 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1931,6 +1931,7 @@ export default { exportCurrentPageXlsxWithSql: "Export Current Page as XLSX with SQL", exportCurrentPageJson: "Export Current Page as JSON", exportCurrentPageMarkdown: "Export Current Page as Markdown", + exportCurrentPageHtml: "Export Current Page as HTML", exportCurrentPageSql: "Export Current Page as SQL INSERT", exportCurrentPageTxt: "Export Current Page as TXT", exportCurrentResultCsv: "Export Current Result Set All Data as CSV", @@ -1938,12 +1939,14 @@ export default { exportCurrentResultXlsxWithSql: "Export Current Result Set All Data as XLSX with SQL", exportCurrentResultJson: "Export Current Result Set All Data as JSON", exportCurrentResultMarkdown: "Export Current Result Set All Data as Markdown", + exportCurrentResultHtml: "Export Current Result Set All Data as HTML", exportCurrentResultSql: "Export Current Result Set All Data as SQL INSERT", exportCurrentResultTxt: "Export Current Result Set All Data as TXT", exportAllResultsXlsx: "Export This Run's All Result Sets as XLSX", exportAllResultsXlsxWithSql: "Export This Run's All Result Sets as XLSX with SQL", exportJson: "Export JSON", exportMarkdown: "Export Markdown", + exportHtml: "Export HTML", exportSql: "Export SQL INSERT", sqlInsertModeTitle: "SQL INSERT Export Mode", sqlInsertModePrompt: "Choose how rows are written to the SQL INSERT file:", @@ -1957,6 +1960,7 @@ export default { exportSelectedRowsXlsxWithSql: "Export Selected Rows as XLSX with SQL", exportSelectedRowsJson: "Export Selected Rows as JSON", exportSelectedRowsMarkdown: "Export Selected Rows as Markdown", + exportSelectedRowsHtml: "Export Selected Rows as HTML", exportSelectedRowsSql: "Export Selected Rows as SQL INSERT", exportSelectedRowsTxt: "Export Selected Rows as TXT", exported: "Exported", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 47fab6a88..f85b5cb3e 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1804,6 +1804,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "Exportar página actual como XLSX con SQL", exportCurrentPageJson: "Exportar página actual como JSON", exportCurrentPageMarkdown: "Exportar página actual como Markdown", + exportCurrentPageHtml: "Exportar página actual como HTML", exportCurrentPageSql: "Exportar página actual como SQL INSERT", exportCurrentPageTxt: "Exportar página actual como TXT", exportCurrentResultCsv: "Exportar todos los datos del resultado actual como CSV", @@ -1811,12 +1812,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "Exportar todos los datos del resultado actual como XLSX con SQL", exportCurrentResultJson: "Exportar todos los datos del resultado actual como JSON", exportCurrentResultMarkdown: "Exportar todos los datos del resultado actual como Markdown", + exportCurrentResultHtml: "Exportar todos los datos del resultado actual como HTML", exportCurrentResultSql: "Exportar todos los datos del resultado actual como SQL INSERT", exportCurrentResultTxt: "Exportar todos los datos del resultado actual como TXT", exportAllResultsXlsx: "Exportar todos los resultados de esta ejecución como XLSX", exportAllResultsXlsxWithSql: "Exportar todos los resultados de esta ejecución como XLSX con SQL", exportJson: "Exportar JSON", exportMarkdown: "Exportar Markdown", + exportHtml: "Exportar HTML", exportSql: "Exportar SQL INSERT", sqlInsertModeTitle: "Modo de exportación SQL INSERT", sqlInsertModePrompt: "Elige cómo se escribirán las filas en el archivo SQL INSERT:", @@ -1830,6 +1833,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "Exportar filas seleccionadas como XLSX con SQL", exportSelectedRowsJson: "Exportar filas seleccionadas como JSON", exportSelectedRowsMarkdown: "Exportar filas seleccionadas como Markdown", + exportSelectedRowsHtml: "Exportar filas seleccionadas como HTML", exportSelectedRowsSql: "Exportar filas seleccionadas como SQL INSERT", exportSelectedRowsTxt: "Exportar filas seleccionadas como TXT", exported: "Exportado", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 01aafe5c4..9ed60ea0d 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1802,6 +1802,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "Esporta pagina corrente come XLSX con SQL", exportCurrentPageJson: "Esporta pagina corrente come JSON", exportCurrentPageMarkdown: "Esporta pagina corrente come Markdown", + exportCurrentPageHtml: "Esporta pagina corrente come HTML", exportCurrentPageSql: "Esporta pagina corrente come SQL INSERT", exportCurrentPageTxt: "Esporta pagina corrente come TXT", exportCurrentResultCsv: "Esporta tutti i dati del risultato corrente come CSV", @@ -1809,12 +1810,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "Esporta tutti i dati del risultato corrente come XLSX con SQL", exportCurrentResultJson: "Esporta tutti i dati del risultato corrente come JSON", exportCurrentResultMarkdown: "Esporta tutti i dati del risultato corrente come Markdown", + exportCurrentResultHtml: "Esporta tutti i dati del risultato corrente come HTML", exportCurrentResultSql: "Esporta tutti i dati del risultato corrente come SQL INSERT", exportCurrentResultTxt: "Esporta tutti i dati del risultato corrente come TXT", exportAllResultsXlsx: "Esporta tutti i set di risultati di questa esecuzione come XLSX", exportAllResultsXlsxWithSql: "Esporta tutti i set di risultati di questa esecuzione come XLSX con SQL", exportJson: "Esporta JSON", exportMarkdown: "Esporta Markdown", + exportHtml: "Esporta HTML", exportSql: "Esporta SQL INSERT", sqlInsertModeTitle: "Modalità di esportazione SQL INSERT", sqlInsertModePrompt: "Scegli come scrivere le righe nel file SQL INSERT:", @@ -1828,6 +1831,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "Esporta Righe Selezionate come XLSX con SQL", exportSelectedRowsJson: "Esporta Righe Selezionate come JSON", exportSelectedRowsMarkdown: "Esporta Righe Selezionate come Markdown", + exportSelectedRowsHtml: "Esporta Righe Selezionate come HTML", exportSelectedRowsSql: "Esporta Righe Selezionate come SQL INSERT", exportSelectedRowsTxt: "Esporta Righe Selezionate come TXT", exported: "Esportato", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 3efe0462f..d65269c02 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1816,6 +1816,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "現在のページをSQL付きXLSXでエクスポート", exportCurrentPageJson: "現在のページをJSONでエクスポート", exportCurrentPageMarkdown: "現在のページをMarkdownでエクスポート", + exportCurrentPageHtml: "現在のページをHTMLでエクスポート", exportCurrentPageSql: "現在のページをSQL INSERTでエクスポート", exportCurrentPageTxt: "現在のページをTXTでエクスポート", exportCurrentResultCsv: "現在の結果セット全データをCSVでエクスポート", @@ -1823,12 +1824,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "現在の結果セット全データをSQL付きXLSXでエクスポート", exportCurrentResultJson: "現在の結果セット全データをJSONでエクスポート", exportCurrentResultMarkdown: "現在の結果セット全データをMarkdownでエクスポート", + exportCurrentResultHtml: "現在の結果セット全データをHTMLでエクスポート", exportCurrentResultSql: "現在の結果セット全データをSQL INSERTでエクスポート", exportCurrentResultTxt: "現在の結果セット全データをTXTでエクスポート", exportAllResultsXlsx: "今回実行の全結果セットをXLSXでエクスポート", exportAllResultsXlsxWithSql: "今回実行の全結果セットをSQL付きXLSXでエクスポート", exportJson: "JSONエクスポート", exportMarkdown: "Markdownエクスポート", + exportHtml: "HTMLエクスポート", exportSql: "SQL INSERTエクスポート", sqlInsertModeTitle: "SQL INSERT エクスポートモード", sqlInsertModePrompt: "SQL INSERT ファイルへの行の書き出し方法を選択してください:", @@ -1842,6 +1845,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "選択行をSQL付きXLSXでエクスポート", exportSelectedRowsJson: "選択行をJSONでエクスポート", exportSelectedRowsMarkdown: "選択行をMarkdownでエクスポート", + exportSelectedRowsHtml: "選択行をHTMLでエクスポート", exportSelectedRowsSql: "選択行をSQL INSERTでエクスポート", exportSelectedRowsTxt: "選択行をTXTでエクスポート", exported: "エクスポートしました", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 6206aeb67..85b37f4c9 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1803,6 +1803,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "현재 페이지를 SQL 포함 XLSX로 내보내기", exportCurrentPageJson: "현재 페이지를 JSON으로 내보내기", exportCurrentPageMarkdown: "현재 페이지를 Markdown으로 내보내기", + exportCurrentPageHtml: "현재 페이지를 HTML로 내보내기", exportCurrentPageSql: "현재 페이지를 SQL INSERT로 내보내기", exportCurrentPageTxt: "현재 페이지를 TXT로 내보내기", exportCurrentResultCsv: "현재 결과 집합 전체를 CSV로 내보내기", @@ -1810,12 +1811,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "현재 결과 집합 전체를 SQL 포함 XLSX로 내보내기", exportCurrentResultJson: "현재 결과 집합 전체를 JSON으로 내보내기", exportCurrentResultMarkdown: "현재 결과 집합 전체를 Markdown으로 내보내기", + exportCurrentResultHtml: "현재 결과 집합 전체를 HTML로 내보내기", exportCurrentResultSql: "현재 결과 집합 전체를 SQL INSERT로 내보내기", exportCurrentResultTxt: "현재 결과 집합 전체를 TXT로 내보내기", exportAllResultsXlsx: "이번 실행의 모든 결과 집합을 XLSX로 내보내기", exportAllResultsXlsxWithSql: "이번 실행의 모든 결과 집합을 SQL 포함 XLSX로 내보내기", exportJson: "JSON 내보내기", exportMarkdown: "Markdown 내보내기", + exportHtml: "HTML 내보내기", exportSql: "SQL INSERT 내보내기", sqlInsertModeTitle: "SQL INSERT 내보내기 모드", sqlInsertModePrompt: "SQL INSERT 파일에 행을 기록할 방식을 선택하세요:", @@ -1829,6 +1832,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "선택한 행을 SQL 포함 XLSX로 내보내기", exportSelectedRowsJson: "선택한 행을 JSON으로 내보내기", exportSelectedRowsMarkdown: "선택한 행을 Markdown으로 내보내기", + exportSelectedRowsHtml: "선택한 행을 HTML로 내보내기", exportSelectedRowsSql: "선택한 행을 SQL INSERT로 내보내기", exportSelectedRowsTxt: "선택한 행을 TXT로 내보내기", exported: "내보냈습니다", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index d37e81126..0a1b36c43 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1804,6 +1804,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "Exportar página atual como XLSX com SQL", exportCurrentPageJson: "Exportar página atual como JSON", exportCurrentPageMarkdown: "Exportar página atual como Markdown", + exportCurrentPageHtml: "Exportar página atual como HTML", exportCurrentPageSql: "Exportar página atual como SQL INSERT", exportCurrentPageTxt: "Exportar página atual como TXT", exportCurrentResultCsv: "Exportar todos os dados do resultado atual como CSV", @@ -1811,12 +1812,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "Exportar todos os dados do resultado atual como XLSX com SQL", exportCurrentResultJson: "Exportar todos os dados do resultado atual como JSON", exportCurrentResultMarkdown: "Exportar todos os dados do resultado atual como Markdown", + exportCurrentResultHtml: "Exportar todos os dados do resultado atual como HTML", exportCurrentResultSql: "Exportar todos os dados do resultado atual como SQL INSERT", exportCurrentResultTxt: "Exportar todos os dados do resultado atual como TXT", exportAllResultsXlsx: "Exportar todos os conjuntos de resultados desta execução como XLSX", exportAllResultsXlsxWithSql: "Exportar todos os conjuntos de resultados desta execução como XLSX com SQL", exportJson: "Exportar JSON", exportMarkdown: "Exportar Markdown", + exportHtml: "Exportar HTML", exportSql: "Exportar SQL INSERT", sqlInsertModeTitle: "Modo de exportação SQL INSERT", sqlInsertModePrompt: "Escolha como as linhas serão gravadas no arquivo SQL INSERT:", @@ -1830,6 +1833,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "Exportar Linhas Selecionadas como XLSX com SQL", exportSelectedRowsJson: "Exportar Linhas Selecionadas como JSON", exportSelectedRowsMarkdown: "Exportar Linhas Selecionadas como Markdown", + exportSelectedRowsHtml: "Exportar Linhas Selecionadas como HTML", exportSelectedRowsSql: "Exportar Linhas Selecionadas como SQL INSERT", exportSelectedRowsTxt: "Exportar Linhas Selecionadas como TXT", exported: "Exportado", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 0f4c52023..8848eafbb 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -1798,6 +1798,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "Geçerli Sayfayı SQL ile XLSX Olarak Dışa Aktar", exportCurrentPageJson: "Geçerli Sayfayı JSON Olarak Dışa Aktar", exportCurrentPageMarkdown: "Geçerli Sayfayı Markdown Olarak Dışa Aktar", + exportCurrentPageHtml: "Geçerli Sayfayı HTML Olarak Dışa Aktar", exportCurrentPageSql: "Geçerli Sayfayı SQL INSERT Olarak Dışa Aktar", exportCurrentPageTxt: "Geçerli Sayfayı TXT Olarak Dışa Aktar", exportCurrentResultCsv: "Geçerli Sonuç Kümesinin Tüm Verisini CSV Olarak Dışa Aktar", @@ -1805,12 +1806,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "Geçerli Sonuç Kümesinin Tüm Verisini SQL ile XLSX Olarak Dışa Aktar", exportCurrentResultJson: "Geçerli Sonuç Kümesinin Tüm Verisini JSON Olarak Dışa Aktar", exportCurrentResultMarkdown: "Geçerli Sonuç Kümesinin Tüm Verisini Markdown Olarak Dışa Aktar", + exportCurrentResultHtml: "Geçerli Sonuç Kümesinin Tüm Verisini HTML Olarak Dışa Aktar", exportCurrentResultSql: "Geçerli Sonuç Kümesinin Tüm Verisini SQL INSERT Olarak Dışa Aktar", exportCurrentResultTxt: "Geçerli Sonuç Kümesinin Tüm Verisini TXT Olarak Dışa Aktar", exportAllResultsXlsx: "Bu Çalıştırmanın Tüm Sonuç Kümelerini XLSX Olarak Dışa Aktar", exportAllResultsXlsxWithSql: "Bu Çalıştırmanın Tüm Sonuç Kümelerini SQL ile XLSX Olarak Dışa Aktar", exportJson: "JSON Dışa Aktar", exportMarkdown: "Markdown Dışa Aktar", + exportHtml: "HTML Dışa Aktar", exportSql: "SQL INSERT Dışa Aktar", exportTxt: "TXT Dışa Aktar", exportSelectedRowsCsv: "Seçili Satırları CSV Olarak Dışa Aktar", @@ -1818,6 +1821,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "Seçili Satırları SQL ile XLSX Olarak Dışa Aktar", exportSelectedRowsJson: "Seçili Satırları JSON Olarak Dışa Aktar", exportSelectedRowsMarkdown: "Seçili Satırları Markdown Olarak Dışa Aktar", + exportSelectedRowsHtml: "Seçili Satırları HTML Olarak Dışa Aktar", exportSelectedRowsSql: "Seçili Satırları SQL INSERT Olarak Dışa Aktar", exportSelectedRowsTxt: "Seçili Satırları TXT Olarak Dışa Aktar", exported: "Dışa aktarıldı", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index df7174001..0c76bf7ba 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1845,6 +1845,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "导出当前页 XLSX(含 SQL)", exportCurrentPageJson: "导出当前页 JSON", exportCurrentPageMarkdown: "导出当前页 Markdown", + exportCurrentPageHtml: "导出当前页 HTML", exportCurrentPageSql: "导出当前页 SQL INSERT", exportCurrentPageTxt: "导出当前页 TXT", exportCurrentResultCsv: "导出当前结果集全部数据 CSV", @@ -1852,12 +1853,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "导出当前结果集全部数据 XLSX(含 SQL)", exportCurrentResultJson: "导出当前结果集全部数据 JSON", exportCurrentResultMarkdown: "导出当前结果集全部数据 Markdown", + exportCurrentResultHtml: "导出当前结果集全部数据 HTML", exportCurrentResultSql: "导出当前结果集全部数据 SQL INSERT", exportCurrentResultTxt: "导出当前结果集全部数据 TXT", exportAllResultsXlsx: "导出本次执行全部结果集为 XLSX", exportAllResultsXlsxWithSql: "导出本次执行全部结果集为 XLSX(含 SQL)", exportJson: "导出 JSON", exportMarkdown: "导出 Markdown", + exportHtml: "导出 HTML", exportSql: "导出 SQL INSERT", sqlInsertModeTitle: "SQL INSERT 导出模式", sqlInsertModePrompt: "请选择 SQL INSERT 文件的写出模式:", @@ -1871,6 +1874,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "导出选中行为 XLSX(含 SQL)", exportSelectedRowsJson: "导出选中行为 JSON", exportSelectedRowsMarkdown: "导出选中行为 Markdown", + exportSelectedRowsHtml: "导出选中行为 HTML", exportSelectedRowsSql: "导出选中行为 SQL INSERT", exportSelectedRowsTxt: "导出选中行为 TXT", exported: "导出完成", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index fd211c706..e2743ac5c 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1802,6 +1802,7 @@ export default withEnglishFallback({ exportCurrentPageXlsxWithSql: "匯出目前頁 XLSX(含 SQL)", exportCurrentPageJson: "匯出目前頁 JSON", exportCurrentPageMarkdown: "匯出目前頁 Markdown", + exportCurrentPageHtml: "匯出目前頁 HTML", exportCurrentPageSql: "匯出目前頁 SQL INSERT", exportCurrentPageTxt: "匯出目前頁 TXT", exportCurrentResultCsv: "匯出目前結果集全部資料 CSV", @@ -1809,12 +1810,14 @@ export default withEnglishFallback({ exportCurrentResultXlsxWithSql: "匯出目前結果集全部資料 XLSX(含 SQL)", exportCurrentResultJson: "匯出目前結果集全部資料 JSON", exportCurrentResultMarkdown: "匯出目前結果集全部資料 Markdown", + exportCurrentResultHtml: "匯出目前結果集全部資料 HTML", exportCurrentResultSql: "匯出目前結果集全部資料 SQL INSERT", exportCurrentResultTxt: "匯出目前結果集全部資料 TXT", exportAllResultsXlsx: "匯出本次執行全部結果集為 XLSX", exportAllResultsXlsxWithSql: "匯出本次執行全部結果集為 XLSX(含 SQL)", exportJson: "匯出 JSON", exportMarkdown: "匯出 Markdown", + exportHtml: "匯出 HTML", exportSql: "匯出 SQL INSERT", sqlInsertModeTitle: "SQL INSERT 匯出模式", sqlInsertModePrompt: "請選擇 SQL INSERT 檔案的資料列寫入方式:", @@ -1828,6 +1831,7 @@ export default withEnglishFallback({ exportSelectedRowsXlsxWithSql: "匯出選取資料列為 XLSX(含 SQL)", exportSelectedRowsJson: "匯出選取資料列為 JSON", exportSelectedRowsMarkdown: "匯出選取資料列為 Markdown", + exportSelectedRowsHtml: "匯出選取資料列為 HTML", exportSelectedRowsSql: "匯出選取資料列為 SQL INSERT", exportSelectedRowsTxt: "匯出選取資料列為 TXT", exported: "匯出完成", diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts index fa1024a2e..c03ff91ad 100644 --- a/apps/desktop/src/lib/backend/api.ts +++ b/apps/desktop/src/lib/backend/api.ts @@ -516,6 +516,7 @@ export const exportQueryResultXlsx = forward("exportQueryResultXlsx"); export const exportQueryResultsXlsx = forward("exportQueryResultsXlsx"); export const exportQueryResultJson = forward("exportQueryResultJson"); export const exportQueryResultMarkdown = forward("exportQueryResultMarkdown"); +export const exportQueryResultHtml = forward("exportQueryResultHtml"); export const startTableExport = forward("startTableExport"); export const cancelTableExport = forward("cancelTableExport"); export const startQueryResultExport = forward("startQueryResultExport"); diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index e3e3cb92f..c5408ad5d 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -3212,6 +3212,11 @@ export async function exportQueryResultMarkdown(filePath: string, columns: strin downloadTextFile(filePath, "export.md", result.content, "text/markdown;charset=utf-8"); } +export async function exportQueryResultHtml(filePath: string, title: string | undefined, columns: string[], rows: readonly (readonly XlsxCellValue[])[]): Promise { + const result = await post<{ content: string }>("/api/export/query-result-html", { title, columns, rows }); + downloadTextFile(filePath, "export.html", result.content, "text/html;charset=utf-8"); +} + // --------------------------------------------------------------------------- // Redis // --------------------------------------------------------------------------- diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index ec7851316..f02ab450b 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -5650,6 +5650,17 @@ export async function exportQueryResultMarkdown(filePath: string, columns: strin }); } +export async function exportQueryResultHtml(filePath: string, title: string | undefined, columns: string[], rows: readonly (readonly XlsxCellValue[])[]): Promise { + return invoke("export_query_result_html", { + request: { + filePath, + title, + columns, + rows, + }, + }); +} + export * from "@/lib/backend/mq-tauri"; export * from "@/lib/backend/mqtt-tauri"; export * from "@/lib/backend/nacos-tauri"; diff --git a/crates/dbx-core/src/text_export.rs b/crates/dbx-core/src/text_export.rs index f02e105aa..5525cbcd0 100644 --- a/crates/dbx-core/src/text_export.rs +++ b/crates/dbx-core/src/text_export.rs @@ -4,6 +4,8 @@ use serde_json::{Map, Value}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QueryResultTextExportData { + #[serde(default)] + pub title: Option, pub columns: Vec, pub rows: Vec>, } @@ -94,15 +96,193 @@ fn pad(value: &str, width: usize) -> String { format!("{value}{}", " ".repeat(width - current)) } +pub fn format_html(data: &QueryResultTextExportData) -> String { + let now = chrono_local_now(); + let heading = data.title.as_deref().unwrap_or("Query Result"); + let row_count = data.rows.len(); + let col_count = data.columns.len(); + + let mut html = String::with_capacity(4096 + col_count * 64 + row_count * col_count * 48); + + html.push_str("\n\n\n"); + html.push_str(" \n"); + html.push_str(" \n"); + html.push_str(&format!(" {}\n", html_escape(heading))); + html.push_str( + r#" + + +
+"# + ); + + // Header + html.push_str("
\n"); + html.push_str(&format!("

{}

\n", html_escape(heading))); + html.push_str(&format!( + "
{} rows · {} columns · {} · DBX
\n", + row_count, + col_count, + html_escape(&now) + )); + html.push_str("
\n"); + + // Table card + html.push_str("
\n"); + html.push_str("
\n"); + html.push_str(" \n \n \n"); + for col in &data.columns { + html.push_str(&format!(" \n", html_escape(col))); + } + html.push_str(" \n \n \n"); + + for row in &data.rows { + html.push_str(" \n"); + for cell in row { + let (text, css_class) = html_cell_value(cell); + if css_class.is_empty() { + html.push_str(&format!(" \n", html_escape(&text))); + } else { + html.push_str(&format!(" \n", css_class, html_escape(&text))); + } + } + html.push_str(" \n"); + } + + html.push_str(" \n
{}
{}{}
\n
\n
\n"); + html.push_str("
Exported by DBX
\n"); + html.push_str("
\n\n\n"); + html +} + +fn html_escape(value: &str) -> String { + let mut out = String::with_capacity(value.len()); + for ch in value.chars() { + match ch { + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '&' => out.push_str("&"), + '"' => out.push_str("""), + '\'' => out.push_str("'"), + '\n' => out.push_str("
"), + '\r' => {} + _ => out.push(ch), + } + } + out +} + +fn html_cell_value(value: &Value) -> (String, &'static str) { + match value { + Value::Null => ("NULL".to_string(), "null"), + Value::Bool(true) => ("true".to_string(), "boolean true"), + Value::Bool(false) => ("false".to_string(), "boolean false"), + Value::Number(n) => (n.to_string(), "number"), + Value::String(s) => (s.clone(), ""), + other => (other.to_string(), ""), + } +} + +fn chrono_local_now() -> String { + chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string() +} + #[cfg(test)] mod tests { use serde_json::{json, Value}; - use super::{format_json, format_markdown, QueryResultTextExportData}; + use super::{format_html, format_json, format_markdown, QueryResultTextExportData}; #[test] fn formats_json_rows_as_objects() { let out = format_json(&QueryResultTextExportData { + title: None, columns: vec!["id".to_string(), "name".to_string(), "active".to_string(), "note".to_string()], rows: vec![vec![json!(1), json!("Ada"), json!(true), Value::Null]], }) @@ -124,6 +304,7 @@ mod tests { #[test] fn formats_markdown_with_escaped_pipes_and_newlines() { let out = format_markdown(&QueryResultTextExportData { + title: None, columns: vec!["id".to_string(), "payload|kind".to_string()], rows: vec![vec![json!(1), json!("a|b")], vec![json!(2), json!("line one\nline two")]], }); @@ -140,4 +321,101 @@ mod tests { .join("\n") ); } + + #[test] + fn formats_html_document_structure() { + let out = format_html(&QueryResultTextExportData { + title: None, + columns: vec!["id".to_string(), "name".to_string()], + rows: vec![vec![json!(1), json!("Ada")]], + }); + + assert!(out.starts_with(""), "must start with doctype"); + assert!(out.ends_with("\n"), "must end with closing html tag"); + assert!(out.contains("")); + assert!(out.contains("