feat(export): add DB2 comments to XLSX headers

This commit is contained in:
大峰 authored and GitHub committed 2026-07-29 22:37:05 +08:00
1 parent 51b5c8ae95
commit 843f945581
35 files changed
+640 -87

No files matched your search

@@ -143,7 +143,7 @@ const emit = defineEmits<{
formatError: [message: string];
execute: [source: SqlExecutionOverride];
executeInNewResultTab: [source: SqlExecutionOverride];
exportQuery: [payload: { sql: string; format: "csv" | "xlsx" | "txt" }];
exportQuery: [payload: { sql: string; format: "csv" | "xlsx" | "txt"; columnComments?: (string | null)[] }];
save: [];
clickTable: [target: SqlObjectNavigationTarget];
viewTableData: [target: SqlObjectNavigationTarget];
@@ -1016,7 +1016,7 @@ function executeInNewResultTabFromContextMenu() {
function exportQueryFromContextMenu(format: "csv" | "xlsx" | "txt") {
const sql = executableSql.value;
if (!sql.trim()) return;
emit("exportQuery", { sql, format });
emit("exportQuery", { sql, format, columnComments: undefined });
}
async function copySelectedSqlFromContextMenu() {
@@ -0,0 +1,57 @@
<script setup lang="ts">
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";
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });
const selected = ref<"original" | "comment">("original");
const emit = defineEmits<{
confirm: [useCommentHeader: boolean];
cancel: [];
}>();
function onConfirm() {
open.value = false;
emit("confirm", selected.value === "comment");
}
function onCancel() {
open.value = false;
emit("cancel");
}
function onOpenChange(value: boolean) {
if (!value) onCancel();
}
</script>
<template>
<Dialog v-model:open="open" @update:open="onOpenChange">
<DialogContent class="sm:max-w-sm" @interact-outside.prevent>
<DialogHeader>
<DialogTitle>{{ t("grid.xlsxHeaderTitle") }}</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="original" 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>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="onCancel">{{ t("common.cancel") }}</Button>
<Button @click="onConfirm">{{ t("common.confirm") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,33 @@
// @vitest-environment happy-dom
import { createApp, nextTick } from "vue";
import { afterEach, describe, expect, it } from "vitest";
import i18n from "@/i18n";
import XlsxHeaderDialog from "../XlsxHeaderDialog.vue";
describe("XlsxHeaderDialog", () => {
afterEach(() => {
document.body.innerHTML = "";
});
it("emits cancel when the built-in close button dismisses the dialog", async () => {
const container = document.createElement("div");
document.body.appendChild(container);
let cancelCount = 0;
const app = createApp(XlsxHeaderDialog, {
open: true,
onCancel: () => {
cancelCount += 1;
},
});
app.use(i18n);
app.mount(container);
await nextTick();
document.querySelector<HTMLElement>('[data-slot="dialog-close"]')!.click();
await nextTick();
expect(cancelCount).toBe(1);
app.unmount();
});
});
@@ -867,7 +867,7 @@ function requestQueryEditorExecuteInNewResultTab() {
return queryEditorRef.value?.requestExecuteInNewResultTab();
}
async function handleExportQuery(payload: { sql: string; format: "csv" | "xlsx" | "txt" }) {
async function handleExportQuery(payload: { sql: string; format: "csv" | "xlsx" | "txt"; columnComments?: (string | null)[] }) {
const tab = props.activeTab;
if (!tab || tab.mode !== "query") return;
let filePath = `query-result.${payload.format}`;
@@ -878,7 +878,7 @@ async function handleExportQuery(payload: { sql: string; format: "csv" | "xlsx"
if (!picked) return;
filePath = picked as string;
}
await queryStore.exportQuerySqlDirect(tab.id, payload.sql, payload.format, filePath);
await queryStore.exportQuerySqlDirect(tab.id, payload.sql, payload.format, filePath, payload.columnComments);
}
function pasteClipboardAsSqlInCondition() {
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onActivated, onBeforeUnmount, ref, watch, type Component } from "vue";
import { computed, createApp, nextTick, onActivated, onBeforeUnmount, ref, watch, type Component } from "vue";
import { RecycleScroller } from "vue-virtual-scroller";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
import {
@@ -46,6 +46,7 @@ import {
X,
} from "@lucide/vue";
import { useI18n } from "vue-i18n";
import i18n from "@/i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -53,6 +54,7 @@ import { SearchableSelect } from "@/components/ui/searchable-select";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue";
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
import * as api from "@/lib/backend/api";
import type { ColumnInfo, ConnectionConfig, ForeignKeyInfo, IndexInfo, ObjectBrowserViewMode, ObjectBrowserViewport, ObjectInfo, ObjectSourceKind, ObjectStatistics, TableInfoTab, TreeNode, TriggerInfo } from "@/types/database";
import { sortTablesByFkDependency, type TableWithFk } from "@/lib/table/tableDependencySort";
@@ -1782,11 +1784,50 @@ async function exportData(row: ObjectBrowserRow, format: "csv" | "json" | "sql")
await exportDataLegacy(row, format);
}
async function exportDataXlsx(row: ObjectBrowserRow) {
await exportTableData(row, "xlsx");
function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<boolean | null> {
if (!hasComments) return Promise.resolve(false);
return new Promise((resolve) => {
const container = document.createElement("div");
document.body.appendChild(container);
const app = createApp(XlsxHeaderDialog, {
open: true,
onConfirm: (useCommentHeader: boolean) => {
resolve(useCommentHeader);
app.unmount();
document.body.removeChild(container);
},
onCancel: () => {
resolve(null);
app.unmount();
document.body.removeChild(container);
},
});
app.use(i18n);
app.mount(container);
});
}
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx") {
async function exportDataXlsx(row: ObjectBrowserRow) {
const schema = row.schema || selectedSchema.value;
let useCommentHeader = false;
let columnInfos: ColumnInfo[] | undefined;
try {
columnInfos = await api.getColumns(props.connection.id, props.database, schema || props.database, row.name, props.catalog);
const hasComments = columnInfos.some((col) => col.comment && col.comment.trim().length > 0);
const result = await showObjectBrowserXlsxHeaderDialog(hasComments);
if (result === null) return;
useCommentHeader = result;
} catch {
// Column fetch failed, fallback to export without comments
columnInfos = undefined;
}
await exportTableData(row, "xlsx", columnInfos, useCommentHeader);
}
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx", columnInfos?: ColumnInfo[], useCommentHeader = false) {
const schema = row.schema || selectedSchema.value;
// Save dialog first
@@ -1814,7 +1855,18 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx") {
let task: ExportTask | null = null;
try {
const queryColumns = props.connection.db_type === "neo4j" ? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name, props.catalog)).map((column) => column.name) : undefined;
let columns: string[] | undefined;
let columnComments: (string | null)[] | undefined;
if (columnInfos) {
columns = columnInfos.map((c) => c.name);
if (format === "xlsx" && useCommentHeader) {
columnComments = columnInfos.map((c) => c.comment ?? null);
}
} else if (props.connection.db_type === "neo4j") {
const infos = await api.getColumns(props.connection.id, props.database, schema || props.database, row.name, props.catalog);
columns = infos.map((c) => c.name);
}
task = addExportTask(row.name, format, filePath);
const currentTask = task;
@@ -1827,7 +1879,8 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx") {
tableName: row.name,
filePath,
format,
columns: queryColumns,
columns,
columnComments: format === "xlsx" ? columnComments : undefined,
batchSize: settingsStore.editorSettings.exportBatchSize,
rowLimit,
};