feat(export): support name and comment XLSX headers

This commit is contained in:
二丫讲梵
2026-08-23 01:40:22 +08:00
committed by GitHub
parent 0908a944e8
commit c4630540dd
15 changed files with 171 additions and 63 deletions
@@ -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");
+1
View File
@@ -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",
+1
View File
@@ -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.",
+1
View File
@@ -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.",
+1
View File
@@ -2034,6 +2034,7 @@ export default withEnglishFallback({
xlsxHeaderPrompt: "Excel エクスポート時に使用するヘッダー形式を選択してください:",
xlsxHeaderOriginal: "ヘッダーにフィールド名を使用",
xlsxHeaderComment: "ヘッダーにコメントを使用",
xlsxHeaderNameAndComment: "ヘッダーにフィールド名とコメントを使用",
searchMode: "検索モード",
searchModeHint: "Ctrl+F 検索時に一致する行をフィルタリングするか、全行を表示したまま一致箇所をハイライトするかを選択します。",
searchModeFilter: "フィルター",
+1
View File
@@ -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.",
+1
View File
@@ -1559,6 +1559,7 @@ export default withEnglishFallback({
xlsxHeaderPrompt: "请选择导出 Excel 时使用的表头格式:",
xlsxHeaderOriginal: "表头使用字段名称",
xlsxHeaderComment: "表头使用注释",
xlsxHeaderNameAndComment: "表头使用字段名称 + 注释",
copied: "已复制",
cut: "已剪切",
copyFailed: "复制失败:{message}",
+1
View File
@@ -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);
});
});
+17
View File
@@ -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;
}