165 行
7.3 KiB
TypeScript
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
|