dceed1caa1
Merge remote-tracking branch 'origin/main' into feature/046 #46 #46 #46 #46 #46 #46 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #339
287 lines
8.5 KiB
TypeScript
287 lines
8.5 KiB
TypeScript
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||
import { motion } from 'framer-motion'
|
||
import { useEffect } from 'react'
|
||
import { Helmet } from 'react-helmet-async'
|
||
import { useLocation } from 'react-router-dom'
|
||
|
||
import TagLink from '@/components/TagLink'
|
||
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 { apiPut } from '@/lib/api'
|
||
import { fetchPostChanges } from '@/lib/posts'
|
||
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
|
||
import { fetchTag } from '@/lib/tags'
|
||
import { cn, dateString, originalCreatedAtString } 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 tagId = query.get ('tag')
|
||
const page = Number (query.get ('page') ?? 1)
|
||
const limit = Number (query.get ('limit') ?? 20)
|
||
|
||
// 投稿列の結合で使用
|
||
let rowsCnt: number
|
||
|
||
const { data: tag } =
|
||
tagId
|
||
? useQuery ({ queryKey: tagsKeys.show (tagId),
|
||
queryFn: () => fetchTag (tagId) })
|
||
: { data: null }
|
||
|
||
const { data, isLoading: loading } = useQuery ({
|
||
queryKey: postsKeys.changes ({ ...(id && { post: id }),
|
||
...(tagId && { tag: tagId }),
|
||
page, limit }),
|
||
queryFn: () => fetchPostChanges ({ ...(id && { post: id }),
|
||
...(tagId && { tag: tagId }),
|
||
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])
|
||
|
||
const layoutIds: string[] = []
|
||
|
||
return (
|
||
<MainArea>
|
||
<Helmet>
|
||
<title>{`耕作履歴 | ${ SITE_TITLE }`}</title>
|
||
</Helmet>
|
||
|
||
<PageTitle>
|
||
耕作履歴
|
||
{id && <>: 投稿 {<PrefetchLink to={`/posts/${ id }`}>#{id}</PrefetchLink>}</>}
|
||
{tag && <>(<TagLink tag={tag} withWiki={false} withCount={false}/>)</>}
|
||
</PageTitle>
|
||
|
||
{loading ? 'Loading...' : (
|
||
<>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full min-w-[1200px] table-fixed border-collapse">
|
||
<colgroup>
|
||
{/* 投稿 */}
|
||
<col className="w-64"/>
|
||
{/* 版 */}
|
||
<col className="w-40"/>
|
||
{/* タイトル */}
|
||
<col className="w-96"/>
|
||
{/* URL */}
|
||
<col className="w-96"/>
|
||
{/* タグ */}
|
||
<col className="w-[48rem]"/>
|
||
{/* TODO: 親投稿 */}
|
||
{/* <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">URL</th>
|
||
<th className="p-2 text-left">タグ</th>
|
||
{/* TODO: 親投稿の履歴 */}
|
||
{/* <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, i) => {
|
||
const withPost = i === 0 || change.postId !== changes[i - 1].postId
|
||
if (withPost)
|
||
{
|
||
rowsCnt = 1
|
||
for (let j = i + 1;
|
||
(j < changes.length
|
||
&& change.postId === changes[j].postId);
|
||
++j)
|
||
++rowsCnt
|
||
}
|
||
|
||
let layoutId: string | undefined = `page-${ change.postId }`
|
||
if (layoutIds.includes (layoutId))
|
||
layoutId = undefined
|
||
else
|
||
layoutIds.push (layoutId)
|
||
|
||
return (
|
||
<tr key={`${ change.postId }.${ change.versionNo }`}
|
||
className={cn ('even:bg-gray-100 dark:even:bg-gray-700',
|
||
withPost && 'border-t')}>
|
||
{withPost && (
|
||
<td className="align-top p-2 bg-white dark:bg-[#242424] border-r"
|
||
rowSpan={rowsCnt}>
|
||
<PrefetchLink to={`/posts/${ change.postId }`}>
|
||
<motion.div
|
||
layoutId={layoutId}
|
||
transition={{ layout: { duration: .2, ease: 'easeOut' } }}>
|
||
<img src={change.thumbnail.current
|
||
|| change.thumbnailBase.current
|
||
|| undefined}
|
||
alt={change.title.current || change.url.current}
|
||
title={change.title.current || change.url.current || undefined}
|
||
className="w-40"/>
|
||
</motion.div>
|
||
</PrefetchLink>
|
||
</td>)}
|
||
<td className="p-2">{change.postId}.{change.versionNo}</td>
|
||
<td className="p-2 break-all">{renderDiff (change.title)}</td>
|
||
<td className="p-2 break-all">{renderDiff (change.url)}</td>
|
||
<td className="p-2">
|
||
{change.tags.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>
|
||
{/* TODO: 親投稿の履歴 */}
|
||
{/* <td className="p-2">
|
||
{change.parentPosts.map ((pp, i) => (
|
||
pp.type === 'added'
|
||
? (
|
||
<ins
|
||
key={i}
|
||
className="mr-2 text-green-600 dark:text-green-400">
|
||
{pp.title}
|
||
</ins>)
|
||
: (
|
||
pp.type === 'removed'
|
||
? (
|
||
<del
|
||
key={i}
|
||
className="mr-2 text-red-600 dark:text-red-400">
|
||
{pp.title}
|
||
</del>)
|
||
: (
|
||
<span key={i} className="mr-2">
|
||
{pp.title}
|
||
</span>))))}
|
||
</td> */}
|
||
<td className="p-2">
|
||
{change.versionNo === 1
|
||
? originalCreatedAtString (change.originalCreatedFrom.current,
|
||
change.originalCreatedBefore.current)
|
||
: renderDiff ({
|
||
current: originalCreatedAtString (
|
||
change.originalCreatedFrom.current,
|
||
change.originalCreatedBefore.current),
|
||
prev: originalCreatedAtString (
|
||
change.originalCreatedFrom.prev,
|
||
change.originalCreatedBefore.prev) })}
|
||
</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.title.current
|
||
|| change.url.current }』を版 ${
|
||
change.versionNo } に差戻します.\nよろしいですか?`)))
|
||
return
|
||
|
||
try
|
||
{
|
||
await apiPut (
|
||
`/posts/${ change.postId }`,
|
||
{ title: change.title.current,
|
||
tags: change.tags
|
||
.filter (t => t.type !== 'removed')
|
||
.map (t => t.name)
|
||
.filter (t => t.slice (0, 5) !== 'nico:')
|
||
.join (' '),
|
||
parent_post_ids:
|
||
(change.parentPosts ?? [])
|
||
.filter (p => p.type !== 'removed')
|
||
.map (p => p.id)
|
||
.join (' '),
|
||
original_created_from:
|
||
change.originalCreatedFrom.current,
|
||
original_created_before:
|
||
change.originalCreatedBefore.current })
|
||
|
||
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
|