このコミットが含まれているのは:
2026-10-26 18:45:28 +09:00
コミット b4139f8370
13個のファイルの変更、199行の追加、45行の削除
+25 -9
ファイルの表示
@@ -9,7 +9,7 @@ import FormField from '@/components/common/FormField'
import HelpPopover from '@/components/common/HelpPopover'
import TagInput from '@/components/common/TagInput'
import { Button } from '@/components/ui/button'
import { normaliseCountsParams } from '@/lib/counts'
import { countsUseLatest, normaliseCountsParams } from '@/lib/counts'
import { useClientMotion } from '@/lib/useClientMotion'
import { inputClass } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
@@ -20,11 +20,13 @@ import type { CountsResource } from '@/lib/counts'
const CountsControls = (
{ resource, applied, open, setOpen, execute, displayJSON, reorder, actions }: {
{ resource, applied, open, setOpen, execute, displayJSON, displayRowNumbers,
reorder, actions }: {
resource: CountsResource; applied: URLSearchParams; open: boolean
setOpen: (open: boolean) => void
execute: (params: URLSearchParams) => void
displayJSON: (enabled: boolean) => void
displayRowNumbers: (enabled: boolean) => void
reorder: (value: string) => void; actions?: ReactNode },
) => {
const { label, message } = useUITranslation ('counts')
@@ -33,17 +35,19 @@ const CountsControls = (
const [draft, setDraft] = useState (() => new URLSearchParams (applied))
const order = applied.get ('order')
const nestedJSON = applied.get ('nested_json')
const rowNumbers = applied.get ('row_numbers')
useEffect (() => {
setDraft (previous => {
const next = new URLSearchParams (previous)
for (const [key, value] of Object.entries ({ order, nested_json: nestedJSON }))
const display = { order, nested_json: nestedJSON, row_numbers: rowNumbers }
for (const [key, value] of Object.entries (display))
{
if (value == null) next.delete (key)
else next.set (key, value)
}
return next
})
}, [order, nestedJSON])
}, [order, nestedJSON, rowNumbers])
const mode = draft.get ('mode') ?? 'summary'
const history = draft.get ('definition') === 'history'
const update = (key: string, value: string) => setDraft (previous => {
@@ -66,13 +70,18 @@ const CountsControls = (
{label (key)}<DateTimeField precision="second" value={draft.get (key) ?? ''}
aria-label={message (key).text} onChange={value => update (key, value ?? '')}/>
</label>)}
</> : <label className="flex flex-wrap items-center gap-1 text-sm">
</> : <div className="flex flex-wrap items-center gap-1 text-sm">
{label ('referenceAt')}<HelpPopover label={message ('referenceAt')}>
{label ('referenceHelp')}</HelpPopover>
<DateTimeField precision="second" value={draft.get ('at') ?? ''}
aria-label={message ('referenceAt').text}
<label className="inline-flex items-center gap-1">
<input type="checkbox" checked={countsUseLatest (draft)}
onChange={event => update ('current', event.target.checked ? '1' : '0')}/>
{label ('latest')}</label>
<DateTimeField precision="second" disabled={countsUseLatest (draft)}
value={draft.get ('at') ?? ''}
aria-label={message ('referenceAt').text} lang={message ('referenceAt').language}
onChange={value => update ('at', value ?? '')}/>
</label>}
</div>}
</div>
<div className="flex flex-wrap items-center gap-2 border-b py-1 dark:border-slate-700">
<Button type="submit" size="sm">{label ('run')}</Button>
@@ -87,7 +96,14 @@ const CountsControls = (
value={mode} onChange={event => update ('mode', event.target.value)}>
{['summary', 'details'].map (value => <option key={value} value={value}>
{message (value).text}</option>)}
</select>{actions}
</select>
<label className="inline-flex items-center gap-1 text-sm">
<input type="checkbox" checked={draft.get ('row_numbers') === '1'}
onChange={event => {
update ('row_numbers', event.target.checked ? '1' : '0')
displayRowNumbers (event.target.checked)
}}/>{label ('rowNumbers')}
</label>{actions}
</div>
<AnimatedCollapse id={id} open={open}>
<div className="max-h-[35dvh] overflow-y-auto overscroll-contain
+12 -5
ファイルの表示
@@ -1,5 +1,6 @@
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'
@@ -7,7 +8,8 @@ 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, countsImageURL, countsNestedRows } from '@/lib/countsDisplay'
import { countsCellCopy, countsCellText, countsImageURL, countsNestedRows }
from '@/lib/countsDisplay'
import useDialogue from '@/lib/dialogues/useDialogue'
import { useLocale } from '@/lib/useLocale'
import { useCountsDefinition } from '@/lib/countsDefinitions'
@@ -30,6 +32,8 @@ const CountsResults = (
const locale = useLocale ()
const dialogue = useDialogue ()
const columns = selectedCountColumns (resource, params, definition)
const rowNumbers = params.get ('row_numbers') === '1'
const tableColumns = rowNumbers ? ['row_number', ...columns] : columns
const fields = fieldsForCounts (resource, definition)
const sorts = readCountSort (params.get ('sort') ?? 'created_at:desc')
const sort = (key: string, additive: boolean) =>
@@ -94,14 +98,15 @@ const CountsResults = (
}
return (
<div className="min-h-0 flex-1 overflow-auto" data-counts-results aria-busy={pending}>
{columns.length === 0 ? <p>{label ('nested.noColumns')}</p> : (
{tableColumns.length === 0 ? <p>{label ('nested.noColumns')}</p> : (
<CountsSelectableTable family={resource}
columns={columns}
identity={`${resource}:${countsRequestParams (params)}:${columns.join (',')}:${
columns={tableColumns}
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">
@@ -114,8 +119,10 @@ const CountsResults = (
field => field.key === column)?.children != null}/>)}
</tr></thead>
<tbody>
{(data.versions ?? []).map (row => (
{(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
+17
ファイルの表示
@@ -0,0 +1,17 @@
import CountsResizeHandle from '@/components/counts/CountsResizeHandle'
import { useUITranslation } from '@/lib/useUITranslation'
const CountsRowNumber = ({ value }: { value?: number }) => {
const { label, message } = useUITranslation ('counts')
if (value != null) return <td data-counts-copy={String (value)}
className="border-b px-2 py-1 text-right align-top tabular-nums dark:border-slate-700">
{value}</td>
return <th data-counts-heading={message ('rowNumbers').text} className="relative px-2 py-1">
{label ('rowNumbers')}
<CountsResizeHandle field="row_number" label={message ('rowNumbers')}/>
</th>
}
export default CountsRowNumber
+12 -5
ファイルの表示
@@ -1,4 +1,5 @@
import PrefetchLink from '@/components/PrefetchLink'
import CountsRowNumber from '@/components/counts/CountsRowNumber'
import CountsSelectableTable from '@/components/counts/CountsSelectableTable'
import CountsSortHeader from '@/components/counts/CountsSortHeader'
import { changeCountSort, countsRequestParams, readCountSort, writeCountSort } from '@/lib/counts'
@@ -15,11 +16,13 @@ const CountsSummaryResults = (
pending: boolean; update: (key: string, value: string) => void },
) => {
const { label, message } = useUITranslation ('counts')
const rowNumbers = params.get ('row_numbers') === '1'
const rows = data.rows ?? []
const cols = data.cols ?? []
const axes = [...rows, ...cols]
const cells = data.cells ?? []
const rowKeys = [...new Set (cells.map (cell => JSON.stringify (cell.values.slice (0, rows.length))))]
const rowKeys = [...new Set (cells.map (cell => JSON.stringify (
cell.values.slice (0, rows.length))))]
const columnKeys = data.columnValues?.map (values => JSON.stringify (values))
?? [...new Set (cells.map (cell => JSON.stringify (cell.values.slice (rows.length))))]
if (cols.length === 0 && columnKeys.length === 0) columnKeys.push ('[]')
@@ -46,12 +49,13 @@ const CountsSummaryResults = (
next.delete ('page')
return `/counts/${resource}?${next}`
}
const keys = [...rows.map ((_, index) => `a${index}`),
const keys = [...(rowNumbers ? ['row_number'] : []), ...rows.map ((_, index) => `a${index}`),
...columnKeys.map ((_, index) => `cell${index}`)]
return <div className="min-h-0 flex-1 overflow-auto" data-counts-results aria-busy={pending}>
<CountsSelectableTable family={`pivot_${resource}`} columns={keys}
identity={`${countsRequestParams (params)}:${keys}`} className="text-left text-sm">
<thead className="sticky top-0 z-20"><tr>
{rowNumbers && <CountsRowNumber/>}
{rows.map ((axis, index) => <CountsSortHeader key={axis.field} field={`a${index}`}
label={message (countsFieldLabel (axis.field))} sorts={sorts}
change={(key, additive) => update ('sort', writeCountSort (
@@ -65,16 +69,18 @@ const CountsSummaryResults = (
...new TextEncoder ().encode (key)].map (byte =>
byte.toString (16).padStart (2, '0')).join ('')}`
return <CountsSortHeader key={key} field={`cell${index}`}
label={{ text: name || message ('count').text, language: message ('count').language }}
label={{ text: name || message ('count').text,
language: message ('count').language }}
sorts={sorts.map (sort => sort.key === sortKey
? { ...sort, key: `cell${index}` } : sort)}
change={(_, additive) => update ('sort', writeCountSort (
changeCountSort (sorts, sortKey, additive)))}/>
})}
</tr></thead>
<tbody>{rowKeys.map (rowKey => {
<tbody>{rowKeys.map ((rowKey, rowIndex) => {
const rowValues = JSON.parse (rowKey) as (string | number | null)[]
return <tr key={rowKey}>
{rowNumbers && <CountsRowNumber value={(data.page - 1) * data.limit + rowIndex + 1}/>}
{rowValues.map ((value, index) => <td key={index}
className="overflow-hidden break-words border px-2 py-1 dark:border-slate-700"
data-counts-copy={display (value, rows[index], index)}>
@@ -82,7 +88,8 @@ const CountsSummaryResults = (
{columnKeys.map (columnKey => {
const columnValues = JSON.parse (columnKey) as (string | number | null)[]
const values = [...rowValues, ...columnValues]
const cell = cells.find (cell => JSON.stringify (cell.values) === JSON.stringify (values))
const cell = cells.find (cell => JSON.stringify (
cell.values) === JSON.stringify (values))
return <td key={columnKey} data-counts-copy={String (cell?.count ?? 0)}
className="border px-2 py-1 dark:border-slate-700">
<PrefetchLink to={drill (values)}>{cell?.count ?? 0}</PrefetchLink>