ファイル
btrc-hub/frontend/src/pages/materials/MaterialHistoryPage.tsx
T
2026-10-18 12:18:52 +09:00

207 行
6.6 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { fetchMaterialChanges } from '@/lib/materials'
import { materialsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useUITranslation } from '@/lib/useUITranslation'
import { localisedDateString, inputClass } from '@/lib/utils'
import type { FC, FormEvent } from 'react'
const MaterialHistoryPage: FC = () => {
const { message, label } = useUITranslation ('history')
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation ()
const navigate = useLocaleNavigate ()
const query = new URLSearchParams (location.search)
const materialId = query.get ('material_id') ?? ''
const tag = query.get ('tag') ?? ''
const eventType = query.get ('event_type') ?? ''
const page = Number (query.get ('page') ?? 1)
const limit = Number (query.get ('limit') ?? 20)
const [materialIdInput, setMaterialIdInput] = useState (materialId)
const [tagInput, setTagInput] = useState (tag)
const [eventTypeInput, setEventTypeInput] = useState (eventType)
useEffect (() => {
setMaterialIdInput (materialId)
setTagInput (tag)
setEventTypeInput (eventType)
}, [eventType, materialId, tag])
const { data, isLoading, isError } = useQuery ({
queryKey: materialsKeys.changes ({
...(materialId && { materialId }),
...(tag && { tag }),
...(eventType && { eventType }),
page,
limit }, locale),
queryFn: () => fetchMaterialChanges ({
...(materialId && { materialId }),
...(tag && { tag }),
...(eventType && { eventType }),
page,
limit }, locale) })
const versions = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
useEffect (() => {
document.querySelector ('table')?.scrollIntoView ({
behavior: clientScrollBehaviour (animationMode) })
}, [animationMode, location.search])
const handleSearch = (event: FormEvent) => {
event.preventDefault ()
const qs = new URLSearchParams ()
if (materialIdInput.trim ())
qs.set ('material_id', materialIdInput.trim ())
if (tagInput.trim ())
qs.set ('tag', tagInput.trim ())
if (eventTypeInput.trim ())
qs.set ('event_type', eventTypeInput.trim ())
qs.set ('page', '1')
qs.set ('limit', String (limit))
navigate (`/materials/changes?${ qs.toString () }`)
}
return (
<MainArea>
<Helmet>
<title lang={message ('materialTitle').language}>
{message ('materialTitle').text} | {SITE_TITLE}
</title>
</Helmet>
<div className="space-y-5">
<PageTitle>{label ('materialTitle')}</PageTitle>
<form
onSubmit={handleSearch}
className="max-w-3xl rounded-lg border border-stone-200 bg-white p-4
text-stone-900 dark:border-stone-700 dark:bg-stone-900
dark:text-stone-100">
<div className="space-y-3">
<FormField label={label ('materialId')}>
{({ invalid }) => (
<input
type="text"
value={materialIdInput}
aria-label={message ('materialId').text}
lang={message ('materialId').language}
onChange={e => setMaterialIdInput (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label={label ('tagName')}>
{({ invalid }) => (
<input
type="text"
value={tagInput}
aria-label={message ('tagName').text}
lang={message ('tagName').language}
onChange={e => setTagInput (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
</div>
<button
type="submit"
className="mt-4 rounded-full border border-stone-300 bg-white px-4 py-2
text-sm text-stone-900 hover:bg-stone-100 dark:border-stone-700
dark:bg-stone-900 dark:text-stone-100 dark:hover:bg-stone-800">
{label ('filter')}
</button>
</form>
{isLoading
? label ('common:loading')
: (isError
? (
<p className="text-red-600 dark:text-red-300" role="alert">
{label ('materialError')}
</p>)
: (
<>
<div className="overflow-x-auto">
<table className="w-full min-w-[1200px] table-fixed border-collapse">
<colgroup>
<col className="w-48"/>
<col className="w-28"/>
<col className="w-24"/>
<col className="w-64"/>
<col className="w-64"/>
<col className="w-80"/>
<col className="w-80"/>
<col className="w-48"/>
</colgroup>
<thead className="border-b-2 border-black dark:border-white">
<tr>
<th className="p-2 text-left">{label ('wiki:dateTime')}</th>
<th className="p-2 text-left">{label ('materialId')}</th>
<th className="p-2 text-left">{label ('version')}</th>
<th className="p-2 text-left">{label ('tagName')}</th>
<th className="p-2 text-left">{label ('filename')}</th>
<th className="p-2 text-left">{label ('shared:url')}</th>
<th className="p-2 text-left">{label ('exportPath')}</th>
<th className="p-2 text-left">{label ('actor')}</th>
</tr>
</thead>
<tbody>
{versions.map (version => (
<tr
key={version.id}
className="even:bg-gray-100 dark:even:bg-gray-700">
<td className="p-2">
{localisedDateString (version.createdAt, locale)}
</td>
<td className="p-2">
<PrefetchLink to={`/materials/${ version.materialId }`}>
#{version.materialId}
</PrefetchLink>
</td>
<td className="p-2">{version.versionNo}</td>
<td className="p-2 break-all">{version.tagName}</td>
<td className="p-2 break-all">{version.fileFilename}</td>
<td className="p-2 break-all">{version.url}</td>
<td className="p-2 break-all">
{version.exportPathsJson.legacy_drive}
</td>
<td className="p-2">
{version.createdByUser
? version.createdByUser.name || `#${ version.createdByUser.id }`
: label ('bot')}
</td>
</tr>))}
</tbody>
</table>
</div>
<Pagination page={page} totalPages={totalPages}/>
</>))}
</div>
</MainArea>)
}
export default MaterialHistoryPage