feat(export): open export folder

This commit is contained in:
zipg 2026-07-15 19:35:18 +08:00 committed by GitHub
parent 1a8c1b19a0
commit b6581d3dc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 53 additions and 3 deletions

View File

@ -1,12 +1,17 @@
<script setup lang="ts">
import { computed } from "vue";
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, X } from "@lucide/vue";
import { Loader2, CheckCircle2, XCircle, AlertCircle, FolderOpen, X } from "@lucide/vue";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import * as api from "@/lib/backend/api";
const { t } = useI18n();
const { toast } = useToast();
const open = defineModel<boolean>("open", { default: false });
const isRevealing = ref(false);
const props = defineProps<{
title: string;
@ -16,6 +21,7 @@ const props = defineProps<{
totalRows: number | null;
status: string;
errorMessage: string | null;
filePath?: string | null;
disableCancel?: boolean;
}>();
@ -26,6 +32,7 @@ const emit = defineEmits<{
const isActive = computed(() => props.status === "Running" || props.status === "Writing");
const isFinished = computed(() => props.status === "Done" || props.status === "Error" || props.status === "Cancelled");
const canRevealFile = computed(() => props.status === "Done" && !!props.filePath && isTauriRuntime());
const progressPercent = computed(() => {
if (!props.totalRows || props.totalRows <= 0) return 0;
return Math.min(100, Math.round((props.rowsExported / props.totalRows) * 100));
@ -39,6 +46,19 @@ const rowsText = computed(() => {
}
return t("exportProgress.rowsExported", { count: props.rowsExported.toLocaleString() });
});
async function revealExportFile() {
if (!props.filePath || isRevealing.value) return;
isRevealing.value = true;
try {
await api.revealPathInFileManager(props.filePath);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
toast(t("exportProgress.openFolderFailed", { message }), 5000);
} finally {
isRevealing.value = false;
}
}
</script>
<template>
@ -108,6 +128,11 @@ const rowsText = computed(() => {
</Button>
</template>
<template v-else-if="isFinished">
<Button v-if="canRevealFile" size="sm" :disabled="isRevealing" @click="revealExportFile">
<Loader2 v-if="isRevealing" class="mr-1 h-3.5 w-3.5 animate-spin" />
<FolderOpen v-else class="mr-1 h-3.5 w-3.5" />
{{ t("exportProgress.openFolder") }}
</Button>
<Button variant="outline" size="sm" @click="emit('update:open', false)">
{{ t("exportProgress.close") }}
</Button>

View File

@ -6591,6 +6591,7 @@ const exportProgressState = ref({
totalRows: null as number | null,
status: "",
errorMessage: null as string | null,
filePath: null as string | null,
});
const exportCancelHandler = ref<(() => Promise<void>) | null>(null);

View File

@ -103,6 +103,7 @@ export interface UseDataGridExportOptions {
totalRows: number | null;
status: string;
errorMessage: string | null;
filePath: string | null;
}>;
exportCancelHandler?: Ref<(() => Promise<void>) | null>;
}
@ -688,6 +689,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
totalRows: null,
status: "Running",
errorMessage: null,
filePath: null,
};
exportProgressDialog.value = true;
}
@ -733,6 +735,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
if (needsFullExport && exportProgressState) {
exportProgressState.value = {
...exportProgressState.value,
filePath: outputPath,
status: "Done",
rowsExported: result.rows.length,
totalRows: result.rows.length,
@ -920,6 +923,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
totalRows: null,
status: "Running",
errorMessage: null,
filePath: outputPath,
};
exportProgressDialog.value = true;
}
@ -1073,6 +1077,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
totalRows: null,
status: "Running",
errorMessage: null,
filePath: outputPath,
};
}
if (exportProgressDialog) exportProgressDialog.value = true;
@ -1160,6 +1165,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
totalRows: request.totalRows ?? null,
status: "Running",
errorMessage: null,
filePath: outputPath,
};
}
if (exportProgressDialog) exportProgressDialog.value = true;

View File

