このコミットが含まれているのは:
2026-10-26 19:31:00 +09:00
コミット f44a436e20
12個のファイルの変更、95行の追加、41行の削除
+19 -14
ファイルの表示
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import DateTimeField from '@/components/common/DateTimeField'
import { Button } from '@/components/ui/button'
@@ -9,18 +9,25 @@ import useDialogue from '@/lib/dialogues/useDialogue'
import { inputClass } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
import type { CountsCondition, CountsDefinition } from '@/lib/countsDefinitions'
const ConditionEditor = ({ definition, initial, save }: {
definition: CountsDefinition; initial?: CountsCondition
save: (condition: CountsCondition) => void },
const ConditionEditor = (
{ definition, initial, controls, save }: {
definition: CountsDefinition; initial?: CountsCondition
controls: DialogueFormControls
save: (condition: CountsCondition) => void },
) => {
const { label, message } = useUITranslation ('counts')
const fields = countsFilterFields (definition)
const [field, setField] = useState (initial?.field ?? 'resource_id')
const [operator, setOperator] = useState (initial?.operator ?? 'eq')
const operators = definition.fields.find (item => item.key === field)?.operators ?? []
useEffect (() => {
controls.setActions ([{ label: label (initial == null ? 'add' : 'shared:actions.edit'),
onSelect: () => save ({ ...initial, field, operator }) }])
}, [controls, field, operator, initial, label, save])
return <div className="space-y-2">
<select value={field} aria-label={message ('field').text} className={inputClass ()}
onChange={event => { setField (event.target.value); setOperator ('eq') }}>
@@ -32,8 +39,6 @@ const ConditionEditor = ({ definition, initial, save }: {
{operators.map (key => <option key={key} value={key}>
{message (`operators.${key}`).text}</option>)}
</select>
<Button type="button" onClick={() => save ({ ...initial, field, operator })}>
{label (initial == null ? 'add' : 'shared:actions.edit')}</Button>
</div>
}
@@ -48,14 +53,14 @@ const CountsAdditionalConditions = ({ params, update }: {
const conditions = readCountsJSON<CountsCondition[]> (params, 'conditions', [])
const write = (next: CountsCondition[]) => update ('conditions', JSON.stringify (next))
const edit = (index?: number) => {
void dialogue.form ({ title: label ('additionalConditions'), body: controls => (
<ConditionEditor definition={definition}
initial={index == null ? undefined : conditions[index]}
save={condition => {
write (index == null ? [...conditions, condition]
: conditions.map ((item, position) => position === index ? condition : item))
controls.close ()
}}/>) })
const initial = index == null ? undefined : conditions[index]
const save = (condition: CountsCondition) => {
write (index == null ? [...conditions, condition]
: conditions.map ((item, position) => position === index ? condition : item))
}
dialogue.form ({ title: label ('additionalConditions'), footerLayout: 'inline',
body: controls => <ConditionEditor definition={definition}
initial={initial} controls={controls} save={save}/> })
}
const patch = (index: number, change: Partial<CountsCondition>) => write (
conditions.map ((item, position) => position === index ? { ...item, ...change } : item))
+39 -10
ファイルの表示
@@ -9,7 +9,9 @@ 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 { countsUseLatest, normaliseCountsParams } from '@/lib/counts'
import { countsUseLatest, fieldsForCounts, normaliseCountsParams,
selectedCountColumns } from '@/lib/counts'
import { useCountsDefinition } from '@/lib/countsDefinitions'
import { useClientMotion } from '@/lib/useClientMotion'
import { inputClass } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
@@ -21,21 +23,36 @@ import type { CountsResource } from '@/lib/counts'
const CountsControls = (
{ resource, applied, open, setOpen, execute, displayJSON, displayRowNumbers,
reorder, actions }: {
displayThumbnail, reorder, actions }: {
resource: CountsResource; applied: URLSearchParams; open: 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 order = applied.get ('order')
const nestedJSON = applied.get ('nested_json')
const rowNumbers = applied.get ('row_numbers')
const thumbnail = selectedCountColumns (resource, applied, definition).includes ('thumbnail')
const details = applied.get ('mode') === 'details'
const hasNested = details && fieldsForCounts (resource, definition)
.some (field => field.children != null)
useEffect (() => {
setDraft (previous => {
const next = new URLSearchParams (previous)
const columns = selectedCountColumns (resource, previous, definition)
.filter (column => column !== 'thumbnail')
next.set ('columns', (thumbnail ? ['thumbnail', ...columns] : columns).join (','))
return next
})
}, [thumbnail, resource, definition])
useEffect (() => {
setDraft (previous => {
const next = new URLSearchParams (previous)
@@ -95,14 +112,32 @@ const CountsControls = (
{['summary', 'details'].map (value => <option key={value} value={value}>
{message (value).text}</option>)}
</select>
{actions}
</div>
<fieldset className="flex flex-wrap items-center gap-x-3 gap-y-1 border-b py-1
dark:border-slate-700">
<legend className="sr-only">{label ('displaySettings')}</legend>
<label className="inline-flex items-center gap-1 text-sm">
<input type="checkbox" checked={draft.get ('row_numbers') === '1'}
onChange={event => {
update ('row_numbers', event.target.checked ? '1' : '0')
displayRowNumbers (event.target.checked)
}}/>{label ('rowNumbers')}
</label>{actions}
</div>
</label>
{details && resource === 'posts' && <label
className="inline-flex items-center gap-1 text-sm">
<input type="checkbox" checked={thumbnail}
onChange={event => displayThumbnail (event.target.checked)}/>
{label ('columns.thumbnailDisplay')}
</label>}
{hasNested && <label className="inline-flex items-center gap-1 text-sm">
<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>}
</fieldset>
<AnimatedCollapse id={id} open={open}>
<div className="max-h-[35dvh] overflow-y-auto overscroll-contain
border-b py-2 dark:border-slate-700 md:max-h-[40dvh]">
@@ -111,12 +146,6 @@ const CountsControls = (
{mode === 'summary' ? <CountsAxes params={draft} update={update}/>
: <>
<legend className="mb-1 font-semibold">{label ('output')}</legend>
<label className="mb-1 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>
<CountsFieldTree resource={resource} params={draft} update={(key, value) => {
update (key, value)
if (key === 'order') reorder (value)
+3 -1
ファイルの表示
@@ -109,9 +109,11 @@ const CountsFieldTree = (
update ('children', countChildPaths (fields).filter (
key => key.includes ('.') && selected.has (key)).join (','))
}
const visibleFields = parent === ''
? fields.filter (field => field.key !== 'thumbnail') : fields
return (
<>
<FieldGroup fields={fields} path={parent} params={params} enabled selected={selected}
<FieldGroup fields={visibleFields} path={parent} params={params} enabled selected={selected}
select={select} update={update}/>
</>)
}
+7 -2
ファイルの表示
@@ -50,14 +50,19 @@ const CountsResults = (
switch (column)
{
case 'resource_id':
if (resource === 'nico_tags') return <PrefetchLink to="/nico/tags">
{row.resourceId}</PrefetchLink>
if (resource === 'nico_tags') return row.name ? (
<PrefetchLink to={`/tags/nico?${new URLSearchParams ({ name: row.name })}`}>
{row.resourceId}</PrefetchLink>) : row.resourceId
if (resource === 'tag_groups') return <PrefetchLink
to={`/wiki/${encodeURIComponent (`タググループ:${row.name ?? ''}`)}`}>
{row.resourceId}</PrefetchLink>
return <PrefetchLink to={`/${resource === 'tag_descriptions' ? 'tags' : resource}/${
encodeURIComponent (row.resourceId)}`}>
{row.resourceId}</PrefetchLink>
case 'name':
return resource === 'nico_tags' && row.name ? (
<PrefetchLink to={`/tags/nico?${new URLSearchParams ({ name: row.name })}`}>
{value}</PrefetchLink>) : value
case 'thumbnail':
{
const url = countsImageURL (row.thumbnailBase)
+1 -1
ファイルの表示
@@ -87,7 +87,7 @@ const CountsSortHeader = (
}}>
<span className="truncate" lang={label.language}>{label.text}</span>
{priority >= 0 && <span aria-hidden>
{direction === 'asc' ? '↑' : '↓'}{sorts.length > 1 && priority + 1}</span>}
{direction === 'asc' ? '▲' : '▼'}{sorts.length > 1 && priority + 1}</span>}
</Button>
<CountsResizeHandle field={field} label={label}/>
</th>)
+4 -1
ファイルの表示
@@ -237,7 +237,10 @@ const DialogueProvider: FC<Props> = ({ children }) => {
</Button>))}
</div>
<div className="flex w-full flex-col gap-2 md:w-auto md:flex-row">
<div className={cn (
'flex w-full gap-2 md:w-auto md:flex-row',
active.options.footerLayout === 'inline'
? 'flex-row justify-end [&>button]:w-auto' : 'flex-col')}>
<Button
className="w-full md:w-auto"
variant="outline"