このコミットが含まれているのは:
@@ -12,9 +12,7 @@ import TopNavUser from '@/components/TopNavUser'
|
|||||||
import TranslatedText from '@/components/TranslatedText'
|
import TranslatedText from '@/components/TranslatedText'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||||
import { languageUIMessage } from '@/lib/i18n'
|
|
||||||
import { isLocaleCode,
|
import { isLocaleCode,
|
||||||
localeLanguage,
|
|
||||||
localeFromPath,
|
localeFromPath,
|
||||||
switchLocalePath,
|
switchLocalePath,
|
||||||
unlocalisedPath } from '@/lib/locales'
|
unlocalisedPath } from '@/lib/locales'
|
||||||
@@ -134,7 +132,7 @@ export const menuOutline = (
|
|||||||
const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
|
const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
|
||||||
const { message, label } = useUITranslation ('shared')
|
const { message, label } = useUITranslation ('shared')
|
||||||
const locale = useLocale ()
|
const locale = useLocale ()
|
||||||
const localeName = languageUIMessage (localeLanguage (locale), locale).text
|
const localeName = locales.find (entry => entry.code === locale)?.name
|
||||||
const navigate = useLocaleNavigate ()
|
const navigate = useLocaleNavigate ()
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const currentLocation = useRef (location)
|
const currentLocation = useRef (location)
|
||||||
@@ -449,7 +447,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
|
|||||||
}}>
|
}}>
|
||||||
{locales.map (entry => isLocaleCode (entry.code) && (
|
{locales.map (entry => isLocaleCode (entry.code) && (
|
||||||
<option key={entry.code} value={entry.code}>
|
<option key={entry.code} value={entry.code}>
|
||||||
{formatUIString (languageUIMessage (localeLanguage (entry.code), locale).text, locale)}
|
{formatUIString (entry.name, entry.code)}
|
||||||
</option>))}
|
</option>))}
|
||||||
</select>
|
</select>
|
||||||
</label>)
|
</label>)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor,
|
import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor,
|
||||||
closestCenter, useDroppable, useSensor, useSensors } from '@dnd-kit/core'
|
closestCenter, pointerWithin, useDroppable, useSensor, useSensors }
|
||||||
|
from '@dnd-kit/core'
|
||||||
import { SortableContext, arrayMove, rectSortingStrategy,
|
import { SortableContext, arrayMove, rectSortingStrategy,
|
||||||
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||||
import { useId, useRef, useState } from 'react'
|
import { useId, useRef, useState } from 'react'
|
||||||
@@ -25,7 +26,7 @@ const sameGroups = (left: Groups, right: Groups): boolean =>
|
|||||||
const Group = ({ id, children }: { id: string; children: ReactNode }) => {
|
const Group = ({ id, children }: { id: string; children: ReactNode }) => {
|
||||||
const drop = useDroppable ({ id: `group:${id}` })
|
const drop = useDroppable ({ id: `group:${id}` })
|
||||||
return <div ref={drop.setNodeRef} className={cn (
|
return <div ref={drop.setNodeRef} className={cn (
|
||||||
'flex min-h-10 flex-wrap items-start gap-1 rounded p-1',
|
'min-w-0 rounded',
|
||||||
drop.isOver && 'ring-1 ring-blue-500 dark:ring-blue-400')}>{children}</div>
|
drop.isOver && 'ring-1 ring-blue-500 dark:ring-blue-400')}>{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,12 @@ const SortableList = (
|
|||||||
drag.current = null
|
drag.current = null
|
||||||
setActive (null)
|
setActive (null)
|
||||||
}
|
}
|
||||||
return <DndContext sensors={sensors} collisionDetection={closestCenter}
|
return <DndContext sensors={sensors} collisionDetection={args => {
|
||||||
|
const hits = pointerWithin (args)
|
||||||
|
const available = hits.find (hit => hit.id === 'group:available')
|
||||||
|
return owner (String (args.active.id), groups) !== 'available'
|
||||||
|
&& available != null ? [available] : closestCenter (args)
|
||||||
|
}}
|
||||||
accessibility={{ announcements: {
|
accessibility={{ announcements: {
|
||||||
onDragStart: event => accessibleName (String (event.active.id)).text,
|
onDragStart: event => accessibleName (String (event.active.id)).text,
|
||||||
onDragOver: event => event.over == null ? undefined : (
|
onDragOver: event => event.over == null ? undefined : (
|
||||||
@@ -110,15 +116,17 @@ const SortableList = (
|
|||||||
}}>
|
}}>
|
||||||
|
|
||||||
{/* Keep indexes and droppable geometry stable until the drop is committed. */}
|
{/* Keep indexes and droppable geometry stable until the drop is committed. */}
|
||||||
{Object.entries (groups).map (([group, ids]) => <div key={group}>
|
{Object.entries (groups).map (([group, ids]) => <Group key={group} id={group}>
|
||||||
{renderGroup (group, <SortableContext items={ids} strategy={rectSortingStrategy}>
|
{renderGroup (group, <SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||||
<Group id={group}>{ids.map (id => <SortableItem key={id} id={id}
|
<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}`}
|
name={accessibleName (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
|
||||||
moveGroup={direction => moveGroup (id, direction)}>
|
moveGroup={direction => moveGroup (id, direction)}>
|
||||||
{renderItem (id, group)}
|
{renderItem (id, group)}
|
||||||
</SortableItem>)}</Group>
|
</SortableItem>)}
|
||||||
|
</div>
|
||||||
</SortableContext>)}
|
</SortableContext>)}
|
||||||
</div>)}
|
</Group>)}
|
||||||
{typeof document !== 'undefined' && createPortal (
|
{typeof document !== 'undefined' && createPortal (
|
||||||
<DragOverlay adjustScale={false}
|
<DragOverlay adjustScale={false}
|
||||||
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import LocalisedText from '@/components/LocalisedText'
|
import LocalisedText from '@/components/LocalisedText'
|
||||||
import SortableList from '@/components/common/SortableList'
|
import SortableList from '@/components/common/SortableList'
|
||||||
import { countsAxisFields, countsAxisLabel, countsFilterFields,
|
import { countsAxisFields, countsFilterFields, countsFieldLabel,
|
||||||
useCountsDefinition }
|
useCountsDefinition }
|
||||||
from '@/lib/countsDefinitions'
|
from '@/lib/countsDefinitions'
|
||||||
import { useClientMotion } from '@/lib/useClientMotion'
|
import { useClientMotion } from '@/lib/useClientMotion'
|
||||||
import { cn, inputClass } from '@/lib/utils'
|
import { cn, inputClass } from '@/lib/utils'
|
||||||
import { useLocale } from '@/lib/useLocale'
|
|
||||||
import { useUITranslation } from '@/lib/useUITranslation'
|
import { useUITranslation } from '@/lib/useUITranslation'
|
||||||
|
|
||||||
import { readCountState, encodeCountState } from '@/lib/queryState'
|
import { readCountState, encodeCountState } from '@/lib/queryState'
|
||||||
@@ -18,7 +17,6 @@ const CountsAxes = ({ params, update }: {
|
|||||||
) => {
|
) => {
|
||||||
const { label, message } = useUITranslation ('counts')
|
const { label, message } = useUITranslation ('counts')
|
||||||
const definition = useCountsDefinition ()
|
const definition = useCountsDefinition ()
|
||||||
const locale = useLocale ()
|
|
||||||
const motion = useClientMotion ()
|
const motion = useClientMotion ()
|
||||||
const available = countsAxisFields (definition, params.get ('definition') === 'history')
|
const available = countsAxisFields (definition, params.get ('definition') === 'history')
|
||||||
const rows = readCountState<CountsAxis[]> (params, 'rows', [])
|
const rows = readCountState<CountsAxis[]> (params, 'rows', [])
|
||||||
@@ -31,7 +29,7 @@ const CountsAxes = ({ params, update }: {
|
|||||||
const name = (id: string) => {
|
const name = (id: string) => {
|
||||||
const axis = axes.find (item => item.field === id)
|
const axis = axes.find (item => item.field === id)
|
||||||
return axis == null ? message (available.find (field => field.key === id)?.label ?? 'axis')
|
return axis == null ? message (available.find (field => field.key === id)?.label ?? 'axis')
|
||||||
: countsAxisLabel (axis, definition, locale)
|
: message (countsFieldLabel (axis.field))
|
||||||
}
|
}
|
||||||
const commit = (next: Record<string, string[]>) => {
|
const commit = (next: Record<string, string[]>) => {
|
||||||
for (const group of ['rows', 'cols'] as const)
|
for (const group of ['rows', 'cols'] as const)
|
||||||
|
|||||||
@@ -226,7 +226,8 @@ const CountsSummaryTable = (
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
return <div className="min-h-0 flex-1 overflow-auto" data-counts-results aria-busy={pending}>
|
return <div className="min-h-0 flex-1 overflow-auto" data-counts-results aria-busy={pending}>
|
||||||
<CountsSelectableTable family={`pivot_${resource}`} columns={keys} stretched
|
<CountsSelectableTable family={`pivot_${resource}`} columns={keys}
|
||||||
|
stretched={keys.filter (key => key !== 'row_number').length > 1}
|
||||||
copyTable={() => ({ columns: keys.map ((_, index) => ({ label: headingRows[0][index] })),
|
copyTable={() => ({ columns: keys.map ((_, index) => ({ label: headingRows[0][index] })),
|
||||||
headerRows: headingRows, rows: matrix })}
|
headerRows: headingRows, rows: matrix })}
|
||||||
identity={`${countsRequestParams (params)}:${JSON.stringify (keys)}`}
|
identity={`${countsRequestParams (params)}:${JSON.stringify (keys)}`}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
"localeAliases": "別名({{language}})",
|
"localeAliases": "別名({{language}})",
|
||||||
"localeAliasesWithScript": "別名({{language}}・{{script}})",
|
"localeAliasesWithScript": "別名({{language}}・{{script}})",
|
||||||
"run": "集計実行",
|
"run": "集計実行",
|
||||||
|
"columnLimit": {
|
||||||
|
"title": "列数が上限を超えてゐます",
|
||||||
|
"message": "集計表の列は最大{{max}}列です。列の項目を変更するか、抽出条件で対象を絞り込んでください。"
|
||||||
|
},
|
||||||
"conditions": "集計条件",
|
"conditions": "集計条件",
|
||||||
"output": "出力項目",
|
"output": "出力項目",
|
||||||
"filters": "抽出条件",
|
"filters": "抽出条件",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { countsRequestParams, countsDefinitionParams, countsExportPayload, fieldsForCounts, fetchCounts,
|
import { countsRequestParams, countsDefinitionParams, countsExportPayload, fieldsForCounts, fetchCounts,
|
||||||
isCountsState, normaliseCountsParams, selectedCountColumns } from '@/lib/counts'
|
isCountsState, normaliseCountsParams, selectedCountColumns } from '@/lib/counts'
|
||||||
import { countsExportLabels } from '@/lib/countsDisplay'
|
import { countsExportLabels } from '@/lib/countsDisplay'
|
||||||
|
import { isApiError } from '@/lib/api'
|
||||||
import { CountsDefinitionContext, countsAxisFields, countsAxisLabel,
|
import { CountsDefinitionContext, countsAxisFields, countsAxisLabel,
|
||||||
visibleCountsResources, countsFieldLabel,
|
visibleCountsResources, countsFieldLabel,
|
||||||
fetchCountsDefinition }
|
fetchCountsDefinition }
|
||||||
@@ -42,6 +43,16 @@ type PendingCountsRun = {
|
|||||||
target?: string }
|
target?: string }
|
||||||
|
|
||||||
|
|
||||||
|
const tooManyColumns = (failure: unknown): number | 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
|
||||||
|
return body.error === 'too_many_columns' && typeof maximum === 'number'
|
||||||
|
? maximum : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const CountsResourceContent = (
|
const CountsResourceContent = (
|
||||||
{ resource, definition, definitionError }: {
|
{ resource, definition, definitionError }: {
|
||||||
resource: CountsResource; definition?: CountsDefinition; definitionError: boolean },
|
resource: CountsResource; definition?: CountsDefinition; definitionError: boolean },
|
||||||
@@ -85,6 +96,7 @@ const CountsResourceContent = (
|
|||||||
cacheParams.set ('definition', state ? 'latest' : 'history')
|
cacheParams.set ('definition', state ? 'latest' : 'history')
|
||||||
const [running, setRunning] = useState (false)
|
const [running, setRunning] = useState (false)
|
||||||
const [runError, setRunError] = useState (false)
|
const [runError, setRunError] = useState (false)
|
||||||
|
const [runColumnLimit, setRunColumnLimit] = useState<number | null> (null)
|
||||||
const pendingRun = useRef<PendingCountsRun | null> (null)
|
const pendingRun = useRef<PendingCountsRun | null> (null)
|
||||||
const applied = useRef (params)
|
const applied = useRef (params)
|
||||||
const retainedParams = useRef (params)
|
const retainedParams = useRef (params)
|
||||||
@@ -92,7 +104,8 @@ const CountsResourceContent = (
|
|||||||
const [exporting, setExporting] = useState (false)
|
const [exporting, setExporting] = useState (false)
|
||||||
const exportInFlight = useRef (false)
|
const exportInFlight = useRef (false)
|
||||||
const [exportError, setExportError] = useState (false)
|
const [exportError, setExportError] = useState (false)
|
||||||
const { data, dataUpdatedAt, isFetching: fetching, isPending: pending, isError: errorFlg,
|
const { data, error: queryError, dataUpdatedAt, isFetching: fetching,
|
||||||
|
isPending: pending, isError: errorFlg,
|
||||||
isPlaceholderData: placeholder } = useQuery ({
|
isPlaceholderData: placeholder } = useQuery ({
|
||||||
enabled: definition != null && params.has ('timezone') && !running,
|
enabled: definition != null && params.has ('timezone') && !running,
|
||||||
staleTime: params.has ('execution_at') ? 5 * 60 * 1000 : 0,
|
staleTime: params.has ('execution_at') ? 5 * 60 * 1000 : 0,
|
||||||
@@ -124,6 +137,7 @@ const CountsResourceContent = (
|
|||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
setRunning (false)
|
setRunning (false)
|
||||||
setRunError (false)
|
setRunError (false)
|
||||||
|
setRunColumnLimit (null)
|
||||||
return () => {
|
return () => {
|
||||||
const pending = pendingRun.current
|
const pending = pendingRun.current
|
||||||
pendingRun.current = null
|
pendingRun.current = null
|
||||||
@@ -162,6 +176,7 @@ const CountsResourceContent = (
|
|||||||
const previous = new URLSearchParams (params)
|
const previous = new URLSearchParams (params)
|
||||||
pendingRun.current = pending
|
pendingRun.current = pending
|
||||||
setRunError (false)
|
setRunError (false)
|
||||||
|
setRunColumnLimit (null)
|
||||||
setRunning (true)
|
setRunning (true)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -199,11 +214,12 @@ const CountsResourceContent = (
|
|||||||
}
|
}
|
||||||
else setParams (committed, { preventScrollReset: true })
|
else setParams (committed, { preventScrollReset: true })
|
||||||
}
|
}
|
||||||
catch
|
catch (failure)
|
||||||
{
|
{
|
||||||
if (pendingRun.current !== pending) return
|
if (pendingRun.current !== pending) return
|
||||||
pendingRun.current = null
|
pendingRun.current = null
|
||||||
setRunning (false)
|
setRunning (false)
|
||||||
|
setRunColumnLimit (tooManyColumns (failure))
|
||||||
setRunError (true)
|
setRunError (true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,9 +228,11 @@ const CountsResourceContent = (
|
|||||||
const waiting = running || (!definitionError && (definition == null
|
const waiting = running || (!definitionError && (definition == null
|
||||||
|| pending || fetching || placeholder
|
|| pending || fetching || placeholder
|
||||||
|| (!params.has ('execution_at') && !errorFlg)))
|
|| (!params.has ('execution_at') && !errorFlg)))
|
||||||
|
const columnLimit = runColumnLimit ?? tooManyColumns (queryError)
|
||||||
|
|
||||||
|
|
||||||
const update = (key: string, value: string, extra?: Record<string, string>) => {
|
const update = (key: string, value: string, extra?: Record<string, string>) => {
|
||||||
|
setRunColumnLimit (null)
|
||||||
const next = new URLSearchParams (params)
|
const next = new URLSearchParams (params)
|
||||||
if (value || ['sort', 'columns', 'children', 'conditions', 'drill'].includes (key))
|
if (value || ['sort', 'columns', 'children', 'conditions', 'drill'].includes (key))
|
||||||
setCountState (next, key, value)
|
setCountState (next, key, value)
|
||||||
@@ -275,7 +293,7 @@ const CountsResourceContent = (
|
|||||||
<CountsTableLayout>
|
<CountsTableLayout>
|
||||||
<div data-sidebar-container
|
<div data-sidebar-container
|
||||||
className="flex min-h-0 flex-1 flex-col overflow-hidden md:flex-row">
|
className="flex min-h-0 flex-1 flex-col overflow-hidden md:flex-row">
|
||||||
<PageHead title={title} includeSiteName={false}/>
|
<PageHead title={title}/>
|
||||||
<Button type="button" variant="ghost" size="sm" className="shrink-0 md:hidden"
|
<Button type="button" variant="ghost" size="sm" className="shrink-0 md:hidden"
|
||||||
aria-expanded={sidebarOpen} aria-controls="counts-targets"
|
aria-expanded={sidebarOpen} aria-controls="counts-targets"
|
||||||
onClick={() => setSidebarOpen (!sidebarOpen)}>{label ('target')}</Button>
|
onClick={() => setSidebarOpen (!sidebarOpen)}>{label ('target')}</Button>
|
||||||
@@ -347,7 +365,11 @@ const CountsResourceContent = (
|
|||||||
|
|
||||||
{exportError && <p role="alert">{label ('common:fetchError')}</p>}
|
{exportError && <p role="alert">{label ('common:fetchError')}</p>}
|
||||||
</div>
|
</div>
|
||||||
{(definitionError || errorFlg || runError) && <p role="alert">{label ('common:fetchError')}</p>}
|
{columnLimit != null ? <div role="alert">
|
||||||
|
<p>{label ('columnLimit.title')}</p>
|
||||||
|
<p>{label ('columnLimit.message', { max: columnLimit })}</p>
|
||||||
|
</div> : ((definitionError || errorFlg || runError)
|
||||||
|
&& <p role="alert">{label ('common:fetchError')}</p>)}
|
||||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
|
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
|
||||||
aria-busy={waiting}>
|
aria-busy={waiting}>
|
||||||
<div inert={waiting} className="flex min-h-0 flex-1 flex-col">
|
<div inert={waiting} className="flex min-h-0 flex-1 flex-col">
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする