fix(tags): rename-onto-existing in the image modal now merges, not errors
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 26s
CI / frontend-build (push) Successful in 32s
CI / integration (push) Successful in 3m7s

The image-modal tag kebab's rename dialog still showed a leftover stub
('Merging two tags into one lands in FC-2c') on a name collision, dead-ending
the operator. The merge machinery has existed for a while — the Tags view
already resolves rename collisions this way. Wire TagRenameDialog to it: on the
409 collision hint, show the same merge confirmation FandomSetDialog uses
(target name, image associations moved, alias kept) and POST /api/tags/<id>/merge
into the existing tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 20:56:45 -04:00
parent 7daf90f41e
commit 9770dd3474
@@ -2,25 +2,55 @@
<v-card> <v-card>
<v-card-title>Rename tag</v-card-title> <v-card-title>Rename tag</v-card-title>
<v-card-text> <v-card-text>
<v-text-field <template v-if="!collision">
v-model="newName" label="New name" density="compact" <v-text-field
autofocus @keydown.enter="submit" v-model="newName" label="New name" density="compact"
/> autofocus @keydown.enter="submit"
<v-alert v-if="errorMsg" type="error" variant="tonal" density="compact" class="mt-2"> />
{{ errorMsg }} <v-alert
<div v-if="isCollision" class="text-caption mt-1"> v-if="errorMsg" type="error" variant="tonal" density="compact"
Merging two tags into one lands in FC-2c. class="mt-2"
</div> >{{ errorMsg }}</v-alert>
</v-alert> </template>
<template v-else>
<!-- A tag of this (kind, fandom) already has that name. Renaming onto it
is a merge, not a fork same resolution the Tags view offers. -->
<v-alert type="warning" variant="tonal" density="compact" class="mb-3">
A {{ tag.kind }} tag named {{ collision.target.name }} already exists.
</v-alert>
<p class="text-body-2">
Merge {{ tag.name }} into {{ collision.target.name }}?
{{ collision.source_image_count }} image
association{{ collision.source_image_count === 1 ? '' : 's' }}
will move over and this tag will be deleted{{
collision.will_alias ? ' (its name kept as a tagger alias)' : '' }}.
</p>
<v-alert
v-if="errorMsg" type="error" variant="tonal" density="compact"
class="mt-3"
>{{ errorMsg }}</v-alert>
</template>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="$emit('cancel')">Cancel</v-btn> <template v-if="!collision">
<v-btn <v-btn :disabled="busy" @click="$emit('cancel')">Cancel</v-btn>
color="primary" rounded="pill" <v-btn
:disabled="!newName.trim() || newName === tag.name" color="primary" rounded="pill"
:loading="busy" @click="submit" :disabled="!newName.trim() || newName === tag.name"
>Rename</v-btn> :loading="busy" @click="submit"
>Rename</v-btn>
</template>
<template v-else>
<v-btn :disabled="busy" @click="collision = null; errorMsg = null">
Back
</v-btn>
<v-btn
color="warning" variant="flat" rounded="pill"
:loading="busy" @click="onMerge"
>Merge</v-btn>
</template>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>
@@ -35,22 +65,41 @@ const emit = defineEmits(['renamed', 'cancel'])
const api = useApi() const api = useApi()
const newName = ref(props.tag.name) const newName = ref(props.tag.name)
const errorMsg = ref(null) const errorMsg = ref(null)
const isCollision = ref(false) const collision = ref(null)
const busy = ref(false) const busy = ref(false)
async function submit() { async function submit() {
if (!newName.value.trim() || newName.value === props.tag.name) return if (!newName.value.trim() || newName.value === props.tag.name) return
busy.value = true busy.value = true
errorMsg.value = null errorMsg.value = null
isCollision.value = false
try { try {
const updated = await api.patch(`/api/tags/${props.tag.id}`, { const updated = await api.patch(`/api/tags/${props.tag.id}`, {
body: { name: newName.value.trim() } body: { name: newName.value.trim() }
}) })
emit('renamed', updated) emit('renamed', updated)
} catch (e) {
// 409 → the new name collides with an existing tag of the same
// (kind, fandom). Offer to merge into it rather than dead-ending.
if (e.status === 409 && e.body && e.body.target) {
collision.value = e.body
} else {
errorMsg.value = e.message
}
} finally {
busy.value = false
}
}
async function onMerge() {
busy.value = true
errorMsg.value = null
try {
await api.post(`/api/tags/${props.tag.id}/merge`, {
body: { target_id: collision.value.target.id }
})
emit('renamed', { id: collision.value.target.id, name: collision.value.target.name })
} catch (e) { } catch (e) {
errorMsg.value = e.message errorMsg.value = e.message
isCollision.value = e.status === 409
} finally { } finally {
busy.value = false busy.value = false
} }