feat(export): open export folder
This commit is contained in:
parent
1a8c1b19a0
commit
b6581d3dc9
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1020,6 +1020,8 @@ export default withEnglishFallback({
|
|||
rowsExported: "{count}行をエクスポートしました",
|
||||
rowsShort: "行",
|
||||
cancel: "キャンセル",
|
||||
openFolder: "保存先フォルダーを開く",
|
||||
openFolderFailed: "フォルダーを開けませんでした:{message}",
|
||||
close: "閉じる",
|
||||
tooltip: "エクスポートの進行状況",
|
||||
popoverTitle: "エクスポート",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1079,6 +1079,8 @@ export default withEnglishFallback({
|
|||
rowsExported: "已导出 {count} 行",
|
||||
rowsShort: "行",
|
||||
cancel: "取消",
|
||||
openFolder: "打开所在文件夹",
|
||||
openFolderFailed: "打开文件夹失败:{message}",
|
||||
close: "关闭",
|
||||
tooltip: "后台任务",
|
||||
failedTooltip: "{count} 个后台任务失败",
|
||||
|
|
|
|||
|
|
@ -1021,6 +1021,8 @@ export default withEnglishFallback({
|
|||
rowsExported: "已匯出 {count} 列",
|
||||
rowsShort: "列",
|
||||
cancel: "取消",
|
||||
openFolder: "開啟所在資料夾",
|
||||
openFolderFailed: "開啟資料夾失敗:{message}",
|
||||
close: "關閉",
|
||||
tooltip: "背景工作",
|
||||
failedTooltip: "{count} 個背景工作失敗 | {count} 個背景工作失敗",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue