このコミットが含まれているのは:
2026-10-28 20:34:11 +09:00
コミット cb3f4c600a
14個のファイルの変更、208行の追加、33行の削除
+1 -2
ファイルの表示
@@ -40,7 +40,7 @@ const ConditionEditor = (
controls.setActions ([{ label: label (initial == null ? 'add' : 'shared:actions.edit'),
onSelect: () => save (condition) }])
}, [controls, condition, initial, label, save])
return <div className="space-y-5 px-1 py-3 md:px-3">
return <div className="space-y-2">
<select value={field} aria-label={message ('field').text}
className={inputClass (false, 'h-11 w-full')}
onChange={event => {
@@ -115,7 +115,6 @@ const CountsAdditionalConditions = ({ params, update }: {
else replace (index, condition)
}
void dialogue.form ({ title: label ('additionalConditions'), footerLayout: 'inline',
size: 'large',
body: controls => <ConditionEditor definition={definition}
initial={initial} controls={controls} save={save}/> })
}
+8 -5
ファイルの表示
@@ -180,9 +180,11 @@ const CountsControls = (
</label>}
</fieldset>
<AnimatedCollapse id={id} open={open}>
<div className="grid gap-3 border-b py-2 dark:border-slate-700 md:grid-cols-2">
<div className="max-h-[20dvh] min-h-0 min-w-0 overflow-y-auto overscroll-contain
md:max-h-[40dvh]" style={{ direction: 'rtl' }}>
<div className="grid max-h-[40dvh] auto-rows-max gap-3 overflow-y-auto overscroll-contain
border-b py-2 [direction:ltr] dark:border-slate-700 md:max-h-none md:auto-rows-auto
md:grid-cols-2 md:overflow-visible">
<div className="min-h-0 min-w-0 md:max-h-[40dvh] md:overflow-y-auto
md:overscroll-contain md:[direction:rtl]">
<fieldset className="min-w-0" data-counts-output style={{ direction: 'ltr' }}>
{mode === 'summary' ? <CountsAxes params={draft} update={update}/>
: <>
@@ -194,8 +196,9 @@ const CountsControls = (
</>}
</fieldset>
</div>
<div className="max-h-[20dvh] min-h-0 min-w-0 overflow-y-auto overscroll-contain
md:max-h-[40dvh]" style={{ direction: 'ltr' }}>
<div className="min-h-0 min-w-0 border-t pt-2 dark:border-slate-700
md:max-h-[40dvh] md:overflow-y-auto md:overscroll-contain md:border-t-0 md:pt-0"
style={{ direction: 'ltr' }}>
<fieldset className="min-w-0 space-y-2" data-counts-filters>
<legend className="font-semibold">{label ('filters')}</legend>
{history && <div className="flex min-w-0 max-w-full items-center gap-1">
+10 -3
ファイルの表示
@@ -47,9 +47,15 @@ const CountsNestedTable = (
{String (value)}</PrefetchLink>
}
const display = countsLogicalName (row, field, locale)
const nameLocale = fields.find (item => item.key === field)?.nameLocale
const metadata = fields.find (item => item.key === field)
const nameLocale = metadata?.nameLocale
if (nameLocale != null)
return <LocalisedText language={nameLocale.languageCode}>{display.text}</LocalisedText>
{
if (metadata?.multipleValues)
return <span lang={nameLocale.code} className="whitespace-pre-line">{display.text}</span>
return <LocalisedText language={nameLocale.languageCode}
className="whitespace-pre-line">{display.text}</LocalisedText>
}
if (typeof value === 'boolean')
return <span aria-label={display.text} lang={display.language}>{value ? '✓' : '✗'}</span>
if (display.language != null)
@@ -59,7 +65,8 @@ const CountsNestedTable = (
return display.text
}
if (countsNestedIsScalar (fields, path, params))
return <span>{countsNestedScalarText (rows, fields, path, params, locale)}</span>
return <span className="whitespace-pre-line">
{countsNestedScalarText (rows, fields, path, params, locale)}</span>
return (
<div className="counts-nested w-full min-w-0 max-w-full">
{params.get ('nested_json') === '1' ? (
+6 -1
ファイルの表示
@@ -48,7 +48,12 @@ const CountsResults = (
title={message (field.label, field.labelValues)} params={params} update={update}/>
}
if (field.nameLocale != null)
return <LocalisedText language={field.nameLocale.languageCode}>{value}</LocalisedText>
{
if (field.multipleValues)
return <span lang={field.nameLocale.code} className="whitespace-pre-line">{value}</span>
return <LocalisedText language={field.nameLocale.languageCode}
className="whitespace-pre-line">{value}</LocalisedText>
}
switch (column)
{
case 'resource_id':
+48 -4
ファイルの表示
@@ -44,6 +44,44 @@ const cellAt = (table: HTMLTableElement, point: Point) =>
cellGrid (table)[point.row]?.[point.column]
const scrollToCell = (table: HTMLTableElement, point: Point, previous: Point) => {
const cell = cellAt (table, point)
const container = table.parentElement?.closest<HTMLElement> (
'[data-counts-scroll],[data-counts-results]')
if (cell == null || container == null) return
const viewport = container.getBoundingClientRect ()
const top = viewport.top + container.clientTop
const left = viewport.left + container.clientLeft
const bottom = top + container.clientHeight
const right = left + container.clientWidth
const header = table.tHead?.getBoundingClientRect ()
const visibleTop = header == null ? top : Math.max (top, Math.min (bottom, header.bottom))
const rect = cell.getBoundingClientRect ()
const lastRow = (table.tBodies[0]?.rows.length ?? 1) - 1
const verticalEdge = (point.row === 0 || point.row < previous.row) ? 'start'
: ((point.row === lastRow || point.row > previous.row) ? 'end'
: (Math.abs (rect.top - visibleTop) <= Math.abs (rect.bottom - bottom) ? 'start' : 'end'))
const offset = (
start: number, end: number, minimum: number, maximum: number,
edge?: 'start' | 'end',
) => {
if (maximum <= minimum) return 0
const oversized = end - start > maximum - minimum
// Vertical merged cells follow the destination and movement direction.
// Horizontal scrolling retains its existing nearest-edge behaviour.
if (oversized && edge != null) return edge === 'start' ? start - minimum : end - maximum
if (start < minimum && end > maximum) return 0
if (start < minimum) return oversized ? end - maximum : start - minimum
if (end > maximum) return oversized ? start - minimum : end - maximum
return 0
}
const vertical = offset (rect.top, rect.bottom, visibleTop, bottom, verticalEdge)
const horizontal = offset (rect.left, rect.right, left, right)
if (vertical !== 0) container.scrollTop += vertical
if (horizontal !== 0) container.scrollLeft += horizontal
}
const CountsSelectableTable = (
{ family, columns, identity, children, className, name, copyTable, stretched = false }: {
family: string
@@ -74,6 +112,7 @@ const CountsSelectableTable = (
const { label, message } = useUITranslation ('counts')
const clearTextSelection = useCallback (() => {
setTextMode (null)
setCaret (null)
}, [])
useLayoutEffect (() => {
if (textMode == null) return
@@ -488,12 +527,18 @@ const CountsSelectableTable = (
}
return
}
if (event.key === 'Escape')
if (event.key === 'Escape' || (event.key === 'Enter' && textCell != null))
{
const textModeFlg = textCell != null
clearTextSelection ()
if (!textModeFlg) setStored (null)
if (textModeFlg)
{
window.getSelection ()?.removeAllRanges ()
event.currentTarget.focus ({ preventScroll: true })
}
else setStored (null)
event.preventDefault ()
event.stopPropagation ()
return
}
if (event.key === 'Enter' && textCell == null && selection != null)
@@ -567,8 +612,7 @@ const CountsSelectableTable = (
+ (key === 'ArrowRight' ? 1 : (key === 'ArrowLeft' ? -1 : 0)))) }
setStored ({ identity, anchor: event.shiftKey && selection != null
? selection.anchor : point, end: point })
cellAt (event.currentTarget, point)?.scrollIntoView ({
block: 'nearest', inline: 'nearest' })
scrollToCell (event.currentTarget, point, previous)
}}>
<caption className="sr-only">
<span id={`${id}-hint`} className="sr-only">{label ('selection.hint')}</span>
+13 -4
ファイルの表示
@@ -98,6 +98,7 @@ export type CountsResult = {
export type CountSort = { key: string; direction: 'asc' | 'desc' }
export type CountField = { key: string; label: string; children?: CountField[]
labelValues?: Record<string, string>
multipleValues?: boolean
nameLocale?: { code: string; languageCode: string; scriptCode: string } }
export type CountRecord = Record<string, unknown>
@@ -335,12 +336,17 @@ export const normaliseCountsParams = (
const params = new URLSearchParams (source)
if (definition?.nameColumns != null)
{
const names = definition.nameColumns.map (field => field.key)
const names = definition.nameColumns.filter (field => !field.multipleValues)
.map (field => field.key)
const current = definition.nameColumns.find (field =>
field.nameLocale?.code === currentViewingLocale ())?.key
!field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key
const aliases = definition.nameColumns.find (field =>
field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key
const rootKey = (key: string) => (key === 'name' && current != null) ? current
: ((key === 'aliases' && aliases != null) ? aliases : key)
if (resource === 'tags' && params.has ('columns'))
params.set ('columns', params.get ('columns')!.split (' ').flatMap (key =>
key === 'names' ? names : [key === 'name' && current != null ? current : key])
key === 'names' ? names : [rootKey (key)])
.join (' '))
if (params.has ('children'))
{
@@ -348,12 +354,15 @@ export const normaliseCountsParams = (
params.set ('children', selected.flatMap (key => {
if (['tags.names', 'parents.names'].includes (key))
return names.map (name => `${key.slice (0, -6)}.${name}`)
if (['tags.aliases', 'parents.aliases'].includes (key) && aliases != null)
return [`${key.slice (0, -8)}.${aliases}`]
if (key.startsWith ('tags.aliases.') || key.startsWith ('parents.aliases.')) return []
return key.startsWith ('tags.names.') || key.startsWith ('parents.names.') ? [] : [key]
}).join (' '))
}
if (resource === 'tags' && params.has ('order'))
params.set ('order', params.get ('order')!.split (' ').flatMap (key =>
key === 'names' ? names : [key === 'name' && current != null ? current : key])
key === 'names' ? names : [rootKey (key)])
.join (' '))
}
for (const [key, value] of params)
+41 -2
ファイルの表示
@@ -1,6 +1,6 @@
import { createContext, useContext } from 'react'
import { translateUIMessage } from '@/lib/i18n'
import { i18n, translateUIMessage } from '@/lib/i18n'
import { apiGet } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
@@ -26,6 +26,7 @@ export type CountsFieldMetadata = {
labels?: string[]; multiple?: boolean; candidateSource?: 'categories' | 'tags'
setType?: 'tag_reference'; inputTypes?: string[]
labelValues?: Record<string, string>
nameLocale?: CountField['nameLocale']
choices?: { value: string; label: string }[] }
export type CountsDefinition = {
fields: CountsFieldMetadata[]
@@ -46,10 +47,48 @@ export const useCountsDefinition = (): CountsDefinition => {
}
const localiseCountsDefinition = (
definition: CountsDefinition, locale: LocaleCode,
): CountsDefinition => {
const heading = (name: CountField['nameLocale'], aliasesFlg: boolean) => {
if (name == null) return null
const key = `languageNames.${name.languageCode}`
const language = i18n.exists (key, { ns: 'counts', lng: locale })
? translateUIMessage (key, 'counts', locale).text : name.languageCode
const scripts = new Set ((definition.nameColumns ?? [])
.filter (field => field.nameLocale?.languageCode === name.languageCode)
.map (field => field.nameLocale!.scriptCode.toLowerCase ()))
const label = aliasesFlg ? 'counts:localeAliases' : 'counts:localeName'
return { label: scripts.size > 1 ? `${label}WithScript` : label,
labelValues: { language, script: name.scriptCode } }
}
const columns = (fields: CountField[]): CountField[] => fields.map (field => ({
...field, ...heading (field.nameLocale, field.multipleValues === true),
...(field.children == null ? { } : { children: columns (field.children) }) }))
const metadata = (fields: CountsFieldMetadata[]): CountsFieldMetadata[] =>
fields.map (field => {
const aliasesFlg = field.key.split ('.').pop()?.startsWith ('aliases_') === true
const name = heading (field.nameLocale, aliasesFlg)
return name == null ? field : { ...field, labelValues: name.labelValues,
labels: field.labels?.map (label =>
['counts:localeName', 'counts:localeAliases'].includes (label) ? name.label : label) }
})
return { ...definition,
fields: metadata (definition.fields),
...(definition.outputFields == null ? { }
: { outputFields: columns (definition.outputFields) }),
...(definition.nameColumns == null ? { }
: { nameColumns: columns (definition.nameColumns) }),
...(definition.filterFields == null ? { }
: { filterFields: metadata (definition.filterFields) }) }
}
export const fetchCountsDefinition = (
resource: CountsResource, locale: LocaleCode = 'ja',
): Promise<CountsDefinition> => apiGet (
): Promise<CountsDefinition> => apiGet<CountsDefinition> (
`/counts/definitions/${encodeURIComponent (resource)}`, { locale })
.then (definition => localiseCountsDefinition (definition, locale))
export type CountsCandidate = {
value: string; id?: number; name: string | null; category?: string | null
+4
ファイルの表示
@@ -58,6 +58,7 @@ export const countsCellText = (
case 'created_at': return date (row.createdAt)
default:
{
if (column.startsWith ('aliases_')) return countsLogicalName (row, column, locale).text
const key = column.replace (/_([a-z])/g, (_, letter: string) => letter.toUpperCase ())
const value = row[key]
if (['discarded_at', 'updated_at'].includes (column) && typeof value === 'string')
@@ -123,6 +124,9 @@ export const countsLogicalName = (
): { text: string; language?: string } => {
const value = countsFieldValue (row, field)
if (value == null) return { text: '' }
if (field.startsWith ('aliases_') && Array.isArray (value))
return { text: value.map (entry => typeof entry === 'string' ? entry
: String (entry?.name ?? '')).join ('\n') }
if (field === 'sections')
return { text: (countsSectionRanges (value, locale) ?? []).join (
translateUIMessage ('sections.separator', 'counts', locale).text) }
+9 -1
ファイルの表示
@@ -8,7 +8,15 @@
"materials": "素材の集計・明細"
},
"target": "集計定義",
"localeName": "{{locale}}名",
"localeName": "名称({{language}})",
"localeNameWithScript": "名称({{language}}・{{script}})",
"localeAliases": "別名({{language}})",
"localeAliasesWithScript": "別名({{language}}・{{script}})",
"languageNames": {
"ja": "日本語",
"en": "英語",
"ain": "アイヌ語"
},
"run": "集計実行",
"conditions": "集計条件",
"output": "出力項目",