|
|
|
@@ -1,9 +1,11 @@
|
|
|
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
|
|
|
import { useEffect } from 'react'
|
|
|
|
|
import { useEffect, useId, useState } from 'react'
|
|
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
|
import { useLocation } from 'react-router-dom'
|
|
|
|
|
|
|
|
|
|
import { useLocale } from '@/lib/useLocale'
|
|
|
|
|
import PrefetchLink from '@/components/PrefetchLink'
|
|
|
|
|
import LocalisedText from '@/components/LocalisedText'
|
|
|
|
|
import PageTitle from '@/components/common/PageTitle'
|
|
|
|
|
import Pagination from '@/components/common/Pagination'
|
|
|
|
|
import MainArea from '@/components/layout/MainArea'
|
|
|
|
@@ -12,6 +14,7 @@ import PageHead from '@/components/common/PageHead'
|
|
|
|
|
import { apiPut } from '@/lib/api'
|
|
|
|
|
import { useDialogue } from '@/lib/dialogues/useDialogue'
|
|
|
|
|
import { clientScrollBehaviour } from '@/lib/clientAnimation'
|
|
|
|
|
import { localeLanguage } from '@/lib/locales'
|
|
|
|
|
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
|
|
|
|
import { fetchTagChanges } from '@/lib/tags'
|
|
|
|
|
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
|
|
|
|
@@ -20,7 +23,60 @@ import { cn, localisedDateString } from '@/lib/utils'
|
|
|
|
|
|
|
|
|
|
import type { FC, ReactNode } from 'react'
|
|
|
|
|
|
|
|
|
|
import type { TagNameSnapshot } from '@/types'
|
|
|
|
|
import type { TagNameSnapshot, TagVersion } from '@/types'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const normaliseNameSnapshot = (value: unknown): TagNameSnapshot | null => {
|
|
|
|
|
if (value == null || typeof value !== 'object' || Array.isArray (value))
|
|
|
|
|
return null
|
|
|
|
|
const entry = value as Record<string, unknown>
|
|
|
|
|
const automatic = entry.autoGenerated ?? entry.auto_generated
|
|
|
|
|
const script = entry.scriptCode ?? entry.script_code
|
|
|
|
|
const aliases = Array.isArray (entry.aliases) ? entry.aliases : []
|
|
|
|
|
return {
|
|
|
|
|
name: typeof entry.name === 'string' ? entry.name : null,
|
|
|
|
|
scriptCode: typeof script === 'string' ? script : null,
|
|
|
|
|
autoGenerated: typeof automatic === 'boolean' ? automatic : null,
|
|
|
|
|
aliases: aliases.flatMap<TagNameSnapshot['aliases'][number]> (alias => {
|
|
|
|
|
if (typeof alias === 'string')
|
|
|
|
|
return [{ name: alias, scriptCode: null }]
|
|
|
|
|
if (alias == null || typeof alias !== 'object' || typeof alias.name !== 'string')
|
|
|
|
|
return []
|
|
|
|
|
const aliasScript = alias.scriptCode ?? alias.script_code
|
|
|
|
|
return [{ name: alias.name,
|
|
|
|
|
scriptCode: typeof aliasScript === 'string' ? aliasScript : null }]
|
|
|
|
|
}) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const snapshotNames = (names: TagVersion['names']): Record<string, TagNameSnapshot> => {
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
const entries = typeof names === 'string' ? JSON.parse (names) : names
|
|
|
|
|
if (entries == null || typeof entries !== 'object' || Array.isArray (entries))
|
|
|
|
|
return { }
|
|
|
|
|
return Object.fromEntries (Object.entries (entries).flatMap (([language, value]) => {
|
|
|
|
|
const snapshot = normaliseNameSnapshot (value)
|
|
|
|
|
return snapshot == null ? [] : [[language, snapshot]]
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return { }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const renderPrimaryName = (
|
|
|
|
|
snapshot: TagNameSnapshot,
|
|
|
|
|
language: string,
|
|
|
|
|
label: (key: string) => ReactNode,
|
|
|
|
|
) => (
|
|
|
|
|
<>
|
|
|
|
|
<span lang={language} className="inline-block whitespace-nowrap">{snapshot.name}</span>
|
|
|
|
|
{snapshot.name != null && snapshot.autoGenerated === true
|
|
|
|
|
&& label ('nameChanges.uncertified')}
|
|
|
|
|
</>)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const renderNameSnapshot = (
|
|
|
|
@@ -31,36 +87,30 @@ const renderNameSnapshot = (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<div>
|
|
|
|
|
{label ('name')}:
|
|
|
|
|
<span lang={language} className="inline-block whitespace-nowrap">{snapshot.name}</span>
|
|
|
|
|
{renderPrimaryName (snapshot, language, label)}
|
|
|
|
|
</div>
|
|
|
|
|
{snapshot.scriptCode != null && (
|
|
|
|
|
<div>{label ('nameChanges.script')}:<code>{snapshot.scriptCode}</code></div>)}
|
|
|
|
|
{snapshot.aliases.length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
{label ('tags:aliases')}:
|
|
|
|
|
{snapshot.aliases.map (alias => (
|
|
|
|
|
<span key={`${alias.name}:${alias.scriptCode}`} className="mr-2">
|
|
|
|
|
<span lang={language} className="inline-block whitespace-nowrap">{alias.name}</span>
|
|
|
|
|
{alias.scriptCode != null && <code> ({alias.scriptCode})</code>}
|
|
|
|
|
</span>))}
|
|
|
|
|
</div>)}
|
|
|
|
|
{snapshot.autoGenerated != null && (
|
|
|
|
|
<div>
|
|
|
|
|
{label (snapshot.autoGenerated ? 'nameChanges.automatic' : 'nameChanges.certified')}
|
|
|
|
|
</div>)}
|
|
|
|
|
</div>)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const renderDiff = (
|
|
|
|
|
diff: { current: string | null; prev: string | null },
|
|
|
|
|
format: (value: string) => ReactNode = value => value,
|
|
|
|
|
const renderDiff = <T,> (
|
|
|
|
|
diff: { current: T | null; prev: T | null },
|
|
|
|
|
format: (value: T) => ReactNode,
|
|
|
|
|
equal: (prev: T | null, current: T | null) => boolean = Object.is,
|
|
|
|
|
) => (
|
|
|
|
|
<>
|
|
|
|
|
{diff.prev !== diff.current
|
|
|
|
|
{!(equal (diff.prev, diff.current))
|
|
|
|
|
? (
|
|
|
|
|
<>
|
|
|
|
|
<del className="text-red-600 dark:text-red-400">
|
|
|
|
|
{diff.prev && <>{format (diff.prev)}<br/></>}
|
|
|
|
|
{diff.prev != null && diff.prev !== '' && <>{format (diff.prev)}<br/></>}
|
|
|
|
|
</del>
|
|
|
|
|
<ins className="text-green-600 dark:text-green-400">
|
|
|
|
|
{diff.current != null && format (diff.current)}
|
|
|
|
@@ -70,6 +120,73 @@ const renderDiff = (
|
|
|
|
|
</>)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const TagVersionNames = ({ change }: { change: TagVersion }) => {
|
|
|
|
|
const locale = useLocale ()
|
|
|
|
|
const language = localeLanguage (locale)
|
|
|
|
|
const { message, label } = useUITranslation ('history')
|
|
|
|
|
const { i18n } = useTranslation ('history')
|
|
|
|
|
const [expanded, setExpanded] = useState (false)
|
|
|
|
|
const panelId = useId ()
|
|
|
|
|
const names = snapshotNames (change.names)
|
|
|
|
|
const nameChanges = (change.nameChanges ?? []).map (entry => ({
|
|
|
|
|
...entry,
|
|
|
|
|
current: normaliseNameSnapshot (entry.current),
|
|
|
|
|
prev: normaliseNameSnapshot (entry.prev) }))
|
|
|
|
|
const selected = nameChanges.find (entry => entry.languageCode === language)
|
|
|
|
|
const legacyName = (name: string | null): TagNameSnapshot | null =>
|
|
|
|
|
name == null ? null : {
|
|
|
|
|
name, autoGenerated: null, scriptCode: null, aliases: [] }
|
|
|
|
|
const current =
|
|
|
|
|
selected != null
|
|
|
|
|
? selected.current
|
|
|
|
|
: names[language] ?? legacyName (change.name.current)
|
|
|
|
|
const previous =
|
|
|
|
|
selected != null
|
|
|
|
|
? selected.prev
|
|
|
|
|
: ((change.nameChanges != null && change.name.prev === change.name.current)
|
|
|
|
|
? current : legacyName (change.name.prev))
|
|
|
|
|
const otherLanguages = [...new Set ([...Object.keys (names),
|
|
|
|
|
...nameChanges.map (entry => entry.languageCode)])]
|
|
|
|
|
.filter (code => code !== language).sort ()
|
|
|
|
|
const otherChanges = nameChanges.filter (entry => entry.languageCode !== language)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{renderDiff ({ current, prev: previous },
|
|
|
|
|
snapshot => renderPrimaryName (snapshot, language, label),
|
|
|
|
|
(prev, next) => prev?.name === next?.name
|
|
|
|
|
&& prev?.autoGenerated === next?.autoGenerated)}
|
|
|
|
|
{otherLanguages.length > 0 && (
|
|
|
|
|
<div className="mt-2 text-sm">
|
|
|
|
|
<button type="button" className="text-left underline"
|
|
|
|
|
aria-expanded={expanded} aria-controls={panelId}
|
|
|
|
|
onClick={() => setExpanded (value => !value)}>
|
|
|
|
|
{label ('nameChanges.otherLanguages')}
|
|
|
|
|
{otherChanges.length > 0 && label ('nameChanges.changed')}
|
|
|
|
|
</button>
|
|
|
|
|
<div id={panelId} hidden={!expanded} className="mt-2 space-y-2">
|
|
|
|
|
{otherLanguages.map (code => {
|
|
|
|
|
const entry = nameChanges.find (value => value.languageCode === code)
|
|
|
|
|
const key = `languageNames.${code}`
|
|
|
|
|
const known = i18n.exists (key, { ns: 'history', lng: locale })
|
|
|
|
|
const snapshot = names[code]
|
|
|
|
|
return (
|
|
|
|
|
<div key={code}>
|
|
|
|
|
<LocalisedText language={known ? code : 'und'} className="font-semibold">
|
|
|
|
|
{known ? message (key).text : code}
|
|
|
|
|
</LocalisedText>
|
|
|
|
|
{entry != null
|
|
|
|
|
? renderDiff (entry,
|
|
|
|
|
value => renderNameSnapshot (value, code, label))
|
|
|
|
|
: snapshot != null && renderNameSnapshot (snapshot, code, label)}
|
|
|
|
|
</div>)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>)}
|
|
|
|
|
</>)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const TagHistoryPage: FC = () => {
|
|
|
|
|
const { message, label } = useUITranslation ('history')
|
|
|
|
|
const dialogue = useDialogue ()
|
|
|
|
@@ -150,23 +267,7 @@ const TagHistoryPage: FC = () => {
|
|
|
|
|
className={cn ('even:bg-gray-100 dark:even:bg-gray-700')}>
|
|
|
|
|
<td className="p-2">{change.tagId}.{change.versionNo}</td>
|
|
|
|
|
<td className="p-2 break-all">
|
|
|
|
|
{renderDiff (change.name)}
|
|
|
|
|
{(change.nameChanges?.length ?? 0) > 0 && (
|
|
|
|
|
<div className="mt-2 space-y-2 text-sm">
|
|
|
|
|
<div className="font-semibold">{label ('nameChanges.title')}</div>
|
|
|
|
|
{change.nameChanges?.map (entry => (
|
|
|
|
|
<div key={entry.languageCode}>
|
|
|
|
|
<code>{entry.languageCode}</code>
|
|
|
|
|
{entry.prev != null && (
|
|
|
|
|
<div className="text-red-600 line-through dark:text-red-400">
|
|
|
|
|
{renderNameSnapshot (entry.prev, entry.languageCode, label)}
|
|
|
|
|
</div>)}
|
|
|
|
|
{entry.current != null && (
|
|
|
|
|
<div className="text-green-600 underline dark:text-green-400">
|
|
|
|
|
{renderNameSnapshot (entry.current, entry.languageCode, label)}
|
|
|
|
|
</div>)}
|
|
|
|
|
</div>))}
|
|
|
|
|
</div>)}
|
|
|
|
|
<TagVersionNames change={change}/>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="p-2 break-all">
|
|
|
|
|
{renderDiff (change.category,
|
|
|
|
|