f5b632ed89
Reviewed-on: #397 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
310 行
9.5 KiB
TypeScript
310 行
9.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 { useDialogue } from '@/components/dialogues/DialogueProvider'
|
|
import MainArea from '@/components/layout/MainArea'
|
|
import { toast } from '@/components/ui/use-toast'
|
|
import { SITE_TITLE } from '@/config'
|
|
import { clientAnimationTransition,
|
|
clientScrollBehaviour } from '@/lib/clientAnimation'
|
|
import { fetchPostChanges, updatePost } from '@/lib/posts'
|
|
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
|
|
import { fetchTag } from '@/lib/tags'
|
|
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
|
|
import { cn, dateString, originalCreatedAtString } from '@/lib/utils'
|
|
|
|
import type { FC, MouseEvent } from 'react'
|
|
|
|
import type { PostVersion } from '@/types'
|
|
|
|
|
|
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}
|
|
</>)
|
|
|
|
|
|
const PostHistoryPage: FC = () => {
|
|
const dialogue = useDialogue ()
|
|
const behaviourSettings = useClientBehaviourSettings ()
|
|
const animationMode = behaviourSettings.animation ?? 'normal'
|
|
const layoutTransition = clientAnimationTransition (
|
|
animationMode,
|
|
{ normal: { duration: .2, ease: 'easeOut' as const } },
|
|
)
|
|
|
|
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 tagQueryId = tagId ?? ''
|
|
const { data: tag } = useQuery ({
|
|
enabled: Boolean (tagId),
|
|
queryKey: tagsKeys.show (tagQueryId),
|
|
queryFn: () => fetchTag (tagQueryId) })
|
|
|
|
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 ()
|
|
|
|
const handleRevert = async (e: MouseEvent<HTMLAnchorElement>, change: PostVersion) => {
|
|
e.preventDefault ()
|
|
|
|
if (!(await dialogue.confirm ({
|
|
title: '差戻の確認',
|
|
description: `『${ change.title.current || change.url.current }』を版 ${
|
|
change.versionNo } に差戻します.\nよろしいですか?`,
|
|
confirmText: '差戻' })))
|
|
return
|
|
|
|
try
|
|
{
|
|
const id = change.postId
|
|
const title = change.title.current
|
|
const tags =
|
|
change.tags
|
|
.filter (t => t.type !== 'removed')
|
|
.map (t => t.name)
|
|
.filter (t => t.slice (0, 5) !== 'nico:')
|
|
.join (' ')
|
|
const parentPostIds =
|
|
(change.parentPosts ?? [])
|
|
.filter (p => p.type !== 'removed')
|
|
.map (p => p.id)
|
|
.join (' ')
|
|
const duration =
|
|
change.videoMs.current == null
|
|
? null
|
|
: String (change.videoMs.current / 1_000)
|
|
const originalCreatedFrom = change.originalCreatedFrom.current
|
|
const originalCreatedBefore = change.originalCreatedBefore.current
|
|
await updatePost ({ id, title, tags, duration, parentPostIds,
|
|
originalCreatedFrom, originalCreatedBefore },
|
|
{ force: true })
|
|
|
|
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
|
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
|
|
|
toast ({ description: '差戻しました.' })
|
|
}
|
|
catch
|
|
{
|
|
toast ({ description: '差戻に失敗……' })
|
|
}
|
|
}
|
|
|
|
useEffect (() => {
|
|
document.querySelector ('table')?.scrollIntoView ({
|
|
behavior: clientScrollBehaviour (animationMode) })
|
|
}, [animationMode, 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={animationMode === 'off' ? undefined : layoutId}
|
|
transition={{ layout: layoutTransition }}>
|
|
<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 => await handleRevert (e, change)}>
|
|
復元
|
|
</a>
|
|
</td>
|
|
</tr>)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<Pagination page={page} totalPages={totalPages}/>
|
|
</>)}
|
|
</MainArea>)
|
|
}
|
|
|
|
export default PostHistoryPage
|