mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-02 00:29:30 +00:00
feat(export): move text exports to Rust
This commit is contained in:
@@ -92,7 +92,7 @@ import {
|
||||
treeNodeRowAction,
|
||||
treeNodeRowDoubleClickAction,
|
||||
} from "@/lib/treeNodeClick";
|
||||
import { formatJson, formatSqlInsert } from "@/lib/exportFormats";
|
||||
import { formatSqlInsert } from "@/lib/exportFormats";
|
||||
import { fetchTableDataForExport } from "@/lib/tableDataExport";
|
||||
import {
|
||||
buildCreateDatabaseSql,
|
||||
@@ -1199,33 +1199,6 @@ async function saveFileContent(content: string, defaultFileName: string, filterN
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBinaryFileContent(
|
||||
content: Uint8Array,
|
||||
defaultFileName: string,
|
||||
filterName: string,
|
||||
filterExt: string,
|
||||
) {
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({
|
||||
defaultPath: defaultFileName,
|
||||
filters: [{ name: filterName, extensions: [filterExt] }],
|
||||
});
|
||||
if (path) await writeFile(path, content);
|
||||
} else {
|
||||
const blob = new Blob([content], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = defaultFileName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportStructure() {
|
||||
const node = props.node;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
@@ -1282,18 +1255,24 @@ async function exportData(format: "csv" | "json" | "sql") {
|
||||
return;
|
||||
}
|
||||
|
||||
let content: string;
|
||||
let ext: string;
|
||||
|
||||
if (format === "json") {
|
||||
ext = "json";
|
||||
content = formatJson(result.columns, result.rows);
|
||||
} else {
|
||||
ext = "sql";
|
||||
content = formatSqlInsert(qualifiedName, result.columns, result.rows, quoteIdent);
|
||||
let outputPath = `${node.label}.json`;
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await save({
|
||||
defaultPath: outputPath,
|
||||
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
return;
|
||||
}
|
||||
|
||||
await saveFileContent(content, `${node.label}.${ext}`, ext.toUpperCase(), ext);
|
||||
const content = formatSqlInsert(qualifiedName, result.columns, result.rows, quoteIdent);
|
||||
await saveFileContent(content, `${node.label}.sql`, "SQL", "sql");
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { computed, type ComputedRef, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { formatJson } from "@/lib/exportFormats";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
formatSelectionAsCsv,
|
||||
@@ -238,35 +237,6 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
await copyText(`${header}\n${body}`);
|
||||
}
|
||||
|
||||
// --- File save helpers ---
|
||||
async function saveFileContent(
|
||||
content: string,
|
||||
defaultFileName: string,
|
||||
filterName: string,
|
||||
filterExt: string,
|
||||
): Promise<boolean> {
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({
|
||||
defaultPath: defaultFileName,
|
||||
filters: [{ name: filterName, extensions: [filterExt] }],
|
||||
});
|
||||
if (!path) return false;
|
||||
await writeTextFile(path, "" + content);
|
||||
return true;
|
||||
} else {
|
||||
const blob = new Blob(["", content], { type: "text/csv;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = defaultFileName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Export functions ---
|
||||
async function runExclusiveExport(action: () => Promise<void>) {
|
||||
const finish = tryStartExclusiveActivation(exportGuard);
|
||||
@@ -303,10 +273,22 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
async function exportJson(rowIds?: number[]) {
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
const rows = rowsToExport(rowIds).map((item) => item.data);
|
||||
if (await saveFileContent(formatJson(columns.value, rows), "export.json", "JSON", "json")) {
|
||||
toast(t("grid.exported"));
|
||||
let outputPath = "export.json";
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await save({
|
||||
defaultPath: outputPath,
|
||||
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
await api.exportQueryResultJson(
|
||||
outputPath,
|
||||
columns.value,
|
||||
rowsToExport(rowIds).map((item) => item.data),
|
||||
);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
@@ -316,13 +298,22 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
async function exportMarkdown(rowIds?: number[]) {
|
||||
await runExclusiveExport(async () => {
|
||||
try {
|
||||
const cols = columns.value;
|
||||
const visibleRows = rowsToExport(rowIds).map((item) => item.data);
|
||||
const { formatMarkdownTable } = await import("@/lib/markdownTable");
|
||||
const md = formatMarkdownTable({ columns: cols, rows: visibleRows });
|
||||
if (await saveFileContent(md, "export.md", "Markdown", "md")) {
|
||||
toast(t("grid.exported"));
|
||||
let outputPath = "export.md";
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await save({
|
||||
defaultPath: outputPath,
|
||||
filters: [{ name: "Markdown", extensions: ["md"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
await api.exportQueryResultMarkdown(
|
||||
outputPath,
|
||||
columns.value,
|
||||
rowsToExport(rowIds).map((item) => item.data),
|
||||
);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,8 @@ export const exportDatabaseSql = forward("exportDatabaseSql");
|
||||
export const cancelDatabaseExport = forward("cancelDatabaseExport");
|
||||
export const exportQueryResultCsv = forward("exportQueryResultCsv");
|
||||
export const exportQueryResultXlsx = forward("exportQueryResultXlsx");
|
||||
export const exportQueryResultJson = forward("exportQueryResultJson");
|
||||
export const exportQueryResultMarkdown = forward("exportQueryResultMarkdown");
|
||||
|
||||
// Redis
|
||||
export const redisListDatabases = forward("redisListDatabases");
|
||||
|
||||
@@ -7,17 +7,6 @@ export function formatCsv(columns: string[], rows: ExportCellValue[][]): string
|
||||
return `${header}\n${body}`;
|
||||
}
|
||||
|
||||
export function formatJson(columns: string[], rows: ExportCellValue[][]): string {
|
||||
const data = rows.map((row) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
columns.forEach((col, i) => {
|
||||
obj[col] = row[i];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
export function formatSqlInsert(
|
||||
qualifiedName: string,
|
||||
columns: string[],
|
||||
|
||||
@@ -709,6 +709,17 @@ export async function exportQueryResultCsv(
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function downloadTextFile(filePath: string, fallbackFileName: string, content: string, mimeType: string): void {
|
||||
const fileName = filePath.split(/[\\/]/).pop() || fallbackFileName;
|
||||
const blob = new Blob(["\uFEFF", content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export async function exportQueryResultXlsx(
|
||||
filePath: string,
|
||||
sheetName: string | undefined,
|
||||
@@ -733,6 +744,24 @@ export async function exportQueryResultXlsx(
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export async function exportQueryResultJson(
|
||||
filePath: string,
|
||||
columns: string[],
|
||||
rows: readonly (readonly XlsxCellValue[])[],
|
||||
): Promise<void> {
|
||||
const result = await post<{ content: string }>("/api/export/query-result-json", { columns, rows });
|
||||
downloadTextFile(filePath, "export.json", result.content, "application/json;charset=utf-8");
|
||||
}
|
||||
|
||||
export async function exportQueryResultMarkdown(
|
||||
filePath: string,
|
||||
columns: string[],
|
||||
rows: readonly (readonly XlsxCellValue[])[],
|
||||
): Promise<void> {
|
||||
const result = await post<{ content: string }>("/api/export/query-result-markdown", { columns, rows });
|
||||
downloadTextFile(filePath, "export.md", result.content, "text/markdown;charset=utf-8");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Redis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { GridCellValue } from "./dataGridSql.ts";
|
||||
|
||||
export interface MarkdownTableData {
|
||||
columns: string[];
|
||||
rows: GridCellValue[][];
|
||||
}
|
||||
|
||||
export function formatMarkdownTable(data: MarkdownTableData): string {
|
||||
const normalizedColumns = data.columns.map(markdownCell);
|
||||
const normalizedRows = data.rows.map((row) => row.map((cell) => markdownCell(displayCell(cell))));
|
||||
const widths = normalizedColumns.map((column, index) =>
|
||||
Math.max(column.length, ...normalizedRows.map((row) => row[index]?.length ?? 0), 3),
|
||||
);
|
||||
const header = `| ${normalizedColumns.map((column, index) => pad(column, widths[index])).join(" | ")} |`;
|
||||
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
||||
const body = normalizedRows
|
||||
.map((row) => `| ${row.map((cell, index) => pad(cell, widths[index])).join(" | ")} |`)
|
||||
.join("\n");
|
||||
return `${[header, separator, body].filter(Boolean).join("\n")}\n`;
|
||||
}
|
||||
|
||||
function displayCell(value: GridCellValue): string {
|
||||
if (value === null) return "NULL";
|
||||
if (typeof value === "boolean") return value ? "true" : "false";
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function markdownCell(value: string): string {
|
||||
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
||||
}
|
||||
|
||||
function pad(value: string, width: number): string {
|
||||
return value.padEnd(width);
|
||||
}
|
||||
@@ -1023,3 +1023,31 @@ export async function exportQueryResultXlsx(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function exportQueryResultJson(
|
||||
filePath: string,
|
||||
columns: string[],
|
||||
rows: readonly (readonly XlsxCellValue[])[],
|
||||
): Promise<void> {
|
||||
return invoke("export_query_result_json", {
|
||||
request: {
|
||||
filePath,
|
||||
columns,
|
||||
rows,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function exportQueryResultMarkdown(
|
||||
filePath: string,
|
||||
columns: string[],
|
||||
rows: readonly (readonly XlsxCellValue[])[],
|
||||
): Promise<void> {
|
||||
return invoke("export_query_result_markdown", {
|
||||
request: {
|
||||
filePath,
|
||||
columns,
|
||||
rows,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user