feat(export): add DB2 comments to XLSX headers
This commit is contained in:
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,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user