feat(fc2b): wire SuggestionsPanel + rename into TagPanel; reject-on-remove

TagPanel now renders SuggestionsPanel below the current-tags area and
adds a per-chip kebab → Rename… (TagRenameDialog). modal store's
removeTag now also POSTs suggestions/dismiss after the delete so removing
an auto-applied tag records a per-image rejection and the allowlist
maintenance sweep won't re-apply it (closes the remove→re-apply loop
identified in the spec).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 07:56:22 -04:00
parent 5ae2154e88
commit 2a21ede912
2 changed files with 47 additions and 0 deletions
@@ -10,6 +10,19 @@
> >
<v-icon start size="x-small">{{ iconFor(tag.kind) }}</v-icon> <v-icon start size="x-small">{{ iconFor(tag.kind) }}</v-icon>
{{ tag.name }}<span v-if="tag.fandom_id"></span> {{ tag.name }}<span v-if="tag.fandom_id"></span>
<v-menu>
<template #activator="{ props: mp }">
<v-icon
v-bind="mp" size="x-small" class="ml-1"
icon="mdi-dots-vertical" @click.stop
/>
</template>
<v-list density="compact">
<v-list-item @click="openRename(tag)">
<v-list-item-title>Rename</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-chip> </v-chip>
<span v-if="!modal.current?.tags?.length" class="text-caption">No tags yet.</span> <span v-if="!modal.current?.tags?.length" class="text-caption">No tags yet.</span>
</div> </div>
@@ -23,6 +36,19 @@
<v-alert v-if="errorMsg" type="error" variant="tonal" class="mt-2" closable> <v-alert v-if="errorMsg" type="error" variant="tonal" class="mt-2" closable>
{{ errorMsg }} {{ errorMsg }}
</v-alert> </v-alert>
<SuggestionsPanel
v-if="modal.currentImageId != null"
:image-id="modal.currentImageId"
/>
<v-dialog v-model="renameDialog" max-width="420">
<TagRenameDialog
v-if="renameTarget"
:tag="renameTarget"
@renamed="onRenamed" @cancel="renameDialog = false"
/>
</v-dialog>
</aside> </aside>
</template> </template>
@@ -31,6 +57,8 @@ import { ref } from 'vue'
import { useModalStore } from '../../stores/modal.js' import { useModalStore } from '../../stores/modal.js'
import { useTagStore } from '../../stores/tags.js' import { useTagStore } from '../../stores/tags.js'
import TagAutocomplete from './TagAutocomplete.vue' import TagAutocomplete from './TagAutocomplete.vue'
import SuggestionsPanel from './SuggestionsPanel.vue'
import TagRenameDialog from './TagRenameDialog.vue'
const modal = useModalStore() const modal = useModalStore()
const store = useTagStore() const store = useTagStore()
@@ -58,6 +86,19 @@ async function onPickNew(payload) {
try { await modal.createAndAdd(payload) } try { await modal.createAndAdd(payload) }
catch (e) { errorMsg.value = e.message } catch (e) { errorMsg.value = e.message }
} }
const renameDialog = ref(false)
const renameTarget = ref(null)
function openRename(tag) {
renameTarget.value = tag
renameDialog.value = true
}
async function onRenamed() {
renameDialog.value = false
// Reflect the new name in the modal's current tag list without a full reload.
await modal.reloadTags()
}
</script> </script>
<style scoped> <style scoped>
+6
View File
@@ -53,7 +53,13 @@ export const useModalStore = defineStore('modal', () => {
const prev = current.value.tags const prev = current.value.tags
current.value.tags = current.value.tags.filter(t => t.id !== tagId) current.value.tags = current.value.tags.filter(t => t.id !== tagId)
try { try {
// FC-2b: removal also records a per-image rejection (suggestions/dismiss
// is the rejection-recording endpoint) so the allowlist maintenance
// task won't re-apply this tag to this image.
await api.delete(`/api/images/${currentImageId.value}/tags/${tagId}`) await api.delete(`/api/images/${currentImageId.value}/tags/${tagId}`)
await api.post(`/api/images/${currentImageId.value}/suggestions/dismiss`, {
body: { tag_id: tagId }
})
} catch (e) { } catch (e) {
current.value.tags = prev current.value.tags = prev
window.__fcToast?.({ text: `Failed to remove tag: ${e.message}`, type: 'error' }) window.__fcToast?.({ text: `Failed to remove tag: ${e.message}`, type: 'error' })