feat(backup): support custom backup file name templates and path preview
This commit is contained in:
31 files changed
+744
-125
No files matched your search
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, nextTick, ref, watch, type ComponentPublicInstance } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { HelpTooltip } from "@/components/ui/tooltip";
|
||||
import { Check, ChevronDown, FolderOpen, Loader2, Search } from "@lucide/vue";
|
||||
import type { DatabaseBackupExecutionConfig } from "@/lib/backup/scheduledDatabaseBackup";
|
||||
import { databaseBackupFileNamePatternIsValid, type DatabaseBackupExecutionConfig } from "@/lib/backup/scheduledDatabaseBackup";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -20,8 +21,11 @@ const props = withDefaults(
|
||||
tablePatternsInput: string;
|
||||
loadingDatabases: boolean;
|
||||
databaseLoadError?: string;
|
||||
runDirectoryPattern?: string;
|
||||
runDirectoryPatternValid?: boolean;
|
||||
outputPathPreview?: string;
|
||||
}>(),
|
||||
{ connections: () => [], selectedDatabases: () => [], databaseOptions: () => [], databaseLoadError: "" },
|
||||
{ connections: () => [], selectedDatabases: () => [], databaseOptions: () => [], databaseLoadError: "", runDirectoryPatternValid: true, outputPathPreview: "" },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -30,12 +34,15 @@ const emit = defineEmits<{
|
||||
toggleDatabase: [database: string];
|
||||
"update:allDatabases": [value: boolean];
|
||||
"update:tablePatternsInput": [value: string];
|
||||
"update:runDirectoryPattern": [value: string];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionPickerOpen = ref(false);
|
||||
const connectionSearch = ref("");
|
||||
const databaseSearch = ref("");
|
||||
const connectionPickerTrigger = ref<ComponentPublicInstance | null>(null);
|
||||
const connectionPickerWidth = ref<number>();
|
||||
|
||||
const selectedConnectionName = computed(() => props.connections.find((connection) => connection.id === props.draft.connectionId)?.name || props.draft.connectionId);
|
||||
const filteredConnections = computed(() => {
|
||||
@@ -48,14 +55,39 @@ const filteredDatabaseOptions = computed(() => {
|
||||
if (!query) return props.databaseOptions;
|
||||
return props.databaseOptions.filter((database) => database.toLocaleLowerCase().includes(query));
|
||||
});
|
||||
const runDirectoryTemplateVariables = computed(() => [
|
||||
{ token: "{schedule}", description: t("databaseBackup.templateVariableSchedule") },
|
||||
{ token: "{date}", description: t("databaseBackup.templateVariableDate") },
|
||||
{ token: "{timestamp}", description: t("databaseBackup.templateVariableTimestamp") },
|
||||
{ token: "{runId}", description: t("databaseBackup.templateVariableRunId") },
|
||||
]);
|
||||
const fileNameTemplateVariables = computed(() => [
|
||||
{ token: "{schedule}", description: t("databaseBackup.templateVariableSchedule") },
|
||||
{ token: "{date}", description: t("databaseBackup.templateVariableDate") },
|
||||
{ token: "{timestamp}", description: t("databaseBackup.templateVariableTimestamp") },
|
||||
{ token: "{database}", description: t("databaseBackup.templateVariableDatabase") },
|
||||
{ token: "{runId}", description: t("databaseBackup.templateVariableRunId") },
|
||||
]);
|
||||
|
||||
function selectConnection(connectionId: string) {
|
||||
emit("changeConnection", connectionId);
|
||||
connectionPickerOpen.value = false;
|
||||
}
|
||||
|
||||
watch(connectionPickerOpen, (open) => {
|
||||
if (!open) connectionSearch.value = "";
|
||||
function syncConnectionPickerWidth() {
|
||||
const element = connectionPickerTrigger.value?.$el;
|
||||
if (!(element instanceof HTMLElement)) return;
|
||||
const width = Math.ceil(element.getBoundingClientRect().width);
|
||||
if (width > 0) connectionPickerWidth.value = width;
|
||||
}
|
||||
|
||||
watch(connectionPickerOpen, async (open) => {
|
||||
if (!open) {
|
||||
connectionSearch.value = "";
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
syncConnectionPickerWidth();
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -67,17 +99,17 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-5 py-1">
|
||||
<div class="backup-config-fields grid gap-5 py-1">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.connection") }}</Label>
|
||||
<Popover v-model:open="connectionPickerOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button data-backup-connection-picker type="button" variant="outline" role="combobox" :aria-expanded="connectionPickerOpen" class="w-full justify-between font-normal">
|
||||
<Button ref="connectionPickerTrigger" data-backup-connection-picker type="button" variant="outline" role="combobox" :aria-expanded="connectionPickerOpen" class="w-full justify-between font-normal" @click="syncConnectionPickerWidth">
|
||||
<span class="truncate">{{ selectedConnectionName }}</span>
|
||||
<ChevronDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" class="w-[var(--reka-popover-trigger-width)] p-1">
|
||||
<PopoverContent align="start" :collision-padding="24" class="w-[var(--reka-popover-trigger-width)] max-w-[calc(100vw-48px)] p-1" :style="connectionPickerWidth ? { width: `${connectionPickerWidth}px` } : undefined">
|
||||
<div class="relative">
|
||||
<Search class="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input v-model="connectionSearch" data-backup-connection-search class="h-9 pl-8" :aria-label="t('databaseBackup.searchConnections')" :placeholder="t('databaseBackup.searchConnections')" />
|
||||
@@ -101,14 +133,59 @@ watch(
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.destination") }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model="draft.destinationDirectory" readonly class="min-w-0" />
|
||||
<Button variant="outline" size="icon" class="shrink-0" :title="t('databaseBackup.selectDestination')" @click="emit('chooseDestination')">
|
||||
<div class="backup-destination-field">
|
||||
<Input v-model="draft.destinationDirectory" readonly :title="draft.destinationDirectory" />
|
||||
<Button variant="outline" size="icon" class="backup-destination-picker" :title="t('databaseBackup.selectDestination')" @click="emit('chooseDestination')">
|
||||
<FolderOpen class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Label>{{ t("databaseBackup.outputPathTemplate") }}</Label>
|
||||
|
||||
<div v-if="runDirectoryPattern !== undefined" class="space-y-2">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Label class="text-sm font-normal">{{ t("databaseBackup.runDirectoryPattern") }}</Label>
|
||||
<HelpTooltip :label="t('databaseBackup.templateVariablesHelp')" side="right" content-class="w-80 max-w-[calc(100vw-32px)]">
|
||||
<div class="space-y-2">
|
||||
<div class="font-medium">{{ t("databaseBackup.templateVariablesTitle") }}</div>
|
||||
<div v-for="variable in runDirectoryTemplateVariables" :key="variable.token" class="flex items-start gap-2">
|
||||
<code class="w-20 shrink-0 font-mono font-medium">{{ variable.token }}</code>
|
||||
<span>{{ variable.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<Input :model-value="runDirectoryPattern" data-backup-run-directory-pattern :aria-invalid="!runDirectoryPatternValid" @update:model-value="(value: string | number) => emit('update:runDirectoryPattern', String(value))" />
|
||||
<p class="text-xs text-muted-foreground">{{ t("databaseBackup.runDirectoryPatternHint") }}</p>
|
||||
<p v-if="!runDirectoryPatternValid" class="text-xs text-destructive">{{ t("databaseBackup.runDirectoryPatternInvalid") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Label class="text-sm font-normal">{{ t("databaseBackup.fileNamePattern") }}</Label>
|
||||
<HelpTooltip :label="t('databaseBackup.templateVariablesHelp')" side="right" content-class="w-80 max-w-[calc(100vw-32px)]">
|
||||
<div class="space-y-2">
|
||||
<div class="font-medium">{{ t("databaseBackup.templateVariablesTitle") }}</div>
|
||||
<div v-for="variable in fileNameTemplateVariables" :key="variable.token" class="flex items-start gap-2">
|
||||
<code class="w-20 shrink-0 font-mono font-medium">{{ variable.token }}</code>
|
||||
<span>{{ variable.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<Input v-model="draft.fileNamePattern" data-backup-file-name-pattern :aria-invalid="!databaseBackupFileNamePatternIsValid(draft.fileNamePattern || '')" />
|
||||
<p class="text-xs text-muted-foreground">{{ t("databaseBackup.fileNamePatternHint") }}</p>
|
||||
<p v-if="!databaseBackupFileNamePatternIsValid(draft.fileNamePattern || '')" class="text-xs text-destructive">{{ t("databaseBackup.fileNamePatternInvalid") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="outputPathPreview" data-backup-output-path-preview class="backup-output-path-preview space-y-1 rounded-md border border-border/70 bg-muted/30 px-3 py-2">
|
||||
<div class="text-xs font-medium text-muted-foreground">{{ t("databaseBackup.outputPathPreview") }}</div>
|
||||
<div class="backup-output-path-value font-mono text-xs text-foreground" :title="outputPathPreview">{{ outputPathPreview }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.compression") }}</Label>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
@@ -176,3 +253,62 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.backup-config-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.backup-config-fields > * {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.backup-destination-field {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.backup-destination-field > [data-slot="input"] {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.backup-destination-picker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.backup-output-path-preview {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.backup-output-path-value {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,9 @@ import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
|
||||
import {
|
||||
DEFAULT_DATABASE_BACKUP_RUN_DIRECTORY_PATTERN,
|
||||
DEFAULT_DATABASE_BACKUP_FILE_NAME_PATTERN,
|
||||
databaseBackupFileNamePatternIsValid,
|
||||
databaseBackupFilePath,
|
||||
databaseBackupRunDirectory,
|
||||
databaseBackupRunDirectoryPatternIsValid,
|
||||
nextDatabaseBackupRunAt,
|
||||
@@ -114,6 +117,7 @@ function newBackupConfig(connectionId = sqlConnections.value[0]?.id ?? ""): Data
|
||||
includeObjects: true,
|
||||
dropTableIfExists: false,
|
||||
outputCompression: "none",
|
||||
fileNamePattern: DEFAULT_DATABASE_BACKUP_FILE_NAME_PATTERN,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,18 +159,34 @@ const canSave = computed(() => {
|
||||
const hasContent = draft.value.includeStructure || draft.value.includeData || draft.value.includeObjects;
|
||||
const hasDatabaseScope = allDatabases.value || selectedDatabases.value.length > 0;
|
||||
const hasTableScope = draft.value.tableFilterMode === "all" || normalizeDatabaseBackupTablePatterns(tablePatternsInput.value).length > 0;
|
||||
return !!draft.value.name.trim() && !!draft.value.connectionId && !!draft.value.destinationDirectory.trim() && databaseBackupRunDirectoryPatternIsValid(draft.value.runDirectoryPattern || "") && hasContent && hasDatabaseScope && hasTableScope && !saving.value && !loadingDatabases.value;
|
||||
return (
|
||||
!!draft.value.name.trim() &&
|
||||
!!draft.value.connectionId &&
|
||||
!!draft.value.destinationDirectory.trim() &&
|
||||
databaseBackupRunDirectoryPatternIsValid(draft.value.runDirectoryPattern || "") &&
|
||||
databaseBackupFileNamePatternIsValid(draft.value.fileNamePattern || "") &&
|
||||
hasContent &&
|
||||
hasDatabaseScope &&
|
||||
hasTableScope &&
|
||||
!saving.value &&
|
||||
!loadingDatabases.value
|
||||
);
|
||||
});
|
||||
const nextRunPreview = computed(() => nextDatabaseBackupRunAt(draft.value, new Date()));
|
||||
const runDirectoryPreview = computed(() => {
|
||||
if (!draft.value.destinationDirectory.trim() || !databaseBackupRunDirectoryPatternIsValid(draft.value.runDirectoryPattern || "")) return "";
|
||||
return databaseBackupRunDirectory(draft.value.destinationDirectory, draft.value.runDirectoryPattern || "", draft.value.name.trim() || t("databaseBackup.defaultScheduleName"), nextRunPreview.value, "preview01");
|
||||
const scheduleOutputPathPreview = computed(() => {
|
||||
if (!draft.value.destinationDirectory.trim() || !databaseBackupRunDirectoryPatternIsValid(draft.value.runDirectoryPattern || "") || !databaseBackupFileNamePatternIsValid(draft.value.fileNamePattern || "")) return "";
|
||||
const directory = databaseBackupRunDirectory(draft.value.destinationDirectory, draft.value.runDirectoryPattern || "", draft.value.name.trim() || t("databaseBackup.defaultScheduleName"), nextRunPreview.value, "preview01");
|
||||
return databaseBackupFilePath(directory, draft.value.name.trim() || t("databaseBackup.defaultScheduleName"), "database", nextRunPreview.value, "preview01", draft.value.outputCompression, draft.value.fileNamePattern);
|
||||
});
|
||||
const oneShotOutputPathPreview = computed(() => {
|
||||
if (!oneShotDraft.value.destinationDirectory.trim() || !databaseBackupFileNamePatternIsValid(oneShotDraft.value.fileNamePattern || "")) return "";
|
||||
return databaseBackupFilePath(oneShotDraft.value.destinationDirectory, t("databaseBackup.oneShotName"), "database", new Date(), "preview01", oneShotDraft.value.outputCompression, oneShotDraft.value.fileNamePattern);
|
||||
});
|
||||
const canStartOneShot = computed(() => {
|
||||
const hasContent = oneShotDraft.value.includeStructure || oneShotDraft.value.includeData || oneShotDraft.value.includeObjects;
|
||||
const hasDatabaseScope = allDatabases.value || selectedDatabases.value.length > 0;
|
||||
const hasTableScope = oneShotDraft.value.tableFilterMode === "all" || normalizeDatabaseBackupTablePatterns(tablePatternsInput.value).length > 0;
|
||||
return !!oneShotDraft.value.connectionId && !!oneShotDraft.value.destinationDirectory.trim() && hasContent && hasDatabaseScope && hasTableScope && !oneShotStarting.value && !loadingDatabases.value;
|
||||
return !!oneShotDraft.value.connectionId && !!oneShotDraft.value.destinationDirectory.trim() && databaseBackupFileNamePatternIsValid(oneShotDraft.value.fileNamePattern || "") && hasContent && hasDatabaseScope && hasTableScope && !oneShotStarting.value && !loadingDatabases.value;
|
||||
});
|
||||
|
||||
function connectionName(connectionId: string): string {
|
||||
@@ -771,12 +791,12 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
|
||||
</div>
|
||||
|
||||
<Dialog v-model:open="scheduleDialogOpen">
|
||||
<DialogContent class="dbx-form-dialog dbx-form-dialog--lg max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
|
||||
<DialogContent class="dbx-backup-dialog dbx-form-dialog dbx-form-dialog--lg max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-x-hidden overflow-y-auto pr-8 [scrollbar-gutter:stable]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ editingScheduleId ? t("databaseBackup.editSchedule") : t("databaseBackup.addSchedule") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="grid gap-5 py-1">
|
||||
<div class="backup-schedule-form grid gap-5 py-1">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.scheduleName") }}</Label>
|
||||
<Input v-model="draft.name" />
|
||||
@@ -791,24 +811,17 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
|
||||
:table-patterns-input="tablePatternsInput"
|
||||
:loading-databases="loadingDatabases"
|
||||
:database-load-error="databaseLoadError"
|
||||
:run-directory-pattern="draft.runDirectoryPattern"
|
||||
:run-directory-pattern-valid="databaseBackupRunDirectoryPatternIsValid(draft.runDirectoryPattern || '')"
|
||||
:output-path-preview="scheduleOutputPathPreview"
|
||||
@change-connection="changeConnection"
|
||||
@choose-destination="chooseDestination"
|
||||
@toggle-database="toggleDatabase"
|
||||
@update:all-databases="setAllDatabases"
|
||||
@update:table-patterns-input="(value: string) => (tablePatternsInput = value)"
|
||||
@update:run-directory-pattern="(value: string) => (draft.runDirectoryPattern = value)"
|
||||
/>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.runDirectoryPattern") }}</Label>
|
||||
<Input v-model="draft.runDirectoryPattern" data-backup-run-directory-pattern :aria-invalid="!databaseBackupRunDirectoryPatternIsValid(draft.runDirectoryPattern || '')" />
|
||||
<p class="text-xs text-muted-foreground">{{ t("databaseBackup.runDirectoryPatternHint") }}</p>
|
||||
<div v-if="runDirectoryPreview" data-backup-run-directory-preview class="space-y-1 rounded-md border border-border/70 bg-muted/30 px-3 py-2">
|
||||
<div class="text-xs font-medium text-muted-foreground">{{ t("databaseBackup.runDirectoryPreview") }}</div>
|
||||
<div class="break-all font-mono text-xs text-foreground">{{ runDirectoryPreview }}</div>
|
||||
</div>
|
||||
<p v-if="!databaseBackupRunDirectoryPatternIsValid(draft.runDirectoryPattern || '')" class="text-xs text-destructive">{{ t("databaseBackup.runDirectoryPatternInvalid") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.frequency") }}</Label>
|
||||
@@ -865,7 +878,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="oneShotDialogOpen">
|
||||
<DialogContent class="dbx-form-dialog dbx-form-dialog--lg max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
|
||||
<DialogContent class="dbx-backup-dialog dbx-form-dialog dbx-form-dialog--lg max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-x-hidden overflow-y-auto pr-8 [scrollbar-gutter:stable]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("databaseBackup.oneShotBackup") }}</DialogTitle>
|
||||
<p class="text-sm text-muted-foreground">{{ t("databaseBackup.oneShotDescription") }}</p>
|
||||
@@ -880,6 +893,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
|
||||
:table-patterns-input="tablePatternsInput"
|
||||
:loading-databases="loadingDatabases"
|
||||
:database-load-error="databaseLoadError"
|
||||
:output-path-preview="oneShotOutputPathPreview"
|
||||
@change-connection="changeConnection"
|
||||
@choose-destination="chooseDestination"
|
||||
@toggle-database="toggleDatabase"
|
||||
@@ -949,3 +963,12 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.backup-schedule-form {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
+3
-3
@@ -437,7 +437,7 @@ describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
|
||||
expect(mocks.listDatabases).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("previews the rendered run directory and updates it when the template changes", async () => {
|
||||
it("previews the rendered output path and updates it when the directory template changes", async () => {
|
||||
mocks.connections.push({ id: "mysql-1", name: "Local MySQL", db_type: "mysql" });
|
||||
await mountSettings();
|
||||
|
||||
@@ -446,7 +446,7 @@ describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
|
||||
buttonWithTitle(String(i18n.global.t("databaseBackup.selectDestination"))).click();
|
||||
await flush();
|
||||
|
||||
const preview = currentDialog().querySelector<HTMLElement>("[data-backup-run-directory-preview]");
|
||||
const preview = currentDialog().querySelector<HTMLElement>("[data-backup-output-path-preview]");
|
||||
expect(preview?.textContent).toContain("/backups/dbx-backup__");
|
||||
expect(preview?.textContent).toContain("preview0");
|
||||
|
||||
@@ -457,7 +457,7 @@ describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
|
||||
await flush();
|
||||
|
||||
expect(currentDialog().textContent).toContain("{timestamp}");
|
||||
expect(currentDialog().querySelector<HTMLElement>("[data-backup-run-directory-preview]")?.textContent).toContain("/backups/archive/");
|
||||
expect(currentDialog().querySelector<HTMLElement>("[data-backup-output-path-preview]")?.textContent).toContain("/backups/archive/");
|
||||
});
|
||||
|
||||
it("opens an independent one-shot dialog without schedule fields", async () => {
|
||||
|
||||
Reference in new issue
Block a user