fc3k(ui): rename + relocate BackupConfirmModal → modal/DestructiveConfirmModal; add tier + projectedCounts props
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="modelValue"
|
||||
max-width="520" persistent
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ titleVerb }}
|
||||
{{ kindLabel }}<span v-if="runId"> #{{ runId }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
:type="alertType"
|
||||
variant="tonal" density="compact" class="mb-3"
|
||||
>
|
||||
<strong>{{ warningText }}</strong>
|
||||
<div v-if="description" class="mt-1">{{ description }}</div>
|
||||
</v-alert>
|
||||
|
||||
<div v-if="projectedCounts" class="fc-counts mb-3">
|
||||
<div v-for="(v, k) in projectedCounts" :key="k">
|
||||
<span class="fc-counts-key">{{ k }}:</span>
|
||||
<span class="fc-counts-val">{{ v }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="tier === 'C'">
|
||||
<div class="text-body-2 mb-2">Type the following to confirm:</div>
|
||||
<div class="fc-token mb-3">{{ expectedToken }}</div>
|
||||
<v-text-field
|
||||
v-model="typed"
|
||||
variant="outlined" density="compact" hide-details
|
||||
autofocus
|
||||
placeholder="paste the token above"
|
||||
/>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="onCancel">Cancel</v-btn>
|
||||
<v-btn
|
||||
:color="confirmColor"
|
||||
variant="flat" rounded="pill"
|
||||
:disabled="!canConfirm"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ titleVerb }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, required: true },
|
||||
action: { type: String, required: true }, // 'restore' | 'delete'
|
||||
kind: { type: String, required: true }, // 'db' | 'images' | 'artist' | 'tag' | 'images-selection'
|
||||
runId: { type: [Number, String], default: '' }, // numeric id or sha8 string
|
||||
description: { type: String, default: '' },
|
||||
tier: { type: String, default: 'C' }, // 'B' | 'C'
|
||||
projectedCounts: { type: Object, default: null },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const typed = ref('')
|
||||
const expectedToken = computed(
|
||||
() => `${props.action}-${props.kind}-${props.runId}`,
|
||||
)
|
||||
const titleVerb = computed(
|
||||
() => props.action === 'restore' ? 'Restore' : 'Delete',
|
||||
)
|
||||
const kindLabel = computed(() => ({
|
||||
db: 'database backup',
|
||||
images: 'images backup',
|
||||
artist: 'artist',
|
||||
tag: 'tag',
|
||||
'images-selection': 'image selection',
|
||||
}[props.kind] || props.kind))
|
||||
const alertType = computed(
|
||||
() => props.action === 'restore' ? 'warning' : 'error',
|
||||
)
|
||||
const confirmColor = computed(
|
||||
() => props.action === 'restore' ? 'warning' : 'error',
|
||||
)
|
||||
const warningText = computed(() => (
|
||||
props.action === 'restore'
|
||||
? 'This replaces current state with the backup. There is no undo.'
|
||||
: 'This permanently deletes the listed items. Cannot be recovered.'
|
||||
))
|
||||
const canConfirm = computed(
|
||||
() => props.tier === 'B'
|
||||
? true
|
||||
: typed.value === expectedToken.value,
|
||||
)
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (open) typed.value = ''
|
||||
})
|
||||
|
||||
function onCancel() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
function onConfirm() {
|
||||
emit('confirm', expectedToken.value)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-token {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: rgb(var(--v-theme-surface-light));
|
||||
padding: 6px 10px; border-radius: 4px;
|
||||
font-size: 14px; word-break: break-all;
|
||||
}
|
||||
.fc-counts {
|
||||
display: grid; grid-template-columns: max-content auto;
|
||||
gap: 4px 12px;
|
||||
font-size: 13px;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
.fc-counts-key { font-weight: 500; text-transform: capitalize; }
|
||||
.fc-counts-val { font-variant-numeric: tabular-nums; }
|
||||
</style>
|
||||
@@ -100,7 +100,7 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useBackupStore } from '../../stores/backup.js'
|
||||
import BackupConfirmModal from './BackupConfirmModal.vue'
|
||||
import BackupConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
||||
import BackupRunsTable from './BackupRunsTable.vue'
|
||||
|
||||
const store = useBackupStore()
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="modelValue"
|
||||
max-width="520" persistent
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ action === 'restore' ? 'Restore' : 'Delete' }}
|
||||
{{ kind === 'db' ? 'database' : 'images' }} backup #{{ runId }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
:type="action === 'restore' ? 'warning' : 'error'"
|
||||
variant="tonal" density="compact" class="mb-3"
|
||||
>
|
||||
<strong>{{ warningText }}</strong>
|
||||
<div class="mt-1">{{ description }}</div>
|
||||
</v-alert>
|
||||
|
||||
<div class="text-body-2 mb-2">
|
||||
Type the following to confirm:
|
||||
</div>
|
||||
<div class="fc-token mb-3">{{ expectedToken }}</div>
|
||||
|
||||
<v-text-field
|
||||
v-model="typed"
|
||||
variant="outlined" density="compact" hide-details
|
||||
autofocus
|
||||
placeholder="paste the token above"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="onCancel">Cancel</v-btn>
|
||||
<v-btn
|
||||
:color="action === 'restore' ? 'warning' : 'error'"
|
||||
variant="flat" rounded="pill"
|
||||
:disabled="typed !== expectedToken"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ action === 'restore' ? 'Restore' : 'Delete' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, required: true },
|
||||
action: { type: String, required: true }, // 'restore' | 'delete'
|
||||
kind: { type: String, required: true }, // 'db' | 'images'
|
||||
runId: { type: Number, required: true },
|
||||
description: { type: String, default: '' },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const typed = ref('')
|
||||
const expectedToken = computed(
|
||||
() => `${props.action}-${props.kind}-${props.runId}`,
|
||||
)
|
||||
const warningText = computed(() => (
|
||||
props.action === 'restore'
|
||||
? 'This replaces current state with the backup. There is no undo.'
|
||||
: 'This deletes the backup record + on-disk files. Cannot be recovered.'
|
||||
))
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (open) typed.value = ''
|
||||
})
|
||||
|
||||
function onCancel() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
function onConfirm() {
|
||||
emit('confirm', expectedToken.value)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-token {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: rgb(var(--v-theme-surface-light));
|
||||
padding: 6px 10px; border-radius: 4px;
|
||||
font-size: 14px; word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user