#712 — fandom picker showed no existing fandoms: loadFandoms enumerated via /tags/autocomplete with q=' ', which the backend strips to empty → returns []. Switch to the cursor-paged /tags/directory?kind=fandom (loop all pages); drop the load-once guard so the dialog reflects fandoms created elsewhere. #711 — modal tag-chip kebab never opened: the kebab + menu were nested INSIDE the v-chip, which swallowed the click / mis-anchored the teleported menu. Un-nest it as a sibling v-btn using the standard v-menu activator slot (Vuetify wires the click and stacks the overlay above the modal natively). Removes the openTagId workaround. #700 — ESC didn't close the modal after accepting a suggested tag: the guard suppressed close while ANY .v-overlay--active existed, which includes tooltips — a lingering tooltip blocked the close. Exclude .v-tooltip from the guard so only real interactive overlays (menus/dialogs) keep ESC from closing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,8 @@ const store = useTagStore()
|
||||
const selectedId = ref(null)
|
||||
const newName = ref('')
|
||||
|
||||
onMounted(() => { if (store.fandomCache.length === 0) store.loadFandoms() })
|
||||
// Always refresh on open so the list reflects fandoms created elsewhere (#712).
|
||||
onMounted(() => store.loadFandoms())
|
||||
|
||||
async function onCreate() {
|
||||
const f = await store.createFandom(newName.value.trim())
|
||||
|
||||
@@ -84,7 +84,8 @@ const busy = ref(false)
|
||||
const error = ref(null)
|
||||
const collision = ref(null)
|
||||
|
||||
onMounted(() => { if (store.fandomCache.length === 0) store.loadFandoms() })
|
||||
// Always refresh on open so the list reflects fandoms created elsewhere (#712).
|
||||
onMounted(() => store.loadFandoms())
|
||||
|
||||
async function onCreate() {
|
||||
const name = newName.value.trim()
|
||||
|
||||
@@ -106,7 +106,11 @@ function onKeyDown(ev) {
|
||||
// overlay's own Esc handling fire instead of closing the whole
|
||||
// modal mid-interaction. Vuetify marks open overlays with
|
||||
// `.v-overlay--active`.
|
||||
if (document.querySelector('.v-overlay--active')) return
|
||||
// EXCLUDE tooltips (`.v-tooltip`): they're also `.v-overlay--active` while
|
||||
// shown, so one lingering after a hover/click (e.g. just after accepting a
|
||||
// suggested tag) wrongly suppressed the close (#700). Only real interactive
|
||||
// overlays (menus/dialogs) should keep ESC from closing the modal.
|
||||
if (document.querySelector('.v-overlay--active:not(.v-tooltip)')) return
|
||||
ev.preventDefault()
|
||||
emit('close')
|
||||
} else if (ev.key === 'ArrowLeft') {
|
||||
|
||||
@@ -2,45 +2,43 @@
|
||||
<aside class="fc-tag-panel" aria-label="Tags for this image">
|
||||
<h3 class="fc-tag-panel__title">Tags</h3>
|
||||
<div class="fc-tag-panel__chips">
|
||||
<v-chip
|
||||
<!-- #711: the kebab + its menu used to be NESTED inside the v-chip, which
|
||||
swallowed/mis-routed the click and mis-anchored the (teleported) menu
|
||||
so it never opened. Render the kebab as a SIBLING of the chip and use
|
||||
the standard v-menu activator slot — Vuetify wires the click and
|
||||
stacks the overlay above the modal natively. -->
|
||||
<span
|
||||
v-for="tag in modal.current?.tags || []"
|
||||
:key="tag.id" size="small" closable
|
||||
:color="store.colorFor(tag.kind)" variant="tonal"
|
||||
@click:close="onRemove(tag.id)"
|
||||
:key="tag.id" class="fc-tag-panel__chip"
|
||||
>
|
||||
<v-icon start size="x-small">{{ iconFor(tag.kind) }}</v-icon>
|
||||
{{ tag.name }}<span v-if="tag.fandom_id">→</span>
|
||||
<!-- Operator-flagged 2026-06-04: the `#activator` + `v-bind` menu
|
||||
never opened inside this teleported modal. Drive it explicitly
|
||||
instead (same mechanism as the dialogs below, which work): the
|
||||
icon toggles `openTagId` with @click.stop (shielding the chip's
|
||||
close button), and `activator="parent"` + `:open-on-click=false`
|
||||
anchors the menu for positioning only. One tag's menu open at a
|
||||
time, so a single id is enough. -->
|
||||
<span class="kebab-wrap">
|
||||
<v-icon
|
||||
size="x-small" class="ml-1 kebab-icon"
|
||||
icon="mdi-dots-vertical"
|
||||
@click.stop="openTagId = openTagId === tag.id ? null : tag.id"
|
||||
/>
|
||||
<v-menu
|
||||
:model-value="openTagId === tag.id"
|
||||
activator="parent" :open-on-click="false"
|
||||
@update:model-value="v => { if (!v) openTagId = null }"
|
||||
>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="openRename(tag)">
|
||||
<v-list-item-title>Rename…</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="tag.kind === 'character'" @click="openSetFandom(tag)"
|
||||
>
|
||||
<v-list-item-title>Set fandom…</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</span>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
size="small" closable
|
||||
:color="store.colorFor(tag.kind)" variant="tonal"
|
||||
@click:close="onRemove(tag.id)"
|
||||
>
|
||||
<v-icon start size="x-small">{{ iconFor(tag.kind) }}</v-icon>
|
||||
{{ tag.name }}<span v-if="tag.fandom_id">→</span>
|
||||
</v-chip>
|
||||
<v-menu location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props" icon="mdi-dots-vertical" size="x-small"
|
||||
variant="text" density="comfortable"
|
||||
class="fc-tag-panel__kebab" @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-item
|
||||
v-if="tag.kind === 'character'" @click="openSetFandom(tag)"
|
||||
>
|
||||
<v-list-item-title>Set fandom…</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</span>
|
||||
<span v-if="!modal.current?.tags?.length" class="text-caption">No tags yet.</span>
|
||||
</div>
|
||||
|
||||
@@ -88,9 +86,6 @@ import FandomSetDialog from './FandomSetDialog.vue'
|
||||
const modal = useModalStore()
|
||||
const store = useTagStore()
|
||||
const errorMsg = ref(null)
|
||||
// Which tag chip's kebab menu is open (only one at a time). Drives each
|
||||
// chip menu's v-model so opening never depends on Vuetify's activator click.
|
||||
const openTagId = ref(null)
|
||||
|
||||
const KIND_ICONS = {
|
||||
general: 'mdi-tag', character: 'mdi-account-circle',
|
||||
@@ -153,6 +148,7 @@ async function onFandomUpdated() {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.fc-tag-panel__chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.kebab-wrap { display: inline-flex; align-items: center; }
|
||||
.kebab-icon { cursor: pointer; }
|
||||
.fc-tag-panel__chip { display: inline-flex; align-items: center; gap: 1px; }
|
||||
.fc-tag-panel__kebab { opacity: 0.7; }
|
||||
.fc-tag-panel__chip:hover .fc-tag-panel__kebab { opacity: 1; }
|
||||
</style>
|
||||
|
||||
@@ -34,10 +34,20 @@ export const useTagStore = defineStore('tags', () => {
|
||||
}
|
||||
|
||||
async function loadFandoms() {
|
||||
fandomCache.value = await api.get('/api/tags/autocomplete', {
|
||||
params: { q: ' ', kind: 'fandom', limit: 200 }
|
||||
})
|
||||
return fandomCache.value
|
||||
// List ALL fandoms via the cursor-paged tag directory. (#712: the old impl
|
||||
// abused /tags/autocomplete with q=' ', which the backend strips to empty
|
||||
// and returns [] — so the picker showed no existing fandoms.)
|
||||
const all = []
|
||||
let cursor = null
|
||||
do {
|
||||
const params = { kind: 'fandom', limit: 200 }
|
||||
if (cursor) params.cursor = cursor
|
||||
const body = await api.get('/api/tags/directory', { params })
|
||||
all.push(...(body.cards || []))
|
||||
cursor = body.next_cursor
|
||||
} while (cursor)
|
||||
fandomCache.value = all
|
||||
return all
|
||||
}
|
||||
|
||||
async function createFandom(name) {
|
||||
|
||||
Reference in New Issue
Block a user