From 978f49adcc547a570fc5d726e35ca1bb6a7ee8b5 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 9 Jun 2026 19:35:59 -0400 Subject: [PATCH] feat(tags): show a character's fandom on its chip (truncated) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A character chip with a fandom only rendered a bare arrow. Surface the fandom NAME inline, truncated to 15 chars (full name in the tooltip). Resolve the name via a Tag self-join in both tag paths the modal uses — list_for_image (/api/images//tags) and gallery get_image_with_tags (/api/gallery/image/) — so chips show the fandom on first open and after any reload. Falls back to the bare arrow when only fandom_id is known. Operator-asked 2026-06-09. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/api/tags.py | 1 + backend/app/services/gallery_service.py | 20 +++++++++++++++++--- backend/app/services/tag_service.py | 23 +++++++++++++++++++---- frontend/src/components/modal/TagChip.vue | 19 ++++++++++++++++--- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/backend/app/api/tags.py b/backend/app/api/tags.py index 59dc286..b189c1f 100644 --- a/backend/app/api/tags.py +++ b/backend/app/api/tags.py @@ -172,6 +172,7 @@ async def list_tags_for_image(image_id: int): "name": t.name, "kind": t.kind.value, "fandom_id": t.fandom_id, + "fandom_name": t.fandom_name, } for t in tags ] diff --git a/backend/app/services/gallery_service.py b/backend/app/services/gallery_service.py index 9c31476..44cbe66 100644 --- a/backend/app/services/gallery_service.py +++ b/backend/app/services/gallery_service.py @@ -558,13 +558,26 @@ class GalleryService: record = await self.session.get(ImageRecord, image_id) if record is None: return None + # Self-join Tag to resolve a character's fandom NAME (not just id) so the + # modal chip can label it without an N+1 — mirrors list_for_image. + fandom_alias = Tag.__table__.alias("fandom_lookup") tag_stmt = ( - select(Tag) - .join(image_tag, image_tag.c.tag_id == Tag.id) + select( + Tag.id, + Tag.name, + Tag.kind, + Tag.fandom_id, + fandom_alias.c.name.label("fandom_name"), + ) + .select_from( + Tag.__table__ + .join(image_tag, image_tag.c.tag_id == Tag.id) + .outerjoin(fandom_alias, Tag.fandom_id == fandom_alias.c.id) + ) .where(image_tag.c.image_record_id == image_id) .order_by(Tag.kind.asc(), Tag.name.asc()) ) - tags = (await self.session.execute(tag_stmt)).scalars().all() + tags = (await self.session.execute(tag_stmt)).all() # Fetch the canonical post.post_date for this image (if any) so # the modal can show "Posted on " alongside import date. posted_at = None @@ -608,6 +621,7 @@ class GalleryService: "name": t.name, "kind": t.kind.value if hasattr(t.kind, "value") else t.kind, "fandom_id": t.fandom_id, + "fandom_name": t.fandom_name, } for t in tags ], diff --git a/backend/app/services/tag_service.py b/backend/app/services/tag_service.py index 11192a4..335afc3 100644 --- a/backend/app/services/tag_service.py +++ b/backend/app/services/tag_service.py @@ -225,14 +225,29 @@ class TagService: ) ) - async def list_for_image(self, image_id: int) -> Sequence[Tag]: + async def list_for_image(self, image_id: int) -> Sequence: + """Tags on an image, ordered (kind, name). Each row carries the fandom's + NAME (not just fandom_id) via a self-join on Tag, so the UI can label a + character chip with its fandom without an N+1 (mirrors the + autocomplete/directory resolution).""" + fandom_alias = Tag.__table__.alias("fandom_lookup") stmt = ( - select(Tag) - .join(image_tag, image_tag.c.tag_id == Tag.id) + select( + Tag.id, + Tag.name, + Tag.kind, + Tag.fandom_id, + fandom_alias.c.name.label("fandom_name"), + ) + .select_from( + Tag.__table__ + .join(image_tag, image_tag.c.tag_id == Tag.id) + .outerjoin(fandom_alias, Tag.fandom_id == fandom_alias.c.id) + ) .where(image_tag.c.image_record_id == image_id) .order_by(Tag.kind.asc(), Tag.name.asc()) ) - return (await self.session.execute(stmt)).scalars().all() + return (await self.session.execute(stmt)).all() async def _keep_as_alias(self, tag_id: int) -> bool: """A merged-away tag's old name must survive as an alias iff the ML diff --git a/frontend/src/components/modal/TagChip.vue b/frontend/src/components/modal/TagChip.vue index e93710d..b6df29d 100644 --- a/frontend/src/components/modal/TagChip.vue +++ b/frontend/src/components/modal/TagChip.vue @@ -11,7 +11,10 @@ @click:close="$emit('remove', tag.id)" > {{ iconFor(tag.kind) }} - {{ tag.name }} + {{ tag.name }}