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>
96 lines
2.9 KiB
Vue
96 lines
2.9 KiB
Vue
<template>
|
|
<v-card class="fc-post-maint">
|
|
<CardHeading icon="mdi-post-outline" title="Post maintenance" />
|
|
<v-card-text>
|
|
<p class="fc-muted text-body-2 mb-4">
|
|
Remove <strong>bare posts</strong> — post rows with no images (neither
|
|
their own nor a cross-posted duplicate) and no attachments. These are the
|
|
empty “Post 12345 / (no description)” shells a backfill can leave when a
|
|
post's only content was a duplicate that links to an earlier post. Posts
|
|
that gained a duplicate-image link are spared.
|
|
</p>
|
|
|
|
<v-alert
|
|
v-if="store.lastError"
|
|
type="warning" variant="tonal" density="compact" class="mb-3"
|
|
>{{ store.lastError }}</v-alert>
|
|
|
|
<v-btn
|
|
color="accent" variant="flat" rounded="pill"
|
|
prepend-icon="mdi-magnify"
|
|
:loading="loadingPreview"
|
|
class="mb-3"
|
|
@click="onPreview"
|
|
>Preview bare posts</v-btn>
|
|
|
|
<div v-if="preview">
|
|
<p class="text-body-2 mb-2">
|
|
<strong>{{ preview.count }}</strong> bare post(s).
|
|
<span v-if="preview.count > 50" class="fc-muted">
|
|
Showing first 50.
|
|
</span>
|
|
<span v-else-if="!preview.count" class="fc-muted">
|
|
Nothing to clean up.
|
|
</span>
|
|
</p>
|
|
<SampleNameGrid
|
|
v-if="preview.sample_names?.length"
|
|
:names="preview.sample_names" class="mb-3"
|
|
/>
|
|
<v-btn
|
|
color="error" variant="flat" rounded="pill"
|
|
prepend-icon="mdi-delete-sweep"
|
|
:disabled="!preview.count"
|
|
:loading="committing"
|
|
@click="onCommit"
|
|
>Delete {{ preview.count }} bare post(s)</v-btn>
|
|
<span
|
|
v-if="deleted != null"
|
|
class="ml-3 text-caption text-success"
|
|
>Deleted {{ deleted }} ✓</span>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
|
|
import { useAdminStore } from '../../stores/admin.js'
|
|
import SampleNameGrid from '../common/SampleNameGrid.vue'
|
|
import CardHeading from '../common/CardHeading.vue'
|
|
|
|
const store = useAdminStore()
|
|
const preview = ref(null)
|
|
const loadingPreview = ref(false)
|
|
const committing = ref(false)
|
|
const deleted = ref(null)
|
|
|
|
async function onPreview() {
|
|
loadingPreview.value = true
|
|
deleted.value = null
|
|
try {
|
|
preview.value = await store.pruneBarePosts({ dryRun: true })
|
|
} finally {
|
|
loadingPreview.value = false
|
|
}
|
|
}
|
|
|
|
async function onCommit() {
|
|
committing.value = true
|
|
try {
|
|
const result = await store.pruneBarePosts({ dryRun: false })
|
|
deleted.value = result.deleted ?? 0
|
|
// Reflect the completed sweep — the predicate is identical to the preview,
|
|
// so after a commit there is nothing left to delete.
|
|
preview.value = { count: 0, sample_names: [] }
|
|
} finally {
|
|
committing.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-post-maint { border-radius: 8px; }
|
|
</style>
|