タグ履歴ページ (#321) (#330)

#321

#321

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #330
このコミットはPull リクエスト #330 でマージされました.
このコミットが含まれているのは:
2026-04-24 02:21:26 +09:00
コミット 6235b293f0
10個のファイルの変更572行の追加8行の削除
+212
ファイルの表示
@@ -0,0 +1,212 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { CATEGORY_NAMES } from '@/consts'
import { apiPut } from '@/lib/api'
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { fetchTagChanges } from '@/lib/tags'
import { cn, dateString } from '@/lib/utils'
import type { FC } from 'react'
const renderDiff = (diff: { current: string | null; prev: string | null }) => (
<>
{(diff.prev && diff.prev !== diff.current) && (
<>
<del className="text-red-600 dark:text-red-400">
{diff.prev}
</del>
{diff.current && <br/>}
</>)}
{diff.current}
</>)
export default (() => {
const location = useLocation ()
const query = new URLSearchParams (location.search)
const id = query.get ('id')
const page = Number (query.get ('page') ?? 1)
const limit = Number (query.get ('limit') ?? 20)
const { data, isLoading: loading } = useQuery ({
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }) })
const changes = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
const qc = useQueryClient ()
useEffect (() => {
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
}, [location.search])
return (
<MainArea>
<Helmet>
<title>{`タグ定義変更履歴 | ${ SITE_TITLE }`}</title>
</Helmet>
<PageTitle>
{id && <>: {<PrefetchLink to={`/tags/${ id }`}>#{id}</PrefetchLink>}</>}
</PageTitle>
{loading ? 'Loading...' : (
<>
<div className="overflow-x-auto">
<table className="w-full min-w-[1200px] table-fixed border-collapse">
<colgroup>
{/* 版 */}
<col className="w-40"/>
{/* 名称 */}
<col className="w-96"/>
{/* カテゴリ */}
<col className="w-96"/>
{/* 別名 */}
<col className="w-[48rem]"/>
{/* 上位タグ */}
<col className="w-96"/>
{/* 更新日時 */}
<col className="w-64"/>
{/* (差戻ボタン) */}
<col className="w-20"/>
</colgroup>
<thead className="border-b-2 border-black dark:border-white">
<tr>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
<th className="p-2"/>
</tr>
</thead>
<tbody>
{changes.map (change => (
<tr key={`${ change.tagId }.${ change.versionNo }`}
className={cn ('even:bg-gray-100 dark:even:bg-gray-700')}>
<td className="p-2">{change.tagId}.{change.versionNo}</td>
<td className="p-2 break-all">{renderDiff (change.name)}</td>
<td className="p-2 break-all">
{renderDiff ({
current: CATEGORY_NAMES[change.category.current],
prev: (change.category.prev
&& CATEGORY_NAMES[change.category.prev]) })}
</td>
<td className="p-2">
{change.aliases.map ((tag, i) => (
tag.type === 'added'
? (
<ins
key={i}
className="mr-2 text-green-600 dark:text-green-400">
{tag.name}
</ins>)
: (
tag.type === 'removed'
? (
<del
key={i}
className="mr-2 text-red-600 dark:text-red-400">
{tag.name}
</del>)
: (
<span key={i} className="mr-2">
{tag.name}
</span>))))}
</td>
<td className="p-2">
{change.parentTags.map ((tag, i) => (
tag.type === 'added'
? (
<ins
key={i}
className="mr-2 text-green-600 dark:text-green-400">
{tag.tag.name}
</ins>)
: (
tag.type === 'removed'
? (
<del
key={i}
className="mr-2 text-red-600 dark:text-red-400">
{tag.tag.name}
</del>)
: (
<span key={i} className="mr-2">
{tag.tag.name}
</span>))))}
</td>
<td className="p-2">
{change.createdByUser
? (
<PrefetchLink to={`/users/${ change.createdByUser.id }`}>
{change.createdByUser.name
|| `名もなきニジラー(#${ change.createdByUser.id }`}
</PrefetchLink>)
: 'bot 操作'}
<br/>
{dateString (change.createdAt)}
</td>
<td className="p-2">
<a
href="#"
onClick={async e => {
e.preventDefault ()
if (!(confirm (
`タグ『${ change.name.current }』を版 ${
change.versionNo } に差戻します.\nよろしいですか?`)))
return
try
{
await apiPut (
`/tags/${ change.tagId }`,
{ name: change.name.current,
category: change.category.current,
aliases:
change.aliases
.filter (t => t.type !== 'removed')
.map (t => t.name)
.join (' '),
parent_tags:
change.parentTags
.filter (t => t.type !== 'removed')
.map (t => t.tag.name)
.join (' ') })
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
toast ({ description: '差戻しました.' })
}
catch
{
toast ({ description: '差戻に失敗……' })
}
}}>
</a>
</td>
</tr>))}
</tbody>
</table>
</div>
<Pagination page={page} totalPages={totalPages}/>
</>)}
</MainArea>)
}) satisfies FC