172 行
6.3 KiB
TypeScript
172 行
6.3 KiB
TypeScript
import { useEffect, useId, useState } from 'react'
|
|
|
|
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 { normaliseCountsParams } from '@/lib/counts'
|
|
import { fetchTagByName } from '@/lib/tags'
|
|
import { inputClass } from '@/lib/utils'
|
|
import { useLocale } from '@/lib/useLocale'
|
|
import { useUITranslation } from '@/lib/useUITranslation'
|
|
|
|
import type { CountsResource } from '@/lib/counts'
|
|
|
|
|
|
const CountsControls = (
|
|
{ resource, applied, open, setOpen, execute, displayJSON, reorder }: {
|
|
resource: CountsResource
|
|
applied: URLSearchParams
|
|
open: boolean
|
|
setOpen: (open: boolean) => void
|
|
execute: (params: URLSearchParams) => void
|
|
displayJSON: (enabled: boolean) => void
|
|
reorder: (value: string) => void },
|
|
) => {
|
|
const { label, message } = useUITranslation ('counts')
|
|
const locale = useLocale ()
|
|
const id = useId ()
|
|
const [draft, setDraft] = useState (() => new URLSearchParams (applied))
|
|
const order = applied.get ('order')
|
|
const nestedJSON = applied.get ('nested_json')
|
|
useEffect (() => {
|
|
setDraft (previous => {
|
|
const next = new URLSearchParams (previous)
|
|
for (const [key, value] of Object.entries ({ order, nested_json: nestedJSON }))
|
|
{
|
|
if (value == null) next.delete (key)
|
|
else next.set (key, value)
|
|
}
|
|
return next
|
|
})
|
|
}, [order, nestedJSON])
|
|
const [error, setError] = useState (false)
|
|
const [pending, setPending] = useState (false)
|
|
const mode = draft.get ('mode') ?? 'summary'
|
|
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)
|
|
else next.delete (key)
|
|
return next
|
|
})
|
|
}
|
|
const submit = async () => {
|
|
setError (false)
|
|
setPending (true)
|
|
try
|
|
{
|
|
const next = new URLSearchParams (draft)
|
|
for (const key of resource === 'posts' ? ['tag'] : ['resource_id'])
|
|
{
|
|
const value = next.get (key)?.trim () ?? ''
|
|
if (value && !/^[1-9][0-9]*$/.test (value))
|
|
{
|
|
const tag = await fetchTagByName (value, locale)
|
|
if (tag == null)
|
|
throw new Error ()
|
|
next.set (key, String (tag.id))
|
|
}
|
|
}
|
|
next.delete ('page')
|
|
execute (normaliseCountsParams (resource, next))
|
|
}
|
|
catch
|
|
{
|
|
setError (true)
|
|
}
|
|
finally
|
|
{
|
|
setPending (false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form className="shrink-0" onSubmit={event => {
|
|
event.preventDefault ()
|
|
void submit ()
|
|
}}>
|
|
<div className="flex flex-wrap items-center gap-2 border-b pb-2 dark:border-slate-700">
|
|
<Button type="submit" size="sm" disabled={pending}>{label ('run')}</Button>
|
|
<Button type="button" size="sm" variant="outline" aria-expanded={open}
|
|
aria-controls={id} onClick={() => setOpen (!open)}>
|
|
{label ('conditions')}<span aria-hidden>{open ? '▴' : '▾'}</span>
|
|
</Button>
|
|
<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)}>
|
|
<option value="summary" lang={message ('summary').language}>
|
|
{message ('summary').text}</option>
|
|
<option value="details" lang={message ('details').language}>
|
|
{message ('details').text}</option>
|
|
</select>
|
|
</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 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')}
|
|
<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) => {
|
|
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>
|
|
<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>
|
|
<div className="grid gap-2 md:grid-cols-2">
|
|
{['from', 'before'].map (key => (
|
|
<FormField key={key} label={label (`fields.${key}`)}>
|
|
{() => <DateTimeField value={draft.get (key) ?? ''}
|
|
aria-label={message (`fields.${key}`).text}
|
|
lang={message (`fields.${key}`).language}
|
|
onChange={value => update (key, value ?? '')}/>}
|
|
</FormField>))}
|
|
</div>
|
|
<div className="grid gap-2 md:grid-cols-2">
|
|
{[['resource_id', resource === 'posts' ? 'post' : 'tag'], ['user_id', 'actor'],
|
|
...(resource === 'posts' ? [['tag', 'tag']] : []), ['limit', 'limit']]
|
|
.map (([key, field]) => (
|
|
<FormField key={key} label={label (`fields.${field}`)}>
|
|
{() => <input aria-label={message (`fields.${field}`).text}
|
|
lang={message (`fields.${field}`).language}
|
|
type={key === 'limit' ? 'number' : 'text'} min={1} max={100}
|
|
value={draft.get (key) ?? (key === 'limit' ? '20' : '')}
|
|
onChange={event => update (key, event.target.value)}
|
|
className={inputClass (false, 'py-1')}/>}
|
|
</FormField>))}
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
</form>)
|
|
}
|
|
|
|
|
|
export default CountsControls
|