Files
FabledCurator/frontend/src/components/modal/FandomSetDialog.vue
T
bvandeusen e678d1dfdf
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 13s
CI / frontend-build (push) Successful in 26s
CI / intimp (push) Successful in 3m47s
CI / intapi (push) Successful in 7m52s
CI / intcore (push) Successful in 8m58s
feat(tags): fandom-edit UI in tags directory + image modal
Adds the missing UI to change a character tag's fandom, in both places:

- FandomSetDialog (shared): pick an existing fandom, create a new one, or
  clear it; on a name collision in the target fandom it surfaces a merge
  confirmation and resolves via setFandom(merge:true). Reuses the tags
  store's fandom cache.
- TagCard kebab gains "Set fandom…" for character tags (→ TagsView opens
  the dialog, reloads on success).
- TagPanel chip kebab gains "Set fandom…" for character tags (→ reloads the
  modal's tag list on success).
- tags store: setFandom(tagId, fandomId, {merge}) action + test.

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

129 lines
3.9 KiB
Vue

<template>
<v-card>
<v-card-title class="text-body-1">Fandom for {{ tag.name }}</v-card-title>
<v-card-text>
<template v-if="!collision">
<v-autocomplete
v-model="selectedId"
:items="store.fandomCache"
:item-title="(f) => f.name" :item-value="(f) => f.id"
label="Fandom" clearable density="compact"
:hint="selectedId == null
? 'No fandom — the character will be unassigned.' : ''"
persistent-hint
/>
<v-divider class="my-3" />
<p class="text-caption mb-2">Or create a new fandom:</p>
<div class="d-flex" style="gap: 8px;">
<v-text-field
v-model="newName" placeholder="New fandom name"
density="compact" hide-details
@keydown.enter.prevent="onCreate"
/>
<v-btn
:disabled="!newName.trim() || busy" rounded="pill"
@click="onCreate"
>Create</v-btn>
</div>
<v-alert
v-if="error" type="error" variant="tonal" density="compact"
class="mt-3"
>{{ error }}</v-alert>
</template>
<template v-else>
<v-alert type="warning" variant="tonal" density="compact" class="mb-3">
A character named “{{ tag.name }}” already exists in that fandom.
</v-alert>
<p class="text-body-2">
Merge this tag 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>
</template>
</v-card-text>
<v-card-actions>
<v-spacer />
<template v-if="!collision">
<v-btn variant="text" :disabled="busy" @click="$emit('cancel')">
Cancel
</v-btn>
<v-btn
color="primary" rounded="pill" :loading="busy"
:disabled="selectedId === (tag.fandom_id ?? null)"
@click="onSave"
>Save</v-btn>
</template>
<template v-else>
<v-btn variant="text" :disabled="busy" @click="collision = null">
Back
</v-btn>
<v-btn
color="warning" variant="flat" rounded="pill" :loading="busy"
@click="onConfirmMerge"
>Merge</v-btn>
</template>
</v-card-actions>
</v-card>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useTagStore } from '../../stores/tags.js'
const props = defineProps({ tag: { type: Object, required: true } })
const emit = defineEmits(['updated', 'cancel'])
const store = useTagStore()
const selectedId = ref(props.tag.fandom_id ?? null)
const newName = ref('')
const busy = ref(false)
const error = ref(null)
const collision = ref(null)
onMounted(() => { if (store.fandomCache.length === 0) store.loadFandoms() })
async function onCreate() {
const name = newName.value.trim()
if (!name) return
busy.value = true
error.value = null
try {
const f = await store.createFandom(name)
selectedId.value = f.id
newName.value = ''
} catch (e) {
error.value = e.message || String(e)
} finally {
busy.value = false
}
}
async function save(merge) {
busy.value = true
error.value = null
try {
const body = await store.setFandom(props.tag.id, selectedId.value, { merge })
emit('updated', body)
} catch (e) {
// 409 on first attempt → surface the merge confirmation; cross-fandom
// collisions can't go through the regular /merge endpoint, so the
// resolution is a second setFandom with merge: true.
if (!merge && e.status === 409 && e.body && e.body.target) {
collision.value = e.body
} else {
error.value = e.message || String(e)
collision.value = null
}
} finally {
busy.value = false
}
}
function onSave() { save(false) }
function onConfirmMerge() { save(true) }
</script>