mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 05:20:02 +00:00
feat(export): support name and comment XLSX headers
This commit is contained in:
@@ -3,20 +3,21 @@ 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";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
const selected = ref<"original" | "comment">("original");
|
||||
const selected = ref<XlsxHeaderMode>("name");
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [useCommentHeader: boolean];
|
||||
confirm: [mode: XlsxHeaderMode];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
function onConfirm() {
|
||||
open.value = false;
|
||||
emit("confirm", selected.value === "comment");
|
||||
emit("confirm", selected.value);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
@@ -39,18 +40,22 @@ function onOpenChange(value: boolean) {
|
||||
<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" />
|
||||
<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>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="onCancel">{{ t("common.cancel") }}</Button>
|
||||
<Button @click="onConfirm">{{ t("common.confirm") }}</Button>
|
||||
<Button data-xlsx-header-confirm @click="onConfirm">{{ t("common.confirm") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -30,4 +30,27 @@ describe("XlsxHeaderDialog", () => {
|
||||
expect(cancelCount).toBe(1);
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("emits the combined name and comment mode", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
let selectedMode = "";
|
||||
const app = createApp(XlsxHeaderDialog, {
|
||||
open: true,
|
||||
onConfirm: (mode: string) => {
|
||||
selectedMode = mode;
|
||||
},
|
||||
});
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
|
||||
document.querySelector<HTMLInputElement>('input[value="name-comment"]')!.click();
|
||||
await nextTick();
|
||||
document.querySelector<HTMLButtonElement>("[data-xlsx-header-confirm]")!.click();
|
||||
await nextTick();
|
||||
|
||||
expect(selectedMode).toBe("name-comment");
|
||||
app.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2576,13 +2576,8 @@ const allColumnTypes = computed(() =>
|
||||
const visibleColumnTypes = computed(() => visibleColumnIndexes.value.map((index) => allColumnTypes.value[index]));
|
||||
const allColumnTypeVisualKinds = computed(() => allColumnTypes.value.map((type) => resolveDataGridTypeVisualKind(type, resolvedDatabaseType.value)));
|
||||
const visibleColumnTypeVisualKinds = computed(() => visibleColumnIndexes.value.map((index) => allColumnTypeVisualKinds.value[index] ?? "unknown"));
|
||||
const visibleColumnComments = computed(() =>
|
||||
visibleColumnIndexes.value.map((index) => {
|
||||
const ordinalComments = props.resultColumnComments;
|
||||
if (ordinalComments) return ordinalComments[index];
|
||||
return dataGridColumnCommentFor(columnCommentMap.value, props.result.columns[index] ?? "", props.sourceColumns?.[index]);
|
||||
}),
|
||||
);
|
||||
const allColumnComments = computed(() => props.result.columns.map((column, index) => resolvedColumnComment(column, index)));
|
||||
const visibleColumnComments = computed(() => visibleColumnIndexes.value.map((index) => allColumnComments.value[index]));
|
||||
const visibleColumnCount = computed(() => visibleColumnIndexes.value.length);
|
||||
|
||||
const numericColumnRightAlign = computed(() => (settingsStore.editorSettings.numericColumnRightAlign ?? true) && !showTranspose.value);
|
||||
@@ -7615,6 +7610,8 @@ const {
|
||||
database: computed(() => props.executionDatabase ?? props.database),
|
||||
context: computed(() => props.context),
|
||||
sourceColumns: visibleSourceColumns,
|
||||
columnComments: visibleColumnComments,
|
||||
allColumnComments,
|
||||
mongoDocuments: computed(() => props.result.mongo_copy_documents ?? props.result.mongo_documents),
|
||||
columnTypes: visibleColumnTypes,
|
||||
allColumnTypes,
|
||||
|
||||
@@ -84,6 +84,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 { copyToClipboard, eventTargetAllowsAppClipboardShortcut } from "@/lib/common/clipboard";
|
||||
import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableDataCopy, tableClipboardMatchesTarget, tableClipboardMenuState, tableClipboardSourceContext, tableDataCopyColumnOptions, type PasteTableMode, type TableClipboardContext } from "@/lib/table/tableClipboard";
|
||||
import { buildSingleDdlExportFileContent } from "@/lib/export/ddlExport";
|
||||
@@ -1923,16 +1924,16 @@ async function exportData(row: ObjectBrowserRow, format: "csv" | "json" | "sql")
|
||||
else await exportTableData(row, format);
|
||||
}
|
||||
|
||||
function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<boolean | null> {
|
||||
if (!hasComments) return Promise.resolve(false);
|
||||
function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<XlsxHeaderMode | null> {
|
||||
if (!hasComments) return Promise.resolve("name");
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const app = createApp(XlsxHeaderDialog, {
|
||||
open: true,
|
||||
onConfirm: (useCommentHeader: boolean) => {
|
||||
resolve(useCommentHeader);
|
||||
onConfirm: (mode: XlsxHeaderMode) => {
|
||||
resolve(mode);
|
||||
app.unmount();
|
||||
document.body.removeChild(container);
|
||||
},
|
||||
@@ -1949,24 +1950,24 @@ function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<boolea
|
||||
|
||||
async function exportDataXlsx(row: ObjectBrowserRow) {
|
||||
const schema = row.schema || selectedSchema.value;
|
||||
let useCommentHeader = false;
|
||||
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 hasComments = columnInfos.some((col) => col.comment && col.comment.trim().length > 0);
|
||||
const hasComments = hasXlsxHeaderComments(columnInfos.map((column) => column.comment));
|
||||
const result = await showObjectBrowserXlsxHeaderDialog(hasComments);
|
||||
if (result === null) return;
|
||||
useCommentHeader = result;
|
||||
headerMode = result;
|
||||
} catch {
|
||||
// Column fetch failed, fallback to export without comments
|
||||
columnInfos = undefined;
|
||||
}
|
||||
|
||||
await exportTableData(row, "xlsx", columnInfos, useCommentHeader);
|
||||
await exportTableData(row, "xlsx", columnInfos, headerMode);
|
||||
}
|
||||
|
||||
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], useCommentHeader = false) {
|
||||
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name") {
|
||||
const schema = row.schema || selectedSchema.value;
|
||||
|
||||
// Save dialog first
|
||||
@@ -2004,8 +2005,9 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
if (format === "csv") {
|
||||
await api.exportQueryResultCsv(filePath, result.columns, result.rows);
|
||||
} else {
|
||||
const comments = useCommentHeader ? result.columns.map((name) => columnInfos?.find((column) => column.name.toLocaleLowerCase() === name.toLocaleLowerCase())?.comment ?? null) : undefined;
|
||||
await api.exportQueryResultXlsx(filePath, row.name, result.columns, result.column_types ?? result.columns.map(() => ""), comments, result.rows);
|
||||
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);
|
||||
}
|
||||
toast(t("grid.exported"));
|
||||
return;
|
||||
@@ -2015,8 +2017,12 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
|
||||
if (columnInfos) {
|
||||
columns = columnInfos.map((c) => c.name);
|
||||
if (format === "xlsx" && useCommentHeader) {
|
||||
columnComments = columnInfos.map((c) => c.comment ?? null);
|
||||
if (format === "xlsx") {
|
||||
columnComments = buildXlsxHeaderOverrides(
|
||||
columns,
|
||||
columnInfos.map((column) => column.comment),
|
||||
headerMode,
|
||||
);
|
||||
}
|
||||
} else if (props.connection.db_type === "neo4j") {
|
||||
const infos = await api.getColumns(props.connection.id, props.database, schema || props.database, row.name, props.catalog);
|
||||
|
||||
@@ -13,8 +13,8 @@ const mocks = vi.hoisted(() => ({
|
||||
vi.mock("@/components/export/XlsxHeaderDialog.vue", () => ({
|
||||
default: {
|
||||
emits: ["confirm"],
|
||||
mounted(this: { $emit: (event: string, value: boolean) => void }) {
|
||||
queueMicrotask(() => this.$emit("confirm", true));
|
||||
mounted(this: { $emit: (event: string, value: string) => void }) {
|
||||
queueMicrotask(() => this.$emit("confirm", "name-comment"));
|
||||
},
|
||||
render() {
|
||||
return null;
|
||||
@@ -120,11 +120,23 @@ describe("useDataGridExport XLSX headers", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("maps comments independently for every result sheet", async () => {
|
||||
it("maps combined headers independently for every result sheet", async () => {
|
||||
const state = useDataGridExport(createOptions());
|
||||
|
||||
await state.exportAllResultsXlsx();
|
||||
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["Identifier"] }), expect.objectContaining({ sheetName: "Names", columnComments: ["Display name"] })]));
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Identifier)"] }), expect.objectContaining({ sheetName: "Names", columnComments: ["name (Display name)"] })]));
|
||||
});
|
||||
|
||||
it("uses ordinal result comments when single-table metadata is unavailable", async () => {
|
||||
const options = createOptions();
|
||||
options.tableMeta = computed(() => undefined);
|
||||
options.columnComments = computed(() => ["Joined identifier"]);
|
||||
options.allColumnComments = computed(() => ["Joined identifier"]);
|
||||
const state = useDataGridExport(options);
|
||||
|
||||
await state.exportAllResultsXlsx();
|
||||
|
||||
expect(mocks.exportQueryResultsXlsx).toHaveBeenCalledWith(expect.any(String), expect.arrayContaining([expect.objectContaining({ sheetName: "Ids", columnComments: ["id (Joined identifier)"] })]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +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";
|
||||
|
||||
/**
|
||||
* Format metadata for backend table exports. Each entry maps a format key
|
||||
@@ -82,6 +83,8 @@ export interface UseDataGridExportOptions {
|
||||
database: ComputedRef<string | undefined>;
|
||||
context: ComputedRef<"results" | "table-data" | undefined>;
|
||||
sourceColumns: ComputedRef<Array<string | undefined> | undefined>;
|
||||
columnComments?: ComputedRef<Array<string | undefined>>;
|
||||
allColumnComments?: ComputedRef<Array<string | undefined>>;
|
||||
mongoDocuments?: ComputedRef<unknown[] | undefined>;
|
||||
columnTypes: ComputedRef<Array<string | undefined> | undefined>;
|
||||
allColumnTypes?: ComputedRef<Array<string | undefined> | undefined>;
|
||||
@@ -164,6 +167,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
tableMeta,
|
||||
copyInsertTargetLabel,
|
||||
sourceColumns,
|
||||
columnComments: columnCommentsOption,
|
||||
allColumnComments: allColumnCommentsOption,
|
||||
databaseType,
|
||||
identifierQuote,
|
||||
connectionId,
|
||||
@@ -250,33 +255,49 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return pattern ? { ...result, rows: formatTemporalRowsForExport(result.rows, result.columnTypes, pattern) } : result;
|
||||
}
|
||||
|
||||
function buildColumnComments(columns: string[]): (string | null)[] | undefined {
|
||||
function tableCommentsForColumns(targetColumns: readonly string[]): Array<string | undefined> {
|
||||
const meta = tableMeta.value;
|
||||
if (!meta?.columns) return undefined;
|
||||
if (!meta?.columns) return targetColumns.map(() => undefined);
|
||||
const commentMap = new Map<string, string>();
|
||||
for (const col of meta.columns) {
|
||||
if (col.comment) commentMap.set(col.name, col.comment);
|
||||
if (col.comment) commentMap.set(col.name.toLocaleLowerCase(), col.comment);
|
||||
}
|
||||
const result = columns.map((col) => commentMap.get(col) ?? null);
|
||||
return result.some((c) => c !== null) ? result : undefined;
|
||||
return targetColumns.map((column) => commentMap.get(column.toLocaleLowerCase()));
|
||||
}
|
||||
|
||||
function hasTableComments(): boolean {
|
||||
const meta = tableMeta.value;
|
||||
if (!meta?.columns) return false;
|
||||
return meta.columns.some((col) => col.comment && col.comment.trim().length > 0);
|
||||
const visibleXlsxColumnComments = computed(() => columnCommentsOption?.value ?? tableCommentsForColumns(columns.value));
|
||||
const allXlsxColumnComments = computed(() => allColumnCommentsOption?.value ?? tableCommentsForColumns(allColumns.value));
|
||||
|
||||
function commentsForExportColumns(targetColumns: readonly string[], preferAllColumns = true): Array<string | undefined> {
|
||||
const sourceColumns = preferAllColumns ? allColumns.value : columns.value;
|
||||
const sourceComments = preferAllColumns ? allXlsxColumnComments.value : visibleXlsxColumnComments.value;
|
||||
if (targetColumns.length === sourceColumns.length && targetColumns.every((column, index) => column === sourceColumns[index])) {
|
||||
return [...sourceComments];
|
||||
}
|
||||
|
||||
const commentsByName = new Map<string, string>();
|
||||
sourceColumns.forEach((column, index) => {
|
||||
const comment = sourceComments[index];
|
||||
if (comment) commentsByName.set(column.toLocaleLowerCase(), comment);
|
||||
});
|
||||
for (const column of tableMeta.value?.columns ?? []) {
|
||||
if (column.comment && !commentsByName.has(column.name.toLocaleLowerCase())) {
|
||||
commentsByName.set(column.name.toLocaleLowerCase(), column.comment);
|
||||
}
|
||||
}
|
||||
return targetColumns.map((column) => commentsByName.get(column.toLocaleLowerCase()));
|
||||
}
|
||||
|
||||
function showXlsxHeaderDialog(): Promise<boolean | null> {
|
||||
if (!hasTableComments()) return Promise.resolve(false);
|
||||
function showXlsxHeaderDialog(): Promise<XlsxHeaderMode | null> {
|
||||
if (!hasXlsxHeaderComments(allXlsxColumnComments.value) && !hasXlsxHeaderComments(visibleXlsxColumnComments.value)) return Promise.resolve("name");
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const app = createApp(XlsxHeaderDialog, {
|
||||
open: true,
|
||||
onConfirm: (useCommentHeader: boolean) => {
|
||||
resolve(useCommentHeader);
|
||||
onConfirm: (mode: XlsxHeaderMode) => {
|
||||
resolve(mode);
|
||||
app.unmount();
|
||||
document.body.removeChild(container);
|
||||
},
|
||||
@@ -291,7 +312,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; rows: CellValue[][] } {
|
||||
function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; columnComments: Array<string | undefined>; rows: CellValue[][] } {
|
||||
const hiddenColumnIndexes = new Set(result.hidden_column_indexes ?? []);
|
||||
const exportedColumnIndexes = result.columns.map((_, index) => index).filter((index) => !hiddenColumnIndexes.has(index));
|
||||
const hasHiddenColumns = exportedColumnIndexes.length !== result.columns.length;
|
||||
@@ -303,6 +324,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return {
|
||||
columns: hasHiddenColumns ? exportedColumnIndexes.map((index) => result.columns[index]!) : result.columns,
|
||||
columnTypes: hasHiddenColumns ? exportedColumnIndexes.map((index) => result.column_types?.[index] ?? "") : (result.column_types ?? []),
|
||||
columnComments: hasHiddenColumns ? exportedColumnIndexes.map((index) => allXlsxColumnComments.value[index]) : [...allXlsxColumnComments.value],
|
||||
rows: hasHiddenColumns ? rows.map((row) => exportedColumnIndexes.map((index) => row[index])) : rows,
|
||||
};
|
||||
}
|
||||
@@ -312,12 +334,12 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void,
|
||||
useFullExport = true,
|
||||
formatDateTime = true,
|
||||
useCommentHeader = false,
|
||||
headerMode: XlsxHeaderMode = "name",
|
||||
): Promise<{ columns: string[]; columnTypes: string[]; columnComments?: (string | null)[]; rows: CellValue[][] }> {
|
||||
if (useFullExport && rowIds === undefined && fullExportResult && !hasCompleteLocalResult?.value) {
|
||||
const result = await fullExportResult(onProgress);
|
||||
if (result) {
|
||||
const columnComments = useCommentHeader ? buildColumnComments(result.columns) : undefined;
|
||||
const columnComments = buildXlsxHeaderOverrides(result.columns, commentsForExportColumns(result.columns), headerMode);
|
||||
return { ...applyGlobalDateTimeExportFormat({ columns: result.columns, columnTypes: result.column_types ?? [], rows: result.rows }, formatDateTime), columnComments };
|
||||
}
|
||||
}
|
||||
@@ -328,10 +350,10 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
// silently change what the export contains.
|
||||
if (useFullExport && rowIds === undefined && hasCompleteLocalResult?.value && completeLocalResult?.value) {
|
||||
const normalized = normalizeCompleteLocalResult(completeLocalResult.value);
|
||||
const columnComments = useCommentHeader ? buildColumnComments(normalized.columns) : undefined;
|
||||
return { ...applyGlobalDateTimeExportFormat(normalized, formatDateTime), columnComments };
|
||||
const columnComments = buildXlsxHeaderOverrides(normalized.columns, normalized.columnComments, headerMode);
|
||||
return { ...applyGlobalDateTimeExportFormat({ columns: normalized.columns, columnTypes: normalized.columnTypes, rows: normalized.rows }, formatDateTime), columnComments };
|
||||
}
|
||||
const commentHeader = useCommentHeader ? buildColumnComments(columns.value) : undefined;
|
||||
const commentHeader = buildXlsxHeaderOverrides(columns.value, visibleXlsxColumnComments.value, headerMode);
|
||||
return {
|
||||
...applyGlobalDateTimeExportFormat(
|
||||
{
|
||||
@@ -861,13 +883,13 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
async function exportXlsxResult(rowIds: number[] | undefined, includeSqlSheet: boolean) {
|
||||
const useCommentHeader = await showXlsxHeaderDialog();
|
||||
if (useCommentHeader === null) return;
|
||||
const headerMode = await showXlsxHeaderDialog();
|
||||
if (headerMode === null) return;
|
||||
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
if (await exportQueryResultViaBackend("xlsx", rowIds, includeSqlSheet, useCommentHeader)) return;
|
||||
if (await exportFullTableDataViaBackend("xlsx", rowIds, useCommentHeader)) return;
|
||||
if (await exportQueryResultViaBackend("xlsx", rowIds, includeSqlSheet, headerMode)) return;
|
||||
if (await exportFullTableDataViaBackend("xlsx", rowIds, headerMode)) return;
|
||||
|
||||
let outputPath = exportFileName("export", "xlsx");
|
||||
if (isTauriRuntime()) {
|
||||
@@ -909,7 +931,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
},
|
||||
true,
|
||||
true,
|
||||
useCommentHeader,
|
||||
headerMode,
|
||||
);
|
||||
if (needsFullExport && exportProgressState) {
|
||||
exportProgressState.value = {
|
||||
@@ -952,8 +974,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
async function exportCurrentPageXlsxResult(includeSqlSheet: boolean) {
|
||||
const useCommentHeader = await showXlsxHeaderDialog();
|
||||
if (useCommentHeader === null) return;
|
||||
const headerMode = await showXlsxHeaderDialog();
|
||||
if (headerMode === null) return;
|
||||
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
@@ -967,7 +989,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(undefined, undefined, false, true, useCommentHeader);
|
||||
const result = await resultToExport(undefined, undefined, false, true, headerMode);
|
||||
await writeXlsxResult(outputPath, result, includeSqlSheet);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
@@ -985,8 +1007,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
async function exportAllResultsXlsxResult(includeSqlSheet: boolean) {
|
||||
const useCommentHeader = await showXlsxHeaderDialog();
|
||||
if (useCommentHeader === null) return;
|
||||
const headerMode = await showXlsxHeaderDialog();
|
||||
if (headerMode === null) return;
|
||||
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
@@ -1010,7 +1032,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
sheetName: sheet.sheetName,
|
||||
columns: sheet.result.columns,
|
||||
columnTypes: sheet.result.column_types ?? [],
|
||||
columnComments: useCommentHeader ? buildColumnComments(sheet.result.columns) : undefined,
|
||||
columnComments: buildXlsxHeaderOverrides(sheet.result.columns, commentsForExportColumns(sheet.result.columns), headerMode),
|
||||
rows: formatTemporalRowsForExport(sheet.result.rows, sheet.result.column_types ?? [], exportPattern),
|
||||
numericColumnRightAlign: rightAlign,
|
||||
}));
|
||||
@@ -1031,7 +1053,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
await exportAllResultsXlsxResult(true);
|
||||
}
|
||||
|
||||
async function exportFullTableDataViaBackend(format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt", rowIds?: number[], useCommentHeader = false): Promise<boolean> {
|
||||
async function exportFullTableDataViaBackend(format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt", rowIds?: number[], headerMode: XlsxHeaderMode = "name"): 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.
|
||||
@@ -1092,7 +1114,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
format,
|
||||
columns: columns.value,
|
||||
columnTypes: columnTypes.value,
|
||||
columnComments: useCommentHeader ? buildColumnComments(columns.value) : undefined,
|
||||
columnComments: format === "xlsx" ? buildXlsxHeaderOverrides(columns.value, visibleXlsxColumnComments.value, headerMode) : undefined,
|
||||
primaryKeys: meta.primaryKeys,
|
||||
whereInput: whereInput.value,
|
||||
orderBy: orderBy.value,
|
||||
@@ -1128,7 +1150,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function exportQueryResultViaBackend(format: "csv" | "xlsx" | "txt" | "sql", rowIds?: number[], includeSqlSheet = false, useCommentHeader = false): Promise<boolean> {
|
||||
async function exportQueryResultViaBackend(format: "csv" | "xlsx" | "txt" | "sql", rowIds?: number[], includeSqlSheet = false, headerMode: XlsxHeaderMode = "name"): Promise<boolean> {
|
||||
if (rowIds !== undefined || context.value !== "results" || !queryResultExportRequest) {
|
||||
return false;
|
||||
}
|
||||
@@ -1160,7 +1182,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
exportTableName: format === "sql" ? tableMeta.value?.tableName : undefined,
|
||||
exportColumnTypes: format === "sql" ? allColumnTypes.value?.map((type) => type ?? null) : undefined,
|
||||
});
|
||||
const columnComments = useCommentHeader ? buildColumnComments(columns.value) : 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;
|
||||
if (!request) throw new Error("Unable to build query result export request");
|
||||
|
||||
|
||||
@@ -1635,6 +1635,7 @@ export default {
|
||||
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",
|
||||
copied: "Copied",
|
||||
copyFailed: "Copy failed: {message}",
|
||||
previewSqlEmpty: "No pending SQL changes to preview",
|
||||
|
||||
@@ -2014,6 +2014,7 @@ export default withEnglishFallback({
|
||||
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",
|
||||
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.",
|
||||
|
||||
@@ -2012,6 +2012,7 @@ export default withEnglishFallback({
|
||||
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",
|
||||
jumpToPage: "Vai alla pagina",
|
||||
importBinaryValue: "Importa da file",
|
||||
binaryImportApplied: "Importati {count} byte, scritti nel database dopo il salvataggio delle modifiche.",
|
||||
|
||||
@@ -2034,6 +2034,7 @@ export default withEnglishFallback({
|
||||
xlsxHeaderPrompt: "Excel エクスポート時に使用するヘッダー形式を選択してください:",
|
||||
xlsxHeaderOriginal: "ヘッダーにフィールド名を使用",
|
||||
xlsxHeaderComment: "ヘッダーにコメントを使用",
|
||||
xlsxHeaderNameAndComment: "ヘッダーにフィールド名とコメントを使用",
|
||||
searchMode: "検索モード",
|
||||
searchModeHint: "Ctrl+F 検索時に一致する行をフィルタリングするか、全行を表示したまま一致箇所をハイライトするかを選択します。",
|
||||
searchModeFilter: "フィルター",
|
||||
|
||||
@@ -2014,6 +2014,7 @@ export default withEnglishFallback({
|
||||
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",
|
||||
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.",
|
||||
|
||||
@@ -1559,6 +1559,7 @@ export default withEnglishFallback({
|
||||
xlsxHeaderPrompt: "请选择导出 Excel 时使用的表头格式:",
|
||||
xlsxHeaderOriginal: "表头使用字段名称",
|
||||
xlsxHeaderComment: "表头使用注释",
|
||||
xlsxHeaderNameAndComment: "表头使用字段名称 + 注释",
|
||||
copied: "已复制",
|
||||
cut: "已剪切",
|
||||
copyFailed: "复制失败:{message}",
|
||||
|
||||
@@ -2018,6 +2018,7 @@ export default withEnglishFallback({
|
||||
xlsxHeaderPrompt: "請選擇匯出 Excel 時使用的表頭格式:",
|
||||
xlsxHeaderOriginal: "表頭使用欄位名稱",
|
||||
xlsxHeaderComment: "表頭使用註解",
|
||||
xlsxHeaderNameAndComment: "表頭使用欄位名稱 + 註解",
|
||||
cut: "已剪下",
|
||||
},
|
||||
exportProgress: {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments } from "../xlsxHeader";
|
||||
|
||||
describe("xlsxHeader", () => {
|
||||
it("builds comment-only and combined header overrides", () => {
|
||||
const columns = ["id", "name", "created_at"];
|
||||
const comments = [" Identifier ", "", undefined];
|
||||
|
||||
expect(buildXlsxHeaderOverrides(columns, comments, "comment")).toEqual(["Identifier", null, null]);
|
||||
expect(buildXlsxHeaderOverrides(columns, comments, "name-comment")).toEqual(["id (Identifier)", null, null]);
|
||||
});
|
||||
|
||||
it("keeps name mode and comment-less exports on the original headers", () => {
|
||||
expect(buildXlsxHeaderOverrides(["id"], ["Identifier"], "name")).toBeUndefined();
|
||||
expect(buildXlsxHeaderOverrides(["id"], [" "], "name-comment")).toBeUndefined();
|
||||
expect(hasXlsxHeaderComments([undefined, " ", "Name"])).toBe(true);
|
||||
expect(hasXlsxHeaderComments([undefined, " "])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export type XlsxHeaderMode = "name" | "comment" | "name-comment";
|
||||
|
||||
export function hasXlsxHeaderComments(comments: readonly (string | null | undefined)[] | undefined): boolean {
|
||||
return comments?.some((comment) => !!comment?.trim()) ?? false;
|
||||
}
|
||||
|
||||
export function buildXlsxHeaderOverrides(columns: readonly string[], comments: readonly (string | null | undefined)[] | undefined, mode: XlsxHeaderMode): (string | null)[] | undefined {
|
||||
if (mode === "name") return undefined;
|
||||
|
||||
const overrides = columns.map((column, index) => {
|
||||
const comment = comments?.[index]?.trim();
|
||||
if (!comment) return null;
|
||||
return mode === "comment" ? comment : `${column} (${comment})`;
|
||||
});
|
||||
|
||||
return overrides.some((header) => header !== null) ? overrides : undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user