9deebfa133
The icon+title v-card-title heading (d-flex align-center + gap + <v-icon size=small> + <span>) was hand-rolled identically in 13 cards/dialogs (15 heading instances). Consolidate to <CardHeading icon title> (components/common) with an iconColor prop (error headings) and a default slot for trailing content (spacer+actions, inline status chip). Adopted everywhere the pattern appears — all-or-nothing per the hardened DRY process. Over-DRY guard: plain text-only <v-card-title> one-liners are NOT this pattern and stay; DownloadDetailModal leads with a status CHIP (not an icon), a different concept, left alone. §8b: the only remaining d-flex align-center v-card-title is that intentional variant. Catalog updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
2.5 KiB
Vue
93 lines
2.5 KiB
Vue
<template>
|
|
<v-card class="fc-danger-zone mt-8" variant="outlined">
|
|
<CardHeading icon="mdi-alert-octagon" icon-color="error" title="Danger zone" />
|
|
<v-card-text>
|
|
<p class="text-body-2 fc-muted mb-4">
|
|
Cascade-delete this artist and every image, source, post, and
|
|
attachment associated with them. This cannot be undone.
|
|
Recoverable only from an FC-3h backup.
|
|
</p>
|
|
<v-btn
|
|
color="error" variant="flat" rounded="pill"
|
|
prepend-icon="mdi-delete-forever"
|
|
:loading="loading"
|
|
@click="onClick"
|
|
>Delete artist & cascade</v-btn>
|
|
|
|
<DestructiveConfirmModal
|
|
v-model="modalOpen"
|
|
action="delete"
|
|
kind="artist"
|
|
:run-id="artistId"
|
|
tier="C"
|
|
:projected-counts="projectedCounts"
|
|
:description="modalDescription"
|
|
@confirm="onConfirm"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
|
|
import DestructiveConfirmModal from '../modal/DestructiveConfirmModal.vue'
|
|
import { useAdminStore } from '../../stores/admin.js'
|
|
import CardHeading from '../common/CardHeading.vue'
|
|
|
|
const props = defineProps({
|
|
slug: { type: String, required: true },
|
|
artistId: { type: Number, required: true },
|
|
artistName: { type: String, required: true },
|
|
})
|
|
|
|
const router = useRouter()
|
|
const store = useAdminStore()
|
|
const loading = ref(false)
|
|
const modalOpen = ref(false)
|
|
const projected = ref(null)
|
|
|
|
const projectedCounts = computed(() => projected.value?.projected || null)
|
|
|
|
const modalDescription = computed(
|
|
() => projected.value
|
|
? `Artist “${props.artistName}” — `
|
|
+ `${projected.value.projected.images} images, `
|
|
+ `${projected.value.projected.sources} sources, `
|
|
+ `${Math.round(projected.value.projected.bytes_on_disk / 1_048_576)} MiB on disk`
|
|
: '',
|
|
)
|
|
|
|
async function onClick() {
|
|
loading.value = true
|
|
try {
|
|
projected.value = await store.projectArtistCascade(props.slug)
|
|
modalOpen.value = true
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function onConfirm(token) {
|
|
loading.value = true
|
|
try {
|
|
const result = await store.dispatchArtistCascade(props.slug, token)
|
|
const taskId = result.task_id
|
|
router.push('/artists')
|
|
if (taskId) {
|
|
store.pollTaskUntilDone(taskId).catch(() => {})
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-danger-zone {
|
|
border-color: rgb(var(--v-theme-error));
|
|
border-radius: 8px;
|
|
}
|
|
</style>
|