このコミットが含まれているのは:
2026-10-18 18:28:05 +09:00
コミット 569b9234c6
25個のファイルの変更、483行の追加、144行の削除
+47 -11
ファイルの表示
@@ -1,6 +1,6 @@
// TODO: TagSearch と共通化する.
import { useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
@@ -46,11 +46,19 @@ const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 })
const [focused, setFocused] = useState (false)
const [suggestions, setSuggestions] = useState<Tag[]> ([])
const [request, setRequest] = useState ({ query: '' })
const [result, setResult] = useState<{
locale: string
request: { query: string }
tags: Tag[] }> ({ locale, request, tags: [] })
const currentToken = getTokenAt (tags, Math.min (bounds.start, tags.length)).token
const suggestions = (result.locale === locale && result.request === request
&& request.query === currentToken) ? result.tags : []
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
const handleTagSelect = (tag: Tag) => {
setSuggestionsVsbl (false)
setRequest ({ query: '' })
const textarea = ref.current!
const newValue = replaceToken (tags, bounds.start, bounds.end, tag.name + ' ')
setTags (newValue)
@@ -62,8 +70,10 @@ const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
})
}
const recompute = async (pos: number, v: string = tags) => {
const recompute = (pos: number, v: string = tags) => {
const { start, end, token } = getTokenAt (v, pos)
setRequest ({ query: token })
setSuggestionsVsbl (token.trim () !== '')
if (!(token.trim ()))
{
setSuggestionsVsbl (false)
@@ -71,15 +81,41 @@ const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
}
setBounds ({ start, end })
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: {
q: token,
nico: '0' } })
const nextSuggestions = data.filter (t => t.postCount > 0)
setSuggestions (nextSuggestions)
setSuggestionsVsbl (nextSuggestions.length > 0)
}
useEffect (() => {
if (request.query.trim () === '')
return
const controller = new AbortController ()
let active = true
void (async () => {
try
{
const tags = await apiGet<Tag[]> ('/tags/autocomplete', {
locale, signal: controller.signal, params: { q: request.query, nico: '0' } })
if (active)
{
setResult ({ locale, request,
tags: tags.filter (tag => tag.postCount > 0) })
}
}
catch
{
if (active)
setResult ({ locale, request, tags: [] })
}
}) ()
return () => {
active = false
controller.abort ()
}
}, [request, locale])
useEffect (() => {
setSuggestionsVsbl (false)
setRequest ({ query: '' })
}, [locale])
return (
<FormField className="relative w-full" label={label ('tags')} messages={errors}>
{({ describedBy, invalid }) => (
@@ -93,7 +129,7 @@ const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
onChange={ev => setTags (ev.target.value)}
onSelect={async (ev: SyntheticEvent<HTMLTextAreaElement>) => {
const pos = (ev.target as HTMLTextAreaElement).selectionStart
await recompute (pos)
await recompute (pos, (ev.target as HTMLTextAreaElement).value)
}}
onFocus={() => setFocused (true)}
onBlur={() => {
+53 -16
ファイルの表示
@@ -23,25 +23,57 @@ const TagSearch: FC = () => {
const [activeIndex, setActiveIndex] = useState (-1)
const [search, setSearch] = useState ('')
const [suggestions, setSuggestions] = useState<Tag[]> ([])
const [request, setRequest] = useState ({ query: '' })
const [result, setResult] = useState<{
locale: string
request: { query: string }
tags: Tag[] }> ({ locale, request, tags: [] })
const suggestions = result.locale === locale && result.request === request
? result.tags : []
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => {
setSearch (ev.target.value)
const q = ev.target.value.trim ().split (' ').at (-1)
if (!(q))
{
setSuggestions ([])
return
}
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: { q } })
setSuggestions (data.filter (t => t.postCount > 0))
if (suggestions.length > 0)
setSuggestionsVsbl (true)
const whenChanged = (ev: ChangeEvent<HTMLInputElement>) => {
const value = ev.target.value
const query = value.trim ().split (' ').at (-1) ?? ''
setSearch (value)
setRequest ({ query })
setActiveIndex (-1)
setSuggestionsVsbl (query !== '')
}
useEffect (() => {
if (request.query === '')
return
const controller = new AbortController ()
let active = true
void (async () => {
try
{
const tags = await apiGet<Tag[]> ('/tags/autocomplete', {
locale, signal: controller.signal, params: { q: request.query } })
if (active)
{
setResult ({ locale, request,
tags: tags.filter (tag => tag.postCount > 0) })
}
}
catch
{
if (active)
setResult ({ locale, request, tags: [] })
}
}) ()
return () => {
active = false
controller.abort ()
}
}, [request, locale])
useEffect (() => {
setActiveIndex (-1)
setSuggestionsVsbl (false)
}, [locale])
const handleKeyDown = (ev: KeyboardEvent<HTMLInputElement>) => {
switch (ev.key)
{
@@ -85,6 +117,7 @@ const TagSearch: FC = () => {
match }))
.toString () }`)
setSuggestionsVsbl (false)
setRequest ({ query: '' })
}
}
@@ -95,7 +128,8 @@ const TagSearch: FC = () => {
? ('not:' + tag.name)
: tag.name)
setSearch (parts.join (' ') + ' ')
setSuggestions ([])
setRequest ({ query: '' })
setSuggestionsVsbl (false)
setActiveIndex (-1)
}
@@ -104,6 +138,9 @@ const TagSearch: FC = () => {
const anyFlg = query.get ('match') === 'any'
const tagsQuery = `${ query.get ('tags') ?? '' }${ anyFlg ? ' type:or' : '' }`
setSearch (tagsQuery)
setRequest ({ query: '' })
setActiveIndex (-1)
setSuggestionsVsbl (false)
}, [location.search])
return (
+4 -3
ファイルの表示
@@ -35,15 +35,16 @@ const WikiBody: FC<Props> = ({ title, body, language = 'und' }) => {
const missingPage = message ('missingPage', {
url: `/wiki/new?title=${ encodeURIComponent (title) }` })
const contentLanguage = body ? language : missingPage.language
const matchLanguage = contentLanguage === 'ain-Kana' ? 'ain' : contentLanguage
const matchLanguage = contentLanguage === 'und'
? 'ja' : (contentLanguage === 'ain-Kana' ? 'ain' : contentLanguage)
const { data } = useQuery ({
enabled: Boolean (body),
queryKey: wikiKeys.autolinkCandidates (matchLanguage, locale),
queryFn: () => fetchWikiAutolinkCandidates (matchLanguage, locale) })
const remarkPlugins = useMemo (
() => [() => remarkWikiAutoLink (data ?? [], matchLanguage), remarkGFM],
[data, matchLanguage])
() => [() => remarkWikiAutoLink (data ?? [], matchLanguage, locale), remarkGFM],
[data, matchLanguage, locale])
const components = {
...((contentLanguage === 'ain' || contentLanguage === 'ain-Kana')
? ainuMDXComponents : { }),
+51 -18
ファイルの表示
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import TagSearchBox from '@/components/TagSearchBox'
@@ -27,34 +27,67 @@ const TagInput: FC<Props> = ({
}) => {
const locale = useLocale ()
const [activeIndex, setActiveIndex] = useState (-1)
const [suggestions, setSuggestions] = useState<Tag[]> ([])
const [request, setRequest] = useState ({ query: '' })
const [result, setResult] = useState<{
locale: string
includeNico: boolean
request: { query: string }
tags: Tag[] }> ({ locale, includeNico, request, tags: [] })
const suggestions = (result.locale === locale && result.includeNico === includeNico
&& result.request === request
&& request.query === value.trim ().split (' ').at (-1)) ? result.tags : []
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
// TODO: TagSearch からのコピペのため,共通化を考へる.
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => {
const whenChanged = (ev: ChangeEvent<HTMLInputElement>) => {
setValue (ev.target.value)
const q = ev.target.value.trim ().split (' ').at (-1)
if (!(q))
{
setSuggestions ([])
return
}
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: {
q,
nico: includeNico ? '1' : '0' } })
const nextSuggestions = data.filter (t => t.postCount > 0)
setSuggestions (nextSuggestions)
setSuggestionsVsbl (nextSuggestions.length > 0)
const query = ev.target.value.trim ().split (' ').at (-1) ?? ''
setRequest ({ query })
setActiveIndex (-1)
setSuggestionsVsbl (query !== '')
}
useEffect (() => {
if (request.query === '')
return
const controller = new AbortController ()
let active = true
void (async () => {
try
{
const tags = await apiGet<Tag[]> ('/tags/autocomplete', {
locale, signal: controller.signal,
params: { q: request.query, nico: includeNico ? '1' : '0' } })
if (active)
{
setResult ({ locale, includeNico, request,
tags: tags.filter (tag => tag.postCount > 0) })
}
}
catch
{
if (active)
setResult ({ locale, includeNico, request, tags: [] })
}
}) ()
return () => {
active = false
controller.abort ()
}
}, [request, locale, includeNico])
useEffect (() => {
setActiveIndex (-1)
setSuggestionsVsbl (false)
setRequest ({ query: '' })
}, [locale, includeNico])
// TODO: TagSearch からのコピペのため,共通化を考へる.
const handleTagSelect = (tag: Tag) => {
const parts = value?.split (' ')
parts[parts.length - 1] = tag.name
setValue (parts.join (' ') + ' ')
setSuggestions ([])
setRequest ({ query: '' })
setActiveIndex (-1)
}
+11
ファイルの表示
@@ -35,6 +35,17 @@ export const localeFromPath = (pathname: string): LocaleCode | null => {
}
export const resolveViewingLocale = (
pathname: string,
routeLocale?: unknown,
): LocaleCode =>
isLocaleCode (routeLocale) ? routeLocale : localeFromPath (pathname) ?? 'ja'
export const currentViewingLocale = (): LocaleCode =>
resolveViewingLocale (typeof window === 'undefined' ? '' : window.location.pathname)
export const unlocalisedPath = (pathname: string): string => {
const locale = localeFromPath (pathname)
return locale == null ? pathname : pathname.slice (locale.length + 1) || '/'
+2 -1
ファイルの表示
@@ -118,8 +118,9 @@ const prefetchWikiPageShow: Prefetcher = async (qc, url, locale) => {
if (wikiPage?.body)
{
const language = wikiPage.languageCode
const contentLanguage = wikiPage.languageCode
?? (wikiPage.resourceType === 'tag_description' ? localeLanguage (locale) : 'und')
const language = contentLanguage === 'und' ? 'ja' : contentLanguage
await qc.prefetchQuery ({
queryKey: wikiKeys.autolinkCandidates (language, locale),
queryFn: () => fetchWikiAutolinkCandidates (language, locale) })
+2 -2
ファイルの表示
@@ -60,11 +60,11 @@ export const tagsKeys = {
nicoIndex: (
p: FetchNicoTagsParams,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'nico', 'index', p] as const,
) => ['tags', 'nico', locale, 'index', p] as const,
externalShow: (
id: string,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'nico', id] as const,
) => ['tags', 'nico', locale, id] as const,
show: (
name: string,
locale: LocaleCode = 'ja',
+5 -2
ファイルの表示
@@ -1,6 +1,7 @@
import type { Content, Parent, Root, RootContent } from 'mdast'
import type { WikiAutolinkCandidate } from '@/types'
import type { LocaleCode } from '@/lib/locales'
const escapeForRegExp = (s: string) => s.replace (/[.*+?^${}()|[\]\\]/g, '\\$&')
@@ -34,12 +35,14 @@ const edgeCharacter = (node: Content, first: boolean): string => {
const remarkWikiAutoLink = (
candidates: WikiAutolinkCandidate[],
language: string,
locale: LocaleCode,
): ((tree: Root) => void) => {
const targets = new Map<string, WikiAutolinkCandidate> ()
const ambiguous = new Set<string> ()
for (const candidate of candidates)
{
if (candidate.languageCode !== language || candidate.name.length === 0)
if (candidate.languageCode !== language || candidate.localeCode !== locale
|| candidate.name.length === 0)
continue
const previous = targets.get (candidate.name)
if (previous != null
@@ -89,7 +92,7 @@ const remarkWikiAutoLink = (
parts.push ({ type: 'text', value: value.slice (last, start) })
parts.push ({
type: 'link',
url: `/${candidate.localeCode}/wiki/${encodeURIComponent (candidate.resourceId)}`,
url: `/${locale}/wiki/${encodeURIComponent (candidate.resourceId)}`,
title: null,
children: [{ type: 'text', value: match[0] }],
data: { hProperties: { 'data-wiki': '1' } } })
+2 -2
ファイルの表示
@@ -1,7 +1,7 @@
import { useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { isLocaleCode, localeFromPath, localiseTo } from '@/lib/locales'
import { localiseTo, resolveViewingLocale } from '@/lib/locales'
import type { NavigateFunction, NavigateOptions, To } from 'react-router-dom'
@@ -11,7 +11,7 @@ import type { LocaleCode } from '@/lib/locales'
export const useLocale = (): LocaleCode => {
const { locale } = useParams ()
const location = useLocation ()
return isLocaleCode (locale) ? locale : localeFromPath (location.pathname) ?? 'ja'
return resolveViewingLocale (location.pathname, locale)
}
+42 -25
ファイルの表示
@@ -2,6 +2,7 @@ import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { i18n } from '@/lib/i18n'
import { currentViewingLocale } from '@/lib/locales'
import type { ClassValue } from 'clsx'
@@ -14,37 +15,39 @@ export const toDate = (d: string | Date): Date => typeof d === 'string' ? new Da
export const cn = (...inputs: ClassValue[]) => twMerge (clsx (...inputs))
export const dateString = (
d: string | Date,
unknown: 'month' | 'day' | 'hour' | 'minute' | 'second' | null = null): string =>
toDate (d).toLocaleString (
'ja-JP-u-ca-japanese',
{ era: 'long',
year: 'numeric',
month: (unknown === 'month' ? undefined : 'long'),
type DatePrecision = 'month' | 'day' | 'hour' | 'minute' | 'second' | null
const dateFormatters = new Map<DatePrecision, Intl.DateTimeFormat> ()
let ainuDateFormatter: Intl.DateTimeFormat | null = null
const formatDateString = (
date: Date,
unknown: DatePrecision,
locale: LocaleCode,
): string => {
if (Number.isNaN (date.getTime ()))
return date.toString ()
if (locale === 'ja')
{
if (!dateFormatters.has (unknown))
{
dateFormatters.set (unknown, new Intl.DateTimeFormat ('ja-JP-u-ca-japanese', {
era: 'long', year: 'numeric',
month: unknown === 'month' ? undefined : 'long',
day: unknown != null && ['month', 'day'].includes (unknown) ? undefined : 'numeric',
weekday: unknown != null && ['month', 'day'].includes (unknown) ? undefined : 'short',
hour: unknown == null || ['second', 'minute'].includes (unknown) ? 'numeric' : undefined,
minute: unknown == null || unknown === 'second' ? 'numeric' : undefined,
second: unknown == null ? 'numeric' : undefined })
export const localisedDateString = (
value: string | Date,
locale: LocaleCode,
unknown: Parameters<typeof dateString>[1] = null,
): string => {
if (locale === 'ja')
return dateString (value, unknown)
const date = toDate (value)
if (Number.isNaN (date.getTime ()))
return dateString (value, unknown)
const parts = new Intl.DateTimeFormat ('en-CA', {
second: unknown == null ? 'numeric' : undefined }))
}
return dateFormatters.get (unknown)!.format (date)
}
ainuDateFormatter ??= new Intl.DateTimeFormat ('en-CA', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hourCycle: 'h23' }).formatToParts (date)
hourCycle: 'h23' })
const parts = ainuDateFormatter.formatToParts (date)
const part = (type: string) => parts.find (entry => entry.type === type)?.value ?? ''
const omitDay = unknown != null && ['month', 'day'].includes (unknown)
const dateParts = [part ('year'),
@@ -60,6 +63,20 @@ export const localisedDateString = (
}
export const dateString = (
value: string | Date,
unknown: DatePrecision = null,
locale?: LocaleCode,
): string => formatDateString (toDate (value), unknown, locale ?? currentViewingLocale ())
export const localisedDateString = (
value: string | Date,
locale: LocaleCode,
unknown: DatePrecision = null,
): string => dateString (value, unknown, locale)
export const originalCreatedAtString = (
f: string | Date | null,
b: string | Date | null,
+13 -1
ファイルの表示
@@ -1,6 +1,11 @@
{
"_untranslated": [
"exportPath"
"exportPath",
"restoreScope",
"nameChanges.title",
"nameChanges.script",
"nameChanges.automatic",
"nameChanges.certified"
],
"exportPath": "export_path",
"postTitle": "$t(tags:history)",
@@ -22,6 +27,13 @@
"confirmTitle": "アホシピレ エヌカㇻ",
"confirmPost": "「{{name}}」 ウェㇽソン {{version}} ウン アホシピレ ヤ?\nピㇼカ ヤ?",
"confirmTag": "シロシ「{{name}}」 ウェㇽソン {{version}} ウン アホシピレ ヤ?\nピㇼカ ヤ?",
"restoreScope": "名称・別名は現在表示中の言語だけを差し戻します。全言語の名称は復元しません。",
"nameChanges": {
"title": "言語別の変更",
"script": "文字体系",
"automatic": "自動生成",
"certified": "公証済み"
},
"revert": "アホシピレ",
"restored": "アホシピレ オケレ!",
"restoreFailed": "アホシピレ カ エアイカㇷ゚……",
+7
ファイルの表示
@@ -19,6 +19,13 @@
"confirmTitle": "差戻の確認",
"confirmPost": "『{{name}}』を版 {{version}} に差戻します.\nよろしいですか?",
"confirmTag": "タグ『{{name}}』を版 {{version}} に差戻します.\nよろしいですか?",
"restoreScope": "名称・別名は現在表示中の言語だけを差し戻します。全言語の名称は復元しません。",
"nameChanges": {
"title": "言語別の変更",
"script": "文字体系",
"automatic": "自動生成",
"certified": "公証済み"
},
"revert": "差戻",
"restored": "差戻しました.",
"restoreFailed": "差戻に失敗……",
+1 -1
ファイルの表示
@@ -14,7 +14,7 @@ const TOSPage = () => {
const Terms = terms[`./${ locale }.mdx`] ?? terms['./ja.mdx']
const language = terms[`./${ locale }.mdx`] != null ? locale : 'ja'
if (!terms)
if (!Terms)
throw new Error ('TOSPage: ja.mdx not found')
return (
+1 -1
ファイルの表示
@@ -1,7 +1,7 @@
import MainArea from '@/components/layout/MainArea'
import { dateString } from '@/lib/utils'
export const lastUpdatedAt = dateString ('2026-04-12', 'hour')
export const lastUpdatedAt = dateString ('2026-04-12', 'hour', 'ja')
<MainArea>
+58 -5
ファイルの表示
@@ -12,7 +12,7 @@ import PageHead from '@/components/common/PageHead'
import { apiPut } from '@/lib/api'
import { useDialogue } from '@/lib/dialogues/useDialogue'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { fetchTagChanges } from '@/lib/tags'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useUITranslation } from '@/lib/useUITranslation'
@@ -20,6 +20,36 @@ import { cn, localisedDateString } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
import type { TagNameSnapshot } from '@/types'
const renderNameSnapshot = (
snapshot: TagNameSnapshot,
language: string,
label: (key: string) => ReactNode,
) => (
<div className="space-y-1">
<div>
{label ('name')}:
<span lang={language} className="inline-block whitespace-nowrap">{snapshot.name}</span>
</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 },
@@ -119,7 +149,25 @@ const TagHistoryPage: FC = () => {
<tr key={`${ change.tagId }.${ change.versionNo }`}
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)}</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>)}
</td>
<td className="p-2 break-all">
{renderDiff (change.category,
category => label (`common:categories.${category}`))}
@@ -193,9 +241,13 @@ const TagHistoryPage: FC = () => {
if (!(await dialogue.confirm ({
title: label ('confirmTitle'),
description: label ('confirmTag', {
name: change.name.current ?? '',
version: change.versionNo }),
description: (
<>
{label ('confirmTag', {
name: change.name.current ?? '', version: change.versionNo })}
<br/>
{label ('restoreScope')}
</>),
confirmText: label ('revert'),
cancelText: label ('settings:actions.cancel'),
closeLabel: label ('settings:actions.close') })))
@@ -225,6 +277,7 @@ const TagHistoryPage: FC = () => {
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
qc.invalidateQueries ({ queryKey: wikiKeys.root })
toast ({ description: label ('restored') })
}
catch
+24 -3
ファイルの表示
@@ -21,21 +21,42 @@ const WikiDiffPage: FC = () => {
const location = useLocation ()
const [diff, setDiff] = useState<WikiPageDiff | null> (null)
const [result, setResult] = useState<{
key: string
diff: WikiPageDiff } | null> (null)
const query = new URLSearchParams (location.search)
const from = query.get ('from')
const to = query.get ('to')
const requestKey = JSON.stringify ([locale, id, from, to])
const diff = result?.key === requestKey ? result.diff : null
const displayTitle = diff
? `${ diff.title }${ diff.deprecatedAt != null
? message ('shared:labels.deprecatedSuffix').text : '' }`
: ''
useEffect (() => {
const controller = new AbortController ()
let active = true
void (async () => {
setDiff (await apiGet<WikiPageDiff> (`/wiki/${ id }/diff`, { locale, params: { from, to } }))
try
{
const diff = await apiGet<WikiPageDiff> (`/wiki/${ encodeURIComponent (id ?? '') }/diff`, {
locale, signal: controller.signal, params: { from, to } })
if (active)
setResult ({ key: requestKey, diff })
}
catch (error)
{
if (active)
console.error (error)
}
}) ()
}, [from, id, to, locale])
return () => {
active = false
controller.abort ()
}
}, [from, id, to, locale, requestKey])
return (
<MainArea>
+33 -8
ファイルの表示
@@ -49,11 +49,16 @@ const WikiEditPage: FC<Props> = ({ user }) => {
const [body, setBody] = useState ('')
const [loading, setLoading] = useState (true)
const [loadedSource, setLoadedSource] = useState ('')
const source = `${locale}:${id}`
const pending = loading || loadedSource !== source
const [title, setTitle] = useState ('')
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
useValidationErrors<WikiFormField> ()
const handleSubmit = async () => {
if (pending || !editable)
return
clearValidationErrors ()
const formData = new FormData ()
@@ -65,10 +70,11 @@ const WikiEditPage: FC<Props> = ({ user }) => {
await apiPut (`/wiki/${ id }`, formData,
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
qc.setQueryData (wikiKeys.show (title, { }, locale),
(prev: WikiPage) => ({ ...prev, title, body }))
(prev: WikiPage | undefined) =>
prev == null ? prev : { ...prev, title, body })
qc.invalidateQueries ({ queryKey: wikiKeys.root })
toast ({ title: label ('shared:messages.postSuccess') })
navigate (`/wiki/${ title }`)
navigate (`/wiki/${ encodeURIComponent (title) }`)
}
catch (e)
{
@@ -82,14 +88,33 @@ const WikiEditPage: FC<Props> = ({ user }) => {
if (!(editable))
return
const controller = new AbortController ()
let active = true
void (async () => {
setLoading (true)
const data = await apiGet<WikiPage> (`/wiki/${ id }`, { locale })
setTitle (data.title)
setBody (data.body)
setLoading (false)
try
{
const data = await apiGet<WikiPage> (`/wiki/${ encodeURIComponent (id ?? '') }`, {
locale, signal: controller.signal })
if (active)
{
setTitle (data.title)
setBody (data.body)
setLoadedSource (source)
setLoading (false)
}
}
catch (error)
{
if (active)
console.error (error)
}
}) ()
}, [editable, id, locale])
return () => {
active = false
controller.abort ()
}
}, [editable, id, locale, source])
if (!(editable))
return <Forbidden/>
@@ -100,7 +125,7 @@ const WikiEditPage: FC<Props> = ({ user }) => {
<div className="max-w-xl mx-auto p-4 space-y-4">
<h1 className="text-2xl font-bold mb-2">{label ('editPage')}</h1>
{loading ? label ('common:loading') : (
{pending ? label ('common:loading') : (
<>
<FieldError messages={baseErrors}/>
+1 -1
ファイルの表示
@@ -56,7 +56,7 @@ const WikiHistoryPage: FC = () => {
</td>
<td className="p-2">
<PrefetchLink
to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.revisionId }`}>
to={`/wiki/${ change.wikiPage.id }?version=${ change.revisionId }`}>
{change.wikiPage.title}
</PrefetchLink>
{change.wikiPage.deprecatedAt != null && label ('shared:labels.deprecatedSuffix')}
+12
ファイルの表示
@@ -367,10 +367,22 @@ export type Tag = {
children?: Tag[]
matchedAlias?: string | null }
export type TagNameSnapshot = {
name: string | null
scriptCode: string | null
autoGenerated: boolean | null
aliases: { name: string; scriptCode: string | null }[] }
export type TagNameChange = {
languageCode: string
current: TagNameSnapshot | null
prev: TagNameSnapshot | null }
export type TagVersion = {
tagId: number
versionNo: number
eventType: 'create' | 'update' | 'discard' | 'restore'
nameChanges?: TagNameChange[]
name: { current: string | null; prev: string | null }
category: { current: Category; prev: Category | null }
deprecatedAt: { current: string | null; prev: string | null }