@ -1077,6 +1077,8 @@ export default {
rowsExported: "{count} rows exported",
rowsShort: "rows",
cancel: "Cancel",
openFolder: "Open containing folder",
openFolderFailed: "Failed to open folder: {message}",
close: "Close",
tooltip: "Background Tasks",
failedTooltip: "{count} background task failed | {count} background tasks failed",

View File

@ -1021,6 +1021,8 @@ export default withEnglishFallback({
rowsExported: "{count} filas exportadas",
rowsShort: "filas",
cancel: "Cancelar",
openFolder: "Abrir carpeta contenedora",
openFolderFailed: "No se pudo abrir la carpeta: {message}",
close: "Cerrar",
tooltip: "Progreso de exportación",
popoverTitle: "Exportaciones",

View File

@ -1019,6 +1019,8 @@ export default withEnglishFallback({
rowsExported: "{count} righe esportate",
rowsShort: "righe",
cancel: "Annulla",
openFolder: "Apri cartella contenente",
openFolderFailed: "Impossibile aprire la cartella: {message}",
close: "Chiudi",
tooltip: "Stato Esportazione",
failedTooltip: "{count} attività in background non riuscita | {count} attività in background non riuscite",

View File

@ -1020,6 +1020,8 @@ export default withEnglishFallback({
rowsExported: "{count}行をエクスポートしました",
rowsShort: "行",
cancel: "キャンセル",
openFolder: "保存先フォルダーを開く",
openFolderFailed: "フォルダーを開けませんでした:{message}",
close: "閉じる",
tooltip: "エクスポートの進行状況",
popoverTitle: "エクスポート",

View File

@ -1021,6 +1021,8 @@ export default withEnglishFallback({
rowsExported: "{count} linhas exportadas",
rowsShort: "linhas",
cancel: "Cancelar",
openFolder: "Abrir pasta do arquivo",
openFolderFailed: "Falha ao abrir a pasta: {message}",
close: "Fechar",
tooltip: "Progresso da Exportação",
failedTooltip: "{count} tarefa em segundo plano falhou | {count} tarefas em segundo plano falharam",

View File

@ -1079,6 +1079,8 @@ export default withEnglishFallback({
rowsExported: "已导出 {count} 行",
rowsShort: "行",
cancel: "取消",
openFolder: "打开所在文件夹",
openFolderFailed: "打开文件夹失败:{message}",
close: "关闭",
tooltip: "后台任务",
failedTooltip: "{count} 个后台任务失败",

View File

@ -1021,6 +1021,8 @@ export default withEnglishFallback({
rowsExported: "已匯出 {count} 列",
rowsShort: "列",
cancel: "取消",
openFolder: "開啟所在資料夾",
openFolderFailed: "開啟資料夾失敗:{message}",
close: "關閉",
tooltip: "背景工作",
failedTooltip: "{count} 個背景工作失敗 | {count} 個背景工作失敗",

View File

@ -65,6 +65,7 @@ function buildExportHarness(
totalRows: null as number | null,
status: "",
errorMessage: null as string | null,
filePath: null as string | null,
});
const exportCancelHandler = ref<(() => Promise<void>) | null>(null);
const fullExportResult = vi.fn(async () => {
@ -150,6 +151,7 @@ function buildTableDataExportHarness() {
totalRows: null as number | null,
status: "",
errorMessage: null as string | null,
filePath: null as string | null,
});
const exportCancelHandler = ref<(() => Promise<void>) | null>(null);
@ -626,6 +628,7 @@ test("full query result CSV export streams through the backend without loading a
assert.equal(apiMock.exportQueryResultCsv.mock.calls.length, 0);
assert.equal(exportProgressDialog.value, true);
assert.equal(exportProgressState.value.status, "Done");
assert.equal(exportProgressState.value.filePath, apiMock.startQueryResultExport.mock.calls[0][0].filePath);
});
test("full query result CSV export defaults to the saved SQL title", async () => {
@ -662,11 +665,12 @@ test("table data export keeps the table name as the default file base", async ()
const restoreStorage = installMemoryStorage();
try {
vi.setSystemTime(new Date(2026, 5, 2, 15, 4, 5));
const { composable } = buildTableDataExportHarness();
const { composable, exportProgressState } = buildTableDataExportHarness();
await composable.exportCsv();
assert.equal(apiMock.startTableExport.mock.calls[0][0].filePath, "users_260602150405.csv");
assert.equal(exportProgressState.value.filePath, "users_260602150405.csv");
} finally {
vi.useRealTimers();
restoreStorage();