From 3da9b447d433ab8fb1eb1c38e7e7c67bc8d283ae Mon Sep 17 00:00:00 2001 From: miteruzo Date: Wed, 22 Apr 2026 23:32:05 +0900 Subject: [PATCH] #318 --- backend/app/controllers/tags_controller.rb | 14 +++++-- frontend/src/App.tsx | 2 +- frontend/src/components/TopNav.tsx | 19 ++++++--- frontend/src/lib/prefetchers.ts | 18 ++++++++- frontend/src/pages/tags/TagDetailPage.tsx | 45 +++++++++++++++++----- frontend/src/pages/tags/TagListPage.tsx | 34 +++++++++++----- 6 files changed, 100 insertions(+), 32 deletions(-) diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index 8d16e34..b2adf1d 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -220,8 +220,14 @@ class TagsController < ApplicationController category = params[:category].to_s.strip return head :unprocessable_entity if name.blank? || category.blank? - if tag.nico? != (category == 'nico') - return render json: { error: 'ニコタグのカテゴリ変更はできません.' }, + if name != tag.name && + tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico]) + return render json: { error: 'システム・タグの名称は変更できません.' }, + status: :unprocessable_entity + end + + if tag.nico? || category == 'nico' + return render json: { error: 'ニコタグは変更できません.' }, status: :unprocessable_entity end @@ -258,8 +264,8 @@ class TagsController < ApplicationController tag = Tag.find(params[:id]) - if category.present? && tag.nico? != (category == 'nico') - return render json: { error: 'ニコタグのカテゴリ変更はできません.' }, + if tag.nico? || (category.present? && category == 'nico') + return render json: { error: 'ニコタグは変更できません.' }, status: :unprocessable_entity end diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d6e5496..f8178f6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -56,7 +56,7 @@ const RouteTransitionWrapper = ({ user, setUser }: { }/> }/> }/> - }/> + }/> }/> }/> }> diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 2b5eee2..ca100d4 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -8,7 +8,7 @@ import PrefetchLink from '@/components/PrefetchLink' import TopNavUser from '@/components/TopNavUser' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' import { tagsKeys, wikiKeys } from '@/lib/queryKeys' -import { fetchTagByName } from '@/lib/tags' +import { fetchTag, fetchTagByName } from '@/lib/tags' import { cn } from '@/lib/utils' import { fetchWikiPage } from '@/lib/wiki' @@ -29,6 +29,8 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: { const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (pathName) && wikiId) const wikiTitle = pathName.split ('/')[2] ?? '' + const tagFlg = /^\/tags\/\d+/.test (pathName) + return [ { name: '広場', to: '/posts', subMenu: [ { name: '一覧', to: '/posts' }, @@ -38,10 +40,13 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: { { name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] }, { name: 'タグ', to: '/tags', subMenu: [ { name: 'マスタ', to: '/tags' }, - { name: '別名タグ', to: '/tags/aliases', visible: false }, - { name: '上位タグ', to: '/tags/implications', visible: false }, { name: 'ニコニコ連携', to: '/tags/nico' }, - { name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }] }, + { name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }, + { component: , visible: tagFlg }, + { name: `広場 (${ postCount || 0 })`, + to: `/posts?tags=${ encodeURIComponent (tag?.name) }`, + visible: tagFlg }, + { name: '履歴', to: `/tags/changes?id=${ tag?.id }`, visible: false }] }, { name: '素材', to: '/materials', visible: false, subMenu: [ { name: '一覧', to: '/materials' }, { name: '検索', to: '/materials/search', visible: false }, @@ -114,12 +119,14 @@ export default (({ user }: Props) => { queryKey: wikiKeys.show (wikiIdStr, { }), queryFn: () => fetchWikiPage (wikiIdStr, { }) }) - const effectiveTitle = wikiPage?.title ?? '' + const tagFlg = /^\/tags\/\d+/.test (location.pathname) + const effectiveTitle = (tagFlg ? location.pathname.split ('/')[2] : wikiPage?.title) ?? '' const { data: tag } = useQuery ({ enabled: Boolean (effectiveTitle), queryKey: tagsKeys.show (effectiveTitle), - queryFn: () => fetchTagByName (effectiveTitle) }) + queryFn: () => (tagFlg ? fetchTag : fetchTagByName) (effectiveTitle) }) + const menu = menuOutline ({ tag, wikiId, user, pathName: location.pathname }) const visibleMenu = menu.filter ((item): item is MenuVisibleItem => item.visible ?? true) diff --git a/frontend/src/lib/prefetchers.ts b/frontend/src/lib/prefetchers.ts index ad18e1b..166daa8 100644 --- a/frontend/src/lib/prefetchers.ts +++ b/frontend/src/lib/prefetchers.ts @@ -14,6 +14,7 @@ type Prefetcher = (qc: QueryClient, url: URL) => Promise const mPost = match<{ id: string }> ('/posts/:id') const mWiki = match<{ title: string }> ('/wiki/:title') +const mTag = match<{ id: string }> ('/tags/:id') const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => { @@ -169,6 +170,19 @@ const prefetchTagsIndex: Prefetcher = async (qc, url) => { } +const prefetchTagShow: Prefetcher = async (qc, url) => { + const m = mTag (url.pathname) + if (!(m)) + return + + const { id } = m.params + + await qc.prefetchQuery ({ + queryKey: tagsKeys.show (id), + queryFn: () => fetchTag (id) }) +} + + export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [ { test: u => ['/', '/posts', '/posts/search'].includes (u.pathname), run: prefetchPostsIndex }, @@ -180,7 +194,9 @@ export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] { test: u => (!(['/wiki/new', '/wiki/changes'].includes (u.pathname)) && Boolean (mWiki (u.pathname))), run: prefetchWikiPageShow }, - { test: u => u.pathname === '/tags', run: prefetchTagsIndex }] + { test: u => u.pathname === '/tags', run: prefetchTagsIndex }, + { test: u => u.pathname !== '/tags/nico' && Boolean (mTag (u.pathname)), + run: prefetchTagShow }] export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise => { diff --git a/frontend/src/pages/tags/TagDetailPage.tsx b/frontend/src/pages/tags/TagDetailPage.tsx index d3ea122..80693b3 100644 --- a/frontend/src/pages/tags/TagDetailPage.tsx +++ b/frontend/src/pages/tags/TagDetailPage.tsx @@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' +import TagLink from '@/components/TagLink' import Label from '@/components/common/Label' import PageTitle from '@/components/common/PageTitle' import MainArea from '@/components/layout/MainArea' @@ -9,7 +10,8 @@ import { toast } from '@/components/ui/use-toast' import { CATEGORIES, CATEGORY_NAMES } from '@/consts' import { apiPut } from '@/lib/api' import { postsKeys, tagsKeys } from '@/lib/queryKeys' -import { fetchTagByName } from '@/lib/tags' +import { fetchTag } from '@/lib/tags' +import { cn } from '@/lib/utils' import type { FC, FormEvent } from 'react' @@ -17,18 +19,19 @@ import type { Category, Tag } from '@/types' export default (() => { - const { name: nameRaw } = useParams () - const tagName = String (nameRaw ?? '') - const tagKey = tagsKeys.show (tagName) + const { id } = useParams () + const tagId = String (id ?? '') + const tagKey = tagsKeys.show (tagId) const { data: tag, isLoading: loading } = useQuery ({ queryKey: tagKey, - queryFn: () => fetchTagByName (tagName) }) + queryFn: () => fetchTag (tagId) }) const [name, setName] = useState ('') const [category, setCategory] = useState ('general') const [aliases, setAliases] = useState ('') const [parentTags, setParentTags] = useState ('') + const [disabled, setDisabled] = useState (true) const qc = useQueryClient () @@ -43,7 +46,8 @@ export default (() => { try { - const data = await apiPut (`/tags/${ tag?.id }`, formData) + const data = await apiPut (`/tags/${ id }`, formData) + setName (data.name) setCategory (data.category as Category) setAliases (data.aliases.join (' ')) @@ -61,26 +65,37 @@ export default (() => { useEffect (() => { if (!(tag)) - return + { + setDisabled (true) + return + } setName (tag.name) setCategory (tag.category as Category) setAliases (tag.aliases?.join (' ')) setParentTags (tag.parents?.map (t => t.name).join (' ')) + setDisabled (tag.category === 'nico') }, [tag]) return ( {(loading || !(tag)) ? 'Loading...' : (
- {tag.name} + + +
{/* 名称 */}
+ {/* TODO: 補完に対応させる */} setName (e.target.value)} className="w-full border p-2 rounded"/> @@ -90,10 +105,12 @@ export default (() => {
setAliases (e.target.value)} className="w-full border p-2 rounded"/> @@ -113,8 +132,10 @@ export default (() => { {/* 上位タグ */}
+ {/* TODO: 補完に対応させる */} setParentTags (e.target.value)} className="w-full border p-2 rounded"/> @@ -123,7 +144,11 @@ export default (() => {
diff --git a/frontend/src/pages/tags/TagListPage.tsx b/frontend/src/pages/tags/TagListPage.tsx index 6589b0d..f183aa7 100644 --- a/frontend/src/pages/tags/TagListPage.tsx +++ b/frontend/src/pages/tags/TagListPage.tsx @@ -205,13 +205,15 @@ export default (() => { {loading ? 'Loading...' : (results.length > 0 ? (
- +
- - - + + + + + @@ -226,18 +228,20 @@ export default (() => { + + - + + +
- by="category" - label="カテゴリ" + by="post_count" + label="件数" currentOrder={order} defaultDirection={defaultDirection}/> - by="post_count" - label="件数" + by="category" + label="カテゴリ" currentOrder={order} defaultDirection={defaultDirection}/> 別名上位タグ by="created_at" @@ -262,11 +266,21 @@ export default (() => { {CATEGORY_NAMES[row.category]} {row.postCount}{CATEGORY_NAMES[row.category]}{row.aliases.join (' ')} + {row.parents.map (t => ( + + + ))} + {dateString (row.createdAt)} {dateString (row.updatedAt)}