feat(xlsx): add optional auto filter to xlsx export

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-27 12:18:42 +08:00
1 parent 185db5e8c6
commit d70a1231b0
29 files changed
+285 -119

No files matched your search

@@ -85,7 +85,7 @@ import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceSt
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRenameSql";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxExportOptions, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
import { copyToClipboard, eventTargetAllowsAppClipboardShortcut } from "@/lib/common/clipboard";
import {
defaultPasteTableMode,
@@ -1999,16 +1999,17 @@ async function exportData(row: ObjectBrowserRow, format: "csv" | "json" | "sql")
else await exportTableData(row, format);
}
function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<XlsxHeaderMode | null> {
if (!hasComments) return Promise.resolve("name");
function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<XlsxExportOptions | null> {
if (typeof document === "undefined") return Promise.resolve({ headerMode: "name", autoFilter: false });
return new Promise((resolve) => {
const container = document.createElement("div");
document.body.appendChild(container);
const app = createApp(XlsxHeaderDialog, {
open: true,
onConfirm: (mode: XlsxHeaderMode) => {
resolve(mode);
showHeaderOptions: hasComments,
onConfirm: (exportOptions: XlsxExportOptions) => {
resolve(exportOptions);
app.unmount();
document.body.removeChild(container);
},
@@ -2025,22 +2026,20 @@ function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<XlsxHe
async function exportDataXlsx(row: ObjectBrowserRow) {
const schema = row.schema || selectedSchema.value;
let headerMode: XlsxHeaderMode = "name";
let columnInfos: ColumnInfo[] | undefined;
try {
columnInfos = await api.getColumns(props.connection.id, props.database, schema || props.database, row.name, props.catalog);
const result = await showObjectBrowserXlsxHeaderDialog(hasXlsxHeaderComments(columnInfos.map((column) => column.comment)));
if (result === null) return;
headerMode = result;
} catch {
// Export still works with field-name headers when column metadata is unavailable.
}
await exportTableData(row, "xlsx", columnInfos, headerMode);
const exportOptions = await showObjectBrowserXlsxHeaderDialog(hasXlsxHeaderComments(columnInfos?.map((column) => column.comment)));
if (exportOptions === null) return;
await exportTableData(row, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter);
}
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name") {
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name", autoFilter = true) {
const schema = row.schema || selectedSchema.value;
// Save dialog first
@@ -2080,7 +2079,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
} else {
const comments = result.columns.map((name) => columnInfos?.find((column) => column.name.toLocaleLowerCase() === name.toLocaleLowerCase())?.comment);
const headerOverrides = buildXlsxHeaderOverrides(result.columns, comments, headerMode);
await api.exportQueryResultXlsx(filePath, row.name, result.columns, result.column_types ?? result.columns.map(() => ""), headerOverrides, result.rows);
await api.exportQueryResultXlsx(filePath, row.name, result.columns, result.column_types ?? result.columns.map(() => ""), headerOverrides, result.rows, undefined, autoFilter);
}
toast(t("grid.exported"));
return;
@@ -2116,6 +2115,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
format,
columns,
columnComments: format === "xlsx" ? columnComments : undefined,
autoFilter: format === "xlsx" ? autoFilter : undefined,
batchSize: settingsStore.editorSettings.exportBatchSize,
skipCount: format === "sql",
rowLimit,
@@ -17,19 +17,19 @@ function functionBody(name: string): string {
}
describe("ObjectBrowser XLSX export", () => {
it("asks for a header mode before opening the save dialog when comments exist", () => {
it("asks for export options before opening the save dialog", () => {
const dialog = functionBody("showObjectBrowserXlsxHeaderDialog");
const exportDataXlsx = functionBody("exportDataXlsx");
expect(dialog).toContain('if (!hasComments) return Promise.resolve("name")');
expect(exportDataXlsx).toContain("hasXlsxHeaderComments(columnInfos.map((column) => column.comment))");
expect(exportDataXlsx.indexOf("await showObjectBrowserXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(row, "xlsx", columnInfos, headerMode)'));
expect(dialog).toContain("showHeaderOptions: hasComments");
expect(exportDataXlsx).toContain("hasXlsxHeaderComments(columnInfos?.map((column) => column.comment))");
expect(exportDataXlsx.indexOf("await showObjectBrowserXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(row, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)'));
});
it("falls back to field-name headers when column metadata is unavailable", () => {
const exportDataXlsx = functionBody("exportDataXlsx");
expect(exportDataXlsx).toContain("catch {\n // Export still works with field-name headers when column metadata is unavailable.\n }");
expect(exportDataXlsx).toContain('await exportTableData(row, "xlsx", columnInfos, headerMode)');
expect(exportDataXlsx).toContain('await exportTableData(row, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)');
});
});