このコミットが含まれているのは:
2026-10-31 19:07:14 +09:00
コミット 4f3990feb6
3個のファイルの変更、85行の追加、81行の削除
+75 -71
ファイルの表示
@@ -9,7 +9,6 @@ import { CSS } from '@dnd-kit/utilities'
import SortableItem from '@/components/common/SortableItem'
import { useClientMotion } from '@/lib/useClientMotion'
import { cn } from '@/lib/utils'
import type { ReactNode } from 'react'
@@ -19,8 +18,10 @@ type Name = { text: string; language: string }
const sameGroups = (left: Groups, right: Groups): boolean =>
Object.keys (left).length === Object.keys (right).length
&& Object.keys (left).every (key => left[key].length === right[key]?.length
&& left[key].every ((id, index) => id === right[key][index]))
&& (
Object.keys (left).every (key => (
left[key].length === right[key]?.length
&& left[key].every ((id, index) => id === right[key][index]))))
const Group = ({ id, children }: { id: string; children: ReactNode }) => {
@@ -31,11 +32,11 @@ const Group = ({ id, children }: { id: string; children: ReactNode }) => {
const SortableList = (
{ groups, name, handleName, renderItem, renderGroup, onChange, fixed = [] }: {
groups: Groups; name: (id: string) => Name
handleName?: (id: string) => Name
renderItem: (id: string, group: string) => ReactNode
renderGroup: (id: string, content: ReactNode, over: boolean) => ReactNode
onChange: (groups: Groups) => void; fixed?: string[] },
groups: Groups; name: (id: string) => Name
handleName?: (id: string) => Name
renderItem: (id: string, group: string) => ReactNode
renderGroup: (id: string, content: ReactNode, over: boolean) => ReactNode
onChange: (groups: Groups) => void; fixed?: string[] },
) => {
const drag = useRef<{ id: string; groups: Groups } | null> (null)
const scope = useId ()
@@ -87,69 +88,72 @@ const SortableList = (
setActive (null)
setOverGroup (null)
}
return <DndContext sensors={sensors} collisionDetection={args => {
const hits = pointerWithin (args)
const group = hits.find (hit => String (hit.id).startsWith ('group:'))
const groupName = group?.id.toString ().slice (6)
const item = groupName != null && hits.some (hit =>
owner (String (hit.id), groups) === groupName)
return group != null && !item ? [group] : closestCenter (args)
}}
accessibility={{ announcements: {
onDragStart: event => accessibleName (String (event.active.id)).text,
onDragOver: event => event.over == null ? undefined : (
event.over.id.toString ().startsWith ('group:') ? undefined
: accessibleName (String (event.over.id)).text),
onDragMove: () => undefined, onDragEnd: () => undefined,
onDragCancel: () => undefined },
screenReaderInstructions: {
draggable: active == null ? '' : accessibleName (active).text } }}
onDragStart={event => {
const id = String (event.active.id)
drag.current = { id, groups }
setActive (id)
setOverGroup (owner (id, groups) ?? null)
}} onDragOver={event => {
const id = event.over?.id.toString ()
const group = id == null ? null
: (id.startsWith ('group:') ? id.slice (6) : owner (id, groups) ?? null)
setOverGroup (previous => previous === group ? previous : group)
}} onDragCancel={reset} onDragEnd={event => {
const session = drag.current
reset ()
if (session == null || event.over == null || session.id !== String (event.active.id)
|| !sameGroups (session.groups, groups)) return
const next = relocate (groups, session.id, String (event.over.id))
if (!sameGroups (groups, next)) onChange (next)
}}>
{/* Keep indexes and droppable geometry stable until the drop is committed. */}
{Object.entries (groups).map (([group, ids]) => <Group key={group} id={group}>
{renderGroup (group, <SortableContext items={ids} strategy={rectSortingStrategy}>
<div className="flex min-h-10 flex-wrap items-start gap-1 rounded p-1">
{ids.map (id => <SortableItem key={id} id={id}
name={accessibleName (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
moveGroup={direction => moveGroup (id, direction)}>
{renderItem (id, group)}
</SortableItem>)}
</div>
</SortableContext>, overGroup === group)}
</Group>)}
{typeof document !== 'undefined' && createPortal (
<DragOverlay adjustScale={false}
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
dropAnimation={motion.reduced ? null : {
duration: motion.duration ('drop'), easing: motion.easing,
keyframes: ({ transform }) => [
{ transform: CSS.Translate.toString (transform.initial) },
{ transform: CSS.Translate.toString (transform.final) }] }}>
{active == null ? null : <div lang={name (active).language}
className="w-max max-w-[min(20rem,calc(100vw_-_2rem))] truncate rounded
border border-blue-500 bg-white px-2 py-1 text-sm
text-slate-900 shadow-lg dark:border-blue-400 dark:bg-slate-800 dark:text-slate-100">
{name (active).text}</div>}
</DragOverlay>, document.body)}
</DndContext>
return (
<DndContext
sensors={sensors}
collisionDetection={args => {
const hits = pointerWithin (args)
const group = hits.find (hit => String (hit.id).startsWith ('group:'))
const groupName = group?.id.toString ().slice (6)
const item = groupName != null && hits.some (hit =>
owner (String (hit.id), groups) === groupName)
return group != null && !item ? [group] : closestCenter (args)
}}
accessibility={{ announcements: {
onDragStart: event => accessibleName (String (event.active.id)).text,
onDragOver: event => event.over == null ? undefined : (
event.over.id.toString ().startsWith ('group:') ? undefined
: accessibleName (String (event.over.id)).text),
onDragMove: () => undefined, onDragEnd: () => undefined,
onDragCancel: () => undefined },
screenReaderInstructions: {
draggable: active == null ? '' : accessibleName (active).text } }}
onDragStart={event => {
const id = String (event.active.id)
drag.current = { id, groups }
setActive (id)
setOverGroup (owner (id, groups) ?? null)
}}
onDragOver={event => {
const id = event.over?.id.toString ()
const group = id == null ? null
: (id.startsWith ('group:') ? id.slice (6) : owner (id, groups) ?? null)
setOverGroup (previous => previous === group ? previous : group)
}} onDragCancel={reset} onDragEnd={event => {
const session = drag.current
reset ()
if (session == null || event.over == null || session.id !== String (event.active.id)
|| !sameGroups (session.groups, groups)) return
const next = relocate (groups, session.id, String (event.over.id))
if (!sameGroups (groups, next)) onChange (next)
}}>
{/* Keep indexes and droppable geometry stable until the drop is committed. */}
{Object.entries (groups).map (([group, ids]) => <Group key={group} id={group}>
{renderGroup (group, <SortableContext items={ids} strategy={rectSortingStrategy}>
<div className="flex min-h-10 flex-wrap items-start gap-1 rounded p-1">
{ids.map (id => <SortableItem key={id} id={id}
name={accessibleName (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
moveGroup={direction => moveGroup (id, direction)}>
{renderItem (id, group)}
</SortableItem>)}
</div>
</SortableContext>, overGroup === group)}
</Group>)}
{typeof document !== 'undefined' && createPortal (
<DragOverlay adjustScale={false}
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
dropAnimation={motion.reduced ? null : {
duration: motion.duration ('drop'), easing: motion.easing,
keyframes: ({ transform }) => [
{ transform: CSS.Translate.toString (transform.initial) },
{ transform: CSS.Translate.toString (transform.final) }] }}>
{active == null ? null : <div lang={name (active).language}
className="w-max max-w-[min(20rem,calc(100vw_-_2rem))] truncate rounded
border border-blue-500 bg-white px-2 py-1 text-sm
text-slate-900 shadow-lg dark:border-blue-400 dark:bg-slate-800 dark:text-slate-100">
{name (active).text}</div>}
</DragOverlay>, document.body)}
</DndContext>)
}
+1 -1
ファイルの表示
@@ -239,7 +239,7 @@ export const startAPIDownload = (
): (() => void) => {
const frame = document.createElement ('iframe')
frame.hidden = true
let timer: ReturnType<typeof window.setTimeout> | null = null
let timer: number | null = null
let closed = false
const release = () => {
if (closed) return
+9 -9
ファイルの表示
@@ -21,21 +21,18 @@ import { countsErrorMessage, countsExportLabels } from '@/lib/countsDisplay'
import { isApiError } from '@/lib/api'
import { CountsDefinitionContext, countsAxisFields, countsAxisLabel,
visibleCountsResources, countsFieldLabel,
fetchCountsDefinition }
from '@/lib/countsDefinitions'
fetchCountsDefinition } from '@/lib/countsDefinitions'
import { countsKeys } from '@/lib/queryKeys'
import { setCountState, readCountState } from '@/lib/queryState'
import { cn } from '@/lib/utils'
import { useClientMotion } from '@/lib/useClientMotion'
import useDialogue from '@/lib/dialogues/useDialogue'
import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import { setCountState, readCountState } from '@/lib/queryState'
import type { CountField, CountsResource } from '@/lib/counts'
import type { CountsAxis, CountsDefinition } from '@/lib/countsDefinitions'
import type { CountsResource } from '@/lib/counts'
type PendingCountsRun = {
key: ReturnType<typeof countsKeys.index>
origin: string
@@ -44,7 +41,8 @@ type PendingCountsRun = {
const tooManyColumns = (failure: unknown): number | null => {
if (!isApiError (failure) || failure.response?.status !== 422) return null
if (!isApiError (failure) || failure.response?.status !== 422)
return null
const body = (failure.response.data ?? { }) as {
error?: unknown; maxColumns?: unknown; max_columns?: unknown }
const maximum = body.maxColumns ?? body.max_columns
@@ -251,7 +249,9 @@ const CountsResourceContent = (
setParams (next)
}
const download = async (format: 'csv' | 'json') => {
if (exportInFlight.current || pendingRun.current != null || definition == null) return
if (exportInFlight.current || pendingRun.current != null || definition == null)
return
exportInFlight.current = true
setExporting (true)
setExportError (false)
@@ -259,7 +259,7 @@ const CountsResourceContent = (
{
const axes = [...readCountState<CountsAxis[]> (params, 'rows', []),
...readCountState<CountsAxis[]> (params, 'cols', [])]
const fields = mode === 'summary' ? [
const fields: CountField[] = mode === 'summary' ? [
...axes.map (axis => ({
key: axis.field, label: countsFieldLabel (axis.field) })),
{ key: 'count', label: 'count' }]