このコミットが含まれているのは:
@@ -16,12 +16,15 @@ import { Button } from '@/components/ui/button'
|
||||
import { countsRequestParams, exportCounts, fieldsForCounts, fetchCounts,
|
||||
isCountsState, normaliseCountsParams, selectedCountColumns } from '@/lib/counts'
|
||||
import { countsExportLabels } from '@/lib/countsDisplay'
|
||||
import { selectedCountsSummaryFields } from '@/lib/countsState'
|
||||
import { countsAxisFields, countsResources, countsFieldLabel, readCountsJSON }
|
||||
from '@/lib/countsDefinitions'
|
||||
import { countsKeys } from '@/lib/queryKeys'
|
||||
import { cn, inputClass } from '@/lib/utils'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { CountsAxis } from '@/lib/countsDefinitions'
|
||||
|
||||
import type { CountsResource } from '@/lib/counts'
|
||||
|
||||
|
||||
@@ -40,19 +43,21 @@ const CountsResourcePage = ({ resource }: { resource: CountsResource }) => {
|
||||
const mode = params.get ('mode') ?? 'summary'
|
||||
const state = isCountsState (params)
|
||||
const cacheParams = new URLSearchParams (request)
|
||||
cacheParams.set ('basis', state ? 'state' : 'history')
|
||||
cacheParams.set ('definition', state ? 'latest' : 'history')
|
||||
const [format, setFormat] = useState<'json' | 'csv'> ('json')
|
||||
const [exporting, setExporting] = useState (false)
|
||||
const [exportError, setExportError] = useState (false)
|
||||
const { data, isPending, isError, isPlaceholderData, refetch } = useQuery ({
|
||||
enabled: params.has ('timezone'),
|
||||
queryKey: countsKeys.index (resource, cacheParams.toString (), locale),
|
||||
queryFn: () => fetchCounts (resource, params, locale), retry: false,
|
||||
placeholderData: (previous, query) => {
|
||||
const previousParams = new URLSearchParams (String (query?.queryKey[3] ?? ''))
|
||||
return (query?.queryKey[1] === resource && query?.queryKey[2] === locale
|
||||
&& previousParams.get ('mode') === mode
|
||||
&& previousParams.get ('axis') === params.get ('axis')
|
||||
&& previousParams.get ('basis') === cacheParams.get ('basis')) ? previous : undefined
|
||||
&& previousParams.get ('rows') === params.get ('rows')
|
||||
&& previousParams.get ('cols') === params.get ('cols')
|
||||
&& previousParams.get ('definition') === cacheParams.get ('definition')) ? previous : undefined
|
||||
} })
|
||||
|
||||
useEffect (() => {
|
||||
@@ -61,12 +66,21 @@ const CountsResourcePage = ({ resource }: { resource: CountsResource }) => {
|
||||
}, [canonical, source, setParams])
|
||||
|
||||
useEffect (() => {
|
||||
if (data?.at == null || isPlaceholderData) return
|
||||
const next = new URLSearchParams (canonical)
|
||||
if (!next.get ('at')) next.set ('at', data.at)
|
||||
const timezone = Intl.DateTimeFormat ().resolvedOptions ().timeZone
|
||||
if (params.get ('timezone') !== timezone)
|
||||
{
|
||||
const next = new URLSearchParams (canonical)
|
||||
next.set ('timezone', timezone)
|
||||
setParams (next, { replace: true })
|
||||
}
|
||||
else if (data?.executionAt != null && !isPlaceholderData && !params.has ('execution_at'))
|
||||
{
|
||||
const next = new URLSearchParams (canonical)
|
||||
next.set ('execution_at', data.executionAt)
|
||||
setParams (next, { replace: true })
|
||||
}
|
||||
}, [canonical, data, isPlaceholderData, params, setParams])
|
||||
|
||||
if (next.toString () !== canonical) setParams (next, { replace: true })
|
||||
}, [canonical, data, isPlaceholderData, setParams])
|
||||
|
||||
const update = (key: string, value: string) => {
|
||||
const next = new URLSearchParams (params)
|
||||
@@ -80,18 +94,19 @@ const CountsResourcePage = ({ resource }: { resource: CountsResource }) => {
|
||||
setExportError (false)
|
||||
try
|
||||
{
|
||||
const fields = mode === 'summary' ? selectedCountsSummaryFields (resource, params)
|
||||
const fields = mode === 'summary' ? [
|
||||
...[...readCountsJSON<CountsAxis[]> (params, 'rows', []),
|
||||
...readCountsJSON<CountsAxis[]> (params, 'cols', [])].map (axis => ({
|
||||
key: axis.field, label: countsFieldLabel (axis.field) })),
|
||||
{ key: 'count', label: 'count' }]
|
||||
: selectedCountColumns (resource, params).map (column =>
|
||||
fieldsForCounts (resource).find (field => field.key === column)!)
|
||||
const file = await exportCounts (resource, params, locale, format,
|
||||
await exportCounts (resource, params, locale, format,
|
||||
fields.map (field => field.key), fields.map (field => message (field.label).text),
|
||||
countsExportLabels (resource, locale))
|
||||
const url = URL.createObjectURL (file)
|
||||
const link = document.createElement ('a')
|
||||
link.href = url
|
||||
link.download = `counts-${resource}.${format}`
|
||||
link.click ()
|
||||
setTimeout (() => URL.revokeObjectURL (url), 1000)
|
||||
{ ...countsExportLabels (resource, locale),
|
||||
...Object.fromEntries (countsAxisFields (resource, !state).map (field => [
|
||||
field.key, message (field.label).text])), count: message ('count').text })
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -117,17 +132,23 @@ const CountsResourcePage = ({ resource }: { resource: CountsResource }) => {
|
||||
lang={message ('target').language}>
|
||||
<h2 className="mb-2 font-semibold">{label ('target')}</h2>
|
||||
<ul className="space-y-1">
|
||||
{(['posts', 'tags'] as const).map (target => {
|
||||
const next = new URLSearchParams (params)
|
||||
for (const key of ['resource_id', 'tag', 'columns', 'summary_columns',
|
||||
'sort', 'page', 'children', 'child_sort']) next.delete (key)
|
||||
return <li key={target}><PrefetchLink
|
||||
to={`/counts/${target}?${normaliseCountsParams (target, next)}`}
|
||||
aria-current={target === resource ? 'page' : undefined}
|
||||
className={cn ('block py-1', target === resource && 'font-bold underline')}>
|
||||
{label (target === 'posts' ? 'history:post' : 'navigation:tags')}
|
||||
</PrefetchLink></li>
|
||||
})}
|
||||
{countsResources.map (target => <li key={target}>
|
||||
<span>{label (`resources.${target}`)}</span>
|
||||
<ul className="ml-3">
|
||||
{['latest', 'history'].map (definition => {
|
||||
const next = new URLSearchParams ({ definition, mode })
|
||||
for (const key of ['timezone', 'at', 'execution_at'])
|
||||
if (params.get (key)) next.set (key, params.get (key)!)
|
||||
const active = target === resource
|
||||
&& params.get ('definition') === definition
|
||||
return <li key={definition}><PrefetchLink
|
||||
to={`/counts/${target}?${next}`}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
className={cn ('block py-1', active && 'font-bold underline')}>
|
||||
{label (definition)}</PrefetchLink></li>
|
||||
})}
|
||||
</ul>
|
||||
</li>)}
|
||||
</ul>
|
||||
</nav>
|
||||
</SidebarComponent>
|
||||
@@ -153,7 +174,7 @@ const CountsResourcePage = ({ resource }: { resource: CountsResource }) => {
|
||||
</select>
|
||||
<Button type="button" variant="outline" size="sm"
|
||||
disabled={data == null || isError || isPlaceholderData
|
||||
|| exporting || !params.get ('at')}
|
||||
|| exporting || !params.get ('execution_at')}
|
||||
onClick={() => void download ()}>{label ('export')}</Button>
|
||||
</>}/>
|
||||
{exportError && <p role="alert">{label ('common:fetchError')}</p>}
|
||||
@@ -161,14 +182,7 @@ const CountsResourcePage = ({ resource }: { resource: CountsResource }) => {
|
||||
{isPending && <p>{label ('shared:messages.loadingEllipsis')}</p>}
|
||||
{isError && <p role="alert">{label ('common:fetchError')}</p>}
|
||||
{data != null && <>
|
||||
{data.at && <div className="my-1 flex flex-wrap gap-x-4 gap-y-1 text-xs">
|
||||
<span>{label ('state.boundary')}:{data.at.replace ('T', ' ')}</span>
|
||||
{data.totals && (['posts', 'tags', 'assignments'] as const).map (key => (
|
||||
<span key={key}>{label (`state.totals.${key}`)}:
|
||||
{data.totals![key] ?? '—'}
|
||||
</span>))}
|
||||
</div>}
|
||||
{!state && <p className="my-1 text-xs">{label ('total', { count: data.count })}</p>}
|
||||
<p className="my-1 text-xs">{label ('recordTotal', { count: data.count })}</p>
|
||||
{mode === 'summary' ? <CountsSummaryResults resource={resource}
|
||||
data={data} params={params}
|
||||
pending={isPlaceholderData} update={update}/> : (
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする