このコミットが含まれているのは:
@@ -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>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }]
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする