#321 #321 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #330
This commit was merged in pull request #330.
This commit is contained in:
@@ -27,6 +27,7 @@ import PostSearchPage from '@/pages/posts/PostSearchPage'
|
||||
import ServiceUnavailable from '@/pages/ServiceUnavailable'
|
||||
import SettingPage from '@/pages/users/SettingPage'
|
||||
import TagDetailPage from '@/pages/tags/TagDetailPage'
|
||||
import TagHistoryPage from '@/pages/tags/TagHistoryPage'
|
||||
import TagListPage from '@/pages/tags/TagListPage'
|
||||
import TheatreDetailPage from '@/pages/theatres/TheatreDetailPage'
|
||||
import WikiDetailPage from '@/pages/wiki/WikiDetailPage'
|
||||
@@ -58,6 +59,7 @@ const RouteTransitionWrapper = ({ user, setUser }: {
|
||||
<Route path="/tags" element={<TagListPage/>}/>
|
||||
<Route path="/tags/:id" element={<TagDetailPage/>}/>
|
||||
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
|
||||
<Route path="/tags/changes" element={<TagHistoryPage/>}/>
|
||||
<Route path="/theatres/:id" element={<TheatreDetailPage/>}/>
|
||||
<Route path="/materials" element={<MaterialBasePage/>}>
|
||||
<Route index element={<MaterialListPage/>}/>
|
||||
|
||||
@@ -41,12 +41,14 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
|
||||
{ name: 'タグ', to: '/tags', subMenu: [
|
||||
{ name: 'マスタ', to: '/tags' },
|
||||
{ name: 'ニコニコ連携', to: '/tags/nico' },
|
||||
{ name: '履歴', to: '/tags/changes' },
|
||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' },
|
||||
{ component: <Separator/>, visible: tagFlg },
|
||||
{ name: `広場 (${ postCount || 0 })`,
|
||||
to: `/posts?tags=${ encodeURIComponent (tag?.name ?? '') }`,
|
||||
visible: tagFlg },
|
||||
{ name: '履歴', to: `/tags/changes?id=${ tag?.id }`, visible: false }] },
|
||||
{ name: '履歴', to: `/tags/changes?id=${ tag?.id }`,
|
||||
visible: tagFlg && tag?.category !== 'nico' }] },
|
||||
{ name: '素材', to: '/materials', visible: false, subMenu: [
|
||||
{ name: '一覧', to: '/materials' },
|
||||
{ name: '検索', to: '/materials/search', visible: false },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { match } from 'path-to-regexp'
|
||||
|
||||
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
|
||||
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { fetchTagByName, fetchTag, fetchTags } from '@/lib/tags'
|
||||
import { fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
|
||||
import { fetchWikiPage,
|
||||
fetchWikiPageByTitle,
|
||||
fetchWikiPages } from '@/lib/wiki'
|
||||
@@ -183,6 +183,17 @@ const prefetchTagShow: Prefetcher = async (qc, url) => {
|
||||
}
|
||||
|
||||
|
||||
const prefetchTagChanges: Prefetcher = async (qc, url) => {
|
||||
const id = url.searchParams.get ('id')
|
||||
const page = Number (url.searchParams.get ('page') || 1)
|
||||
const limit = Number (url.searchParams.get ('limit') || 20)
|
||||
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }),
|
||||
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }) })
|
||||
}
|
||||
|
||||
|
||||
export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
|
||||
{ test: u => ['/', '/posts', '/posts/search'].includes (u.pathname),
|
||||
run: prefetchPostsIndex },
|
||||
@@ -195,8 +206,10 @@ export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[]
|
||||
&& Boolean (mWiki (u.pathname))),
|
||||
run: prefetchWikiPageShow },
|
||||
{ test: u => u.pathname === '/tags', run: prefetchTagsIndex },
|
||||
{ test: u => u.pathname !== '/tags/nico' && Boolean (mTag (u.pathname)),
|
||||
run: prefetchTagShow }]
|
||||
{ test: u => (!(['/tags/nico', '/tags/changes'].includes (u.pathname))
|
||||
&& Boolean (mTag (u.pathname))),
|
||||
run: prefetchTagShow },
|
||||
{ test: u => u.pathname === '/tags/changes', run: prefetchTagChanges }]
|
||||
|
||||
|
||||
export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => {
|
||||
|
||||
@@ -9,9 +9,11 @@ export const postsKeys = {
|
||||
['posts', 'changes', p] as const }
|
||||
|
||||
export const tagsKeys = {
|
||||
root: ['tags'] as const,
|
||||
index: (p: FetchTagsParams) => ['tags', 'index', p] as const,
|
||||
show: (name: string) => ['tags', name] as const }
|
||||
root: ['tags'] as const,
|
||||
index: (p: FetchTagsParams) => ['tags', 'index', p] as const,
|
||||
show: (name: string) => ['tags', name] as const,
|
||||
changes: (p: { id?: string; page: number; limit: number }) =>
|
||||
['tags', 'changes', p] as const }
|
||||
|
||||
export const wikiKeys = {
|
||||
root: ['wiki'] as const,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { FetchTagsParams, Tag } from '@/types'
|
||||
import type { FetchTagsParams, Tag, TagVersion } from '@/types'
|
||||
|
||||
|
||||
export const fetchTags = async (
|
||||
@@ -45,3 +45,14 @@ export const fetchTagByName = async (name: string): Promise<Tag | null> => {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const fetchTagChanges = async (
|
||||
{ id, page, limit }: {
|
||||
id?: string
|
||||
page: number
|
||||
limit: number },
|
||||
): Promise<{
|
||||
versions: TagVersion[]
|
||||
count: number }> =>
|
||||
await apiGet ('/tags/versions', { params: { ...(id && { id }), page, limit } })
|
||||
|
||||
@@ -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
|
||||
@@ -175,6 +175,17 @@ export type Tag = {
|
||||
children?: Tag[]
|
||||
matchedAlias?: string | null }
|
||||
|
||||
export type TagVersion = {
|
||||
tagId: number
|
||||
versionNo: number
|
||||
eventType: 'create' | 'update' | 'discard' | 'restore'
|
||||
name: { current: string; prev: string | null }
|
||||
category: { current: Category; prev: Category | null }
|
||||
aliases: { name: string; type: 'context' | 'added' | 'removed' }[]
|
||||
parentTags: { tag: Tag; type: 'context' | 'added' | 'removed' }[]
|
||||
createdAt: string
|
||||
createdByUser: { id: number; name: string | null } | null }
|
||||
|
||||
export type Theatre = {
|
||||
id: number
|
||||
name: string | null
|
||||
|
||||
Reference in New Issue
Block a user