mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 16:24:33 +00:00
feat(data): support connection URLs and XLSX export
This commit is contained in:
@@ -14,7 +14,8 @@ import { useToast } from "@/composables/useToast";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import * as api from "@/lib/api";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { ArrowLeft, ChevronRight, Copy, FolderOpen, Grid3X3, List, Search } from "lucide-vue-next";
|
||||
import { applyParsedConnectionUrl, parseConnectionUrl } from "@/lib/connectionUrl";
|
||||
import { ArrowLeft, ChevronRight, Copy, FolderOpen, Grid3X3, Link2, List, Search } from "lucide-vue-next";
|
||||
|
||||
type DbOption = { value: string; label: string };
|
||||
type DbCategory = { key: string; title: string; options: DbOption[] };
|
||||
@@ -72,6 +73,7 @@ const form = ref(defaultForm());
|
||||
const selectedType = ref("mysql");
|
||||
const customDriverName = ref("");
|
||||
const mongoUseUrl = ref(false);
|
||||
const connectionUrlInput = ref("");
|
||||
const dialogStep = ref<DialogStep>("select");
|
||||
const dbPickerView = ref<DbPickerView>("icon");
|
||||
const dbSearchQuery = ref("");
|
||||
@@ -445,6 +447,23 @@ function resetTestState() {
|
||||
testResult.value = null;
|
||||
}
|
||||
|
||||
function applyConnectionUrl() {
|
||||
try {
|
||||
const parsed = parseConnectionUrl(connectionUrlInput.value, selectedType.value);
|
||||
form.value = applyParsedConnectionUrl(form.value, parsed);
|
||||
selectedType.value = parsed.driverProfile;
|
||||
customDriverName.value = isCustomCompatibleProfile() ? parsed.driverLabel : "";
|
||||
mongoUseUrl.value = !!parsed.useMongoUrl;
|
||||
if (!form.value.name.trim()) {
|
||||
form.value.name = parsed.database || parsed.host || parsed.driverLabel;
|
||||
}
|
||||
resetTestState();
|
||||
toast(t("connection.parseConnectionUrlApplied"), 2000);
|
||||
} catch (e: any) {
|
||||
toast(t("connection.parseConnectionUrlFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyTestResult() {
|
||||
if (!testResultMessage.value) return;
|
||||
await navigator.clipboard.writeText(testResultMessage.value);
|
||||
@@ -457,6 +476,7 @@ function resetForm() {
|
||||
selectedType.value = "mysql";
|
||||
customDriverName.value = "";
|
||||
mongoUseUrl.value = false;
|
||||
connectionUrlInput.value = "";
|
||||
dialogStep.value = "select";
|
||||
dbPickerView.value = "icon";
|
||||
dbSearchQuery.value = "";
|
||||
@@ -677,6 +697,33 @@ async function browseDbFilePath() {
|
||||
|
||||
<TabsContent value="connection" class="m-0">
|
||||
<div class="grid gap-4 py-4 pr-2 max-h-[65vh] overflow-y-auto">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label class="text-right">URL</Label>
|
||||
<div class="col-span-3 flex items-center gap-1">
|
||||
<Input
|
||||
v-model="connectionUrlInput"
|
||||
class="flex-1"
|
||||
:placeholder="t('connection.connectionUrlPlaceholder')"
|
||||
@keydown.enter.prevent="applyConnectionUrl"
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-9 w-9 shrink-0"
|
||||
:disabled="!connectionUrlInput.trim()"
|
||||
:aria-label="t('connection.parseConnectionUrl')"
|
||||
@click="applyConnectionUrl"
|
||||
>
|
||||
<Link2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("connection.parseConnectionUrl") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label class="text-right">{{ t("connection.name") }}</Label>
|
||||
<Input v-model="form.name" class="col-span-3" :placeholder="t('connection.namePlaceholder')" />
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
import { buildDataGridSaveStatements, formatGridSqlLiteral } from "@/lib/dataGridSql";
|
||||
import { formatMarkdownTable } from "@/lib/markdownTable";
|
||||
import { buildXlsxWorkbook } from "@/lib/xlsxExport";
|
||||
import {
|
||||
matchesRowStatusFilter,
|
||||
rowStatusFilterAfterAddingRow,
|
||||
@@ -1592,7 +1593,12 @@ function copyAll() {
|
||||
copyText(`${header}\n${body}`);
|
||||
}
|
||||
|
||||
async function saveFileContent(content: string, defaultFileName: string, filterName: string, filterExt: string) {
|
||||
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");
|
||||
@@ -1600,7 +1606,9 @@ async function saveFileContent(content: string, defaultFileName: string, filterN
|
||||
defaultPath: defaultFileName,
|
||||
filters: [{ name: filterName, extensions: [filterExt] }],
|
||||
});
|
||||
if (path) await writeTextFile(path, "" + content);
|
||||
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);
|
||||
@@ -1609,32 +1617,96 @@ async function saveFileContent(content: string, defaultFileName: string, filterN
|
||||
a.download = defaultFileName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBinaryFileContent(
|
||||
content: Uint8Array,
|
||||
defaultFileName: string,
|
||||
filterName: string,
|
||||
filterExt: string,
|
||||
): Promise<boolean> {
|
||||
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) return false;
|
||||
await writeFile(path, content);
|
||||
return true;
|
||||
} 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);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function exportCsv() {
|
||||
const escape = (v: string) => `"${v.replace(/"/g, '""')}"`;
|
||||
const header = props.result.columns.map(escape).join(",");
|
||||
const body = displayItems.value.map((item) => item.data.map((c) => escape(formatCell(c))).join(",")).join("\n");
|
||||
await saveFileContent(`${header}\n${body}`, "export.csv", "CSV", "csv");
|
||||
try {
|
||||
const escape = (v: string) => `"${v.replace(/"/g, '""')}"`;
|
||||
const header = props.result.columns.map(escape).join(",");
|
||||
const body = displayItems.value.map((item) => item.data.map((c) => escape(formatCell(c))).join(",")).join("\n");
|
||||
if (await saveFileContent(`${header}\n${body}`, "export.csv", "CSV", "csv")) {
|
||||
toast(t("grid.exported"));
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportJson() {
|
||||
const data = displayItems.value.map((item) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
props.result.columns.forEach((col, i) => {
|
||||
obj[col] = item.data[i];
|
||||
try {
|
||||
const data = displayItems.value.map((item) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
props.result.columns.forEach((col, i) => {
|
||||
obj[col] = item.data[i];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
await saveFileContent(JSON.stringify(data, null, 2), "export.json", "JSON", "json");
|
||||
if (await saveFileContent(JSON.stringify(data, null, 2), "export.json", "JSON", "json")) {
|
||||
toast(t("grid.exported"));
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportMarkdown() {
|
||||
const cols = props.result.columns;
|
||||
const visibleRows = displayItems.value.map((item) => item.data);
|
||||
const md = formatMarkdownTable({ columns: cols, rows: visibleRows });
|
||||
await saveFileContent(md, "export.md", "Markdown", "md");
|
||||
try {
|
||||
const cols = props.result.columns;
|
||||
const visibleRows = displayItems.value.map((item) => item.data);
|
||||
const md = formatMarkdownTable({ columns: cols, rows: visibleRows });
|
||||
if (await saveFileContent(md, "export.md", "Markdown", "md")) {
|
||||
toast(t("grid.exported"));
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportXlsx() {
|
||||
try {
|
||||
const workbook = buildXlsxWorkbook({
|
||||
sheetName: props.tableMeta?.tableName || "Export",
|
||||
columns: props.result.columns,
|
||||
rows: displayItems.value.map((item) => item.data),
|
||||
});
|
||||
if (await saveBinaryFileContent(workbook, "export.xlsx", "Excel", "xlsx")) {
|
||||
toast(t("grid.exported"));
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
const sqlOneLiner = computed(() => props.sql?.replace(/\s+/g, " ").trim() || "");
|
||||
@@ -2427,6 +2499,7 @@ defineExpose({
|
||||
<ContextMenuSubTrigger> <FileDown class="w-3.5 h-3.5 mr-2" /> {{ t("grid.export") }} </ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent class="w-max max-w-[min(80vw,16rem)]">
|
||||
<ContextMenuItem @click="exportCsv">{{ t("grid.exportCsv") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="exportXlsx">{{ t("grid.exportXlsx") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="exportJson">{{ t("grid.exportJson") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="exportMarkdown">{{ t("grid.exportMarkdown") }}</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
@@ -2497,6 +2570,7 @@ defineExpose({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="exportCsv">{{ t("grid.exportCsv") }}</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="exportXlsx">{{ t("grid.exportXlsx") }}</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="exportJson">{{ t("grid.exportJson") }}</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="exportMarkdown">{{ t("grid.exportMarkdown") }}</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
Reference in New Issue
Block a user