feat(desktop): improve query result exports and toolbar state

This commit is contained in:
t8y2
2026-06-01 19:26:15 +08:00
parent 302f48ec8b
commit b35c1486e6
9 changed files with 141 additions and 5 deletions
@@ -2270,6 +2270,10 @@ function exportSelectedRowsMarkdown() {
return exportMarkdown(affectedRowIds());
}
function exportSelectedRowsSql() {
return exportSql(affectedRowIds());
}
function isRowActive(index: number): boolean {
const item = displayItems.value[index];
if (item && isRowSelected(item.id)) return true;
@@ -2930,6 +2934,7 @@ const {
exportJson,
exportMarkdown,
exportXlsx,
exportSql,
copySql,
} = useDataGridExport({
columns: visibleColumns,
@@ -2960,12 +2965,14 @@ const exportMenuItems = computed(() => [
{ value: "xlsx", label: t("grid.exportXlsx") },
{ value: "json", label: t("grid.exportJson") },
{ value: "markdown", label: t("grid.exportMarkdown") },
{ value: "sql", label: t("grid.exportSql") },
...(isMultiRow.value
? [
{ value: "selected-csv", label: t("grid.exportSelectedRowsCsv"), separatorBefore: true },
{ value: "selected-xlsx", label: t("grid.exportSelectedRowsXlsx") },
{ value: "selected-json", label: t("grid.exportSelectedRowsJson") },
{ value: "selected-markdown", label: t("grid.exportSelectedRowsMarkdown") },
{ value: "selected-sql", label: t("grid.exportSelectedRowsSql") },
]
: []),
]);
@@ -2980,10 +2987,12 @@ function selectExportMenuItem(value: string) {
xlsx: exportXlsx,
json: exportJson,
markdown: exportMarkdown,
sql: exportSql,
"selected-csv": exportSelectedRowsCsv,
"selected-xlsx": exportSelectedRowsXlsx,
"selected-json": exportSelectedRowsJson,
"selected-markdown": exportSelectedRowsMarkdown,
"selected-sql": exportSelectedRowsSql,
};
actions[value]?.();
}
@@ -4425,6 +4434,7 @@ function exportSubmenu(): ContextMenuItem {
{ label: t("grid.exportXlsx"), action: exportXlsx },
{ label: t("grid.exportJson"), action: exportJson },
{ label: t("grid.exportMarkdown"), action: exportMarkdown },
{ label: t("grid.exportSql"), action: exportSql },
];
if (isMultiRow.value) {
items.push(
@@ -4433,6 +4443,7 @@ function exportSubmenu(): ContextMenuItem {
{ label: t("grid.exportSelectedRowsXlsx"), action: exportSelectedRowsXlsx },
{ label: t("grid.exportSelectedRowsJson"), action: exportSelectedRowsJson },
{ label: t("grid.exportSelectedRowsMarkdown"), action: exportSelectedRowsMarkdown },
{ label: t("grid.exportSelectedRowsSql"), action: exportSelectedRowsSql },
);
}
return { label: t("grid.export"), icon: FileDown, children: items };
@@ -4911,6 +4922,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</TooltipContent>
</Tooltip>
<Button
v-if="props.context !== 'results'"
variant="ghost"
size="sm"
class="h-5 text-xs px-1.5 shrink-0"
@@ -413,9 +413,22 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
{{ t("chart.title") }}
</Button>
<Button
v-if="activeOutputView === 'result' && activeTab.result"
variant="ghost"
size="sm"
class="ml-auto h-6 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
:disabled="activeTab.isExecuting"
@click="refreshData"
>
<Loader2 v-if="activeTab.isExecuting" class="h-3.5 w-3.5 animate-spin" />
<RefreshCcw v-else class="h-3.5 w-3.5" />
{{ t("grid.refresh") }}
</Button>
<Button
variant="ghost"
size="sm"
class="h-6 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
:class="{ 'ml-auto': activeOutputView !== 'result' || !activeTab.result }"
@click="resultsPaneOpen = false"
>
<ChevronDown class="h-3.5 w-3.5" />
@@ -15,6 +15,7 @@ import { displayCellValue, type CellValue } from "@/lib/cellValue";
import { tryStartExclusiveActivation, type ActionActivationGuard } from "@/lib/actionActivation";
import { copyToClipboard } from "@/lib/clipboard";
import { buildDataGridCopyInsertStatement, buildDataGridCopyUpdateStatements } from "@/lib/dataGridSql";
import { formatSqlInsert } from "@/lib/exportFormats";
import type { DatabaseType, QueryResult } from "@/types/database";
interface RowItem {
@@ -531,11 +532,45 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
});
}
async function exportSql(rowIds?: number[]) {
await runExclusiveExport(async () => {
try {
const result = await resultToExport(rowIds);
const exportData = sqlInsertExportData(result);
const content = await formatSqlInsert({
databaseType: databaseType.value,
schema: tableMeta.value?.schema,
tableName: tableMeta.value?.tableName || "table_name",
columns: exportData.columns,
rows: exportData.rows,
});
await saveTextFile(content, `${tableMeta.value?.tableName || "export"}.sql`, "SQL", "sql");
toast(t("grid.exported"));
} catch (e: any) {
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
}
});
}
async function copySql() {
if (!sql.value) return;
await copyText(sql.value);
}
function sqlInsertExportData(result: { columns: string[]; rows: CellValue[][] }): {
columns: string[];
rows: CellValue[][];
} {
const exportColumns = tableMeta.value ? effectiveColumns(sourceColumns.value, result.columns) : result.columns;
const columnIndexes = exportColumns
.map((column, index) => ({ column, index }))
.filter((item): item is { column: string; index: number } => !!item.column);
return {
columns: columnIndexes.map((item) => item.column),
rows: result.rows.map((row) => columnIndexes.map((item) => row[item.index] ?? null)),
};
}
return {
copyText,
copyCell,
@@ -561,10 +596,32 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
exportJson,
exportMarkdown,
exportXlsx,
exportSql,
copySql,
};
}
async function saveTextFile(content: string, defaultFileName: string, filterName: string, filterExt: string) {
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) await writeTextFile(path, content);
return;
}
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = defaultFileName;
a.click();
URL.revokeObjectURL(url);
}
function effectiveColumns(
sourceColumns: Array<string | undefined> | undefined,
columns: string[],
+2
View File
@@ -424,10 +424,12 @@ export default {
exportXlsx: "Export XLSX",
exportJson: "Export JSON",
exportMarkdown: "Export Markdown",
exportSql: "Export SQL INSERT",
exportSelectedRowsCsv: "Export Selected Rows as CSV",
exportSelectedRowsXlsx: "Export Selected Rows as XLSX",
exportSelectedRowsJson: "Export Selected Rows as JSON",
exportSelectedRowsMarkdown: "Export Selected Rows as Markdown",
exportSelectedRowsSql: "Export Selected Rows as SQL INSERT",
exported: "Exported",
exportFailed: "Export failed: {message}",
copied: "Copied",
+2
View File
@@ -422,10 +422,12 @@ export default {
exportXlsx: "Exportar XLSX",
exportJson: "Exportar JSON",
exportMarkdown: "Exportar Markdown",
exportSql: "Exportar SQL INSERT",
exportSelectedRowsCsv: "Exportar filas seleccionadas como CSV",
exportSelectedRowsXlsx: "Exportar filas seleccionadas como XLSX",
exportSelectedRowsJson: "Exportar filas seleccionadas como JSON",
exportSelectedRowsMarkdown: "Exportar filas seleccionadas como Markdown",
exportSelectedRowsSql: "Exportar filas seleccionadas como SQL INSERT",
exported: "Exportado",
exportFailed: "Error al exportar: {message}",
copied: "Copiado",
+2
View File
@@ -420,10 +420,12 @@ export default {
exportXlsx: "导出 XLSX",
exportJson: "导出 JSON",
exportMarkdown: "导出 Markdown",
exportSql: "导出 SQL INSERT",
exportSelectedRowsCsv: "导出选中行为 CSV",
exportSelectedRowsXlsx: "导出选中行为 XLSX",
exportSelectedRowsJson: "导出选中行为 JSON",
exportSelectedRowsMarkdown: "导出选中行为 Markdown",
exportSelectedRowsSql: "导出选中行为 SQL INSERT",
exported: "导出完成",
exportFailed: "导出失败:{message}",
copied: "已复制",
+2
View File
@@ -420,10 +420,12 @@ export default {
exportXlsx: "匯出 XLSX",
exportJson: "匯出 JSON",
exportMarkdown: "匯出 Markdown",
exportSql: "匯出 SQL INSERT",
exportSelectedRowsCsv: "匯出選取資料列為 CSV",
exportSelectedRowsXlsx: "匯出選取資料列為 XLSX",
exportSelectedRowsJson: "匯出選取資料列為 JSON",
exportSelectedRowsMarkdown: "匯出選取資料列為 Markdown",
exportSelectedRowsSql: "匯出選取資料列為 SQL INSERT",
exported: "匯出完成",
exportFailed: "匯出失敗:{message}",
copied: "已複製",
-5
View File
@@ -307,11 +307,6 @@ export const useQueryStore = defineStore("query", () => {
const tab = tabs.value.find((t) => t.id === id);
if (tab) {
tab.sql = sql;
tab.resultSortedSql = undefined;
tab.resultBaseSql = undefined;
tab.queryAnalysis = undefined;
tab.querySourceColumns = undefined;
tab.queryEditabilityReason = undefined;
}
}
+51
View File
@@ -64,6 +64,57 @@ test("renames query tab titles", () => {
assert.equal(tab?.customTitle, true);
});
test("editing query sql preserves the displayed result editability state", () => {
setActivePinia(createPinia());
const store = useQueryStore();
const tabId = store.createTab("conn-1", "db");
const tab = store.tabs.find((item) => item.id === tabId);
assert.ok(tab);
tab.sql = "select id, name from users";
tab.lastExecutedSql = tab.sql;
tab.resultBaseSql = tab.sql;
tab.resultSortedSql = "select id, name from users order by name";
tab.result = {
columns: ["id", "name"],
rows: [[1, "Ada"]],
affected_rows: 0,
execution_time_ms: 1,
};
tab.tableMeta = {
tableName: "users",
columns: [
{
name: "id",
data_type: "integer",
is_nullable: false,
column_default: null,
is_primary_key: true,
extra: null,
},
],
primaryKeys: ["id"],
};
tab.queryAnalysis = {
tableName: "users",
selectStar: false,
columns: [
{ sourceName: "id", resultName: "id", expression: "id" },
{ sourceName: "name", resultName: "name", expression: "name" },
],
};
tab.querySourceColumns = ["id", "name"];
store.updateSql(tabId, "select id, name from users where active = true");
assert.equal(tab.sql, "select id, name from users where active = true");
assert.equal(tab.resultBaseSql, "select id, name from users");
assert.equal(tab.resultSortedSql, "select id, name from users order by name");
assert.deepEqual(tab.querySourceColumns, ["id", "name"]);
assert.equal(tab.queryAnalysis?.tableName, "users");
assert.equal(tab.tableMeta?.tableName, "users");
});
test("evicting cached tab results releases multi-result payloads and sessions", async () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());