From 4f9464d215a603eb5361be2934b8b281684ac381 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 3 Jun 2026 23:26:04 -0400 Subject: [PATCH] feat(gallery,tags): clear active filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps where a filter couldn't be removed: - Gallery: a tag_id filter (from clicking a tag) had no indicator or clear control — only post_id did (PostInfoHeader). Add an "Tag: ✕" chip that clears the filter by dropping tag_id from the URL. New lightweight GET /api/tags/ resolves the name; the store fetches it on filter set. - Tags view: the kind chip-group used mandatory="false" — a STRING ("false" is truthy in JS), which made the group mandatory so the active kind chip couldn't be deselected. Fixed to :mandatory="false" so the filter clears. Tests: GET /tags/ shape + 404; gallery store resolves filterTagName. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/api/tags.py | 18 ++++++++++++++++++ frontend/src/stores/gallery.js | 15 ++++++++++++++- frontend/src/views/GalleryView.vue | 19 ++++++++++++++++++- frontend/src/views/TagsView.vue | 2 +- frontend/test/gallery.spec.js | 13 +++++++++++++ tests/test_api_tag_merge.py | 13 +++++++++++++ 6 files changed, 77 insertions(+), 3 deletions(-) diff --git a/backend/app/api/tags.py b/backend/app/api/tags.py index de086eb..94bddff 100644 --- a/backend/app/api/tags.py +++ b/backend/app/api/tags.py @@ -194,6 +194,24 @@ async def remove_tag_from_image(image_id: int, tag_id: int): return "", 204 +@tags_bp.route("/tags/", methods=["GET"]) +async def get_tag(tag_id: int): + """Resolve a single tag (used by the gallery to label its active + tag-filter chip).""" + async with get_session() as session: + tag = await session.get(Tag, tag_id) + if tag is None: + return jsonify({"error": "tag not found"}), 404 + return jsonify( + { + "id": tag.id, + "name": tag.name, + "kind": tag.kind.value, + "fandom_id": tag.fandom_id, + } + ) + + @tags_bp.route("/tags/", methods=["PATCH"]) async def update_tag(tag_id: int): """Rename and/or re-fandom a tag. Body may carry `name` and/or diff --git a/frontend/src/stores/gallery.js b/frontend/src/stores/gallery.js index de52d5b..407b3a4 100644 --- a/frontend/src/stores/gallery.js +++ b/frontend/src/stores/gallery.js @@ -22,6 +22,9 @@ export const useGalleryStore = defineStore('gallery', () => { const loading = ref(false) const error = ref(null) const filter = ref({ tag_id: null, post_id: null }) + // Display name for the active tag filter, so the gallery can label a + // "Tag: X ✕" chip instead of leaving the filter invisible/unclearable. + const filterTagName = ref(null) const timelineBuckets = ref([]) const timelineLoading = ref(false) @@ -94,10 +97,20 @@ export const useGalleryStore = defineStore('gallery', () => { function setTagFilter(tagId) { filter.value.tag_id = tagId filter.value.post_id = null + filterTagName.value = null + if (tagId) _resolveTagName(tagId) // fire-and-forget; chip label only loadInitial() loadTimeline() } + async function _resolveTagName(tagId) { + try { + filterTagName.value = (await api.get(`/api/tags/${tagId}`)).name + } catch { + // Leave null — the chip falls back to "#". + } + } + function setPostFilter(postId) { filter.value.post_id = postId filter.value.tag_id = null @@ -110,7 +123,7 @@ export const useGalleryStore = defineStore('gallery', () => { return { images, dateGroups, hasMore, isEmpty, loading, error, - filter, timelineBuckets, timelineLoading, + filter, filterTagName, timelineBuckets, timelineLoading, loadInitial, loadMore, loadTimeline, jumpTo, setTagFilter, setPostFilter } }) diff --git a/frontend/src/views/GalleryView.vue b/frontend/src/views/GalleryView.vue index 03c804c..ea318fe 100644 --- a/frontend/src/views/GalleryView.vue +++ b/frontend/src/views/GalleryView.vue @@ -12,6 +12,13 @@