feat(xlsx): add optional auto filter to xlsx export
This commit is contained in:
29 files changed
+285
-119
No files matched your search
@@ -3,21 +3,25 @@ import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
import type { XlsxExportOptions, XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(defineProps<{ showHeaderOptions?: boolean }>(), {
|
||||
showHeaderOptions: true,
|
||||
});
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
const selected = ref<XlsxHeaderMode>("name");
|
||||
const autoFilter = ref(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [mode: XlsxHeaderMode];
|
||||
confirm: [options: XlsxExportOptions];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
function onConfirm() {
|
||||
open.value = false;
|
||||
emit("confirm", selected.value);
|
||||
emit("confirm", { headerMode: selected.value, autoFilter: autoFilter.value });
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
@@ -34,22 +38,31 @@ function onOpenChange(value: boolean) {
|
||||
<Dialog v-model:open="open" @update:open="onOpenChange">
|
||||
<DialogContent class="sm:max-w-sm" @interact-outside.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("grid.xlsxHeaderTitle") }}</DialogTitle>
|
||||
<DialogTitle>{{ t("grid.xlsxExportTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="py-2">
|
||||
<p class="text-sm text-muted-foreground mb-3">{{ t("grid.xlsxHeaderPrompt") }}</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" v-model="selected" value="name" class="h-4 w-4" />
|
||||
<span class="text-sm">{{ t("grid.xlsxHeaderOriginal") }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" v-model="selected" value="comment" class="h-4 w-4" />
|
||||
<span class="text-sm">{{ t("grid.xlsxHeaderComment") }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" v-model="selected" value="name-comment" class="h-4 w-4" />
|
||||
<span class="text-sm">{{ t("grid.xlsxHeaderNameAndComment") }}</span>
|
||||
<div class="space-y-5 py-2">
|
||||
<div v-if="props.showHeaderOptions">
|
||||
<p class="mb-3 text-sm text-muted-foreground">{{ t("grid.xlsxHeaderPrompt") }}</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" v-model="selected" value="name" class="h-4 w-4" />
|
||||
<span class="text-sm">{{ t("grid.xlsxHeaderOriginal") }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" v-model="selected" value="comment" class="h-4 w-4" />
|
||||
<span class="text-sm">{{ t("grid.xlsxHeaderComment") }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" v-model="selected" value="name-comment" class="h-4 w-4" />
|
||||
<span class="text-sm">{{ t("grid.xlsxHeaderNameAndComment") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-muted-foreground">{{ t("grid.xlsxFilterPrompt") }}</p>
|
||||
<label class="flex cursor-pointer items-center gap-2">
|
||||
<input v-model="autoFilter" type="checkbox" class="h-4 w-4" />
|
||||
<span class="text-sm">{{ t("grid.xlsxIncludeAutoFilter") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,14 +31,14 @@ describe("XlsxHeaderDialog", () => {
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("emits the combined name and comment mode", async () => {
|
||||
it("emits the selected header mode and filter option", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
let selectedMode = "";
|
||||
let selectedOptions: { headerMode: string; autoFilter: boolean } | undefined;
|
||||
const app = createApp(XlsxHeaderDialog, {
|
||||
open: true,
|
||||
onConfirm: (mode: string) => {
|
||||
selectedMode = mode;
|
||||
onConfirm: (options: { headerMode: string; autoFilter: boolean }) => {
|
||||
selectedOptions = options;
|
||||
},
|
||||
});
|
||||
app.use(i18n);
|
||||
@@ -50,7 +50,7 @@ describe("XlsxHeaderDialog", () => {
|
||||
document.querySelector<HTMLButtonElement>("[data-xlsx-header-confirm]")!.click();
|
||||
await nextTick();
|
||||
|
||||
expect(selectedMode).toBe("name-comment");
|
||||
expect(selectedOptions).toEqual({ headerMode: "name-comment", autoFilter: false });
|
||||
app.unmount();
|
||||
});
|
||||
});
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
@@ -13,8 +13,8 @@ const mocks = vi.hoisted(() => ({
|
||||
vi.mock("@/components/export/XlsxHeaderDialog.vue", () => ({
|
||||
default: {
|
||||
emits: ["confirm"],
|
||||
mounted(this: { $emit: (event: string, value: string) => void }) {
|
||||
queueMicrotask(() => this.$emit("confirm", "name-comment"));
|
||||
mounted(this: { $emit: (event: string, value: { headerMode: string; autoFilter: boolean }) => void }) {
|
||||
queueMicrotask(() => this.$emit("confirm", { headerMode: "name-comment", autoFilter: false }));
|
||||
},
|
||||
render() {
|
||||
return null;
|
||||
@@ -125,7 +125,11 @@ describe("useDataGridExport XLSX headers", () => {
|
||||
|
||||
await state.exportAllResultsXlsx();
|
||||
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Identifier)"] }), expect.objectContaining({ sheetName: "Names", columnComments: ["name (Display name)"] })]));
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Identifier)"], autoFilter: false }), expect.objectContaining({ sheetName: "Names", columnComments: ["name (Display name)"], autoFilter: false })]),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses ordinal result comments when single-table metadata is unavailable", async () => {
|
||||
@@ -137,6 +141,6 @@ describe("useDataGridExport XLSX headers", () => {
|
||||
|
||||
await state.exportAllResultsXlsx();
|
||||
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Joined identifier)"] })]));
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Joined identifier)"], autoFilter: false })]), false);
|
||||
});
|
||||
});
|
||||
@@ -151,20 +151,20 @@ describe("useSidebarTreeExportRuntime", () => {
|
||||
expect(showStructurePreviewDialog.value).toBe(true);
|
||||
});
|
||||
|
||||
it("prompts for the header mode before it opens the save dialog when comments exist", () => {
|
||||
it("prompts for export options before it opens the save dialog", () => {
|
||||
const exportDataXlsx = functionBody("exportDataXlsx");
|
||||
|
||||
expect(source).toContain('import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue"');
|
||||
expect(exportDataXlsx).toContain("await api.getColumns(");
|
||||
expect(exportDataXlsx).toContain("hasXlsxHeaderComments(columnInfos?.map((column) => column.comment))");
|
||||
expect(exportDataXlsx.indexOf("await showSidebarTreeXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(target, "xlsx", columnInfos, headerMode)'));
|
||||
expect(exportDataXlsx.indexOf("await showSidebarTreeXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(target, "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(target, "xlsx", columnInfos, headerMode)');
|
||||
expect(exportDataXlsx).toContain('await exportTableData(target, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)');
|
||||
});
|
||||
|
||||
it("sends the selected mode's header overrides to both XLSX export paths", () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { formatTemporalRowsForExport } from "@/lib/dataGrid/columnFormatter";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
|
||||
import i18n from "@/i18n";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxExportOptions, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
|
||||
/**
|
||||
* Format metadata for backend table exports. Each entry maps a format key
|
||||
@@ -292,16 +292,17 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return targetColumns.map((column) => commentsByName.get(column.toLocaleLowerCase()));
|
||||
}
|
||||
|
||||
function showXlsxHeaderDialog(): Promise<XlsxHeaderMode | null> {
|
||||
if (!hasXlsxHeaderComments(allXlsxColumnComments.value) && !hasXlsxHeaderComments(visibleXlsxColumnComments.value)) return Promise.resolve("name");
|
||||
function showXlsxHeaderDialog(): 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: hasXlsxHeaderComments(allXlsxColumnComments.value) || hasXlsxHeaderComments(visibleXlsxColumnComments.value),
|
||||
onConfirm: (exportOptions: XlsxExportOptions) => {
|
||||
resolve(exportOptions);
|
||||
app.unmount();
|
||||
document.body.removeChild(container);
|
||||
},
|
||||
@@ -402,24 +403,29 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return resultExportSql?.value || sql.value;
|
||||
}
|
||||
|
||||
async function writeXlsxResult(outputPath: string, result: { columns: string[]; columnTypes: string[]; columnComments?: (string | null)[]; rows: CellValue[][] }, includeSqlSheet: boolean) {
|
||||
async function writeXlsxResult(outputPath: string, result: { columns: string[]; columnTypes: string[]; columnComments?: (string | null)[]; rows: CellValue[][] }, includeSqlSheet: boolean, autoFilter: boolean) {
|
||||
const sqlWorksheet = includeSqlSheet ? buildXlsxSqlWorksheet([{ sql: currentExportSql() || "" }]) : undefined;
|
||||
const rightAlign = useSettingsStore().editorSettings.numericColumnRightAlign;
|
||||
if (!sqlWorksheet) {
|
||||
await api.exportQueryResultXlsx(outputPath, currentXlsxSheetName(), result.columns, result.columnTypes, result.columnComments, result.rows, rightAlign);
|
||||
await api.exportQueryResultXlsx(outputPath, currentXlsxSheetName(), result.columns, result.columnTypes, result.columnComments, result.rows, rightAlign, autoFilter);
|
||||
return;
|
||||
}
|
||||
await api.exportQueryResultsXlsx(outputPath, [
|
||||
{
|
||||
sheetName: currentXlsxSheetName(),
|
||||
columns: result.columns,
|
||||
columnTypes: result.columnTypes,
|
||||
columnComments: result.columnComments,
|
||||
rows: result.rows,
|
||||
numericColumnRightAlign: rightAlign,
|
||||
},
|
||||
sqlWorksheet,
|
||||
]);
|
||||
await api.exportQueryResultsXlsx(
|
||||
outputPath,
|
||||
[
|
||||
{
|
||||
sheetName: currentXlsxSheetName(),
|
||||
columns: result.columns,
|
||||
columnTypes: result.columnTypes,
|
||||
columnComments: result.columnComments,
|
||||
rows: result.rows,
|
||||
numericColumnRightAlign: rightAlign,
|
||||
autoFilter,
|
||||
},
|
||||
{ ...sqlWorksheet, autoFilter },
|
||||
],
|
||||
autoFilter,
|
||||
);
|
||||
}
|
||||
|
||||
function targetedRows(): RowItem[] {
|
||||
@@ -910,13 +916,13 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
async function exportXlsxResult(rowIds: number[] | undefined, includeSqlSheet: boolean) {
|
||||
const headerMode = await showXlsxHeaderDialog();
|
||||
if (headerMode === null) return;
|
||||
const exportOptions = await showXlsxHeaderDialog();
|
||||
if (exportOptions === null) return;
|
||||
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
if (await exportQueryResultViaBackend("xlsx", rowIds, includeSqlSheet, headerMode)) return;
|
||||
if (await exportFullTableDataViaBackend("xlsx", rowIds, headerMode)) return;
|
||||
if (await exportQueryResultViaBackend("xlsx", rowIds, includeSqlSheet, exportOptions.headerMode, exportOptions.autoFilter)) return;
|
||||
if (await exportFullTableDataViaBackend("xlsx", rowIds, exportOptions.headerMode, exportOptions.autoFilter)) return;
|
||||
|
||||
let outputPath = exportFileName("export", "xlsx");
|
||||
if (isTauriRuntime()) {
|
||||
@@ -958,7 +964,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
},
|
||||
true,
|
||||
true,
|
||||
headerMode,
|
||||
exportOptions.headerMode,
|
||||
);
|
||||
if (needsFullExport && exportProgressState) {
|
||||
exportProgressState.value = {
|
||||
@@ -968,7 +974,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
totalRows: result.rows.length,
|
||||
};
|
||||
}
|
||||
await writeXlsxResult(outputPath, result, includeSqlSheet);
|
||||
await writeXlsxResult(outputPath, result, includeSqlSheet, exportOptions.autoFilter);
|
||||
if (needsFullExport && exportProgressState) {
|
||||
exportProgressState.value = {
|
||||
...exportProgressState.value,
|
||||
@@ -1001,8 +1007,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
async function exportCurrentPageXlsxResult(includeSqlSheet: boolean) {
|
||||
const headerMode = await showXlsxHeaderDialog();
|
||||
if (headerMode === null) return;
|
||||
const exportOptions = await showXlsxHeaderDialog();
|
||||
if (exportOptions === null) return;
|
||||
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
@@ -1016,8 +1022,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(undefined, undefined, false, true, headerMode);
|
||||
await writeXlsxResult(outputPath, result, includeSqlSheet);
|
||||
const result = await resultToExport(undefined, undefined, false, true, exportOptions.headerMode);
|
||||
await writeXlsxResult(outputPath, result, includeSqlSheet, exportOptions.autoFilter);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
@@ -1034,8 +1040,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
async function exportAllResultsXlsxResult(includeSqlSheet: boolean) {
|
||||
const headerMode = await showXlsxHeaderDialog();
|
||||
if (headerMode === null) return;
|
||||
const exportOptions = await showXlsxHeaderDialog();
|
||||
if (exportOptions === null) return;
|
||||
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
@@ -1059,12 +1065,13 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
sheetName: sheet.sheetName,
|
||||
columns: sheet.result.columns,
|
||||
columnTypes: sheet.result.column_types ?? [],
|
||||
columnComments: buildXlsxHeaderOverrides(sheet.result.columns, commentsForExportColumns(sheet.result.columns), headerMode),
|
||||
columnComments: buildXlsxHeaderOverrides(sheet.result.columns, commentsForExportColumns(sheet.result.columns), exportOptions.headerMode),
|
||||
rows: formatTemporalRowsForExport(sheet.result.rows, sheet.result.column_types ?? [], exportPattern),
|
||||
numericColumnRightAlign: rightAlign,
|
||||
autoFilter: exportOptions.autoFilter,
|
||||
}));
|
||||
const sqlWorksheet = includeSqlSheet ? buildXlsxSqlWorksheet(sheets.map((sheet) => ({ resultName: sheet.sheetName, sql: sheet.sql || sheet.result.sourceStatement || "" }))) : undefined;
|
||||
await api.exportQueryResultsXlsx(outputPath, sqlWorksheet ? [...worksheets, sqlWorksheet] : worksheets);
|
||||
await api.exportQueryResultsXlsx(outputPath, sqlWorksheet ? [...worksheets, { ...sqlWorksheet, autoFilter: exportOptions.autoFilter }] : worksheets, exportOptions.autoFilter);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
@@ -1080,7 +1087,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
await exportAllResultsXlsxResult(true);
|
||||
}
|
||||
|
||||
async function exportFullTableDataViaBackend(format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt", rowIds?: number[], headerMode: XlsxHeaderMode = "name"): Promise<boolean> {
|
||||
async function exportFullTableDataViaBackend(format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt", rowIds?: number[], headerMode: XlsxHeaderMode = "name", autoFilter = true): Promise<boolean> {
|
||||
const meta = tableMeta.value;
|
||||
// The backend table exporter currently builds two-part table names. External
|
||||
// Doris/StarRocks catalogs need the data-tab paginator's three-part SQL.
|
||||
@@ -1150,6 +1157,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
rowLimit,
|
||||
dateTimeFormat: editorSettings.globalDateTimeExportFormat || undefined,
|
||||
numericColumnRightAlign: editorSettings.numericColumnRightAlign ?? true,
|
||||
autoFilter: format === "xlsx" ? autoFilter : undefined,
|
||||
},
|
||||
(progress) => {
|
||||
if (exportProgressState) {
|
||||
@@ -1177,7 +1185,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function exportQueryResultViaBackend(format: "csv" | "xlsx" | "txt" | "sql", rowIds?: number[], includeSqlSheet = false, headerMode: XlsxHeaderMode = "name"): Promise<boolean> {
|
||||
async function exportQueryResultViaBackend(format: "csv" | "xlsx" | "txt" | "sql", rowIds?: number[], includeSqlSheet = false, headerMode: XlsxHeaderMode = "name", autoFilter = true): Promise<boolean> {
|
||||
if (rowIds !== undefined || context.value !== "results" || !queryResultExportRequest) {
|
||||
return false;
|
||||
}
|
||||
@@ -1210,7 +1218,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
exportColumnTypes: format === "sql" ? allColumnTypes.value?.map((type) => type ?? null) : undefined,
|
||||
});
|
||||
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 } : undefined;
|
||||
const request = baseRequest
|
||||
? { ...baseRequest, 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");
|
||||
|
||||
if (exportProgressState) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
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";
|
||||
|
||||
type StructureCopyFormat = "tsv" | "markdown";
|
||||
@@ -255,16 +255,17 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
}
|
||||
}
|
||||
|
||||
function showSidebarTreeXlsxHeaderDialog(hasComments: boolean): Promise<XlsxHeaderMode | null> {
|
||||
if (!hasComments) return Promise.resolve("name");
|
||||
function showSidebarTreeXlsxHeaderDialog(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);
|
||||
},
|
||||
@@ -304,7 +305,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
};
|
||||
}
|
||||
|
||||
async function exportTableData(target: SidebarTableExportTarget, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name") {
|
||||
async function exportTableData(target: SidebarTableExportTarget, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name", autoFilter = true) {
|
||||
const { connectionId, database } = target;
|
||||
|
||||
let task: ExportTask | null = null;
|
||||
@@ -341,7 +342,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
} else {
|
||||
const comments = result.columns.map((name) => exportColumnInfos?.find((column) => column.name.toLocaleLowerCase() === name.toLocaleLowerCase())?.comment);
|
||||
const headerOverrides = buildXlsxHeaderOverrides(result.columns, comments, headerMode);
|
||||
await api.exportQueryResultXlsx(outputPath, target.tableName, result.columns, result.column_types ?? result.columns.map(() => ""), headerOverrides, result.rows);
|
||||
await api.exportQueryResultXlsx(outputPath, target.tableName, result.columns, result.column_types ?? result.columns.map(() => ""), headerOverrides, result.rows, undefined, autoFilter);
|
||||
}
|
||||
currentTask.status = "Done";
|
||||
currentTask.rowsExported = result.rows.length;
|
||||
@@ -368,6 +369,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
format,
|
||||
columns: queryColumns,
|
||||
columnComments,
|
||||
autoFilter: format === "xlsx" ? autoFilter : undefined,
|
||||
primaryKeys,
|
||||
batchSize: target.batchSize,
|
||||
skipCount: format === "sql",
|
||||
@@ -408,9 +410,9 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
// Export still works with field-name headers when column metadata is unavailable.
|
||||
}
|
||||
|
||||
const headerMode = await showSidebarTreeXlsxHeaderDialog(hasXlsxHeaderComments(columnInfos?.map((column) => column.comment)));
|
||||
if (headerMode === null) return;
|
||||
await exportTableData(target, "xlsx", columnInfos, headerMode);
|
||||
const exportOptions = await showSidebarTreeXlsxHeaderDialog(hasXlsxHeaderComments(columnInfos?.map((column) => column.comment)));
|
||||
if (exportOptions === null) return;
|
||||
await exportTableData(target, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1651,10 +1651,13 @@ export default {
|
||||
exported: "Exported",
|
||||
exportFailed: "Export failed: {message}",
|
||||
xlsxHeaderTitle: "Header Format",
|
||||
xlsxExportTitle: "Export Options",
|
||||
xlsxHeaderPrompt: "Choose the header format for the exported Excel file:",
|
||||
xlsxHeaderOriginal: "Use column names as headers",
|
||||
xlsxHeaderComment: "Use column comments as headers",
|
||||
xlsxHeaderNameAndComment: "Use column names and comments as headers",
|
||||
xlsxFilterPrompt: "Choose whether the exported Excel file includes filters:",
|
||||
xlsxIncludeAutoFilter: "Include filters",
|
||||
copied: "Copied",
|
||||
copyFailed: "Copy failed: {message}",
|
||||
previewSqlEmpty: "No pending SQL changes to preview",
|
||||
|
||||
@@ -2046,10 +2046,13 @@ export default withEnglishFallback({
|
||||
batchAppendPasteEmpty: "No hay filas nuevas en el portapapeles.",
|
||||
batchAppendPasteReadonlyColumn: "Los datos pegados contienen columnas no editables.",
|
||||
xlsxHeaderTitle: "Formato de encabezado",
|
||||
xlsxExportTitle: "Opciones de exportacion",
|
||||
xlsxHeaderPrompt: "Seleccione el formato de encabezado al exportar a Excel:",
|
||||
xlsxHeaderOriginal: "Encabezado usando nombres de campos",
|
||||
xlsxHeaderComment: "Encabezado usando comentarios",
|
||||
xlsxHeaderNameAndComment: "Encabezado usando nombres de campos y comentarios",
|
||||
xlsxFilterPrompt: "Seleccione si el Excel exportado incluye filtros:",
|
||||
xlsxIncludeAutoFilter: "Incluir filtros",
|
||||
jumpToPage: "Ir a la página",
|
||||
importBinaryValue: "Importar desde archivo",
|
||||
binaryImportApplied: "Se importaron {count} bytes, que se escribirán en la base de datos al guardar los cambios.",
|
||||
|
||||
@@ -2044,10 +2044,13 @@ export default withEnglishFallback({
|
||||
batchAppendPasteEmpty: "Negli appunti non ci sono righe da aggiungere.",
|
||||
batchAppendPasteReadonlyColumn: "I dati incollati contengono colonne non modificabili.",
|
||||
xlsxHeaderTitle: "Formato intestazione",
|
||||
xlsxExportTitle: "Opzioni di esportazione",
|
||||
xlsxHeaderPrompt: "Seleziona il formato dell'intestazione da utilizzare per l'esportazione in Excel:",
|
||||
xlsxHeaderOriginal: "Intestazione con nome campo",
|
||||
xlsxHeaderComment: "Intestazione con commento",
|
||||
xlsxHeaderNameAndComment: "Intestazione con nome campo e commento",
|
||||
xlsxFilterPrompt: "Scegli se il file Excel esportato include filtri:",
|
||||
xlsxIncludeAutoFilter: "Includi filtri",
|
||||
jumpToPage: "Vai alla pagina",
|
||||
importBinaryValue: "Importa da file",
|
||||
binaryImportApplied: "Importati {count} byte, scritti nel database dopo il salvataggio delle modifiche.",
|
||||
|
||||
@@ -2066,10 +2066,13 @@ export default withEnglishFallback({
|
||||
batchAppendPasteEmpty: "クリップボードに追加可能な行がありません。",
|
||||
batchAppendPasteReadonlyColumn: "貼り付けたデータに編集不可能な列が含まれています。",
|
||||
xlsxHeaderTitle: "ヘッダー形式",
|
||||
xlsxExportTitle: "エクスポートオプション",
|
||||
xlsxHeaderPrompt: "Excel エクスポート時に使用するヘッダー形式を選択してください:",
|
||||
xlsxHeaderOriginal: "ヘッダーにフィールド名を使用",
|
||||
xlsxHeaderComment: "ヘッダーにコメントを使用",
|
||||
xlsxHeaderNameAndComment: "ヘッダーにフィールド名とコメントを使用",
|
||||
xlsxFilterPrompt: "エクスポートする Excel にフィルターを含めるか選択してください:",
|
||||
xlsxIncludeAutoFilter: "フィルターを含める",
|
||||
searchMode: "検索モード",
|
||||
searchModeHint: "Ctrl+F 検索時に一致する行をフィルタリングするか、全行を表示したまま一致箇所をハイライトするかを選択します。",
|
||||
searchModeFilter: "フィルター",
|
||||
|
||||
@@ -1955,6 +1955,9 @@ export default withEnglishFallback({
|
||||
sortUnsupported: "이 SQL은 전체 결과 정렬을 지원하지 않습니다. 단일 SELECT 쿼리로 다시 시도하세요.",
|
||||
truncatedHint: "{count}행 로드 후 결과가 잘렸습니다. 로드된 데이터를 탐색하려면 푸터 페이지 매김을 사용하세요. 전체 결과를 내보내면 데이터베이스 쿼리를 다시 실행합니다.",
|
||||
victoriaMetricsTruncatedHint: "결과가 로드 한도를 초과하여 처음 {count}행만 로드했습니다. 페이지당 행 수는 로드된 데이터만 나눠 표시합니다. 전체 결과를 가져오려면 시간 범위를 줄이거나 필터를 추가하세요.",
|
||||
xlsxExportTitle: "내보내기 옵션",
|
||||
xlsxFilterPrompt: "내보낸 Excel에 필터를 포함할지 선택하십시오:",
|
||||
xlsxIncludeAutoFilter: "필터 포함",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "이 쿼리는 스트리밍 내보내기를 지원하지 않습니다. 쿼리를 단순화하거나 지원되는 드라이버를 사용하세요.",
|
||||
|
||||
@@ -2046,10 +2046,13 @@ export default withEnglishFallback({
|
||||
batchAppendPasteEmpty: "Não há linhas para adicionar na área de transferência.",
|
||||
batchAppendPasteReadonlyColumn: "Os dados colados contêm colunas não editáveis.",
|
||||
xlsxHeaderTitle: "Formato do cabeçalho",
|
||||
xlsxExportTitle: "Opcoes de exportacao",
|
||||
xlsxHeaderPrompt: "Selecione o formato do cabeçalho a ser usado ao exportar Excel:",
|
||||
xlsxHeaderOriginal: "Cabeçalho usa nome do campo",
|
||||
xlsxHeaderComment: "Cabeçalho usa comentário",
|
||||
xlsxHeaderNameAndComment: "Cabeçalho usa nome do campo e comentário",
|
||||
xlsxFilterPrompt: "Selecione se o Excel exportado inclui filtros:",
|
||||
xlsxIncludeAutoFilter: "Incluir filtros",
|
||||
jumpToPage: "Ir para a página",
|
||||
importBinaryValue: "Importar do arquivo",
|
||||
binaryImportApplied: "{count} bytes importados, gravados no banco de dados após salvar as alterações.",
|
||||
|
||||
@@ -1575,10 +1575,13 @@ export default withEnglishFallback({
|
||||
exported: "导出完成",
|
||||
exportFailed: "导出失败:{message}",
|
||||
xlsxHeaderTitle: "表头格式",
|
||||
xlsxExportTitle: "导出选项",
|
||||
xlsxHeaderPrompt: "请选择导出 Excel 时使用的表头格式:",
|
||||
xlsxHeaderOriginal: "表头使用字段名称",
|
||||
xlsxHeaderComment: "表头使用注释",
|
||||
xlsxHeaderNameAndComment: "表头使用字段名称 + 注释",
|
||||
xlsxFilterPrompt: "请选择是否在导出的 Excel 中包含筛选:",
|
||||
xlsxIncludeAutoFilter: "包含筛选",
|
||||
copied: "已复制",
|
||||
cut: "已剪切",
|
||||
copyFailed: "复制失败:{message}",
|
||||
|
||||
@@ -2050,10 +2050,13 @@ export default withEnglishFallback({
|
||||
batchAppendPasteEmpty: "剪貼簿中沒有可新增的行。",
|
||||
batchAppendPasteReadonlyColumn: "貼上的資料包含不可編輯的列。",
|
||||
xlsxHeaderTitle: "表頭格式",
|
||||
xlsxExportTitle: "匯出選項",
|
||||
xlsxHeaderPrompt: "請選擇匯出 Excel 時使用的表頭格式:",
|
||||
xlsxHeaderOriginal: "表頭使用欄位名稱",
|
||||
xlsxHeaderComment: "表頭使用註解",
|
||||
xlsxHeaderNameAndComment: "表頭使用欄位名稱 + 註解",
|
||||
xlsxFilterPrompt: "請選擇匯出的 Excel 是否包含篩選:",
|
||||
xlsxIncludeAutoFilter: "包含篩選",
|
||||
cut: "已剪下",
|
||||
},
|
||||
exportProgress: {
|
||||
|
||||
@@ -2535,7 +2535,16 @@ function downloadTextFile(filePath: string, fallbackFileName: string, content: s
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export async function exportQueryResultXlsx(filePath: string, sheetName: string | undefined, columns: string[], columnTypes: string[], columnComments: readonly (string | null)[] | undefined, rows: readonly (readonly XlsxCellValue[])[], numericColumnRightAlign?: boolean): Promise<void> {
|
||||
export async function exportQueryResultXlsx(
|
||||
filePath: string,
|
||||
sheetName: string | undefined,
|
||||
columns: string[],
|
||||
columnTypes: string[],
|
||||
columnComments: readonly (string | null)[] | undefined,
|
||||
rows: readonly (readonly XlsxCellValue[])[],
|
||||
numericColumnRightAlign?: boolean,
|
||||
autoFilter?: boolean,
|
||||
): Promise<void> {
|
||||
const { buildXlsxWorkbook } = await import("@/lib/export/xlsxExport");
|
||||
const workbook = buildXlsxWorkbook({
|
||||
sheetName: sheetName || "Export",
|
||||
@@ -2544,6 +2553,7 @@ export async function exportQueryResultXlsx(filePath: string, sheetName: string
|
||||
columnComments,
|
||||
rows,
|
||||
numericColumnRightAlign,
|
||||
autoFilter,
|
||||
});
|
||||
const fileName = filePath.split(/[\\/]/).pop() || "export.xlsx";
|
||||
const blob = new Blob([new Uint8Array(workbook)], {
|
||||
@@ -2566,10 +2576,12 @@ export async function exportQueryResultsXlsx(
|
||||
columnComments?: readonly (string | null)[];
|
||||
rows: readonly (readonly XlsxCellValue[])[];
|
||||
numericColumnRightAlign?: boolean;
|
||||
autoFilter?: boolean;
|
||||
}[],
|
||||
autoFilter?: boolean,
|
||||
): Promise<void> {
|
||||
const { buildXlsxWorkbookMulti } = await import("@/lib/export/xlsxExport");
|
||||
const workbook = buildXlsxWorkbookMulti(worksheets);
|
||||
const workbook = buildXlsxWorkbookMulti(autoFilter === undefined ? worksheets : worksheets.map((worksheet) => ({ ...worksheet, autoFilter })));
|
||||
const fileName = filePath.split(/[\\/]/).pop() || "export.xlsx";
|
||||
const blob = new Blob([new Uint8Array(workbook)], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
|
||||
@@ -4659,6 +4659,7 @@ export interface TableExportRequest {
|
||||
rowLimit?: number | null;
|
||||
dateTimeFormat?: string;
|
||||
numericColumnRightAlign?: boolean;
|
||||
autoFilter?: boolean;
|
||||
}
|
||||
|
||||
export interface TableCsvExportOptions {
|
||||
@@ -4707,6 +4708,7 @@ export interface QueryResultExportRequest {
|
||||
exportColumnTypes?: Array<string | null | undefined>;
|
||||
numericColumnRightAlign?: boolean;
|
||||
columnComments?: Array<string | null> | null;
|
||||
autoFilter?: boolean;
|
||||
identifierQuote?: string;
|
||||
}
|
||||
|
||||
@@ -4846,7 +4848,16 @@ export async function exportTableDataCsv(options: TableCsvExportOptions): Promis
|
||||
return invoke("export_table_data_csv", { request: options });
|
||||
}
|
||||
|
||||
export async function exportQueryResultXlsx(filePath: string, sheetName: string | undefined, columns: string[], columnTypes: string[], columnComments: readonly (string | null)[] | undefined, rows: readonly (readonly XlsxCellValue[])[], numericColumnRightAlign?: boolean): Promise<void> {
|
||||
export async function exportQueryResultXlsx(
|
||||
filePath: string,
|
||||
sheetName: string | undefined,
|
||||
columns: string[],
|
||||
columnTypes: string[],
|
||||
columnComments: readonly (string | null)[] | undefined,
|
||||
rows: readonly (readonly XlsxCellValue[])[],
|
||||
numericColumnRightAlign?: boolean,
|
||||
autoFilter?: boolean,
|
||||
): Promise<void> {
|
||||
return invoke("export_query_result_xlsx", {
|
||||
request: {
|
||||
filePath,
|
||||
@@ -4856,6 +4867,7 @@ export async function exportQueryResultXlsx(filePath: string, sheetName: string
|
||||
columnComments,
|
||||
rows,
|
||||
numericColumnRightAlign,
|
||||
autoFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4869,12 +4881,15 @@ export async function exportQueryResultsXlsx(
|
||||
columnComments?: readonly (string | null)[];
|
||||
rows: readonly (readonly XlsxCellValue[])[];
|
||||
numericColumnRightAlign?: boolean;
|
||||
autoFilter?: boolean;
|
||||
}[],
|
||||
autoFilter?: boolean,
|
||||
): Promise<void> {
|
||||
return invoke("export_query_results_xlsx", {
|
||||
request: {
|
||||
filePath,
|
||||
worksheets,
|
||||
autoFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface XlsxWorksheetData {
|
||||
columnComments?: readonly (string | null)[] | undefined;
|
||||
rows: readonly (readonly XlsxCellValue[])[];
|
||||
numericColumnRightAlign?: boolean;
|
||||
autoFilter?: boolean;
|
||||
}
|
||||
|
||||
interface XlsxWorksheetSegment {
|
||||
@@ -174,14 +175,15 @@ function worksheetXml(segment: XlsxWorksheetSegment): string {
|
||||
return `<row r="${excelRowIndex}">${cells}</row>`;
|
||||
}).join("");
|
||||
|
||||
const autoFilterXml = data.autoFilter === false ? "" : `\n <autoFilter ref="${range}"/>`;
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<dimension ref="${range}"/>
|
||||
<sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>
|
||||
<sheetFormatPr defaultRowHeight="15"/>
|
||||
<cols>${colsXml}</cols>
|
||||
<sheetData>${headerXml}${bodyXml}</sheetData>
|
||||
<autoFilter ref="${range}"/>
|
||||
<sheetData>${headerXml}${bodyXml}</sheetData>${autoFilterXml}
|
||||
</worksheet>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
export type XlsxHeaderMode = "name" | "comment" | "name-comment";
|
||||
|
||||
export interface XlsxExportOptions {
|
||||
headerMode: XlsxHeaderMode;
|
||||
autoFilter: boolean;
|
||||
}
|
||||
|
||||
export function hasXlsxHeaderComments(comments: readonly (string | null | undefined)[] | undefined): boolean {
|
||||
return comments?.some((comment) => !!comment?.trim()) ?? false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user