From 3a0cca5aca9a2654754b80b0a6eacdb25f2b4d21 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 4 Jun 2026 22:50:19 -0400 Subject: [PATCH 1/2] fix(tags): allow creating a character with no fandom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not all characters belong to a fandom (original characters, unsorted). The create flow forced every new character through FandomPicker, whose only outcomes were 'Use this fandom' (disabled until one is picked) or Cancel (which aborts the whole creation) — there was no way to confirm a character with no fandom. - FandomPicker: add a 'No fandom' action that emits confirm(null). - TagAutocomplete.onFandomChosen: pass fandom_id: null when null is emitted. Backend already supported this end to end (Tag.fandom_id nullable, the CHECK only forbids fandom_id on non-character kinds, tag_service find_or_create defaults fandom_id=None, API reads body.get). A fandom can still be assigned later from the chip kebab's 'Set fandom…'. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/modal/FandomPicker.vue | 9 +++++++++ frontend/src/components/modal/TagAutocomplete.vue | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/modal/FandomPicker.vue b/frontend/src/components/modal/FandomPicker.vue index 31189ab..e9ed4a8 100644 --- a/frontend/src/components/modal/FandomPicker.vue +++ b/frontend/src/components/modal/FandomPicker.vue @@ -17,6 +17,10 @@ + + No fandom Cancel Use this fandom @@ -45,4 +49,9 @@ function onConfirm() { const f = store.fandomCache.find(x => x.id === selectedId.value) if (f) emit('confirm', f) } +// Create the character with no fandom. Emits null so the caller knows this +// was a deliberate "unassigned", not a cancel. +function onNoFandom() { + emit('confirm', null) +} diff --git a/frontend/src/components/modal/TagAutocomplete.vue b/frontend/src/components/modal/TagAutocomplete.vue index 9767af2..5131688 100644 --- a/frontend/src/components/modal/TagAutocomplete.vue +++ b/frontend/src/components/modal/TagAutocomplete.vue @@ -147,10 +147,14 @@ function onCreate () { reset() } +// fandom is null when the user picked "No fandom" — characters don't all +// belong to a fandom. The backend already accepts fandom_id: null for the +// character kind (tag.kind check + nullable fandom_id), and a fandom can be +// assigned later from the chip kebab's "Set fandom…". function onFandomChosen (fandom) { fandomDialog.value = false emit('pick-new', { - name: pendingNewName, kind: 'character', fandom_id: fandom.id, + name: pendingNewName, kind: 'character', fandom_id: fandom ? fandom.id : null, }) pendingNewName = null reset() From 86efbf7f2cb0b9d63c617ee744492167a7102efe Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 4 Jun 2026 23:01:55 -0400 Subject: [PATCH 2/2] fix(modal): kebab menus open via explicit v-model, not activator click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-confirmed on a fresh build: both the tag-chip and suggestion kebabs still never opened. The prior 8326e54 'fix' only wrapped them in a — inert for SuggestionItem (no parent capture) — and never addressed why the `#activator`/`v-bind="props"` click failed to toggle the menu inside the teleported ImageViewer modal. The dialogs in that same modal open via v-model and work, so drive the menus the same way: - The activator (v-btn / v-icon) toggles a reactive flag with @click.stop (which also shields the chip's close button / any parent). - The v-menu binds that flag (v-model / :model-value) and uses activator="parent" with :open-on-click="false" purely for positioning, so opening no longer depends on Vuetify's activator-click path. - TagPanel tracks a single openTagId (one chip menu open at a time). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/modal/SuggestionItem.vue | 40 ++++++++++--------- frontend/src/components/modal/TagPanel.vue | 36 ++++++++++------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/modal/SuggestionItem.vue b/frontend/src/components/modal/SuggestionItem.vue index 1a0c541..16e55cc 100644 --- a/frontend/src/components/modal/SuggestionItem.vue +++ b/frontend/src/components/modal/SuggestionItem.vue @@ -19,24 +19,25 @@ > Accept - - - - + + + + Treat as alias for… @@ -51,11 +52,12 @@ diff --git a/frontend/src/components/modal/TagPanel.vue b/frontend/src/components/modal/TagPanel.vue index f1f83dd..319b0b9 100644 --- a/frontend/src/components/modal/TagPanel.vue +++ b/frontend/src/components/modal/TagPanel.vue @@ -10,20 +10,24 @@ > {{ iconFor(tag.kind) }} {{ tag.name }} - - - - + + + + Rename… @@ -84,6 +88,9 @@ 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', @@ -147,4 +154,5 @@ async function onFandomUpdated() { } .fc-tag-panel__chips { display: flex; flex-wrap: wrap; gap: 6px; } .kebab-wrap { display: inline-flex; align-items: center; } +.kebab-icon { cursor: pointer; }