このコミットが含まれているのは:
@@ -14,12 +14,13 @@ import type { CountsResource } from '@/lib/counts'
|
||||
|
||||
|
||||
const CountsControls = (
|
||||
{ resource, applied, open, setOpen, execute }: {
|
||||
{ resource, applied, open, setOpen, execute, displayJSON }: {
|
||||
resource: CountsResource
|
||||
applied: URLSearchParams
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
execute: (params: URLSearchParams) => void },
|
||||
execute: (params: URLSearchParams) => void
|
||||
displayJSON: (enabled: boolean) => void },
|
||||
) => {
|
||||
const { label, message } = useUITranslation ('counts')
|
||||
const locale = useLocale ()
|
||||
@@ -92,6 +93,14 @@ const CountsControls = (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<fieldset className="min-w-0" data-counts-output>
|
||||
<legend className="mb-2 font-semibold">{label ('output')}</legend>
|
||||
<label className="mb-2 flex items-center gap-1">
|
||||
<input type="checkbox" checked={draft.get ('nested_json') === '1'}
|
||||
onChange={event => {
|
||||
update ('nested_json', event.target.checked ? '1' : '0')
|
||||
displayJSON (event.target.checked)
|
||||
}}/>
|
||||
{label ('nested.asJSON')}
|
||||
</label>
|
||||
{mode === 'summary' ? (
|
||||
<label className="flex flex-wrap items-center gap-2">
|
||||
{label ('axis')}
|
||||
|
||||
@@ -4,7 +4,8 @@ import CountsSortHeader from '@/components/counts/CountsSortHeader'
|
||||
import CountsSelectableTable from '@/components/counts/CountsSelectableTable'
|
||||
import { changeCountSort, readCountSort, selectedCountChildren,
|
||||
writeCountSort } from '@/lib/counts'
|
||||
import { countsLogicalName, countsNestedOrder, countsNestedText } from '@/lib/countsDisplay'
|
||||
import { countsLogicalName, countsNestedExport,
|
||||
countsNestedOrder, countsNestedText } from '@/lib/countsDisplay'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
@@ -53,47 +54,53 @@ const CountsNestedTable = (
|
||||
<summary className="cursor-pointer text-xs" title={message ('nested.toggle').text}>
|
||||
<LocalisedText language={title.language}>{title.text}</LocalisedText> ({rows.length})
|
||||
</summary>
|
||||
{columns.length > 0 && rows.length > 0 && (
|
||||
<div className="relative isolate mt-1 max-h-72 w-full min-w-0 overflow-auto">
|
||||
<CountsSelectableTable family={path} columns={columns.map (field => field.key)}
|
||||
identity={`${path}:${params}`} name={title}
|
||||
className="border text-xs dark:border-slate-700">
|
||||
<thead className="sticky top-0 z-10 bg-background text-foreground
|
||||
dark:bg-background dark:text-foreground"><tr>
|
||||
{columns.map (field => (
|
||||
<CountsSortHeader key={field.key} field={field.key}
|
||||
label={message (field.label)}
|
||||
sorts={sorts} structured={field.children != null}
|
||||
change={(key, additive) => {
|
||||
const next = changeCountSort (sorts, key, additive)
|
||||
.map (sort => ({ ...sort, key: `${path}/${sort.key}` }))
|
||||
update ('child_sort', writeCountSort ([
|
||||
...allSorts.filter (sort => !sort.key.startsWith (`${path}/`)),
|
||||
...next]))
|
||||
}}/>))}
|
||||
</tr></thead>
|
||||
<tbody>{ordered.map ((row, index) => (
|
||||
<tr key={index} className="border-t dark:border-slate-700">
|
||||
{columns.map (field => (
|
||||
<td key={field.key}
|
||||
className="overflow-hidden break-words border-t px-2 py-1
|
||||
align-top dark:border-slate-700"
|
||||
data-counts-copy={
|
||||
field.children
|
||||
? countsNestedText (
|
||||
row[field.key] as CountRecord[] ?? [], field.children,
|
||||
`${path}.${field.key}`, params, locale)
|
||||
: countsLogicalName (row, field.key, locale).text}>
|
||||
{field.children
|
||||
? <CountsNestedTable rows={row[field.key] as CountRecord[] ?? []}
|
||||
fields={field.children} path={`${path}.${field.key}`}
|
||||
resource={resource}
|
||||
title={message (field.label)} params={params} update={update}/>
|
||||
: scalar (row, field.key)}
|
||||
</td>))}
|
||||
</tr>))}</tbody>
|
||||
</CountsSelectableTable>
|
||||
</div>)}
|
||||
{params.get ('nested_json') === '1' ? (
|
||||
<pre data-counts-scroll
|
||||
className="mt-1 max-h-72 w-full overflow-auto whitespace-pre-wrap
|
||||
break-words text-xs">
|
||||
{JSON.stringify (countsNestedExport (rows, fields, path, params, locale), null, 2)}
|
||||
</pre>) : (columns.length > 0 && rows.length > 0 && (
|
||||
<div data-counts-scroll
|
||||
className="relative isolate mt-1 max-h-72 w-full min-w-0 overflow-auto">
|
||||
<CountsSelectableTable family={path} columns={columns.map (field => field.key)}
|
||||
identity={`${path}:${params}`} name={title}
|
||||
className="border text-xs dark:border-slate-700">
|
||||
<thead className="sticky top-0 z-10 bg-background text-foreground
|
||||
dark:bg-background dark:text-foreground"><tr>
|
||||
{columns.map (field => (
|
||||
<CountsSortHeader key={field.key} field={field.key}
|
||||
label={message (field.label)}
|
||||
sorts={sorts} structured={field.children != null}
|
||||
change={(key, additive) => {
|
||||
const next = changeCountSort (sorts, key, additive)
|
||||
.map (sort => ({ ...sort, key: `${path}/${sort.key}` }))
|
||||
update ('child_sort', writeCountSort ([
|
||||
...allSorts.filter (sort => !sort.key.startsWith (`${path}/`)),
|
||||
...next]))
|
||||
}}/>))}
|
||||
</tr></thead>
|
||||
<tbody>{ordered.map ((row, index) => (
|
||||
<tr key={index} className="border-t dark:border-slate-700">
|
||||
{columns.map (field => (
|
||||
<td key={field.key}
|
||||
className="overflow-hidden break-words border-t px-2 py-1
|
||||
align-top dark:border-slate-700"
|
||||
data-counts-copy={
|
||||
field.children
|
||||
? countsNestedText (
|
||||
row[field.key] as CountRecord[] ?? [], field.children,
|
||||
`${path}.${field.key}`, params, locale)
|
||||
: countsLogicalName (row, field.key, locale).text}>
|
||||
{field.children
|
||||
? <CountsNestedTable rows={row[field.key] as CountRecord[] ?? []}
|
||||
fields={field.children} path={`${path}.${field.key}`}
|
||||
resource={resource}
|
||||
title={message (field.label)} params={params} update={update}/>
|
||||
: scalar (row, field.key)}
|
||||
</td>))}
|
||||
</tr>))}</tbody>
|
||||
</CountsSelectableTable>
|
||||
</div>))}
|
||||
</details>)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ 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, fieldsForCounts,
|
||||
import { changeCountSort, countColumns, countsRequestParams, fieldsForCounts,
|
||||
readCountSort, selectedCountColumns, writeCountSort } from '@/lib/counts'
|
||||
import { countsCellCopy, countsCellText, countsImageURL } from '@/lib/countsDisplay'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
@@ -78,8 +78,8 @@ const CountsResults = (
|
||||
return <a href={countsImageURL (value) ?? undefined} target="_blank" rel="noreferrer">
|
||||
{value}</a>
|
||||
case 'is_latest':
|
||||
return <span aria-label={message (`boolean.${row.isLatest}`).text}
|
||||
lang={message (`boolean.${row.isLatest}`).language}>{row.isLatest ? '✓' : '✗'}</span>
|
||||
return <LocalisedText language={message ('latestValue').language}>
|
||||
{value}</LocalisedText>
|
||||
case 'user':
|
||||
return row.createdByUser?.name ? value : (
|
||||
<LocalisedText language={message (
|
||||
@@ -96,7 +96,8 @@ const CountsResults = (
|
||||
{!summary && columns.length === 0 ? <p>{label ('nested.noColumns')}</p> : (
|
||||
<CountsSelectableTable family={summary ? 'summary' : resource}
|
||||
columns={summary ? ['group', 'count'] : columns}
|
||||
identity={`${resource}:${params}:${(data.versions ?? []).map (row => row.id)}`}
|
||||
identity={`${resource}:${countsRequestParams (params)}:${columns.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>
|
||||
|
||||
@@ -129,7 +129,7 @@ const CountsSelectableTable = (
|
||||
aria-activedescendant={selection == null ? undefined
|
||||
: `${id}-${selection.end.row}-${selection.end.column}`}
|
||||
style={{ tableLayout: 'fixed', width: totalWidth }}
|
||||
className={cn ('select-none border-separate border-spacing-0 outline-none',
|
||||
className={cn ('counts-grid select-none border-separate border-spacing-0 outline-none',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500',
|
||||
'dark:focus-visible:outline-blue-400',
|
||||
'[&>tbody>tr>td[data-counts-selected]]:bg-blue-100',
|
||||
@@ -144,6 +144,12 @@ const CountsSelectableTable = (
|
||||
const element = event.target as Element
|
||||
if (element.closest ('table') !== event.currentTarget
|
||||
|| element.closest (interactive) != null || event.button !== 0) return
|
||||
if (element instanceof HTMLElement && element.hasAttribute ('data-counts-scroll'))
|
||||
{
|
||||
const rect = element.getBoundingClientRect ()
|
||||
if (event.clientX >= rect.left + element.clientLeft + element.clientWidth
|
||||
|| event.clientY >= rect.top + element.clientTop + element.clientHeight) return
|
||||
}
|
||||
const point = pointFor (element)
|
||||
if (point == null) return
|
||||
event.stopPropagation ()
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
.counts-grid > tbody > tr > td {
|
||||
@apply h-6;
|
||||
}
|
||||
|
||||
.counts-grid > tbody > tr:nth-child(odd) > td:not([data-counts-selected]) {
|
||||
@apply bg-white text-slate-900;
|
||||
}
|
||||
|
||||
.counts-grid > tbody > tr:nth-child(even) > td:not([data-counts-selected]) {
|
||||
@apply bg-slate-50 text-slate-900;
|
||||
}
|
||||
|
||||
.dark .counts-grid > tbody > tr:nth-child(odd) > td:not([data-counts-selected]) {
|
||||
@apply bg-slate-950 text-slate-100;
|
||||
}
|
||||
|
||||
.dark .counts-grid > tbody > tr:nth-child(even) > td:not([data-counts-selected]) {
|
||||
@apply bg-slate-900 text-slate-100;
|
||||
}
|
||||
新しいイシューから参照
ユーザーをブロックする