このコミットが含まれているのは:
@@ -29,6 +29,18 @@ class CountsDefaults
|
|||||||
end
|
end
|
||||||
end.join(' ')
|
end.join(' ')
|
||||||
end
|
end
|
||||||
|
if ['posts', 'tags'].include?(resource)
|
||||||
|
fields = CountsResourceDefinition.new(resource).output_fields
|
||||||
|
selected = details.fetch('columns').split(' ')
|
||||||
|
details['columns'] = fields.map { _1[:key] }.select { selected.include?(_1) }.join(' ')
|
||||||
|
order = lambda do |items, prefix|
|
||||||
|
items.flat_map do |field|
|
||||||
|
path = [prefix, field[:key]].compact.join('.')
|
||||||
|
[path, *order.call(field.fetch(:children, []), path)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
details['order'] = order.call(fields, nil).join(' ')
|
||||||
|
end
|
||||||
['latest', 'history'].each do |definition|
|
['latest', 'history'].each do |definition|
|
||||||
result << { definition:, mode: 'details', params: details }
|
result << { definition:, mode: 'details', params: details }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ class CountsExportProjection
|
|||||||
return @labels.fetch("kind:#{ value }", value) if key == 'kind'
|
return @labels.fetch("kind:#{ value }", value) if key == 'kind'
|
||||||
return @labels.fetch("category:#{ value }", value) if key == 'category'
|
return @labels.fetch("category:#{ value }", value) if key == 'category'
|
||||||
if ['language', 'script'].include?(key) && value.is_a?(Hash)
|
if ['language', 'script'].include?(key) && value.is_a?(Hash)
|
||||||
|
return @labels.fetch("language:#{ value[:code] }", value[:code]) if key == 'language'
|
||||||
return @labels.fetch("#{ key }:#{ value[:code] }",
|
return @labels.fetch("#{ key }:#{ value[:code] }",
|
||||||
value[:name] || @labels.fetch('notRegistered', ''))
|
value[:name] || @labels.fetch('notRegistered', ''))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -115,13 +115,23 @@ class CountsResourceDefinition
|
|||||||
def self.aliases_key(code) = "aliases_#{ code.to_s.downcase.tr('-', '_') }"
|
def self.aliases_key(code) = "aliases_#{ code.to_s.downcase.tr('-', '_') }"
|
||||||
|
|
||||||
def name_columns
|
def name_columns
|
||||||
@name_columns ||= Locale.select(:code, :language_code, :script_code).order(:code)
|
@name_columns ||= begin
|
||||||
.flat_map do |locale|
|
locales = Locale.joins(:language)
|
||||||
codes = { code: locale.code, language_code: locale.language_code,
|
.select('locales.code', 'locales.language_code', 'locales.script_code')
|
||||||
|
.order('languages.created_at ASC', 'languages.code ASC', 'locales.code ASC')
|
||||||
|
codes = locales.map do |locale|
|
||||||
|
{ code: locale.code, language_code: locale.language_code,
|
||||||
script_code: locale.script_code }
|
script_code: locale.script_code }
|
||||||
[{ key: self.class.name_key(locale.code), label: 'counts:localeName', name_locale: codes },
|
end
|
||||||
{ key: self.class.aliases_key(locale.code), label: 'counts:localeAliases',
|
names = codes.map do |locale|
|
||||||
name_locale: codes, multiple_values: true }]
|
{ key: self.class.name_key(locale[:code]), label: 'counts:localeName',
|
||||||
|
name_locale: locale }
|
||||||
|
end
|
||||||
|
aliases = codes.map do |locale|
|
||||||
|
{ key: self.class.aliases_key(locale[:code]), label: 'counts:localeAliases',
|
||||||
|
name_locale: locale, multiple_values: true }
|
||||||
|
end
|
||||||
|
names + aliases
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -200,7 +210,7 @@ class CountsResourceDefinition
|
|||||||
keys.flat_map do |child|
|
keys.flat_map do |child|
|
||||||
next [] if child == 'aliases' && key == 'tags'
|
next [] if child == 'aliases' && key == 'tags'
|
||||||
if child == 'name' && ['tags', 'parents'].include?(key)
|
if child == 'name' && ['tags', 'parents'].include?(key)
|
||||||
next [{ key: child, label: CHILD_LABELS.fetch(child) }, *name_columns]
|
next name_columns
|
||||||
end
|
end
|
||||||
label = key == 'members' && child == 'tagId' ?
|
label = key == 'members' && child == 'tagId' ?
|
||||||
'counts:columns.resourceId' : CHILD_LABELS.fetch(child)
|
'counts:columns.resourceId' : CHILD_LABELS.fetch(child)
|
||||||
@@ -218,6 +228,13 @@ class CountsResourceDefinition
|
|||||||
def condition_fields
|
def condition_fields
|
||||||
fields = output_fields
|
fields = output_fields
|
||||||
if resource == 'tags'
|
if resource == 'tags'
|
||||||
|
fields = fields.map do |field|
|
||||||
|
next field unless field[:key] == 'parents'
|
||||||
|
|
||||||
|
field.merge(children: field[:children] + [
|
||||||
|
{ key: 'name', label: 'history:name' },
|
||||||
|
{ key: 'names', label: 'counts:columns.names', children: child_fields('names') }])
|
||||||
|
end
|
||||||
fields += [{ key: 'name', label: 'counts:columns.name' },
|
fields += [{ key: 'name', label: 'counts:columns.name' },
|
||||||
{ key: 'names', label: 'counts:columns.names', children: child_fields('names') },
|
{ key: 'names', label: 'counts:columns.names', children: child_fields('names') },
|
||||||
{ key: 'aliases', label: 'tags:aliases', children: child_fields('aliases') }]
|
{ key: 'aliases', label: 'tags:aliases', children: child_fields('aliases') }]
|
||||||
@@ -227,6 +244,7 @@ class CountsResourceDefinition
|
|||||||
next field unless field[:key] == 'tags'
|
next field unless field[:key] == 'tags'
|
||||||
|
|
||||||
field.merge(children: field[:children] + [
|
field.merge(children: field[:children] + [
|
||||||
|
{ key: 'name', label: 'history:name' },
|
||||||
{ key: 'names', label: 'counts:columns.names', children: child_fields('names') },
|
{ key: 'names', label: 'counts:columns.names', children: child_fields('names') },
|
||||||
{ key: 'aliases', label: 'tags:aliases', children: child_fields('aliases') }])
|
{ key: 'aliases', label: 'tags:aliases', children: child_fields('aliases') }])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class CountsTableExport
|
|||||||
def body(progress: nil, phase: nil)
|
def body(progress: nil, phase: nil)
|
||||||
Enumerator.new do |output|
|
Enumerator.new do |output|
|
||||||
ApplicationRecord.transaction(isolation: :repeatable_read) do
|
ApplicationRecord.transaction(isolation: :repeatable_read) do
|
||||||
@language_names = Language.pluck(:code, :name).to_h
|
|
||||||
processed = 0
|
processed = 0
|
||||||
started = false
|
started = false
|
||||||
first = true
|
first = true
|
||||||
@@ -256,6 +255,9 @@ class CountsTableExport
|
|||||||
def summary_value(value, index, page)
|
def summary_value(value, index, page)
|
||||||
field = @query.axes.all.fetch(index).fetch('field')
|
field = @query.axes.all.fetch(index).fetch('field')
|
||||||
return @labels.fetch('bot', '') if field == 'user' && value.nil?
|
return @labels.fetch('bot', '') if field == 'user' && value.nil?
|
||||||
|
if ['language_code', 'source_language_code'].include?(field)
|
||||||
|
return @labels.fetch("language:#{ value }", value)
|
||||||
|
end
|
||||||
|
|
||||||
names = page[:axis_labels].fetch("a#{ index }", {})
|
names = page[:axis_labels].fetch("a#{ index }", {})
|
||||||
name = field == 'user' ? names[value.to_s] : names.fetch(value.to_s, value)
|
name = field == 'user' ? names[value.to_s] : names.fetch(value.to_s, value)
|
||||||
@@ -327,7 +329,7 @@ class CountsTableExport
|
|||||||
return @query.population.timezone.utc_to_local(value.utc).iso8601
|
return @query.population.timezone.utc_to_local(value.utc).iso8601
|
||||||
end
|
end
|
||||||
if key == 'language_code' || key == 'source_language_code'
|
if key == 'language_code' || key == 'source_language_code'
|
||||||
return @language_names[value]
|
return @labels.fetch("language:#{ value }", value)
|
||||||
end
|
end
|
||||||
if value.is_a?(Hash)
|
if value.is_a?(Hash)
|
||||||
return value.to_h { |field, item| [@labels.fetch(camel(field.to_s), ''), csv_logical(field, item)] }
|
return value.to_h { |field, item| [@labels.fetch(camel(field.to_s), ''), csv_logical(field, item)] }
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ class CountsTableQuery
|
|||||||
when 'user'
|
when 'user'
|
||||||
User.where(id: values).pluck(:id, :name).to_h.transform_keys(&:to_s)
|
User.where(id: values).pluck(:id, :name).to_h.transform_keys(&:to_s)
|
||||||
when 'language_code', 'source_language_code'
|
when 'language_code', 'source_language_code'
|
||||||
Language.where(code: values).pluck(:code, :name).to_h
|
values.to_h { [_1, _1] }
|
||||||
when 'tags'
|
when 'tags'
|
||||||
labels = tag_labels(values)
|
labels = tag_labels(values)
|
||||||
@axis_names["a#{ index }"] = @tag_names
|
@axis_names["a#{ index }"] = @tag_names
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ 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'
|
||||||
@@ -132,7 +134,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 = locales.find (entry => entry.code === locale)?.name
|
const localeName = languageUIMessage (localeLanguage (locale), locale).text
|
||||||
const navigate = useLocaleNavigate ()
|
const navigate = useLocaleNavigate ()
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const currentLocation = useRef (location)
|
const currentLocation = useRef (location)
|
||||||
@@ -447,7 +449,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 (entry.name, entry.code)}
|
{formatUIString (languageUIMessage (localeLanguage (entry.code), locale).text, locale)}
|
||||||
</option>))}
|
</option>))}
|
||||||
</select>
|
</select>
|
||||||
</label>)
|
</label>)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useId, useState } from 'react'
|
import { useId, useLayoutEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import AnimatedCollapse from '@/components/common/AnimatedCollapse'
|
import AnimatedCollapse from '@/components/common/AnimatedCollapse'
|
||||||
import SortableList from '@/components/common/SortableList'
|
import SortableList from '@/components/common/SortableList'
|
||||||
import { fieldsForCounts, orderedCountFields,
|
import { countChildPaths, fieldsForCounts, normaliseCountsParams, orderedCountFields,
|
||||||
selectedCountChildren, selectedCountColumns, selectedCountFields } from '@/lib/counts'
|
selectedCountChildren, selectedCountColumns, selectedCountFields } from '@/lib/counts'
|
||||||
import { useCountsDefinition } from '@/lib/countsDefinitions'
|
import { useCountsDefinition } from '@/lib/countsDefinitions'
|
||||||
import { useClientMotion } from '@/lib/useClientMotion'
|
import { useClientMotion } from '@/lib/useClientMotion'
|
||||||
@@ -22,12 +22,20 @@ const FieldBranch = ({ field, path, enabled, selected, params, select, update }:
|
|||||||
const [open, setOpen] = useState (false)
|
const [open, setOpen] = useState (false)
|
||||||
const id = useId ()
|
const id = useId ()
|
||||||
const motion = useClientMotion ()
|
const motion = useClientMotion ()
|
||||||
const checked = selected.has (path)
|
const input = useRef<HTMLInputElement> (null)
|
||||||
|
const leaves = countChildPaths (field.children ?? [], path, true)
|
||||||
|
const count = leaves.filter (key => selected.has (key)).length
|
||||||
|
const checked = field.children == null ? selected.has (path)
|
||||||
|
: count > 0 && count === leaves.length
|
||||||
|
const partial = count > 0 && count < leaves.length
|
||||||
|
useLayoutEffect (() => {
|
||||||
|
if (input.current != null) input.current.indeterminate = partial
|
||||||
|
}, [partial])
|
||||||
return <>
|
return <>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<label className="inline-flex items-center gap-1">
|
<label className="inline-flex items-center gap-1">
|
||||||
<input type="checkbox" checked={checked} disabled={!enabled || (field.children != null
|
<input ref={input} type="checkbox" checked={checked} disabled={!enabled}
|
||||||
&& !field.children.some (child => selected.has (`${path}.${child.key}`)))}
|
aria-checked={partial ? 'mixed' : checked}
|
||||||
onChange={event => select (path, event.target.checked)}/>
|
onChange={event => select (path, event.target.checked)}/>
|
||||||
{label (field.label, field.labelValues)}
|
{label (field.label, field.labelValues)}
|
||||||
</label>
|
</label>
|
||||||
@@ -89,48 +97,36 @@ const CountsFieldTree = (
|
|||||||
update: (key: string, value: string) => void },
|
update: (key: string, value: string) => void },
|
||||||
) => {
|
) => {
|
||||||
const definition = useCountsDefinition ()
|
const definition = useCountsDefinition ()
|
||||||
|
const resolved = normaliseCountsParams (resource, params, definition)
|
||||||
const fields = suppliedFields ?? fieldsForCounts (resource, definition)
|
const fields = suppliedFields ?? fieldsForCounts (resource, definition)
|
||||||
const roots = suppliedFields == null ? selectedCountColumns (resource, params, definition)
|
const roots = suppliedFields == null ? selectedCountColumns (resource, resolved, definition)
|
||||||
: selectedCountFields (fields, params, selectionKey, parent).map (field => field.key)
|
: selectedCountFields (fields, resolved, selectionKey, parent).map (field => field.key)
|
||||||
const selected = selectedCountChildren (params)
|
const selected = selectedCountChildren (resolved, fieldsForCounts (resource, definition))
|
||||||
const rootPath = (key: string) => parent ? `${parent}.${key}` : key
|
const rootPath = (key: string) => parent ? `${parent}.${key}` : key
|
||||||
for (const root of roots) selected.add (rootPath (root))
|
for (const root of roots) selected.add (rootPath (root))
|
||||||
const select = (path: string, checked: boolean) => {
|
const select = (path: string, checked: boolean) => {
|
||||||
const root = fields.find (field => rootPath (field.key) === path)
|
const next = new Set (selected)
|
||||||
if (root != null)
|
const paths = countChildPaths (fields, parent)
|
||||||
|
.filter (key => key === path || key.startsWith (`${path}.`))
|
||||||
|
for (const key of paths)
|
||||||
{
|
{
|
||||||
const next = new Set<string> (roots)
|
if (checked) next.add (key)
|
||||||
if (checked) next.add (root.key)
|
else next.delete (key)
|
||||||
else next.delete (root.key)
|
|
||||||
update (selectionKey, fields.filter (field => next.has (field.key))
|
|
||||||
.map (field => field.key).join (' '))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (checked)
|
const nextParams = new URLSearchParams (resolved)
|
||||||
{
|
nextParams.set ('children', [...next].filter (key => key.includes ('.')).join (' '))
|
||||||
const parts = path.split ('.')
|
const children = selectedCountChildren (nextParams, fieldsForCounts (resource, definition))
|
||||||
for (let index = 1; index < parts.length; ++index)
|
|
||||||
selected.add (parts.slice (0, index + 1).join ('.'))
|
|
||||||
}
|
|
||||||
else selected.delete (path)
|
|
||||||
const nextParams = new URLSearchParams (params)
|
|
||||||
nextParams.set ('children', [...selected].filter (key => key.includes ('.')).join (' '))
|
|
||||||
const children = selectedCountChildren (nextParams)
|
|
||||||
update ('children', [...children].join (' '))
|
update ('children', [...children].join (' '))
|
||||||
const nextRoots = new Set (roots)
|
update (selectionKey, fields.filter (field => field.children == null
|
||||||
const ancestor = fields.find (field => path.startsWith (`${rootPath (field.key)}.`))
|
? next.has (rootPath (field.key)) : field.children.some (child =>
|
||||||
if (checked && ancestor != null) nextRoots.add (ancestor.key)
|
children.has (`${rootPath (field.key)}.${child.key}`)))
|
||||||
for (const field of fields)
|
|
||||||
if (field.children != null && !field.children.some (child =>
|
|
||||||
children.has (`${rootPath (field.key)}.${child.key}`))) nextRoots.delete (field.key)
|
|
||||||
update (selectionKey, fields.filter (field => nextRoots.has (field.key))
|
|
||||||
.map (field => field.key).join (' '))
|
.map (field => field.key).join (' '))
|
||||||
}
|
}
|
||||||
const visibleFields = parent === ''
|
const visibleFields = parent === ''
|
||||||
? fields.filter (field => field.key !== 'thumbnail') : fields
|
? fields.filter (field => field.key !== 'thumbnail') : fields
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FieldGroup fields={visibleFields} path={parent} params={params} enabled selected={selected}
|
<FieldGroup fields={visibleFields} path={parent} params={resolved} enabled selected={selected}
|
||||||
select={select} update={update}/>
|
select={select} update={update}/>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-26
@@ -148,10 +148,8 @@ const nested: Partial<Record<CountColumn, CountField[]>> = {
|
|||||||
{ key: 'id', label: 'counts:columns.resourceId' },
|
{ key: 'id', label: 'counts:columns.resourceId' },
|
||||||
{ key: 'versionNo', label: 'history:version' },
|
{ key: 'versionNo', label: 'history:version' },
|
||||||
{ key: 'kind', label: 'counts:columns.kind' },
|
{ key: 'kind', label: 'counts:columns.kind' },
|
||||||
{ key: 'name', label: 'history:name' },
|
|
||||||
{ key: 'category', label: 'common:categoryLabel' },
|
{ key: 'category', label: 'common:categoryLabel' },
|
||||||
{ key: 'sections', label: 'counts:columns.sections' },
|
{ key: 'sections', label: 'counts:columns.sections' }],
|
||||||
{ key: 'names', label: 'counts:columns.names', children: nameFields }],
|
|
||||||
names: nameFields,
|
names: nameFields,
|
||||||
aliases: [{ key: 'languageCode', label: 'counts:columns.language' }, ...aliasFields],
|
aliases: [{ key: 'languageCode', label: 'counts:columns.language' }, ...aliasFields],
|
||||||
parents: [{ key: 'id', label: 'counts:columns.resourceId' }] }
|
parents: [{ key: 'id', label: 'counts:columns.resourceId' }] }
|
||||||
@@ -167,7 +165,8 @@ export const columnsForCounts = (
|
|||||||
? ['title', 'url', 'thumbnail_base', 'tags', 'parents', 'video_ms',
|
? ['title', 'url', 'thumbnail_base', 'tags', 'parents', 'video_ms',
|
||||||
'original_created_from', 'original_created_before']
|
'original_created_from', 'original_created_before']
|
||||||
: (resource === 'tags'
|
: (resource === 'tags'
|
||||||
? ['name', 'names', 'category', 'aliases', 'parents', 'deprecated']
|
? [...(definition?.nameColumns?.map (field => field.key) ?? []),
|
||||||
|
'category', 'parents', 'deprecated']
|
||||||
: (definition?.fields.filter (field => ![
|
: (definition?.fields.filter (field => ![
|
||||||
'resource_id', 'version_no', 'event_type', 'user', 'created_at']
|
'resource_id', 'version_no', 'event_type', 'user', 'created_at']
|
||||||
.includes (field.key)).map (field => field.key) ?? []))),
|
.includes (field.key)).map (field => field.key) ?? []))),
|
||||||
@@ -182,10 +181,8 @@ export const fieldsForCounts = (
|
|||||||
key,
|
key,
|
||||||
label: (key === 'parents' && resource === 'tags') ? 'tags:parents'
|
label: (key === 'parents' && resource === 'tags') ? 'tags:parents'
|
||||||
: (countColumns[key] ?? countsFieldLabel (key)),
|
: (countColumns[key] ?? countsFieldLabel (key)),
|
||||||
children: key === 'parents' && resource === 'tags'
|
children: (key === 'tags' || (key === 'parents' && resource === 'tags'))
|
||||||
? [...(nested.parents ?? []), { key: 'name', label: 'counts:columns.name' },
|
? [...(nested[key] ?? []), ...(definition?.nameColumns ?? [])] : nested[key] }))
|
||||||
{ key: 'names', label: 'counts:columns.names', children: nameFields }]
|
|
||||||
: nested[key] }))
|
|
||||||
|
|
||||||
|
|
||||||
export const orderedCountFields = (
|
export const orderedCountFields = (
|
||||||
@@ -235,7 +232,9 @@ export const selectedCountColumns = (
|
|||||||
const common = ['resource_id', 'version_no', 'event_type', 'user', 'created_at']
|
const common = ['resource_id', 'version_no', 'event_type', 'user', 'created_at']
|
||||||
const defaults: Record<CountsResource, string[]> = {
|
const defaults: Record<CountsResource, string[]> = {
|
||||||
posts: ['title', 'url', 'tags', 'parents', 'video_ms'],
|
posts: ['title', 'url', 'tags', 'parents', 'video_ms'],
|
||||||
tags: ['name', 'category', 'aliases', 'parents'],
|
tags: [...(definition?.nameColumns ?? []).filter (field =>
|
||||||
|
field.nameLocale?.code === currentViewingLocale ()).map (field => field.key),
|
||||||
|
'category', 'parents'],
|
||||||
nico_tags: ['name', 'linked_tags'],
|
nico_tags: ['name', 'linked_tags'],
|
||||||
tag_descriptions: ['language_code', 'body', 'source_language_code'],
|
tag_descriptions: ['language_code', 'body', 'source_language_code'],
|
||||||
tag_groups: ['name', 'description', 'members'],
|
tag_groups: ['name', 'description', 'members'],
|
||||||
@@ -247,16 +246,39 @@ export const selectedCountColumns = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const countChildPaths = (fields: CountField[], prefix = ''): string[] =>
|
export const countChildPaths = (
|
||||||
|
fields: CountField[], prefix = '', leavesOnly = false,
|
||||||
|
): string[] =>
|
||||||
fields.flatMap (field => {
|
fields.flatMap (field => {
|
||||||
const path = prefix ? `${prefix}.${field.key}` : field.key
|
const path = prefix ? `${prefix}.${field.key}` : field.key
|
||||||
return [path, ...countChildPaths (field.children ?? [], path)]
|
return [...((leavesOnly && field.children != null) ? [] : [path]),
|
||||||
|
...countChildPaths (field.children ?? [], path, leavesOnly)]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
export const selectedCountChildren = (params: URLSearchParams): Set<string> => {
|
export const selectedCountChildren = (
|
||||||
|
params: URLSearchParams, fields?: CountField[],
|
||||||
|
): Set<string> => {
|
||||||
const selected = new Set (params.has ('children') ? params.get ('children')!.split (' ')
|
const selected = new Set (params.has ('children') ? params.get ('children')!.split (' ')
|
||||||
: ['tags.name', 'parents.id', 'aliases.name', 'members.tagId'])
|
: ['tags.name', 'parents.id', 'aliases.name', 'members.tagId'])
|
||||||
|
if (fields != null)
|
||||||
|
{
|
||||||
|
const valid = new Set (countChildPaths (fields).filter (path => path.includes ('.')))
|
||||||
|
for (const path of selected) if (!valid.has (path)) selected.delete (path)
|
||||||
|
const visit = (fields: CountField[], prefix = '') => {
|
||||||
|
for (const field of fields)
|
||||||
|
{
|
||||||
|
const path = prefix ? `${prefix}.${field.key}` : field.key
|
||||||
|
if (field.children == null) continue
|
||||||
|
visit (field.children, path)
|
||||||
|
if (prefix === '') continue
|
||||||
|
if (field.children.some (child => selected.has (`${path}.${child.key}`))) selected.add (path)
|
||||||
|
else selected.delete (path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit (fields)
|
||||||
|
return selected
|
||||||
|
}
|
||||||
const clean = (fields: CountField[], prefix: string) => {
|
const clean = (fields: CountField[], prefix: string) => {
|
||||||
for (const field of fields)
|
for (const field of fields)
|
||||||
{
|
{
|
||||||
@@ -344,6 +366,15 @@ export const normaliseCountsParams = (
|
|||||||
field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key
|
field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key
|
||||||
const rootKey = (key: string) => (key === 'name' && current != null) ? current
|
const rootKey = (key: string) => (key === 'name' && current != null) ? current
|
||||||
: ((key === 'aliases' && aliases != null) ? aliases : key)
|
: ((key === 'aliases' && aliases != null) ? aliases : key)
|
||||||
|
const childKeys = (key: string): string[] => {
|
||||||
|
const [parent, field, ...rest] = key.split ('.')
|
||||||
|
if (parent !== 'tags' && (parent !== 'parents' || resource !== 'tags')) return [key]
|
||||||
|
if (field === 'name' && current != null) return [`${parent}.${current}`]
|
||||||
|
if (field === 'names')
|
||||||
|
return names.map (name => `${parent}.${name}`)
|
||||||
|
if (field === 'aliases' && aliases != null) return [`${parent}.${aliases}`]
|
||||||
|
return rest.length > 0 ? [] : [key]
|
||||||
|
}
|
||||||
if (resource === 'tags' && params.has ('columns'))
|
if (resource === 'tags' && params.has ('columns'))
|
||||||
params.set ('columns', params.get ('columns')!.split (' ').flatMap (key =>
|
params.set ('columns', params.get ('columns')!.split (' ').flatMap (key =>
|
||||||
key === 'names' ? names : [rootKey (key)])
|
key === 'names' ? names : [rootKey (key)])
|
||||||
@@ -351,18 +382,12 @@ export const normaliseCountsParams = (
|
|||||||
if (params.has ('children'))
|
if (params.has ('children'))
|
||||||
{
|
{
|
||||||
const selected = params.get ('children')!.split (' ')
|
const selected = params.get ('children')!.split (' ')
|
||||||
params.set ('children', selected.flatMap (key => {
|
params.set ('children', [...new Set (selected.flatMap (childKeys))].join (' '))
|
||||||
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'))
|
if (params.has ('order'))
|
||||||
params.set ('order', params.get ('order')!.split (' ').flatMap (key =>
|
params.set ('order', params.get ('order')!.split (' ').flatMap (key =>
|
||||||
key === 'names' ? names : [rootKey (key)])
|
key.includes ('.') ? childKeys (key)
|
||||||
|
: ((resource === 'tags') ? (key === 'names' ? names : [rootKey (key)]) : [key]))
|
||||||
.join (' '))
|
.join (' '))
|
||||||
}
|
}
|
||||||
for (const [key, value] of params)
|
for (const [key, value] of params)
|
||||||
@@ -419,16 +444,25 @@ export const normaliseCountsParams = (
|
|||||||
}
|
}
|
||||||
params.set (key, value)
|
params.set (key, value)
|
||||||
}
|
}
|
||||||
const children = selectedCountChildren (params)
|
|
||||||
if (params.has ('children')) params.set ('children', [...children].filter (Boolean).join (' '))
|
|
||||||
if (params.has ('columns'))
|
|
||||||
{
|
|
||||||
const fields = fieldsForCounts (resource, definition)
|
const fields = fieldsForCounts (resource, definition)
|
||||||
|
const children = selectedCountChildren (params, definition == null ? undefined : fields)
|
||||||
|
if (params.has ('columns') && definition != null)
|
||||||
|
{
|
||||||
params.set ('columns', params.get ('columns')!.split (' ').filter (key => {
|
params.set ('columns', params.get ('columns')!.split (' ').filter (key => {
|
||||||
const field = fields.find (field => field.key === key)
|
const field = fields.find (field => field.key === key)
|
||||||
return field != null && (field.children == null
|
return field != null && (field.children == null
|
||||||
|| field.children.some (child => children.has (`${key}.${child.key}`)))
|
|| field.children.some (child => children.has (`${key}.${child.key}`)))
|
||||||
}).join (' '))
|
}).join (' '))
|
||||||
|
const roots = new Set (params.get ('columns')!.split (' '))
|
||||||
|
for (const path of children)
|
||||||
|
if (!roots.has (path.split ('.')[0])) children.delete (path)
|
||||||
|
}
|
||||||
|
if (params.has ('children')) params.set ('children', [...children].filter (Boolean).join (' '))
|
||||||
|
if (params.has ('order') && definition != null)
|
||||||
|
{
|
||||||
|
const valid = new Set (countChildPaths (fields))
|
||||||
|
params.set ('order', [...new Set (params.get ('order')!.split (' '))]
|
||||||
|
.filter (key => valid.has (key)).join (' '))
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createContext, useContext } from 'react'
|
import { createContext, useContext } from 'react'
|
||||||
|
|
||||||
import { i18n, translateUIMessage } from '@/lib/i18n'
|
import { languageUIMessage, translateUIMessage } from '@/lib/i18n'
|
||||||
import { apiGet } from '@/lib/api'
|
import { apiGet } from '@/lib/api'
|
||||||
|
|
||||||
import type { LocaleCode } from '@/lib/locales'
|
import type { LocaleCode } from '@/lib/locales'
|
||||||
@@ -52,9 +52,7 @@ const localiseCountsDefinition = (
|
|||||||
): CountsDefinition => {
|
): CountsDefinition => {
|
||||||
const heading = (name: CountField['nameLocale'], aliasesFlg: boolean) => {
|
const heading = (name: CountField['nameLocale'], aliasesFlg: boolean) => {
|
||||||
if (name == null) return null
|
if (name == null) return null
|
||||||
const key = `languageNames.${name.languageCode}`
|
const language = languageUIMessage (name.languageCode, locale).text
|
||||||
const language = i18n.exists (key, { ns: 'counts', lng: locale })
|
|
||||||
? translateUIMessage (key, 'counts', locale).text : name.languageCode
|
|
||||||
const scripts = new Set ((definition.nameColumns ?? [])
|
const scripts = new Set ((definition.nameColumns ?? [])
|
||||||
.filter (field => field.nameLocale?.languageCode === name.languageCode)
|
.filter (field => field.nameLocale?.languageCode === name.languageCode)
|
||||||
.map (field => field.nameLocale!.scriptCode.toLowerCase ()))
|
.map (field => field.nameLocale!.scriptCode.toLowerCase ()))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fieldsForCounts, orderedCountFields, selectedCountChildren,
|
import { fieldsForCounts, orderedCountFields, selectedCountChildren,
|
||||||
sortedCountChildren } from '@/lib/counts'
|
sortedCountChildren } from '@/lib/counts'
|
||||||
import { countsEventTypes, countsFieldLabel } from '@/lib/countsDefinitions'
|
import { countsEventTypes, countsFieldLabel } from '@/lib/countsDefinitions'
|
||||||
import { translateUIMessage } from '@/lib/i18n'
|
import { languageUIMessage, translateUIMessage } from '@/lib/i18n'
|
||||||
import { localisedDateString, msToTime } from '@/lib/utils'
|
import { localisedDateString, msToTime } from '@/lib/utils'
|
||||||
|
|
||||||
import type { CountColumn, CountField, CountRecord, CountsResult,
|
import type { CountColumn, CountField, CountRecord, CountsResult,
|
||||||
@@ -133,15 +133,14 @@ export const countsLogicalName = (
|
|||||||
const code = String (value)
|
const code = String (value)
|
||||||
if (field === 'languageCode' || field === 'scriptCode')
|
if (field === 'languageCode' || field === 'scriptCode')
|
||||||
{
|
{
|
||||||
if (field === 'languageCode' && ['ja', 'ain'].includes (code))
|
if (field === 'languageCode') return languageUIMessage (code, locale)
|
||||||
return translateUIMessage (`history:languageNames.${code}`, 'counts', locale)
|
const master = row.scriptName
|
||||||
const master = row[field === 'languageCode' ? 'languageName' : 'scriptName']
|
|
||||||
if (typeof master === 'string' && master.trim () !== ''
|
if (typeof master === 'string' && master.trim () !== ''
|
||||||
&& master.toLowerCase () !== code.toLowerCase ())
|
&& master.toLowerCase () !== code.toLowerCase ())
|
||||||
return { text: master }
|
return { text: master }
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const type = field === 'languageCode' ? 'language' : 'script'
|
const type = 'script'
|
||||||
if (!displayNames.has (type))
|
if (!displayNames.has (type))
|
||||||
displayNames.set (type, new Intl.DisplayNames (['ja'], { type, fallback: 'none' }))
|
displayNames.set (type, new Intl.DisplayNames (['ja'], { type, fallback: 'none' }))
|
||||||
const text = displayNames.get (type)!.of (code)
|
const text = displayNames.get (type)!.of (code)
|
||||||
@@ -305,9 +304,9 @@ export const countsExportLabels = (
|
|||||||
`common:categories.${key}`, 'counts', locale).text
|
`common:categories.${key}`, 'counts', locale).text
|
||||||
for (const key of ['true', 'false'])
|
for (const key of ['true', 'false'])
|
||||||
labels[`boolean:${key}`] = translateUIMessage (`boolean.${key}`, 'counts', locale).text
|
labels[`boolean:${key}`] = translateUIMessage (`boolean.${key}`, 'counts', locale).text
|
||||||
for (const key of ['ja', 'ain'])
|
for (const key of new Set (['ja', 'ain', 'en', ...(definition?.nameColumns ?? [])
|
||||||
labels[`language:${key}`] = translateUIMessage (
|
.map (field => field.nameLocale?.languageCode).filter (code => code != null)]))
|
||||||
`history:languageNames.${key}`, 'counts', locale).text
|
labels[`language:${key}`] = languageUIMessage (key, locale).text
|
||||||
labels.id = translateUIMessage ('columns.resourceId', 'counts', locale).text
|
labels.id = translateUIMessage ('columns.resourceId', 'counts', locale).text
|
||||||
labels.name = translateUIMessage ('history:name', 'counts', locale).text
|
labels.name = translateUIMessage ('history:name', 'counts', locale).text
|
||||||
for (const event of countsEventTypes)
|
for (const event of countsEventTypes)
|
||||||
@@ -403,11 +402,13 @@ export const countsSummaryValue = (
|
|||||||
const message = (key: string, values?: Record<string, string | number>) =>
|
const message = (key: string, values?: Record<string, string | number>) =>
|
||||||
translateUIMessage (key, 'counts', locale, values).text
|
translateUIMessage (key, 'counts', locale, values).text
|
||||||
if (value == null) return axis.field === 'user' ? message ('bot') : ''
|
if (value == null) return axis.field === 'user' ? message ('bot') : ''
|
||||||
|
if (['language_code', 'source_language_code'].includes (axis.field))
|
||||||
|
return languageUIMessage (String (value), locale).text
|
||||||
const logical = data.axisLabels?.[`a${index}`]?.[String (value)]
|
const logical = data.axisLabels?.[`a${index}`]?.[String (value)]
|
||||||
if (axis.field === 'user') return message ('actorWithId', {
|
if (axis.field === 'user') return message ('actorWithId', {
|
||||||
name: logical || message ('notRegistered'), id: value })
|
name: logical || message ('notRegistered'), id: value })
|
||||||
if (logical != null) return logical
|
if (logical != null) return logical
|
||||||
if (['tags', 'language_code', 'source_language_code'].includes (axis.field))
|
if (axis.field === 'tags')
|
||||||
return message ('notRegistered')
|
return message ('notRegistered')
|
||||||
if (axis.field === 'changed_field') return message (countsFieldLabel (String (value)))
|
if (axis.field === 'changed_field') return message (countsFieldLabel (String (value)))
|
||||||
if (axis.field === 'event_type') return message (`events.${value}`)
|
if (axis.field === 'event_type') return message (`events.${value}`)
|
||||||
|
|||||||
@@ -111,6 +111,16 @@ export const translateUIMessage = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const languageUIMessage = (
|
||||||
|
code: string, locale: string, instance: I18n = i18n,
|
||||||
|
): { text: string; language: string } => {
|
||||||
|
const key = `languageNames.${code}`
|
||||||
|
return instance.exists (key, { ns: 'common', lng: locale })
|
||||||
|
? translateUIMessage (key, 'common', locale, { }, instance)
|
||||||
|
: { text: code, language: 'und' }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const untranslatedKeys = (
|
export const untranslatedKeys = (
|
||||||
locale: LocaleCode,
|
locale: LocaleCode,
|
||||||
namespace = 'navigation',
|
namespace = 'navigation',
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ export const fetchLocales = (): Promise<{ locales: AvailableLocale[] }> =>
|
|||||||
apiGet ('/locales')
|
apiGet ('/locales')
|
||||||
|
|
||||||
|
|
||||||
export const LOCALE_LABELS: Record<LocaleCode, string> = {
|
|
||||||
ja: '日本語', 'ain-Kana': 'ヤウンモシルン アイヌ イタㇰ' }
|
|
||||||
|
|
||||||
export const isLocaleCode = (value: unknown): value is LocaleCode =>
|
export const isLocaleCode = (value: unknown): value is LocaleCode =>
|
||||||
SUPPORTED_LOCALES.some (locale => locale === value)
|
SUPPORTED_LOCALES.some (locale => locale === value)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"_untranslated": ["languageNames.en"],
|
||||||
|
"languageNames": {
|
||||||
|
"ja": "シサㇺ イタㇰ",
|
||||||
|
"ain": "ヤウンモシルン アイヌ イタㇰ"
|
||||||
|
},
|
||||||
"loadingLabel": "$t(common:loading)",
|
"loadingLabel": "$t(common:loading)",
|
||||||
"countLabel": "チピㇱキㇷ゚",
|
"countLabel": "チピㇱキㇷ゚",
|
||||||
"categoryLabel": "イソネカ",
|
"categoryLabel": "イソネカ",
|
||||||
|
|||||||
@@ -44,10 +44,6 @@
|
|||||||
"automatic": "自動生成",
|
"automatic": "自動生成",
|
||||||
"certified": "公証済み"
|
"certified": "公証済み"
|
||||||
},
|
},
|
||||||
"languageNames": {
|
|
||||||
"ja": "シサㇺ イタㇰ",
|
|
||||||
"ain": "ヤウンモシルン アイヌ イタㇰ"
|
|
||||||
},
|
|
||||||
"revert": "アホシピレ",
|
"revert": "アホシピレ",
|
||||||
"restored": "アホシピレ オケレ!",
|
"restored": "アホシピレ オケレ!",
|
||||||
"restoreFailed": "アホシピレ カ エアイカㇷ゚……",
|
"restoreFailed": "アホシピレ カ エアイカㇷ゚……",
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"languageNames": {
|
||||||
|
"ja": "日本語",
|
||||||
|
"ain": "北海道アイヌ語",
|
||||||
|
"en": "英語"
|
||||||
|
},
|
||||||
"loadingLabel": "Loading",
|
"loadingLabel": "Loading",
|
||||||
"countLabel": "件数",
|
"countLabel": "件数",
|
||||||
"categoryLabel": "カテゴリ",
|
"categoryLabel": "カテゴリ",
|
||||||
|
|||||||
@@ -12,11 +12,6 @@
|
|||||||
"localeNameWithScript": "名称({{language}}・{{script}})",
|
"localeNameWithScript": "名称({{language}}・{{script}})",
|
||||||
"localeAliases": "別名({{language}})",
|
"localeAliases": "別名({{language}})",
|
||||||
"localeAliasesWithScript": "別名({{language}}・{{script}})",
|
"localeAliasesWithScript": "別名({{language}}・{{script}})",
|
||||||
"languageNames": {
|
|
||||||
"ja": "日本語",
|
|
||||||
"en": "英語",
|
|
||||||
"ain": "アイヌ語"
|
|
||||||
},
|
|
||||||
"run": "集計実行",
|
"run": "集計実行",
|
||||||
"conditions": "集計条件",
|
"conditions": "集計条件",
|
||||||
"output": "出力項目",
|
"output": "出力項目",
|
||||||
|
|||||||
@@ -31,10 +31,6 @@
|
|||||||
"automatic": "自動生成",
|
"automatic": "自動生成",
|
||||||
"certified": "公証済み"
|
"certified": "公証済み"
|
||||||
},
|
},
|
||||||
"languageNames": {
|
|
||||||
"ja": "日本語",
|
|
||||||
"ain": "北海道アイヌ語"
|
|
||||||
},
|
|
||||||
"revert": "差戻",
|
"revert": "差戻",
|
||||||
"restored": "差戻しました.",
|
"restored": "差戻しました.",
|
||||||
"restoreFailed": "差戻に失敗……",
|
"restoreFailed": "差戻に失敗……",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { apiPut } from '@/lib/api'
|
|||||||
import { useDialogue } from '@/lib/dialogues/useDialogue'
|
import { useDialogue } from '@/lib/dialogues/useDialogue'
|
||||||
import { clientScrollBehaviour } from '@/lib/clientAnimation'
|
import { clientScrollBehaviour } from '@/lib/clientAnimation'
|
||||||
import { localeLanguage } from '@/lib/locales'
|
import { localeLanguage } from '@/lib/locales'
|
||||||
|
import { languageUIMessage } from '@/lib/i18n'
|
||||||
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
import { fetchTagChanges } from '@/lib/tags'
|
import { fetchTagChanges } from '@/lib/tags'
|
||||||
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
|
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
|
||||||
@@ -150,7 +151,7 @@ const TagVersionRows = ({ change, children }: {
|
|||||||
children: (primary: ReactNode) => ReactNode }) => {
|
children: (primary: ReactNode) => ReactNode }) => {
|
||||||
const locale = useLocale ()
|
const locale = useLocale ()
|
||||||
const language = localeLanguage (locale)
|
const language = localeLanguage (locale)
|
||||||
const { message, label } = useUITranslation ('history')
|
const { label } = useUITranslation ('history')
|
||||||
const { i18n } = useTranslation ('history')
|
const { i18n } = useTranslation ('history')
|
||||||
const panelId = useId ()
|
const panelId = useId ()
|
||||||
const names = snapshotNames (change.names)
|
const names = snapshotNames (change.names)
|
||||||
@@ -210,15 +211,14 @@ const TagVersionRows = ({ change, children }: {
|
|||||||
</tr>)}
|
</tr>)}
|
||||||
{otherLanguages.map ((code, index) => {
|
{otherLanguages.map ((code, index) => {
|
||||||
const entry = nameChanges.find (value => value.languageCode === code)
|
const entry = nameChanges.find (value => value.languageCode === code)
|
||||||
const key = `languageNames.${code}`
|
const language = languageUIMessage (code, locale, i18n)
|
||||||
const known = i18n.exists (key, { ns: 'history', lng: locale })
|
|
||||||
const next = entry != null ? entry.current : names[code] ?? null
|
const next = entry != null ? entry.current : names[code] ?? null
|
||||||
const prev = entry != null ? entry.prev : next
|
const prev = entry != null ? entry.prev : next
|
||||||
return (
|
return (
|
||||||
<tr key={code} id={rowIds[index]} hidden={!expanded}>
|
<tr key={code} id={rowIds[index]} hidden={!expanded}>
|
||||||
<th scope="row" className="px-2 pt-1 pb-2 text-left text-sm font-normal">
|
<th scope="row" className="px-2 pt-1 pb-2 text-left text-sm font-normal">
|
||||||
<LocalisedText language={known ? code : 'und'}>
|
<LocalisedText language={language.language}>
|
||||||
{known ? message (key).text : code}
|
{language.text}
|
||||||
</LocalisedText>
|
</LocalisedText>
|
||||||
</th>
|
</th>
|
||||||
<td className="px-2 pt-1 pb-2">
|
<td className="px-2 pt-1 pb-2">
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする