#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 selectedId = ref(null)
|
||||||
const newName = ref('')
|
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() {
|
async function onCreate() {
|
||||||
const f = await store.createFandom(newName.value.trim())
|
const f = await store.createFandom(newName.value.trim())
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ const busy = ref(false)
|
|||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
const collision = 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() {
|
async function onCreate() {
|
||||||
const name = newName.value.trim()
|
const name = newName.value.trim()
|
||||||
|
|||||||
@@ -106,7 +106,11 @@ function onKeyDown(ev) {
|
|||||||
// overlay's own Esc handling fire instead of closing the whole
|
// overlay's own Esc handling fire instead of closing the whole
|
||||||
// modal mid-interaction. Vuetify marks open overlays with
|
// modal mid-interaction. Vuetify marks open overlays with
|
||||||
// `.v-overlay--active`.
|
// `.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()
|
ev.preventDefault()
|
||||||
emit('close')
|
emit('close')
|
||||||
} else if (ev.key === 'ArrowLeft') {
|
} else if (ev.key === 'ArrowLeft') {
|
||||||
|
|||||||
@@ -2,45 +2,43 @@
|
|||||||
<aside class="fc-tag-panel" aria-label="Tags for this image">
|
<aside class="fc-tag-panel" aria-label="Tags for this image">
|
||||||
<h3 class="fc-tag-panel__title">Tags</h3>
|
<h3 class="fc-tag-panel__title">Tags</h3>
|
||||||
<div class="fc-tag-panel__chips">
|
<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 || []"
|
v-for="tag in modal.current?.tags || []"
|
||||||
:key="tag.id" size="small" closable
|
:key="tag.id" class="fc-tag-panel__chip"
|
||||||
:color="store.colorFor(tag.kind)" variant="tonal"
|
|
||||||
@click:close="onRemove(tag.id)"
|
|
||||||
>
|
>
|
||||||
<v-icon start size="x-small">{{ iconFor(tag.kind) }}</v-icon>
|
<v-chip
|
||||||
{{ tag.name }}<span v-if="tag.fandom_id">→</span>
|
size="small" closable
|
||||||
<!-- Operator-flagged 2026-06-04: the `#activator` + `v-bind` menu
|
:color="store.colorFor(tag.kind)" variant="tonal"
|
||||||
never opened inside this teleported modal. Drive it explicitly
|
@click:close="onRemove(tag.id)"
|
||||||
instead (same mechanism as the dialogs below, which work): the
|
>
|
||||||
icon toggles `openTagId` with @click.stop (shielding the chip's
|
<v-icon start size="x-small">{{ iconFor(tag.kind) }}</v-icon>
|
||||||
close button), and `activator="parent"` + `:open-on-click=false`
|
{{ tag.name }}<span v-if="tag.fandom_id">→</span>
|
||||||
anchors the menu for positioning only. One tag's menu open at a
|
</v-chip>
|
||||||
time, so a single id is enough. -->
|
<v-menu location="bottom end">
|
||||||
<span class="kebab-wrap">
|
<template #activator="{ props }">
|
||||||
<v-icon
|
<v-btn
|
||||||
size="x-small" class="ml-1 kebab-icon"
|
v-bind="props" icon="mdi-dots-vertical" size="x-small"
|
||||||
icon="mdi-dots-vertical"
|
variant="text" density="comfortable"
|
||||||
@click.stop="openTagId = openTagId === tag.id ? null : tag.id"
|
class="fc-tag-panel__kebab" @click.stop
|
||||||
/>
|
/>
|
||||||
<v-menu
|
</template>
|
||||||
:model-value="openTagId === tag.id"
|
<v-list density="compact">
|
||||||
activator="parent" :open-on-click="false"
|
<v-list-item @click="openRename(tag)">
|
||||||
@update:model-value="v => { if (!v) openTagId = null }"
|
<v-list-item-title>Rename…</v-list-item-title>
|
||||||
>
|
</v-list-item>
|
||||||
<v-list density="compact">
|
<v-list-item
|
||||||
<v-list-item @click="openRename(tag)">
|
v-if="tag.kind === 'character'" @click="openSetFandom(tag)"
|
||||||
<v-list-item-title>Rename…</v-list-item-title>
|
>
|
||||||
</v-list-item>
|
<v-list-item-title>Set fandom…</v-list-item-title>
|
||||||
<v-list-item
|
</v-list-item>
|
||||||
v-if="tag.kind === 'character'" @click="openSetFandom(tag)"
|
</v-list>
|
||||||
>
|
</v-menu>
|
||||||
<v-list-item-title>Set fandom…</v-list-item-title>
|
</span>
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</span>
|
|
||||||
</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>
|
||||||
|
|
||||||
@@ -88,9 +86,6 @@ import FandomSetDialog from './FandomSetDialog.vue'
|
|||||||
const modal = useModalStore()
|
const modal = useModalStore()
|
||||||
const store = useTagStore()
|
const store = useTagStore()
|
||||||
const errorMsg = ref(null)
|
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 = {
|
const KIND_ICONS = {
|
||||||
general: 'mdi-tag', character: 'mdi-account-circle',
|
general: 'mdi-tag', character: 'mdi-account-circle',
|
||||||
@@ -153,6 +148,7 @@ async function onFandomUpdated() {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.fc-tag-panel__chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
.fc-tag-panel__chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
.kebab-wrap { display: inline-flex; align-items: center; }
|
.fc-tag-panel__chip { display: inline-flex; align-items: center; gap: 1px; }
|
||||||
.kebab-icon { cursor: pointer; }
|
.fc-tag-panel__kebab { opacity: 0.7; }
|
||||||
|
.fc-tag-panel__chip:hover .fc-tag-panel__kebab { opacity: 1; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -34,10 +34,20 @@ export const useTagStore = defineStore('tags', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadFandoms() {
|
async function loadFandoms() {
|
||||||
fandomCache.value = await api.get('/api/tags/autocomplete', {
|
// List ALL fandoms via the cursor-paged tag directory. (#712: the old impl
|
||||||
params: { q: ' ', kind: 'fandom', limit: 200 }
|
// abused /tags/autocomplete with q=' ', which the backend strips to empty
|
||||||
})
|
// and returns [] — so the picker showed no existing fandoms.)
|
||||||
return fandomCache.value
|
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) {
|
async function createFandom(name) {
|
||||||
|
|||||||
Reference in New Issue
Block a user