From 4f3990feb69811eb0e36f0f9c522a93f564e1b4e Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sat, 31 Oct 2026 19:07:14 +0900 Subject: [PATCH] #434 --- .../src/components/common/SortableList.tsx | 146 +++++++++--------- frontend/src/lib/api.ts | 2 +- frontend/src/pages/CountsResourcePage.tsx | 18 +-- 3 files changed, 85 insertions(+), 81 deletions(-) diff --git a/frontend/src/components/common/SortableList.tsx b/frontend/src/components/common/SortableList.tsx index f9ff4a8..1947817 100644 --- a/frontend/src/components/common/SortableList.tsx +++ b/frontend/src/components/common/SortableList.tsx @@ -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 { - 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]) => - {renderGroup (group, -
- {ids.map (id => moveGroup (id, direction)}> - {renderItem (id, group)} - )} -
-
, overGroup === group)} -
)} - {typeof document !== 'undefined' && createPortal ( - [ - { transform: CSS.Translate.toString (transform.initial) }, - { transform: CSS.Translate.toString (transform.final) }] }}> - {active == null ? null :
- {name (active).text}
} -
, document.body)} -
+ return ( + { + 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]) => + {renderGroup (group, +
+ {ids.map (id => moveGroup (id, direction)}> + {renderItem (id, group)} + )} +
+
, overGroup === group)} +
)} + {typeof document !== 'undefined' && createPortal ( + [ + { transform: CSS.Translate.toString (transform.initial) }, + { transform: CSS.Translate.toString (transform.final) }] }}> + {active == null ? null :
+ {name (active).text}
} +
, document.body)} +
) } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0727dec..f9600d8 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -239,7 +239,7 @@ export const startAPIDownload = ( ): (() => void) => { const frame = document.createElement ('iframe') frame.hidden = true - let timer: ReturnType | null = null + let timer: number | null = null let closed = false const release = () => { if (closed) return diff --git a/frontend/src/pages/CountsResourcePage.tsx b/frontend/src/pages/CountsResourcePage.tsx index 23e3809..8637133 100644 --- a/frontend/src/pages/CountsResourcePage.tsx +++ b/frontend/src/pages/CountsResourcePage.tsx @@ -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 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 (params, 'rows', []), ...readCountState (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' }]