From 4958e8f7d470d90ffda17c1c7a49dd6551d23a1a Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 8 Jun 2026 21:59:08 -0400 Subject: [PATCH 1/5] feat(modal): return focus to tag input after accepting a suggestion Accepting an auto-suggested tag (Suggestions panel or the autocomplete dropdown) left focus on , so the operator had to re-click the tag field to add the next one. Expose TagAutocomplete.focus (the existing mobile-aware focusInput) and call it after accept from both paths; SuggestionsPanel emits 'accepted' for the parent to refocus. Operator-asked 2026-06-08. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/modal/SuggestionsPanel.vue | 5 +++++ frontend/src/components/modal/TagAutocomplete.vue | 4 ++++ frontend/src/components/modal/TagPanel.vue | 9 +++++++++ 3 files changed, 18 insertions(+) diff --git a/frontend/src/components/modal/SuggestionsPanel.vue b/frontend/src/components/modal/SuggestionsPanel.vue index e56d7a5..8b61677 100644 --- a/frontend/src/components/modal/SuggestionsPanel.vue +++ b/frontend/src/components/modal/SuggestionsPanel.vue @@ -46,6 +46,9 @@ import SuggestionsCategoryGroup from './SuggestionsCategoryGroup.vue' import AliasPickerDialog from './AliasPickerDialog.vue' const props = defineProps({ imageId: { type: Number, required: true } }) +// 'accepted' lets the parent return focus to the tag input after a suggestion is +// applied (operator-asked 2026-06-08). +const emit = defineEmits(['accepted']) const store = useSuggestionsStore() const modal = useModalStore() @@ -72,6 +75,7 @@ async function onAccept(s) { try { await store.accept(s) await modal.reloadTags() + emit('accepted') } catch (e) { toast({ text: `Accept failed: ${e.message}`, type: 'error' }) } @@ -85,6 +89,7 @@ async function onAliasConfirm(canonicalTagId) { await store.aliasAccept(aliasTarget.value, canonicalTagId) aliasDialog.value = false await modal.reloadTags() + emit('accepted') } catch (e) { toast({ text: `Alias failed: ${e.message}`, type: 'error' }) } diff --git a/frontend/src/components/modal/TagAutocomplete.vue b/frontend/src/components/modal/TagAutocomplete.vue index 7200f09..87c1a20 100644 --- a/frontend/src/components/modal/TagAutocomplete.vue +++ b/frontend/src/components/modal/TagAutocomplete.vue @@ -123,6 +123,10 @@ function focusInput () { nextTick(() => inputRef.value?.focus?.()) } onMounted(focusInput) +// Exposed so the parent (TagPanel) can hand focus back to this field after an +// auto-suggestion is accepted, keeping the keyboard flow on the input instead +// of dropping to (operator-asked 2026-06-08). Mobile guard preserved. +defineExpose({ focus: focusInput }) // Single text input; no kind dropdown. Client-side mirror of the // backend's parse_kind_prefix lives below — kept in sync with diff --git a/frontend/src/components/modal/TagPanel.vue b/frontend/src/components/modal/TagPanel.vue index dfd00c2..64aafa3 100644 --- a/frontend/src/components/modal/TagPanel.vue +++ b/frontend/src/components/modal/TagPanel.vue @@ -13,6 +13,7 @@ @@ -24,6 +25,7 @@ @@ -60,6 +62,12 @@ import FandomSetDialog from './FandomSetDialog.vue' const modal = useModalStore() const suggestions = useSuggestionsStore() const errorMsg = ref(null) +const tagInputRef = ref(null) + +// Return focus to the tag input after a suggestion is accepted (from the +// Suggestions panel or the autocomplete dropdown) so the operator can keep +// typing the next tag without re-clicking the field (operator-asked 2026-06-08). +function focusTagInput() { tagInputRef.value?.focus?.() } async function onRemove(tagId) { errorMsg.value = null @@ -84,6 +92,7 @@ async function onAcceptSuggestion(s) { try { await suggestions.accept(s) await modal.reloadTags() + focusTagInput() } catch (e) { errorMsg.value = e.message } } From e4cebf70d188f4b7570c58ae0551d66cf8712136 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 9 Jun 2026 18:56:45 -0400 Subject: [PATCH 2/5] feat(series): browse search + per-card kebab (rename/delete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Series browse tab had no way to find a series in a long grid and no per-series actions. Add a search field (instant client-side name/artist filter over the already-loaded list) and a kebab on each card with Rename (reuses TagRenameDialog → PATCH /api/tags/, with its collision-merge flow) and Delete (confirm dialog → DELETE /api/admin/tags/; series_page/chapter/ suggestion cascade, images kept). Gap badge moved to the cover's top-left so the kebab can sit top-right. Operator-asked 2026-06-09. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/stores/seriesBrowse.js | 19 +++- frontend/src/views/SeriesView.vue | 132 +++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 5 deletions(-) diff --git a/frontend/src/stores/seriesBrowse.js b/frontend/src/stores/seriesBrowse.js index adb796c..a55e1dc 100644 --- a/frontend/src/stores/seriesBrowse.js +++ b/frontend/src/stores/seriesBrowse.js @@ -27,5 +27,22 @@ export const useSeriesBrowseStore = defineStore('seriesBrowse', () => { return load() } - return { series, sort, artistId, loading, error, load, setSort } + // A series IS a Tag(kind=series); deleting it removes the series structure + // (pages/chapters cascade) via the shared admin tag-delete. Splice the card + // out for instant feedback rather than reloading the whole grid. + async function remove(id) { + await api.delete(`/api/admin/tags/${id}`) + series.value = series.value.filter(s => s.id !== id) + } + + // Reflect an in-place rename (PATCH /api/tags/) without a full reload. + function applyRename(id, name) { + const row = series.value.find(s => s.id === id) + if (row) row.name = name + } + + return { + series, sort, artistId, loading, error, + load, setSort, remove, applyRename, + } }) diff --git a/frontend/src/views/SeriesView.vue b/frontend/src/views/SeriesView.vue index cb3e754..d726df3 100644 --- a/frontend/src/views/SeriesView.vue +++ b/frontend/src/views/SeriesView.vue @@ -15,6 +15,12 @@
+ series: tag and add images.
+
+ No series match “{{ query }}”. +
@@ -48,6 +60,27 @@ mdi-alert-outline gap + + + + + Rename + + + Delete + + +

{{ s.name }}

@@ -73,6 +106,38 @@
+ + + + + + + Delete “{{ deleteTarget.name }}”? + +

+ Removes the series and its chapter/page ordering. The images + themselves are kept — only the series grouping is deleted. +

+ {{ deleteError }} +
+ + + Cancel + Delete series + +
+
+
@@ -140,10 +205,11 @@