このコミットが含まれているのは:
2026-10-26 12:44:22 +09:00
コミット f91f9d0eb5
23個のファイルの変更、637行の追加、316行の削除
+61 -98
ファイルの表示
@@ -1,116 +1,78 @@
import { DndContext, MouseSensor, TouchSensor, closestCenter,
useDraggable, useDroppable, useSensor, useSensors } from '@dnd-kit/core'
import { useId, useState } from 'react'
import { countChildPaths, fieldsForCounts, orderedCountFields, reorderCountFields,
selectedCountChildren, selectedCountColumns, selectedCountFields } from '@/lib/counts'
import { cn } from '@/lib/utils'
import AnimatedCollapse from '@/components/common/AnimatedCollapse'
import SortableList from '@/components/common/SortableList'
import { countChildPaths, fieldsForCounts, orderedCountFields,
selectedCountChildren, selectedCountColumns, selectedCountFields } from '@/lib/counts'
import { useCountsDefinition } from '@/lib/countsDefinitions'
import { useClientMotion } from '@/lib/useClientMotion'
import { useUITranslation } from '@/lib/useUITranslation'
import type { CountField, CountsResource } from '@/lib/counts'
type BranchProps = {
field: CountField
path: string
enabled: boolean
selected: Set<string>
params: URLSearchParams
field: CountField; path: string; enabled: boolean
selected: Set<string>; params: URLSearchParams
select: (path: string, checked: boolean) => void
update: (key: string, value: string) => void
move: (from: string, to: string) => void
previous?: string
next?: string }
update: (key: string, value: string) => void }
const FieldBranch = (
{ field, path, enabled, selected, params, select, update, move, previous, next }: BranchProps,
) => {
const FieldBranch = ({ field, path, enabled, selected, params, select, update }: BranchProps) => {
const { label, message } = useUITranslation ('counts')
const fixed = path === 'thumbnail'
const drag = useDraggable ({ id: field.key, disabled: fixed })
const drop = useDroppable ({ id: field.key, disabled: fixed })
const [open, setOpen] = useState (false)
const id = useId ()
const motion = useClientMotion ()
const checked = selected.has (path)
const control = <label className="inline-flex items-center gap-1">
<input type="checkbox" checked={checked} disabled={!enabled}
onClick={event => event.stopPropagation ()}
onChange={event => select (path, event.target.checked)}/>
{label (field.label)}
</label>
return (
<div ref={drop.setNodeRef} className={cn ('max-w-full rounded border border-slate-200 px-1',
'dark:border-slate-700',
drop.isOver && 'outline outline-1 outline-blue-500 dark:outline-blue-400')}>
<div ref={drag.setNodeRef} className="flex min-w-0 flex-1 items-start gap-1"
style={drag.transform == null ? undefined : {
position: 'relative', zIndex: 1,
transform: `translate3d(${drag.transform.x}px, ${drag.transform.y}px, 0)` }}>
{fixed ? <span className="w-6 shrink-0"/> : (
<button type="button" ref={drag.setActivatorNodeRef}
className="mt-1 w-6 shrink-0 cursor-grab touch-none active:cursor-grabbing"
{...drag.attributes} {...drag.listeners}
aria-keyshortcuts="ArrowUp ArrowDown"
aria-label={message ('reorder', { column: message (field.label).text }).text}
lang={message ('reorder').language}
onClick={event => event.stopPropagation ()}
onKeyDown={event => {
if (!['ArrowUp', 'ArrowDown'].includes (event.key)) return
event.preventDefault ()
event.stopPropagation ()
const target = event.key === 'ArrowUp' ? previous : next
if (target != null) move (field.key, target)
}}><span aria-hidden>⠿</span></button>)}
{field.children ? (
<details className="my-1 min-w-0 flex-1">
<summary className="cursor-pointer">{control}</summary>
<div className="ml-2 border-l pl-1 dark:border-slate-700">
<FieldGroup fields={field.children} path={path} params={params}
enabled={enabled && checked} selected={selected} select={select}
update={update}/>
</div>
</details>) : <div className="my-1">{control}</div>}
</div>
</div>)
return <>
<div className="flex items-center gap-1">
<label className="inline-flex items-center gap-1">
<input type="checkbox" checked={checked} disabled={!enabled}
onChange={event => select (path, event.target.checked)}/>
{label (field.label)}
</label>
{field.children != null && <button type="button" aria-expanded={open}
aria-controls={id} aria-label={message (field.label).text}
lang={message (field.label).language} onClick={() => setOpen (!open)}
className="rounded px-1 focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-blue-500 dark:focus-visible:ring-blue-400">
<span aria-hidden className="inline-block" style={{
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
transition: `transform ${motion.duration ('collapse')}ms ${motion.easing}` }}>▸</span>
</button>}
</div>
{field.children != null && <AnimatedCollapse open={open} id={id}>
<div className="ml-2 border-l pl-1 dark:border-slate-700">
<FieldGroup fields={field.children} path={path} params={params}
enabled={enabled && checked} selected={selected} select={select} update={update}/>
</div>
</AnimatedCollapse>}
</>
}
const FieldGroup = (
{ fields, path, params, enabled, selected, select, update }: {
fields: CountField[]
path: string
params: URLSearchParams
enabled: boolean
selected: Set<string>
select: BranchProps['select']
update: BranchProps['update'] },
const FieldGroup = ({ fields, path, params, enabled, selected, select, update }: {
fields: CountField[]; path: string; params: URLSearchParams
enabled: boolean; selected: Set<string>; select: BranchProps['select']
update: BranchProps['update'] },
) => {
const { message } = useUITranslation ('counts')
const ordered = orderedCountFields (fields, params, path)
const movable = ordered.filter (field => path !== '' || field.key !== 'thumbnail')
const sensors = useSensors (
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
useSensor (TouchSensor, { activationConstraint: { delay: 350, tolerance: 8 } }))
const move = (from: string, to: string) =>
update ('order', reorderCountFields (fields, params, path, from, to))
return (
<DndContext sensors={sensors} collisionDetection={closestCenter}
accessibility={{ announcements: {
onDragStart: () => undefined, onDragOver: () => undefined,
onDragMove: () => undefined,
onDragEnd: () => undefined, onDragCancel: () => undefined },
screenReaderInstructions: { draggable: message ('output').text } }}
onDragEnd={({ active, over }) => {
if (over != null && active.id !== over.id) move (String (active.id), String (over.id))
}}>
<div className="flex flex-wrap items-start gap-1">
{ordered.map (field => {
const position = movable.findIndex (item => item.key === field.key)
return <FieldBranch key={field.key} field={field}
path={path ? `${path}.${field.key}` : field.key}
enabled={enabled} selected={selected} params={params}
select={select} update={update} move={move}
previous={movable[position - 1]?.key} next={movable[position + 1]?.key}/>
})}
</div>
</DndContext>)
const prefix = path ? `${path}.` : ''
return <SortableList groups={{ items: ordered.map (field => field.key) }}
fixed={path === '' ? ['thumbnail'] : []}
name={key => message ('reorder', {
column: message (fields.find (field => field.key === key)!.label).text })}
renderGroup={(_, content) => content}
onChange={groups => {
const current = fields.map (field => `${prefix}${field.key}`)
const other = (params.get ('order') ?? '').split (',')
.filter (key => key && !current.includes (key))
update ('order', [...other, ...groups.items.map (key => `${prefix}${key}`)].join (','))
}} renderItem={key => {
const field = fields.find (field => field.key === key)!
return <FieldBranch field={field} path={`${prefix}${key}`} enabled={enabled}
selected={selected} params={params} select={select} update={update}/>
}}/>
}
@@ -124,8 +86,9 @@ const CountsFieldTree = (
params: URLSearchParams
update: (key: string, value: string) => void },
) => {
const fields = suppliedFields ?? fieldsForCounts (resource)
const roots = suppliedFields == null ? selectedCountColumns (resource, params)
const definition = useCountsDefinition ()
const fields = suppliedFields ?? fieldsForCounts (resource, definition)
const roots = suppliedFields == null ? selectedCountColumns (resource, params, definition)
: selectedCountFields (fields, params, selectionKey, parent).map (field => field.key)
const selected = selectedCountChildren (params)
const rootPath = (key: string) => parent ? `${parent}.${key}` : key