feat(export): support safe background query exports
This commit is contained in:
1 parent
2f44d36018
commit
a07def6042
17 files changed
+577
-195
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed, nextTick } from "vue";
|
||||
import { CaseLower, CaseUpper, Code2, FileCode, Pencil, PencilRuler, Play, Copy, List, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
|
||||
import { CaseLower, CaseUpper, Code2, Download, FileCode, Pencil, PencilRuler, Play, Copy, List, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { CompletionContext } from "@codemirror/autocomplete";
|
||||
import { Transaction, StateEffect } from "@codemirror/state";
|
||||
@@ -138,6 +138,7 @@ const emit = defineEmits<{
|
||||
formatError: [message: string];
|
||||
execute: [source: SqlExecutionOverride];
|
||||
executeInNewResultTab: [source: SqlExecutionOverride];
|
||||
exportQuery: [payload: { sql: string; format: "csv" | "xlsx" | "txt" }];
|
||||
save: [];
|
||||
clickTable: [target: SqlObjectNavigationTarget];
|
||||
viewTableData: [target: SqlObjectNavigationTarget];
|
||||
@@ -1000,6 +1001,12 @@ function executeInNewResultTabFromContextMenu() {
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function exportQueryFromContextMenu(format: "csv" | "xlsx" | "txt") {
|
||||
const sql = executableSql.value;
|
||||
if (!sql.trim()) return;
|
||||
emit("exportQuery", { sql, format });
|
||||
}
|
||||
|
||||
async function copySelectedSqlFromContextMenu() {
|
||||
if (!canCopySelectedSql.value) return;
|
||||
try {
|
||||
@@ -1274,6 +1281,16 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
icon: Play,
|
||||
shortcut: shortcuts.executeSqlInNewResultTab,
|
||||
},
|
||||
{
|
||||
label: t("editor.contextMenu.export"),
|
||||
icon: Download,
|
||||
disabled: !canExecuteContextSql.value,
|
||||
children: [
|
||||
{ label: t("editor.contextMenu.exportQueryResultTo", { format: "CSV" }), action: () => exportQueryFromContextMenu("csv") },
|
||||
{ label: t("editor.contextMenu.exportQueryResultTo", { format: "XLSX" }), action: () => exportQueryFromContextMenu("xlsx") },
|
||||
{ label: t("editor.contextMenu.exportQueryResultTo", { format: "TXT" }), action: () => exportQueryFromContextMenu("txt") },
|
||||
],
|
||||
},
|
||||
]),
|
||||
...queryContextObjectActions(contextObjectTarget.value?.type).map(contextObjectMenuItem),
|
||||
{ label: "", separator: true },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, 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 { Loader2, CheckCircle2, XCircle, AlertCircle, FolderOpen, X } from "@lucide/vue";
|
||||
import { Loader2, CheckCircle2, XCircle, AlertCircle, FolderOpen, Minimize2, X } from "@lucide/vue";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import * as api from "@/lib/backend/api";
|
||||
@@ -24,10 +24,12 @@ const props = defineProps<{
|
||||
errorMessage: string | null;
|
||||
filePath?: string | null;
|
||||
disableCancel?: boolean;
|
||||
canMinimize?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: [];
|
||||
minimize: [];
|
||||
"update:open": [value: boolean];
|
||||
}>();
|
||||
|
||||
@@ -123,6 +125,10 @@ async function revealExportFile() {
|
||||
|
||||
<DialogFooter>
|
||||
<template v-if="isActive">
|
||||
<Button v-if="canMinimize" variant="ghost" size="sm" @click="emit('minimize')">
|
||||
<Minimize2 class="h-3.5 w-3.5 mr-1" />
|
||||
{{ t("exportProgress.minimize") }}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" :disabled="disableCancel" @click="emit('cancel')">
|
||||
<X class="h-3.5 w-3.5 mr-1" />
|
||||
{{ t("exportProgress.cancel") }}
|
||||
|
||||
@@ -5121,6 +5121,7 @@ const exportProgressState = ref({
|
||||
filePath: null as string | null,
|
||||
});
|
||||
const exportCancelHandler = ref<(() => Promise<void>) | null>(null);
|
||||
const exportCanMinimize = ref(false);
|
||||
|
||||
async function cancelActiveExport() {
|
||||
await exportCancelHandler.value?.();
|
||||
@@ -5196,6 +5197,7 @@ const {
|
||||
exportProgressDialog,
|
||||
exportProgressState,
|
||||
exportCancelHandler,
|
||||
exportCanMinimize,
|
||||
});
|
||||
|
||||
function copyExtractorLabel(extractor: DataGridCopyExtractorId): string {
|
||||
@@ -9302,7 +9304,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
/>
|
||||
<ImagePreviewDialog v-if="imagePreviewMounted" v-model:open="imagePreviewOpen" :src="imagePreviewSrc" :title="imagePreviewTitle" />
|
||||
<component v-if="previewDialogOpen && previewDialogConfig" :is="previewDialogConfig.component" v-model:open="previewDialogOpen" v-bind="previewDialogConfig.props" />
|
||||
<ExportProgressDialog v-if="exportProgressDialogMounted" v-model:open="exportProgressDialog" v-bind="exportProgressState" :disable-cancel="!exportCancelHandler" @cancel="cancelActiveExport" />
|
||||
<ExportProgressDialog v-if="exportProgressDialogMounted" v-model:open="exportProgressDialog" v-bind="exportProgressState" :disable-cancel="!exportCancelHandler" :can-minimize="exportCanMinimize" @cancel="cancelActiveExport" @minimize="exportProgressDialog = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode, type ResultRunDisplayMode } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, queryResultExecutionSql, resultGridCacheKey, resultRunItems, resultSourceRange, resultSqlForGrid, statementExecutionMarkers, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import { defaultQueryResultArchiveFileName } from "@/lib/query/queryResultArchive";
|
||||
@@ -865,6 +866,20 @@ function requestQueryEditorExecuteInNewResultTab() {
|
||||
return queryEditorRef.value?.requestExecuteInNewResultTab();
|
||||
}
|
||||
|
||||
async function handleExportQuery(payload: { sql: string; format: "csv" | "xlsx" | "txt" }) {
|
||||
const tab = props.activeTab;
|
||||
if (!tab || tab.mode !== "query") return;
|
||||
let filePath = `query-result.${payload.format}`;
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const filterName = payload.format === "csv" ? "CSV" : payload.format === "xlsx" ? "Excel" : "Text";
|
||||
const picked = await save({ defaultPath: filePath, filters: [{ name: filterName, extensions: [payload.format] }] });
|
||||
if (!picked) return;
|
||||
filePath = picked as string;
|
||||
}
|
||||
await queryStore.exportQuerySqlDirect(tab.id, payload.sql, payload.format, filePath);
|
||||
}
|
||||
|
||||
function pasteClipboardAsSqlInCondition() {
|
||||
return queryEditorRef.value?.pasteClipboardAsSqlInCondition();
|
||||
}
|
||||
@@ -932,6 +947,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
@format-error="emit('formatError')"
|
||||
@execute="emit('execute', $event)"
|
||||
@execute-in-new-result-tab="emit('executeInNewResultTab', $event)"
|
||||
@export-query="handleExportQuery"
|
||||
@save="emit('saveSql')"
|
||||
@click-table="onHandleClickTable"
|
||||
@view-table-data="onHandleViewTableData"
|
||||
|
||||
Reference in new issue
Block a user