158 行
6.7 KiB
TypeScript
158 行
6.7 KiB
TypeScript
import { createContext, useContext } from 'react'
|
|
|
|
import { languageUIMessage, translateUIMessage } from '@/lib/i18n'
|
|
import { apiGet } from '@/lib/api'
|
|
|
|
import type { LocaleCode } from '@/lib/locales'
|
|
import type { CountField, CountsResource } from '@/lib/counts'
|
|
|
|
export type CountsAxis = { field: string; grain?: string }
|
|
export type CountsCondition = {
|
|
field: string; operator: string; grain?: string; value?: string | number | boolean | null; end?: string; blank?: boolean
|
|
input?: 'single' | 'multiple' | 'list'; values?: (string | number | boolean)[] }
|
|
export type CountsFieldType = 'string' | 'integer' | 'datetime' | 'enum'
|
|
| 'json' | 'boolean' | 'interval'
|
|
|
|
export const countsResources: CountsResource[] = [
|
|
'posts', 'tags', 'nico_tags', 'tag_descriptions', 'tag_groups', 'materials']
|
|
|
|
// Navigation visibility only; supported resources and direct URLs remain available.
|
|
// The external-tag entry currently uses Nico tag history.
|
|
export const visibleCountsResources = [
|
|
'posts', 'tags', 'nico_tags'] as const satisfies readonly CountsResource[]
|
|
|
|
export type CountsFieldMetadata = {
|
|
key: string; type: CountsFieldType; nullable: boolean; operators: string[]
|
|
labels?: string[]; multiple?: boolean; candidateSource?: 'categories' | 'tags'
|
|
setType?: 'tag_reference'; inputTypes?: string[]
|
|
labelValues?: Record<string, string>
|
|
nameLocale?: CountField['nameLocale']
|
|
legacy?: boolean
|
|
choices?: { value: string; label: string }[] }
|
|
export type CountsDefinition = {
|
|
fields: CountsFieldMetadata[]
|
|
outputFields?: CountField[]
|
|
nameColumns?: CountField[]
|
|
columnAliases?: { key: string; value: string }[]
|
|
conditionAliases?: { key: string; value: string }[]
|
|
filterFields?: CountsFieldMetadata[]
|
|
defaults?: { definition: string; mode: string; params: { key: string; value: string }[] }[]
|
|
axes: { latest: string[]; history: string[] }
|
|
grains: string[] }
|
|
|
|
export const CountsDefinitionContext = createContext<CountsDefinition | null> (null)
|
|
|
|
|
|
export const useCountsDefinition = (): CountsDefinition => {
|
|
const definition = useContext (CountsDefinitionContext)
|
|
if (definition == null) throw new Error ('Missing Counts definition')
|
|
return definition
|
|
}
|
|
|
|
|
|
const localiseCountsDefinition = (
|
|
definition: CountsDefinition, locale: LocaleCode,
|
|
): CountsDefinition => {
|
|
const heading = (name: CountField['nameLocale'], aliasesFlg: boolean) => {
|
|
if (name == null) return null
|
|
const language = languageUIMessage (name.languageCode, locale).text
|
|
const label = aliasesFlg ? 'counts:localeAliases' : 'counts:localeName'
|
|
return { label: name.scriptCode == null ? label : `${label}WithScript`,
|
|
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<CountsDefinition> (
|
|
`/counts/definitions/${encodeURIComponent (resource)}`, { locale })
|
|
.then (definition => localiseCountsDefinition (definition, locale))
|
|
|
|
export type CountsCandidate = {
|
|
value: string; id?: number; name: string | null; category?: string | null
|
|
names?: Record<string, unknown>
|
|
kind?: 'internal' | 'external' }
|
|
export type CountsCandidatesResult = {
|
|
rows: CountsCandidate[]; page: number; more: boolean; executionAt: string }
|
|
|
|
export const fetchCountsCandidates = (
|
|
params: URLSearchParams,
|
|
locale: LocaleCode,
|
|
signal?: AbortSignal,
|
|
): Promise<CountsCandidatesResult> => apiGet (
|
|
'/counts/definitions/posts/candidates', {
|
|
params: Object.fromEntries (params), locale, signal })
|
|
|
|
export const countsSetOperators = ['in', 'not_in', 'any', 'all', 'none']
|
|
|
|
export const countsConditionInputs = (
|
|
field: CountsFieldMetadata | undefined,
|
|
operator: string,
|
|
): ('single' | 'multiple' | 'list')[] => {
|
|
if (!countsSetOperators.includes (operator)) return ['single']
|
|
if (field?.candidateSource === 'tags') return ['list']
|
|
return field?.candidateSource == null ? ['multiple'] : ['list', 'multiple']
|
|
}
|
|
|
|
|
|
export const countsFilterFields = (
|
|
definition: CountsDefinition,
|
|
): Record<string, CountsFieldType> => Object.fromEntries (
|
|
(definition.filterFields ?? definition.fields).map (field => [field.key, field.type]))
|
|
|
|
|
|
export const countsFieldLabel = (key: string): string => {
|
|
const labels: Record<string, string> = {
|
|
resource_id: 'columns.resourceId', version_no: 'columns.version',
|
|
user: 'history:actor', created_at: 'columns.changedAt', name: 'history:name',
|
|
title: 'columns.postTitle', url: 'columns.postURL', thumbnail_base: 'columns.thumbnailURL',
|
|
tags: 'columns.tags', tags_json: 'columns.tags', names: 'columns.names',
|
|
aliases: 'tags:aliases', category: 'common:categoryLabel',
|
|
language_code: 'columns.language', source_language_code: 'attributes.sourceLanguage',
|
|
changed_field: 'changedField' }
|
|
return labels[key] ?? `attributes.${key}`
|
|
}
|
|
|
|
|
|
export const countsEventTypes = ['create', 'update', 'discard', 'restore'] as const
|
|
|
|
|
|
export const countsAxisLabel = (
|
|
axis: CountsAxis,
|
|
definition: CountsDefinition,
|
|
locale: LocaleCode,
|
|
): { text: string; language: string } => {
|
|
const field = translateUIMessage (countsFieldLabel (axis.field), 'counts', locale)
|
|
if (definition.fields.find (item => item.key === axis.field)?.type !== 'datetime') return field
|
|
return translateUIMessage ('axisWithGrain', 'counts', locale, {
|
|
field: field.text,
|
|
grain: translateUIMessage (`grains.${axis.grain ?? 'month'}`, 'counts', locale).text })
|
|
}
|
|
|
|
|
|
export const countsAxisFields = (
|
|
definition: CountsDefinition,
|
|
history: boolean,
|
|
): CountField[] => definition.axes[history ? 'history' : 'latest']
|
|
.map (key => ({ key, label: countsFieldLabel (key) }))
|