ファイル
btrc-hub/frontend/src/components/counts/CountsResults.tsx
T
2026-10-26 22:57:39 +09:00

165 行
7.3 KiB
TypeScript

import SortableColumns from '@/components/common/SortableColumns'
import LocalisedText from '@/components/LocalisedText'
import PrefetchLink from '@/components/PrefetchLink'
import CountsRowNumber from '@/components/counts/CountsRowNumber'
import CountsNestedTable from '@/components/counts/CountsNestedTable'
import CountsSortHeader from '@/components/counts/CountsSortHeader'
import CountsResizeHandle from '@/components/counts/CountsResizeHandle'
import CountsSelectableTable from '@/components/counts/CountsSelectableTable'
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
import { changeCountSort, countColumns, countsRequestParams, fieldsForCounts,
readCountSort, selectedCountColumns, writeCountSort } from '@/lib/counts'
import { countsCellCopy, countsCellText, countsDetailClipboardTable,
countsImageURL, countsNestedRows } from '@/lib/countsDisplay'
import useDialogue from '@/lib/dialogues/useDialogue'
import { useLocale } from '@/lib/useLocale'
import { useCountsDefinition } from '@/lib/countsDefinitions'
import { useUITranslation } from '@/lib/useUITranslation'
import type { CountColumn, CountsResource,
CountsResult, CountsVersion } from '@/lib/counts'
const CountsResults = (
{ resource, data, params, pending, update }: {
resource: CountsResource
data: CountsResult
params: URLSearchParams
pending: boolean
update: (key: string, value: string) => void },
) => {
const definition = useCountsDefinition ()
const { label, message } = useUITranslation ('counts')
const locale = useLocale ()
const dialogue = useDialogue ()
const columns = selectedCountColumns (resource, params, definition)
const rowNumbers = params.get ('row_numbers') === '1'
const fields = fieldsForCounts (resource, definition)
const sorts = readCountSort (params.get ('sort') ?? 'created_at:desc')
const sort = (key: string, additive: boolean) =>
update ('sort', writeCountSort (changeCountSort (sorts, key, additive)))
const cell = (row: CountsVersion, column: CountColumn) => {
const field = fields.find (field => field.key === column)!
const value = countsCellText (row, column, locale, params)
if (field.children != null)
{
return <CountsNestedTable rows={countsNestedRows (row, column)}
fields={field.children} path={column} resource={resource}
title={message (field.label)} params={params} update={update}/>
}
switch (column)
{
case 'resource_id':
if (resource === 'nico_tags') return row.name ? (
<PrefetchLink to={`/tags/nico?${new URLSearchParams ({ name: row.name })}`}>
{row.resourceId}</PrefetchLink>) : row.resourceId
if (resource === 'tag_groups') return <PrefetchLink
to={`/wiki/${encodeURIComponent (`タググループ:${row.name ?? ''}`)}`}>
{row.resourceId}</PrefetchLink>
return <PrefetchLink to={`/${resource === 'tag_descriptions' ? 'tags' : resource}/${
encodeURIComponent (row.resourceId)}`}>
{row.resourceId}</PrefetchLink>
case 'name':
return resource === 'nico_tags' && row.name ? (
<PrefetchLink to={`/tags/nico?${new URLSearchParams ({ name: row.name })}`}>
{value}</PrefetchLink>) : value
case 'thumbnail':
{
const url = countsImageURL (row.thumbnailBase)
return url == null ? null : (
<button type="button" className="block" aria-label={message (
'shared:labels.thumbnail').text}
onClick={event => {
const trigger = event.currentTarget
void dialogue.form ({
title: label ('shared:labels.thumbnail'),
cancelText: label ('shared:dialogue.close'),
size: 'viewport',
body: () => <img src={url} alt={message ('shared:labels.thumbnail').text}
referrerPolicy="no-referrer" className="h-full w-full object-contain"/>
}).then (() => trigger.focus ({ preventScroll: true }))
}}>
<PostThumbnailPreview url={url} className="h-10 w-16"
alt={message ('shared:labels.thumbnail').text} referrerPolicy="no-referrer"/>
</button>)
}
case 'url':
case 'thumbnail_base':
return <a href={countsImageURL (value) ?? undefined} target="_blank" rel="noreferrer">
{value}</a>
case 'is_latest':
return <LocalisedText language={message ('latestValue').language}>
{value}</LocalisedText>
case 'user':
return row.createdByUser?.name ? value : (
<LocalisedText language={message (
row.createdByUser == null ? 'bot' : 'history:anonymous').language}>
{value}</LocalisedText>)
case 'event_type':
return <LocalisedText language={message (`events.${row.eventType}`).language}>
{value}</LocalisedText>
case 'category':
return <LocalisedText language={message (`common:categories.${row.category}`).language}>
{value}</LocalisedText>
default: return value
}
}
return <SortableColumns ids={columns} fixed={['thumbnail']}
name={key => message (fields.find (field => field.key === key)!.label)}
onChange={next => {
const all = fields.map (field => field.key)
const other = (params.get ('order') ?? '').split (' ').filter (key => !all.includes (key))
update ('order', [...next, ...all.filter (key => !next.includes (key)), ...other].join (' '))
}}>{columns => {
const tableColumns = rowNumbers ? ['row_number', ...columns] : columns
return (
<div className="min-h-0 flex-1 overflow-auto" data-counts-results aria-busy={pending}>
{tableColumns.length === 0 ? <p>{label ('nested.noColumns')}</p> : (
<CountsSelectableTable family={resource}
columns={tableColumns}
copyTable={() => countsDetailClipboardTable (data.versions ?? [],
columns.map (column => fields.find (field => field.key === column)!),
params, locale, rowNumbers ? (data.page - 1) * data.limit + 1 : null)}
identity={`${resource}:${countsRequestParams (params)}:${tableColumns.join (' ')}:${
(data.versions ?? []).map (row => row.id)}`}
className="text-left text-sm">
<thead className="sticky top-0 z-20 bg-background text-foreground
dark:bg-background dark:text-foreground"><tr>
{rowNumbers && <CountsRowNumber/>}
{columns.map (column => column === 'thumbnail'
? <th key={column} data-counts-heading={message (countColumns[column]).text}
className="relative px-2 py-1">
<span className="sr-only">{label (countColumns[column])}</span>
<CountsResizeHandle field={column} label={message (countColumns[column])}/>
</th>
: <CountsSortHeader key={column} field={column}
label={message (fields.find (field => field.key === column)!.label)}
dragId={column}
sorts={sorts} change={sort} structured={fields.find (
field => field.key === column)?.children != null}/>)}
</tr></thead>
<tbody>
{(data.versions ?? []).map ((row, index) => (
<tr key={row.id} className="border-b dark:border-slate-700">
{rowNumbers && <CountsRowNumber
value={(data.page - 1) * data.limit + index + 1}/>}
{columns.map (column => (
<td key={column}
className="overflow-hidden break-words border-b px-2 py-1
align-top dark:border-slate-700"
data-counts-copy={countsCellCopy (
row,
fields.find (field => field.key === column)!, params, locale)}>
{cell (row, column)}</td>))}
</tr>))}
</tbody>
</CountsSelectableTable>)}
{(data.versions?.length ?? 0) === 0
&& <p className="p-2">{label ('empty')}</p>}
</div>)
}}</SortableColumns>
}
export default CountsResults