Files
FabledCurator/frontend/src/components/settings/PostMaintenanceCard.vue
T
bvandeusen 4854d74c5a
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m9s
refactor(ui): SampleNameGrid primitive for maintenance-card previews (DRY pattern sweep)
The preview sample-name grid (scrollable monospace chip grid) was hand-rolled
5 times with verbatim-duplicated markup + CSS — TagMaintenanceCard (×4) and
PostMaintenanceCard. Consolidate to <SampleNameGrid> (components/common): pass
:names for the plain case, default slot for the normalize from→to chips
(styled via :slotted .fc-name). Removed the duplicated .fc-name-grid/.fc-name
CSS from both cards.

Over-DRY guard: only the verbatim-duplicated grid is merged — each card's
preview/commit logic and result-count lines genuinely differ and stay put;
MinDimensionCard's typed-token confirm is a separate variant, untouched.
§8b: fc-name-grid now lives only in SampleNameGrid. Catalog updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:08:15 -04:00

98 lines
2.9 KiB
Vue

<template>
<v-card class="fc-post-maint">
<v-card-title class="d-flex align-center" style="gap: 10px;">
<v-icon icon="mdi-post-outline" size="small" />
<span>Post maintenance</span>
</v-card-title>
<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'
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>