このコミットが含まれているのは:
@@ -1,28 +1,31 @@
|
||||
import { useEffect, useId, useState } from 'react'
|
||||
|
||||
import CountsStateFilters from '@/components/counts/CountsStateFilters'
|
||||
import CountsFieldTree from '@/components/counts/CountsFieldTree'
|
||||
import DateTimeField from '@/components/common/DateTimeField'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { isCountsState, normaliseCountsParams } from '@/lib/counts'
|
||||
import { countsSummaryFields } from '@/lib/countsState'
|
||||
import { fetchTagByName } from '@/lib/tags'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import type { CountsResource } from '@/lib/counts'
|
||||
|
||||
|
||||
const CountsControls = (
|
||||
{ resource, applied, open, setOpen, execute, displayJSON, reorder }: {
|
||||
{ resource, applied, open, setOpen, execute, displayJSON, reorder, actions }: {
|
||||
resource: CountsResource
|
||||
applied: URLSearchParams
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
execute: (params: URLSearchParams) => void
|
||||
displayJSON: (enabled: boolean) => void
|
||||
reorder: (value: string) => void },
|
||||
reorder: (value: string) => void
|
||||
actions?: ReactNode },
|
||||
) => {
|
||||
const { label, message } = useUITranslation ('counts')
|
||||
const locale = useLocale ()
|
||||
@@ -43,13 +46,18 @@ const CountsControls = (
|
||||
}, [order, nestedJSON])
|
||||
const [error, setError] = useState (false)
|
||||
const [pending, setPending] = useState (false)
|
||||
const mode = draft.get ('mode') ?? 'state'
|
||||
const mode = draft.get ('mode') ?? 'summary'
|
||||
const state = isCountsState (draft)
|
||||
const axes = (state && resource === 'posts') ? ['tags', 'month', 'user'] : ['month', 'user']
|
||||
const update = (key: string, value: string) => {
|
||||
setDraft (previous => {
|
||||
const next = new URLSearchParams (previous)
|
||||
if (key === 'mode') next.delete ('sort')
|
||||
if (value || key === 'columns' || key === 'children') next.set (key, value)
|
||||
if (['mode', 'axis', 'basis'].includes (key)) next.delete ('sort')
|
||||
if (key === 'axis' || key === 'basis') next.delete ('summary_columns')
|
||||
if (key === 'basis' && value === 'history' && next.get ('axis') === 'tags')
|
||||
next.set ('axis', 'month')
|
||||
if (value || ['columns', 'children', 'summary_columns'].includes (key))
|
||||
next.set (key, value)
|
||||
else next.delete (key)
|
||||
return next
|
||||
})
|
||||
@@ -60,7 +68,7 @@ const CountsControls = (
|
||||
try
|
||||
{
|
||||
const next = new URLSearchParams (draft)
|
||||
for (const key of (state || resource === 'posts') ? ['tag'] : ['resource_id'])
|
||||
for (const key of resource === 'posts' ? ['tag'] : ['resource_id'])
|
||||
{
|
||||
const value = next.get (key)?.trim () ?? ''
|
||||
if (value && !/^[1-9][0-9]*$/.test (value))
|
||||
@@ -99,14 +107,16 @@ const CountsControls = (
|
||||
<select className={inputClass (false, 'w-auto py-1')}
|
||||
aria-label={message ('mode').text} lang={message ('mode').language}
|
||||
value={mode} onChange={event => update ('mode', event.target.value)}>
|
||||
{[['state', 'summary'], ['trend', 'state.trend'], ['snapshot', 'state.details'],
|
||||
['summary', 'state.history'], ['details', 'state.historyDetails']].map (
|
||||
([value, key]) => <option key={value} value={value} lang={message (key).language}>
|
||||
{message (key).text}</option>)}
|
||||
{['summary', 'details'].map (value => (
|
||||
<option key={value} value={value} lang={message (value).language}>
|
||||
{message (value).text}</option>))}
|
||||
</select>
|
||||
{actions}
|
||||
</div>
|
||||
{error && <p role="alert">{label ('shared:messages.checkInputModern')}</p>}
|
||||
<div id={id} hidden={!open} className="border-b py-2 dark:border-slate-700">
|
||||
<div id={id} hidden={!open}
|
||||
className="max-h-[40dvh] overflow-y-auto overscroll-contain border-b py-2
|
||||
dark:border-slate-700 md:max-h-[45dvh]">
|
||||
<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>
|
||||
@@ -118,33 +128,59 @@ const CountsControls = (
|
||||
}}/>
|
||||
{label ('nested.asJSON')}
|
||||
</label>
|
||||
{(mode === 'state' || mode === 'trend') ? (
|
||||
<p className="text-sm">{label (
|
||||
mode === 'trend' ? 'state.trend' : 'state.list')}</p>) : (mode === 'summary' ? (
|
||||
<label className="flex flex-wrap items-center gap-2">
|
||||
{label ('axis')}
|
||||
<select className={inputClass (false, 'w-auto py-1')}
|
||||
value={draft.get ('axis') ?? 'month'}
|
||||
aria-label={message ('axis').text} lang={message ('axis').language}
|
||||
onChange={event => update ('axis', event.target.value)}>
|
||||
{['month', 'user'].map (axis => (
|
||||
<option key={axis} value={axis} lang={message (`axes.${axis}`).language}>
|
||||
{message (`axes.${axis}`).text}</option>))}
|
||||
</select>
|
||||
</label>) : (
|
||||
<CountsFieldTree resource={resource} params={draft} update={(key, value) => {
|
||||
<FormField label={label ('measure')}>
|
||||
{() => <select value={state ? 'state' : 'history'}
|
||||
className={inputClass (false, 'w-auto py-1')}
|
||||
aria-label={message ('measure').text} lang={message ('measure').language}
|
||||
onChange={event => update ('basis', event.target.value)}>
|
||||
<option value="state" lang={message (`state.totals.${resource}`).language}>
|
||||
{message (`state.totals.${resource}`).text}</option>
|
||||
<option value="history" lang={message ('metric').language}>
|
||||
{message ('metric').text}</option>
|
||||
</select>}
|
||||
</FormField>
|
||||
{mode === 'summary' && <label className="my-2 flex flex-wrap items-center gap-2">
|
||||
{label ('axis')}
|
||||
<select className={inputClass (false, 'w-auto py-1')}
|
||||
value={draft.get ('axis')
|
||||
?? ((state && resource === 'posts') ? 'tags' : 'month')}
|
||||
aria-label={message ('axis').text} lang={message ('axis').language}
|
||||
onChange={event => update ('axis', event.target.value)}>
|
||||
{axes.map (axis => <option key={axis} value={axis}
|
||||
lang={message (`axes.${axis}`).language}>
|
||||
{message (`axes.${axis}`).text}</option>)}
|
||||
</select>
|
||||
</label>}
|
||||
<CountsFieldTree resource={resource} params={draft}
|
||||
fields={mode === 'summary' ? countsSummaryFields (resource, draft) : undefined}
|
||||
selectionKey={mode === 'summary' ? 'summary_columns' : 'columns'}
|
||||
parent={mode === 'summary' ? 'summary' : ''} update={(key, value) => {
|
||||
update (key, value)
|
||||
if (key === 'order') reorder (value)
|
||||
}}/>))}
|
||||
}}/>
|
||||
</fieldset>
|
||||
<fieldset className="min-w-0 space-y-2" data-counts-filters>
|
||||
<legend className="mb-2 font-semibold">{label ('filters')}</legend>
|
||||
{state ? <CountsStateFilters params={draft} update={update}/> : <>
|
||||
<label className="flex items-center gap-1">
|
||||
<FormField label={label ('state.at')}>
|
||||
{() => <DateTimeField value={draft.get ('at') ?? ''}
|
||||
aria-label={message ('state.at').text} lang={message ('state.at').language}
|
||||
onChange={value => update ('at', value ?? '')}/>}
|
||||
</FormField>
|
||||
{!state && <label className="flex items-center gap-1">
|
||||
<input type="checkbox" checked={draft.get ('latest') !== '0'}
|
||||
onChange={event => update ('latest', event.target.checked ? '1' : '0')}/>
|
||||
{label ('latestOnly')}
|
||||
</label>
|
||||
</label>}
|
||||
{state && resource === 'posts' && <FormField label={label ('columns.kind')}>
|
||||
{() => <select value={draft.get ('kind') ?? 'internal'}
|
||||
className={inputClass (false, 'w-auto py-1')}
|
||||
aria-label={message ('columns.kind').text} lang={message ('columns.kind').language}
|
||||
onChange={event => update ('kind', event.target.value)}>
|
||||
{['internal', 'external'].map (kind => <option key={kind} value={kind}
|
||||
lang={message (`kinds.${kind}`).language}>
|
||||
{message (`kinds.${kind}`).text}</option>)}
|
||||
</select>}
|
||||
</FormField>}
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
{['from', 'before'].map (key => (
|
||||
<FormField key={key} label={label (`fields.${key}`)}>
|
||||
@@ -167,7 +203,6 @@ const CountsControls = (
|
||||
className={inputClass (false, 'py-1')}/>}
|
||||
</FormField>))}
|
||||
</div>
|
||||
</>}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする