import { useEffect, useId, useRef, useState } from 'react' import CountsAdditionalConditions from '@/components/counts/CountsAdditionalConditions' import CountsAxes from '@/components/counts/CountsAxes' import CountsFieldTree from '@/components/counts/CountsFieldTree' import AnimatedCollapse from '@/components/common/AnimatedCollapse' import DateTimeField from '@/components/common/DateTimeField' 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 { countsRequestParams, countsUseLatest, fieldsForCounts, normaliseCountsParams, readCountSort, selectedCountChildren, selectedCountColumns } from '@/lib/counts' import { countsNestedIsScalar } from '@/lib/countsDisplay' import { useCountsDefinition } from '@/lib/countsDefinitions' import { useClientMotion } from '@/lib/useClientMotion' import { inputClass } from '@/lib/utils' import { useUITranslation } from '@/lib/useUITranslation' import { readCountState, setCountState, encodeCountState } from '@/lib/queryState' import type { ReactNode } from 'react' import type { CountsResource, CountsResult } from '@/lib/counts' const CountsControls = ( { resource, applied, open, setOpen, execute, displayJSON, displayRowNumbers, displayThumbnail, reorder, actions, conditionLabels, conditionLabelsParams, pending = false }: { resource: CountsResource; applied: URLSearchParams; open: boolean conditionLabels?: CountsResult['conditionLabels'] conditionLabelsParams?: string pending?: boolean setOpen: (open: boolean) => void execute: (params: URLSearchParams) => void displayJSON: (enabled: boolean) => void displayRowNumbers: (enabled: boolean) => void displayThumbnail: (enabled: boolean) => void reorder: (value: string) => void; actions?: ReactNode }, ) => { const { label, message } = useUITranslation ('counts') const definition = useCountsDefinition () const id = useId () const motion = useClientMotion () const [draft, setDraft] = useState (() => new URLSearchParams (applied)) const labelParams = conditionLabelsParams == null ? null : new URLSearchParams (conditionLabelsParams) const thumbnail = selectedCountColumns (resource, applied, definition).includes ('thumbnail') const previousApplied = useRef ({ resource, params: new URLSearchParams (applied) }) const appliedState = applied.toString () const mode = draft.get ('mode') ?? 'summary' const columns = selectedCountColumns (resource, draft, definition) const details = mode === 'details' const nestedFlg = details && fieldsForCounts (resource, definition).some (field => columns.includes (field.key) && field.children != null && !countsNestedIsScalar (field.children, field.key, draft) && field.children.some (child => selectedCountChildren (draft) .has (`${field.key}.${child.key}`))) useEffect (() => { const nextApplied = new URLSearchParams (appliedState) const previous = previousApplied.current previousApplied.current = { resource, params: nextApplied } setDraft (draft => { if (previous.resource !== resource || previous.params.get ('definition') !== nextApplied.get ('definition')) return nextApplied const keys = new Set ([...previous.params.keys (), ...nextApplied.keys ()]) const changed = [...keys].filter (key => previous.params.get (key) !== nextApplied.get (key)) const before = selectedCountColumns (resource, previous.params, definition) const after = selectedCountColumns (resource, nextApplied, definition) const selectionChanged = before.filter (column => column !== 'thumbnail').join (' ') !== after.filter (column => column !== 'thumbnail').join (' ') const internal = ['execution_at', 'timezone', 'order', 'nested_json', 'row_numbers'] if (changed.some (key => key === 'columns' ? selectionChanged : !internal.includes (key))) return nextApplied // Internal URL enrichment and immediate display changes preserve pending edits. const next = new URLSearchParams (draft) for (const key of changed) { if (key === 'columns') { const columns = selectedCountColumns (resource, draft, definition) .filter (column => column !== 'thumbnail') next.set ('columns', (after.includes ('thumbnail') ? ['thumbnail', ...columns] : columns).join (' ')) continue } next.delete (key) for (const value of nextApplied.getAll (key)) next.append (key, value) } return next.toString () === draft.toString () ? draft : next }) }, [appliedState, resource, definition]) const history = draft.get ('definition') === 'history' const update = (key: string, value: string) => setDraft (previous => { const next = new URLSearchParams (previous) setCountState (next, key, value) if (key === 'tags') { const tokens = value.split (/\s+/).filter (token => token && token.toLowerCase () !== 'type:or') const remaining = [...tokens] const references = readCountState[]> ( previous, 'tag_refs', []).flatMap (reference => { const name = String (reference.name) const exact = remaining.indexOf (name) const index = exact >= 0 ? exact : remaining.findIndex (token => token.replace (/^not:/i, '') === name.replace (/^not:/i, '')) if (index < 0) return [] const [token] = remaining.splice (index, 1) return [token === name ? reference : { ...reference, name: token, negative: token.toLowerCase ().startsWith ('not:') }] }) for (const name of [...next.keys ()]) if (name === 'tag_refs' || name.startsWith ('tag_refs.')) next.delete (name) if (references.length > 0) setCountState (next, 'tag_refs', encodeCountState ('tag_refs', references)) } if (key === 'limit') next.delete ('page') if (next.toString () === previous.toString ()) return previous if (['rows', 'cols'].includes (key) && next.get ('sort') !== '') next.delete ('sort') if (key !== 'mode') return next const resolved = normaliseCountsParams (resource, next, definition) const axes = [...readCountState (resolved, 'rows', []), ...readCountState (resolved, 'cols', [])] const allowed = value === 'summary' ? ['count', ...axes.map ((_, index) => `a${index}`)] : fieldsForCounts (resource, definition).map (field => field.key) if (readCountSort (resolved.get ('sort')).some (sort => !allowed.includes (sort.key) && !(value === 'summary' && /^m[0-9]{1,2}$/.test (sort.key)))) next.delete ('sort') return normaliseCountsParams (resource, next, definition) }) return
{ event.preventDefault () if (pending) return const next = new URLSearchParams (draft) next.delete ('page') execute (normaliseCountsParams (resource, next, definition)) }}> {!history &&
{label ('referenceAt')} {label ('referenceHelp')}
update ('at', value ?? '')}/>
}
{actions}
{label ('displaySettings')} {details && } {details && resource === 'posts' && } {nestedFlg && }
{mode === 'summary' ? : <> {label ('output')} { update (key, value) if (key === 'order') reorder (value) }}/> }
{label ('filters')} {history &&
update ('from', value ?? '')}/> {label ('periodSeparator')} update ('through', value ?? '')}/>
}
{['resource_id', 'user_id'].map (key => {() => update (key, event.target.value)}/>} )}
{resource === 'posts' && {() =>
update ('tags', value)}/> {draft.get ('tags') && <> }
}
}
} export default CountsControls