feat(frontend): inline tag rename, merge confirm dialog, TagsView wiring
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<v-dialog :model-value="modelValue" max-width="460" @update:model-value="$emit('cancel')">
|
||||
<v-card>
|
||||
<v-card-title>Merge tags</v-card-title>
|
||||
<v-card-text v-if="collision">
|
||||
<p>
|
||||
Merge <strong>{{ collision.sourceName }}</strong> into
|
||||
<strong>{{ collision.target.name }}</strong>? This moves
|
||||
<strong>{{ collision.sourceImageCount }}</strong>
|
||||
image(s) onto <strong>{{ collision.target.name }}</strong>.
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
<template v-if="collision.willAlias">
|
||||
“{{ collision.sourceName }}” will be kept as an alias of
|
||||
“{{ collision.target.name }}” — the tagger may still predict it.
|
||||
</template>
|
||||
<template v-else>
|
||||
“{{ collision.sourceName }}” will be permanently deleted.
|
||||
</template>
|
||||
</p>
|
||||
<p class="mt-2 text-medium-emphasis">This can't be undone.</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="$emit('cancel')">Cancel</v-btn>
|
||||
<v-btn color="accent" variant="flat" @click="$emit('confirm')">
|
||||
Merge
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
collision: { type: Object, default: null }
|
||||
})
|
||||
defineEmits(['confirm', 'cancel'])
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-card class="fc-tagcard" @click="$emit('open', card.id)">
|
||||
<v-card class="fc-tagcard" @click="onCardClick">
|
||||
<div class="fc-tagcard__previews">
|
||||
<img
|
||||
v-for="(t, i) in card.preview_thumbnails" :key="i"
|
||||
@@ -11,9 +11,25 @@
|
||||
</div>
|
||||
<v-card-text class="fc-tagcard__body">
|
||||
<div class="fc-tagcard__name">
|
||||
{{ card.name }}
|
||||
<span v-if="card.kind === 'character' && card.fandom_name"
|
||||
class="fc-tagcard__fandom">— {{ card.fandom_name }}</span>
|
||||
<template v-if="!editing">
|
||||
{{ card.name }}
|
||||
<span v-if="card.kind === 'character' && card.fandom_name"
|
||||
class="fc-tagcard__fandom">— {{ card.fandom_name }}</span>
|
||||
<v-icon
|
||||
class="fc-tagcard__edit" size="14" icon="mdi-pencil"
|
||||
@click.stop="startEdit"
|
||||
/>
|
||||
</template>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="draft"
|
||||
density="compact" variant="outlined" hide-details
|
||||
autofocus
|
||||
@click.stop
|
||||
@keydown.stop.enter="submit"
|
||||
@keydown.stop.esc="cancel"
|
||||
@blur="cancel"
|
||||
/>
|
||||
</div>
|
||||
<div class="fc-tagcard__meta">
|
||||
<v-chip size="x-small" label>{{ card.kind }}</v-chip>
|
||||
@@ -24,8 +40,31 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ card: { type: Object, required: true } })
|
||||
defineEmits(['open'])
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({ card: { type: Object, required: true } })
|
||||
const emit = defineEmits(['open', 'rename'])
|
||||
|
||||
const editing = ref(false)
|
||||
const draft = ref('')
|
||||
|
||||
function onCardClick() {
|
||||
if (!editing.value) emit('open', props.card.id)
|
||||
}
|
||||
function startEdit() {
|
||||
draft.value = props.card.name
|
||||
editing.value = true
|
||||
}
|
||||
function cancel() {
|
||||
editing.value = false
|
||||
}
|
||||
function submit() {
|
||||
const name = draft.value.trim()
|
||||
editing.value = false
|
||||
if (name && name !== props.card.name) {
|
||||
emit('rename', { id: props.card.id, name })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -48,4 +87,12 @@ defineEmits(['open'])
|
||||
margin-top: 4px;
|
||||
}
|
||||
.fc-tagcard__count { font-variant-numeric: tabular-nums; opacity: 0.8; }
|
||||
.fc-tagcard__edit {
|
||||
opacity: 0;
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
transition: opacity .15s ease;
|
||||
}
|
||||
.fc-tagcard:hover .fc-tagcard__edit { opacity: .6; }
|
||||
.fc-tagcard__edit:hover { opacity: 1; }
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
|
||||
<div class="fc-tags__grid">
|
||||
<TagCard
|
||||
v-for="c in store.cards" :key="c.id" :card="c" @open="openTag"
|
||||
v-for="c in store.cards" :key="c.id" :card="c"
|
||||
@open="openTag" @rename="onRename"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +34,13 @@
|
||||
<v-progress-circular indeterminate color="accent" size="28" />
|
||||
</div>
|
||||
<div v-else-if="store.hasMore" ref="sentinelEl" class="fc-tags__sentinel" />
|
||||
|
||||
<MergeConfirmDialog
|
||||
:model-value="pendingMerge !== null"
|
||||
:collision="pendingMerge"
|
||||
@confirm="confirmMerge"
|
||||
@cancel="pendingMerge = null"
|
||||
/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@@ -41,6 +49,7 @@ import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTagDirectoryStore } from '../stores/tagDirectory.js'
|
||||
import TagCard from '../components/discovery/TagCard.vue'
|
||||
import MergeConfirmDialog from '../components/discovery/MergeConfirmDialog.vue'
|
||||
|
||||
const KINDS = ['artist', 'character', 'copyright', 'general', 'series', 'meta']
|
||||
const store = useTagDirectoryStore()
|
||||
@@ -49,6 +58,18 @@ const router = useRouter()
|
||||
const search = ref('')
|
||||
const kind = ref(null)
|
||||
const sentinelEl = ref(null)
|
||||
const pendingMerge = ref(null)
|
||||
|
||||
async function onRename({ id, name }) {
|
||||
const res = await store.rename(id, name)
|
||||
if (res.collision) pendingMerge.value = res.collision
|
||||
}
|
||||
|
||||
async function confirmMerge() {
|
||||
const c = pendingMerge.value
|
||||
pendingMerge.value = null
|
||||
if (c) await store.merge(c.sourceId, c.target.id)
|
||||
}
|
||||
|
||||
let debounce = null
|
||||
watch(search, (v) => {
|
||||
|
||||
Reference in New Issue
Block a user