Reviewed-on: #433 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
@@ -24,7 +24,15 @@ const postsApi = vi.hoisted (() => ({
|
||||
fetchPosts: vi.fn (),
|
||||
}))
|
||||
|
||||
vi.mock ('@/lib/wiki', () => wikiApi)
|
||||
vi.mock ('@/lib/wiki', () => ({
|
||||
...wikiApi,
|
||||
fetchWikiAutolinkCandidates: async () => [],
|
||||
fetchWikiTag: (reference: { name?: string; wikiId?: string }, locale: string) =>
|
||||
tagsApi.fetchTagByName (reference.name ?? reference.wikiId, locale),
|
||||
fetchWikiPageByReference: (reference: string, ...args: unknown[]) =>
|
||||
/^\d+$/.test (reference)
|
||||
? wikiApi.fetchWikiPage (reference, ...args)
|
||||
: wikiApi.fetchWikiPageByTitle (reference, ...args) }))
|
||||
vi.mock ('@/lib/tags', () => tagsApi)
|
||||
vi.mock ('@/lib/posts', () => postsApi)
|
||||
vi.mock ('@/lib/help', () => ({ getHelpTitleIndex: vi.fn () }))
|
||||
@@ -63,15 +71,15 @@ describe ('WikiDetailPage', () => {
|
||||
vi.mocked (getHelpTitleIndex).mockReturnValueOnce (pending)
|
||||
tagsApi.fetchTagByName.mockResolvedValue (buildTag ({ name: '既存タグ', hasWiki: false }))
|
||||
renderWithProviders (
|
||||
<Routes><Route path="/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
|
||||
{ route: `/wiki/${ encodeURIComponent ('既存タグ') }` })
|
||||
<Routes><Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
|
||||
{ route: `/ja/wiki/${ encodeURIComponent ('既存タグ') }` })
|
||||
await waitFor (() => expect (getHelpTitleIndex).toHaveBeenCalled ())
|
||||
expect (screen.queryByText ('404')).not.toBeInTheDocument ()
|
||||
expect (screen.queryByRole ('link', { name: '新規作成してください' }))
|
||||
.not.toBeInTheDocument ()
|
||||
await act (async () => finish ({ }))
|
||||
expect (await screen.findByRole ('link', { name: '新規作成してください' }))
|
||||
.toHaveAttribute ('href', `/wiki/new?title=${ encodeURIComponent ('既存タグ') }`)
|
||||
.toHaveAttribute ('href', `/ja/wiki/new?title=${ encodeURIComponent ('既存タグ') }`)
|
||||
})
|
||||
|
||||
it ('creates a tag description through the existing form and displays its content', async () => {
|
||||
@@ -79,13 +87,13 @@ describe ('WikiDetailPage', () => {
|
||||
resourceType: 'tag_description' })
|
||||
tagsApi.fetchTagByName.mockResolvedValue (buildTag ({ name: page.title, hasWiki: false }))
|
||||
vi.mocked (apiPost).mockResolvedValueOnce (page)
|
||||
const path = `/wiki/${ encodeURIComponent (page.title) }`
|
||||
const path = `/ja/wiki/${ encodeURIComponent (page.title) }`
|
||||
const { container, queryClient } = renderWithProviders (
|
||||
<>
|
||||
<Location/>
|
||||
<Routes>
|
||||
<Route path="/wiki/new" element={<WikiNewPage user={buildUser ({ role: 'member' })}/>}/>
|
||||
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>
|
||||
<Route path="/ja/wiki/new" element={<WikiNewPage user={buildUser ({ role: 'member' })}/>}/>
|
||||
<Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/>
|
||||
</Routes>
|
||||
</>, { route: path })
|
||||
fireEvent.click (await screen.findByRole ('link', { name: '新規作成してください' }))
|
||||
@@ -108,12 +116,12 @@ describe ('WikiDetailPage', () => {
|
||||
expect (form.get ('title')).toBe (page.title)
|
||||
expect (form.get ('body')).toBe (page.body)
|
||||
expect (apiPost).toHaveBeenCalledWith ('/wiki', form,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
{ locale: 'ja', headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
expect (screen.queryByText ('404')).not.toBeInTheDocument ()
|
||||
expect (queryClient.getQueryData (wikiKeys.show (page.title, { }))).toEqual (page)
|
||||
await waitFor (() => expect (queryClient.getQueryData (wikiKeys.index ({ })))
|
||||
.toEqual ([page]))
|
||||
await waitFor (() => expect (queryClient.getQueryData (tagsKeys.show (page.title)))
|
||||
expect (queryClient.getQueryState (wikiKeys.index ({ }))?.isInvalidated).toBe (true)
|
||||
await waitFor (() => expect (queryClient.getQueryData (
|
||||
wikiKeys.tag ({ name: page.title }, 'ja')))
|
||||
.toMatchObject ({ hasWiki: true }))
|
||||
expect (getHelpTitleIndex).toHaveBeenCalledTimes (1)
|
||||
})
|
||||
@@ -121,7 +129,7 @@ describe ('WikiDetailPage', () => {
|
||||
it ('keeps failed saves on the form without changing cached Wiki or tag state', async () => {
|
||||
const title = '既存タグ'
|
||||
const tag = buildTag ({ name: title, hasWiki: false })
|
||||
const path = `/wiki/new?title=${ encodeURIComponent (title) }`
|
||||
const path = `/ja/wiki/new?title=${ encodeURIComponent (title) }`
|
||||
vi.mocked (apiPost).mockRejectedValueOnce (new Error ('save failed'))
|
||||
const { queryClient } = renderWithProviders (
|
||||
<><Location/><WikiNewPage user={buildUser ({ role: 'member' })}/></>, { route: path })
|
||||
@@ -141,8 +149,8 @@ describe ('WikiDetailPage', () => {
|
||||
|
||||
it ('shows NotFound when neither a tag nor a Help title exists', async () => {
|
||||
renderWithProviders (
|
||||
<Routes><Route path="/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
|
||||
{ route: '/wiki/missing' })
|
||||
<Routes><Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
|
||||
{ route: '/ja/wiki/missing' })
|
||||
expect (await screen.findByText ('404')).toBeInTheDocument ()
|
||||
expect (screen.queryByRole ('link', { name: '新規作成してください' }))
|
||||
.not.toBeInTheDocument ()
|
||||
@@ -153,8 +161,8 @@ describe ('WikiDetailPage', () => {
|
||||
wikiApi.fetchWikiPageByTitle.mockRejectedValue (new Error (message))
|
||||
tagsApi.fetchTagByName.mockResolvedValue (buildTag ({ name: '既存タグ' }))
|
||||
renderWithProviders (
|
||||
<Routes><Route path="/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
|
||||
{ route: `/wiki/${ encodeURIComponent ('既存タグ') }` })
|
||||
<Routes><Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
|
||||
{ route: `/ja/wiki/${ encodeURIComponent ('既存タグ') }` })
|
||||
expect (await screen.findByRole ('alert')).toHaveTextContent (message)
|
||||
expect (getHelpTitleIndex).not.toHaveBeenCalled ()
|
||||
expect (screen.queryByRole ('link', { name: '新規作成してください' }))
|
||||
@@ -174,9 +182,9 @@ describe ('WikiDetailPage', () => {
|
||||
|
||||
renderWithProviders (
|
||||
<Routes>
|
||||
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>
|
||||
<Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/>
|
||||
</Routes>,
|
||||
{ route: '/wiki/%E6%97%A7%E3%82%BF%E3%82%B0' },
|
||||
{ route: '/ja/wiki/%E6%97%A7%E3%82%BF%E3%82%B0' },
|
||||
)
|
||||
|
||||
const marker = await screen.findByText ('(廃止)')
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
|
||||
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import PostList from '@/components/PostList'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import TagLink from '@/components/TagLink'
|
||||
import WikiBody from '@/components/WikiBody'
|
||||
import TabGroup, { Tab } from '@/components/common/TabGroup'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||
import { getHelpTitleIndex } from '@/lib/help'
|
||||
import { localeLanguage } from '@/lib/locales'
|
||||
import { fetchPosts } from '@/lib/posts'
|
||||
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { fetchTagByName } from '@/lib/tags'
|
||||
import { fetchWikiPage, fetchWikiPageByTitle } from '@/lib/wiki'
|
||||
import { helpKeys, postsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { fetchWikiPageByReference, fetchWikiTag } from '@/lib/wiki'
|
||||
import NotFound from '@/pages/NotFound'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { Tag } from '@/types'
|
||||
import type { Tag, WikiTagReference } from '@/types'
|
||||
|
||||
|
||||
const WikiDetailPage: FC = () => {
|
||||
const { message, label } = useUITranslation ('shared')
|
||||
const locale = useLocale ()
|
||||
const params = useParams ()
|
||||
const title = params.title ?? ''
|
||||
const numericReference = /^\d+$/.test (title)
|
||||
|
||||
const location = useLocation ()
|
||||
const navigate = useNavigate ()
|
||||
const navigate = useLocaleNavigate ()
|
||||
|
||||
const defaultTag = useMemo (() => ({ name: title, category: 'general' } as Tag), [title])
|
||||
|
||||
@@ -36,14 +40,14 @@ const WikiDetailPage: FC = () => {
|
||||
const version = query.get ('version') || undefined
|
||||
|
||||
const { data: wikiPage, isLoading: loading, isSuccess, isFetching, error } = useQuery ({
|
||||
enabled: Boolean (title) && !(/^\d+$/.test (title)),
|
||||
queryKey: wikiKeys.show (title, { version }),
|
||||
queryFn: () => fetchWikiPageByTitle (title, { version }) })
|
||||
enabled: Boolean (title),
|
||||
queryKey: wikiKeys.show (title, { version }, locale),
|
||||
queryFn: () => fetchWikiPageByReference (title, { version }, locale) })
|
||||
|
||||
const missingWiki = isSuccess && !(isFetching) && wikiPage == null
|
||||
const { data: titleIndex, isPending: lookupPending,
|
||||
isFetching: lookupFetching, error: lookupError } = useQuery ({
|
||||
enabled: missingWiki,
|
||||
enabled: missingWiki && !numericReference,
|
||||
queryKey: helpKeys.titles ('ja'),
|
||||
queryFn: () => getHelpTitleIndex ('ja'),
|
||||
staleTime: Infinity })
|
||||
@@ -53,28 +57,33 @@ const WikiDetailPage: FC = () => {
|
||||
? titleIndex[lookupTitle] : []), [titleIndex, lookupTitle])
|
||||
|
||||
useEffect (() => {
|
||||
if (missingWiki && !(lookupFetching) && lookupError == null && identifiers.length === 1)
|
||||
if (missingWiki && !numericReference
|
||||
&& !(lookupFetching) && lookupError == null && identifiers.length === 1)
|
||||
navigate (`/help/${ encodeURIComponent (identifiers[0]) }`, { replace: true })
|
||||
}, [missingWiki, lookupFetching, lookupError, identifiers, navigate])
|
||||
}, [missingWiki, numericReference, lookupFetching, lookupError, identifiers, navigate])
|
||||
|
||||
const effectiveTitle = wikiPage?.title ?? title
|
||||
const deprecated = wikiPage?.deprecatedAt != null
|
||||
const tagReference: WikiTagReference = (wikiPage != null || !numericReference)
|
||||
? { name: effectiveTitle } : { wikiId: title }
|
||||
|
||||
const { data: tag, isPending: tagPending,
|
||||
isFetching: tagFetching, error: tagError } = useQuery ({
|
||||
enabled: Boolean (effectiveTitle),
|
||||
queryKey: tagsKeys.show (effectiveTitle),
|
||||
queryFn: () => fetchTagByName (effectiveTitle) })
|
||||
enabled: Boolean (title) && (missingWiki || wikiPage != null),
|
||||
queryKey: wikiKeys.tag (tagReference, locale),
|
||||
queryFn: () => fetchWikiTag (tagReference, locale) })
|
||||
const displayTitle = wikiPage?.title ?? tag?.name ?? title
|
||||
|
||||
const keys = {
|
||||
tags: effectiveTitle, match: 'all', page: 1, limit: 8, url: '', title: '',
|
||||
tags: displayTitle, match: 'all', page: 1, limit: 8, url: '', title: '',
|
||||
originalCreatedFrom: '', originalCreatedTo: '',
|
||||
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '',
|
||||
order: 'original_created_at:desc' } as const
|
||||
const { data } = useQuery ({
|
||||
enabled: Boolean (effectiveTitle) && !(version),
|
||||
queryKey: postsKeys.index (keys),
|
||||
queryFn: () => fetchPosts (keys) })
|
||||
enabled: Boolean (effectiveTitle) && !(version)
|
||||
&& (!numericReference || wikiPage != null || tag != null),
|
||||
queryKey: postsKeys.index (keys, locale),
|
||||
queryFn: () => fetchPosts (keys, locale) })
|
||||
const posts = data?.posts || []
|
||||
|
||||
useEffect (() => {
|
||||
@@ -83,85 +92,72 @@ const WikiDetailPage: FC = () => {
|
||||
|
||||
WikiIdBus.set (wikiPage.id)
|
||||
|
||||
if (wikiPage.title !== title)
|
||||
navigate (`/wiki/${ encodeURIComponent(wikiPage.title) }`, { replace: true })
|
||||
if (!numericReference && wikiPage.title !== title)
|
||||
{
|
||||
navigate ({ pathname: `/wiki/${ encodeURIComponent (wikiPage.title) }`,
|
||||
search: location.search, hash: location.hash }, { replace: true })
|
||||
}
|
||||
|
||||
return () => WikiIdBus.set (null)
|
||||
}, [wikiPage, title, navigate])
|
||||
|
||||
useEffect (() => {
|
||||
if (!(/^\d+$/.test (title)))
|
||||
return
|
||||
|
||||
let active = true
|
||||
void (async () => {
|
||||
try
|
||||
{
|
||||
const data = await fetchWikiPage (title, { })
|
||||
if (active)
|
||||
navigate (`/wiki/${ encodeURIComponent (data.title) }`, { replace: true })
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}) ()
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [title, navigate])
|
||||
}, [wikiPage, title, numericReference, navigate, location.search, location.hash])
|
||||
|
||||
const displayError = error ?? (missingWiki ? lookupError ?? tagError : null)
|
||||
if (displayError != null)
|
||||
return <MainArea><p role="alert">{displayError.message}</p></MainArea>
|
||||
|
||||
const waiting = loading || (isFetching && wikiPage == null)
|
||||
|| (missingWiki && (lookupPending || lookupFetching || tagPending
|
||||
|| (tagFetching && tag == null) || identifiers.length === 1))
|
||||
|| (missingWiki && ((!numericReference
|
||||
&& (lookupPending || lookupFetching || identifiers.length === 1))
|
||||
|| tagPending || (tagFetching && tag == null)))
|
||||
|
||||
if (missingWiki && !(waiting) && (tag == null || version != null))
|
||||
return <NotFound/>
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>{`${ effectiveTitle }${ deprecated ? '(廃止)' : '' } Wiki | ${ SITE_TITLE }`}</title>
|
||||
<PageHead title={{
|
||||
text: `${displayTitle}${deprecated
|
||||
? message ('labels.deprecatedSuffix').text : ''} Wiki`,
|
||||
language: deprecated ? message ('labels.deprecatedSuffix').language : locale }}>
|
||||
{!(wikiPage?.body) && <meta name="robots" content="noindex"/>}
|
||||
</Helmet>
|
||||
</PageHead>
|
||||
|
||||
{(wikiPage && version) && (
|
||||
<div className="text-sm flex gap-3 items-center justify-center
|
||||
border border-gray-700 rounded px-2 py-1 mb-4">
|
||||
{wikiPage.pred ? (
|
||||
<PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.pred }`}>
|
||||
< 古
|
||||
</PrefetchLink>) : '(最古)'}
|
||||
{label ('actions.old')}
|
||||
</PrefetchLink>) : label ('actions.oldest')}
|
||||
|
||||
<span>{wikiPage.updatedAt}</span>
|
||||
|
||||
{wikiPage.succ ? (
|
||||
<PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.succ }`}>
|
||||
新 >
|
||||
</PrefetchLink>) : '(最新)'}
|
||||
{label ('actions.new')}
|
||||
</PrefetchLink>) : label ('actions.latest')}
|
||||
</div>)}
|
||||
|
||||
<article className="prose dark:prose-invert mx-auto p-4">
|
||||
<h1 className="prose-a:no-underline">
|
||||
<TagLink tag={tag ?? { ...defaultTag,
|
||||
name: effectiveTitle,
|
||||
name: displayTitle,
|
||||
deprecatedAt: wikiPage?.deprecatedAt ?? null }}
|
||||
withWiki={false}
|
||||
withCount={false}
|
||||
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
||||
{deprecated && <span>(廃止)</span>}
|
||||
{deprecated && label ('labels.deprecatedSuffix')}
|
||||
</h1>
|
||||
{waiting ? <div>Loading...</div> : <WikiBody title={title} body={wikiPage?.body}/>}
|
||||
{waiting
|
||||
? label ('common:loading')
|
||||
: <WikiBody title={displayTitle} body={wikiPage?.body}
|
||||
language={wikiPage?.languageCode ?? (wikiPage?.resourceType === 'tag_description'
|
||||
? localeLanguage (locale) : 'und')}/>}
|
||||
|
||||
{(!(version) && posts.length > 0) && (
|
||||
<div className="not-prose">
|
||||
<TabGroup>
|
||||
<Tab name="広場">
|
||||
<Tab name={message ('navigation:posts').text}>
|
||||
<PostList posts={posts}/>
|
||||
</Tab>
|
||||
</TabGroup>
|
||||
|
||||
@@ -36,10 +36,10 @@ describe ('WikiDiffPage', () => {
|
||||
await waitFor (() => {
|
||||
expect (api.apiGet).toHaveBeenCalledWith (
|
||||
'/wiki/3/diff',
|
||||
{ params: { from: '1', to: '2' } },
|
||||
{ locale: 'ja', params: { from: '1', to: '2' } },
|
||||
)
|
||||
})
|
||||
expect (screen.getByText ('差分対象')).toBeInTheDocument ()
|
||||
expect (await screen.findByText ('差分対象')).toBeInTheDocument ()
|
||||
expect (screen.getByText ('same')).toBeInTheDocument ()
|
||||
expect (screen.getByText ('added line')).toBeInTheDocument ()
|
||||
expect (screen.getByText ('removed line')).toBeInTheDocument ()
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { apiGet } from '@/lib/api'
|
||||
import { wikiKeys } from '@/lib/queryKeys'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { WikiPageDiff } from '@/types'
|
||||
|
||||
|
||||
const WikiDiffPage: FC = () => {
|
||||
const { message, label } = useUITranslation ('wiki')
|
||||
const locale = useLocale ()
|
||||
const { id } = useParams ()
|
||||
|
||||
const location = useLocation ()
|
||||
|
||||
const [diff, setDiff] = useState<WikiPageDiff | null> (null)
|
||||
|
||||
const query = new URLSearchParams (location.search)
|
||||
const from = query.get ('from')
|
||||
const to = query.get ('to')
|
||||
const { data: diff } = useQuery ({
|
||||
queryKey: wikiKeys.diff (id ?? '', { from, to }, locale),
|
||||
queryFn: () => apiGet<WikiPageDiff> (
|
||||
`/wiki/${ encodeURIComponent (id ?? '') }/diff`, { locale, params: { from, to } }) })
|
||||
const displayTitle = diff
|
||||
? `${ diff.title }${ diff.deprecatedAt != null ? '(廃止)' : '' }`
|
||||
? `${ diff.title }${ diff.deprecatedAt != null
|
||||
? message ('shared:labels.deprecatedSuffix').text : '' }`
|
||||
: ''
|
||||
|
||||
useEffect (() => {
|
||||
void (async () => {
|
||||
setDiff (await apiGet<WikiPageDiff> (`/wiki/${ id }/diff`, { params: { from, to } }))
|
||||
}) ()
|
||||
}, [from, id, to])
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>{`Wiki 差分: ${ displayTitle } | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
<PageHead title={message ('diffTitle', { title: displayTitle })}/>
|
||||
<PageTitle>{displayTitle}</PageTitle>
|
||||
<div className="prose mx-auto p-4">
|
||||
{diff
|
||||
@@ -47,7 +46,7 @@ const WikiDiffPage: FC = () => {
|
||||
d.type === 'removed' && 'bg-red-200 dark:bg-red-800')}>
|
||||
{d.content}
|
||||
</p>)))
|
||||
: 'Loading...'}
|
||||
: label ('common:loading')}
|
||||
</div>
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { textContentOf } from '@/test/text'
|
||||
import WikiEditPage from '@/pages/wiki/WikiEditPage'
|
||||
import { buildUser, buildWikiPage } from '@/test/factories'
|
||||
import { renderWithProviders } from '@/test/render'
|
||||
@@ -50,9 +51,9 @@ describe ('WikiEditPage', () => {
|
||||
renderPage ()
|
||||
|
||||
await waitFor (() => {
|
||||
expect (api.apiGet).toHaveBeenCalledWith ('/wiki/9')
|
||||
expect (api.apiGet).toHaveBeenCalledWith ('/wiki/9', { locale: 'ja' })
|
||||
})
|
||||
expect (screen.getAllByRole ('textbox')[0]).toHaveValue ('既存')
|
||||
expect ((await screen.findAllByRole ('textbox'))[0]).toHaveValue ('既存')
|
||||
expect (screen.getByLabelText ('本文エディタ')).toHaveValue ('本文')
|
||||
})
|
||||
|
||||
@@ -71,12 +72,13 @@ describe ('WikiEditPage', () => {
|
||||
expect (api.apiPut).toHaveBeenCalledWith (
|
||||
'/wiki/9',
|
||||
expect.any (FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
||||
{ locale: 'ja', headers: { 'Content-Type': 'multipart/form-data' } },
|
||||
)
|
||||
})
|
||||
const formData = api.apiPut.mock.calls[0]?.[1] as FormData
|
||||
expect (formData.get ('title')).toBe ('更新済み')
|
||||
expect (formData.get ('body')).toBe ('更新本文')
|
||||
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '投稿成功!' })
|
||||
expect (toastApi.toast).toHaveBeenCalledTimes (1)
|
||||
expect (textContentOf (toastApi.toast.mock.calls[0][0].title)).toBe ('投稿成功!')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import MdEditor from 'react-markdown-editor-lite'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { apiGet, apiPut } from '@/lib/api'
|
||||
import { wikiKeys } from '@/lib/queryKeys'
|
||||
import { canEditContent } from '@/lib/users'
|
||||
@@ -36,21 +37,31 @@ type WikiFormField = 'title' | 'body'
|
||||
|
||||
|
||||
const WikiEditPage: FC<Props> = ({ user }) => {
|
||||
const { message, label } = useUITranslation ('wiki')
|
||||
const locale = useLocale ()
|
||||
const editable = canEditContent (user)
|
||||
|
||||
const { id } = useParams ()
|
||||
|
||||
const navigate = useNavigate ()
|
||||
const navigate = useLocaleNavigate ()
|
||||
|
||||
const qc = useQueryClient ()
|
||||
|
||||
const [body, setBody] = useState ('')
|
||||
const [loading, setLoading] = useState (true)
|
||||
const [title, setTitle] = useState ('')
|
||||
const source = `${locale}:${id}`
|
||||
const { data: wiki, isPending: loading } = useQuery ({
|
||||
queryKey: wikiKeys.show (id ?? '', { }, locale),
|
||||
queryFn: () => apiGet<WikiPage> (`/wiki/${ encodeURIComponent (id ?? '') }`, { locale }),
|
||||
enabled: editable })
|
||||
const [body, setBody] = useState (wiki?.body ?? '')
|
||||
const [loadedSource, setLoadedSource] = useState (wiki == null ? '' : source)
|
||||
const pending = loading || loadedSource !== source
|
||||
const [title, setTitle] = useState (wiki?.title ?? '')
|
||||
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
|
||||
useValidationErrors<WikiFormField> ()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (pending || !editable)
|
||||
return
|
||||
clearValidationErrors ()
|
||||
|
||||
const formData = new FormData ()
|
||||
@@ -60,51 +71,46 @@ const WikiEditPage: FC<Props> = ({ user }) => {
|
||||
try
|
||||
{
|
||||
await apiPut (`/wiki/${ id }`, formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
qc.setQueryData (wikiKeys.show (title, { }),
|
||||
(prev: WikiPage) => ({ ...prev, title, body }))
|
||||
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
qc.setQueryData (wikiKeys.show (title, { }, locale),
|
||||
(prev: WikiPage | undefined) =>
|
||||
prev == null ? prev : { ...prev, title, body })
|
||||
qc.invalidateQueries ({ queryKey: wikiKeys.root })
|
||||
toast ({ title: '投稿成功!' })
|
||||
navigate (`/wiki/${ title }`)
|
||||
toast ({ title: label ('shared:messages.postSuccess') })
|
||||
navigate (`/wiki/${ encodeURIComponent (title) }`)
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
applyValidationError (e)
|
||||
toast ({ title: '投稿失敗', description: '入力を確認してください。' })
|
||||
toast ({ title: label ('shared:messages.postFailed'),
|
||||
description: label ('shared:messages.checkInputModern') })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
if (!(editable))
|
||||
if (!(editable) || wiki == null)
|
||||
return
|
||||
|
||||
void (async () => {
|
||||
setLoading (true)
|
||||
const data = await apiGet<WikiPage> (`/wiki/${ id }`)
|
||||
setTitle (data.title)
|
||||
setBody (data.body)
|
||||
setLoading (false)
|
||||
}) ()
|
||||
}, [editable, id])
|
||||
setTitle (wiki.title)
|
||||
setBody (wiki.body)
|
||||
setLoadedSource (source)
|
||||
}, [editable, source, wiki])
|
||||
|
||||
if (!(editable))
|
||||
return <Forbidden/>
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>{`Wiki ページを編輯 | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
<PageHead title={message ('editPage')}/>
|
||||
<div className="max-w-xl mx-auto p-4 space-y-4">
|
||||
<h1 className="text-2xl font-bold mb-2">Wiki ページを編輯</h1>
|
||||
<h1 className="text-2xl font-bold mb-2">{label ('editPage')}</h1>
|
||||
|
||||
{loading ? 'Loading...' : (
|
||||
{pending ? label ('common:loading') : (
|
||||
<>
|
||||
<FieldError messages={baseErrors}/>
|
||||
|
||||
{/* タイトル */}
|
||||
{/* TODO: タグ補完 */}
|
||||
<FormField label="タイトル" messages={fieldErrors.title}>
|
||||
<FormField label={label ('shared:labels.title')} messages={fieldErrors.title}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<input type="text"
|
||||
value={title}
|
||||
@@ -115,7 +121,7 @@ const WikiEditPage: FC<Props> = ({ user }) => {
|
||||
</FormField>
|
||||
|
||||
{/* 本文 */}
|
||||
<FormField label="本文" messages={fieldErrors.body}>
|
||||
<FormField label={label ('shared:labels.body')} messages={fieldErrors.body}>
|
||||
{() => (
|
||||
<MdEditor value={body}
|
||||
style={{ height: '500px' }}
|
||||
@@ -127,7 +133,7 @@ const WikiEditPage: FC<Props> = ({ user }) => {
|
||||
{/* 送信 */}
|
||||
<button onClick={handleSubmit}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
|
||||
編輯
|
||||
{label ('shared:actions.edit')}
|
||||
</button>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ const renderWiki = (title: string) => {
|
||||
<Location/>
|
||||
<Routes>
|
||||
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>
|
||||
<Route path="/help/:identifier" element={<HelpDetailPage/>}/>
|
||||
<Route path="/ja/help/:identifier" element={<HelpDetailPage/>}/>
|
||||
<Route path="/404" element={<div>unexpected redirect</div>}/>
|
||||
</Routes>
|
||||
</>, { route })
|
||||
|
||||
@@ -32,7 +32,7 @@ describe ('WikiHistoryPage', () => {
|
||||
const link = await screen.findByRole ('link', { name: '旧タグ' })
|
||||
const marker = screen.getByText ('(廃止)')
|
||||
|
||||
expect (link).toHaveAttribute ('href', '/wiki/%E6%97%A7%E3%82%BF%E3%82%B0?version=2')
|
||||
expect (link).toHaveAttribute ('href', '/ja/wiki/3?version=2')
|
||||
expect (marker.closest ('a')).toBeNull ()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet } from '@/lib/api'
|
||||
import { dateString } from '@/lib/utils'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { wikiKeys } from '@/lib/queryKeys'
|
||||
import { localisedDateString } from '@/lib/utils'
|
||||
import { fetchWikiChanges } from '@/lib/wiki'
|
||||
|
||||
import type { WikiPageChange } from '@/types'
|
||||
import type { FC } from 'react'
|
||||
|
||||
|
||||
const WikiHistoryPage: FC = () => {
|
||||
const [changes, setChanges] = useState<WikiPageChange[]> ([])
|
||||
const { message, label } = useUITranslation ('wiki')
|
||||
const locale = useLocale ()
|
||||
|
||||
const location = useLocation ()
|
||||
const query = new URLSearchParams (location.search)
|
||||
const id = query.get ('id')
|
||||
|
||||
useEffect (() => {
|
||||
void (async () => {
|
||||
setChanges (await apiGet<WikiPageChange[]> ('/wiki/changes', { params: id ? { id } : { } }))
|
||||
}) ()
|
||||
}, [id, location.search])
|
||||
const params = id ? { id } : { }
|
||||
const { data: changes = [], isLoading, isError } = useQuery ({
|
||||
queryKey: wikiKeys.changes (params, locale),
|
||||
queryFn: () => fetchWikiChanges (params, locale) })
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>{`Wiki 変更履歴 | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
<PageHead title={message ('history')}/>
|
||||
|
||||
<PageTitle>Wiki 履歴</PageTitle>
|
||||
<PageTitle>{label ('historyHeading')}</PageTitle>
|
||||
{isLoading && <p>{label ('common:loading')}</p>}
|
||||
{isError && <p role="alert">{label ('common:fetchError')}</p>}
|
||||
|
||||
<table className="table-auto w-full border-collapse">
|
||||
<thead className="border-b-2 border-black dark:border-white">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th className="p-2 text-left">タイトル</th>
|
||||
<th className="p-2 text-left">変更</th>
|
||||
<th className="p-2 text-left">日時</th>
|
||||
<th className="p-2 text-left">{label ('shared:labels.title')}</th>
|
||||
<th className="p-2 text-left">{label ('change')}</th>
|
||||
<th className="p-2 text-left">{label ('dateTime')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -51,25 +51,26 @@ const WikiHistoryPage: FC = () => {
|
||||
{change.pred != null && (
|
||||
<PrefetchLink
|
||||
to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}>
|
||||
差分
|
||||
{label ('shared:actions.diff')}
|
||||
</PrefetchLink>)}
|
||||
</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 && <span>(廃止)</span>}
|
||||
{change.wikiPage.deprecatedAt != null && label ('shared:labels.deprecatedSuffix')}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{change.pred == null ? '新規' : '更新'}
|
||||
{change.pred == null
|
||||
? label ('navigation:create') : label ('shared:actions.update')}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<PrefetchLink to={`/users/${ change.user.id }`}>
|
||||
{change.user.name}
|
||||
</PrefetchLink>
|
||||
<br/>
|
||||
{dateString (change.timestamp)}
|
||||
{localisedDateString (change.timestamp, locale)}
|
||||
</td>
|
||||
</tr>))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { textContentOf } from '@/test/text'
|
||||
import WikiNewPage from '@/pages/wiki/WikiNewPage'
|
||||
import { buildUser, buildWikiPage } from '@/test/factories'
|
||||
import { renderWithProviders } from '@/test/render'
|
||||
@@ -50,12 +51,13 @@ describe ('WikiNewPage', () => {
|
||||
expect (api.apiPost).toHaveBeenCalledWith (
|
||||
'/wiki',
|
||||
expect.any (FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
||||
{ locale: 'ja', headers: { 'Content-Type': 'multipart/form-data' } },
|
||||
)
|
||||
})
|
||||
const formData = api.apiPost.mock.calls[0]?.[1] as FormData
|
||||
expect (formData.get ('title')).toBe ('下書き')
|
||||
expect (formData.get ('body')).toBe ('本文')
|
||||
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '投稿成功!' })
|
||||
expect (toastApi.toast).toHaveBeenCalledTimes (1)
|
||||
expect (textContentOf (toastApi.toast.mock.calls[0][0].title)).toBe ('投稿成功!')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import MdEditor from 'react-markdown-editor-lite'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import 'react-markdown-editor-lite/lib/index.css'
|
||||
|
||||
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { apiPost } from '@/lib/api'
|
||||
import { tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { canEditContent } from '@/lib/users'
|
||||
@@ -35,10 +36,12 @@ type WikiFormField = 'title' | 'body'
|
||||
|
||||
|
||||
const WikiNewPage: FC<Props> = ({ user }) => {
|
||||
const { message, label } = useUITranslation ('wiki')
|
||||
const locale = useLocale ()
|
||||
const editable = canEditContent (user)
|
||||
|
||||
const location = useLocation ()
|
||||
const navigate = useNavigate ()
|
||||
const navigate = useLocaleNavigate ()
|
||||
const qc = useQueryClient ()
|
||||
|
||||
const query = new URLSearchParams (location.search)
|
||||
@@ -59,19 +62,20 @@ const WikiNewPage: FC<Props> = ({ user }) => {
|
||||
try
|
||||
{
|
||||
const data = await apiPost<WikiPage> ('/wiki', formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
await qc.cancelQueries ({ queryKey: wikiKeys.root })
|
||||
qc.setQueryData (wikiKeys.show (data.title, { }), data)
|
||||
qc.setQueryData (wikiKeys.show (data.title, { }, locale), data)
|
||||
await Promise.all ([
|
||||
qc.invalidateQueries ({ queryKey: wikiKeys.root }),
|
||||
qc.invalidateQueries ({ queryKey: tagsKeys.root })])
|
||||
toast ({ title: '投稿成功!' })
|
||||
toast ({ title: label ('shared:messages.postSuccess') })
|
||||
navigate (`/wiki/${ encodeURIComponent (data.title) }`)
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
applyValidationError (e)
|
||||
toast ({ title: '投稿失敗', description: '入力を確認してください。' })
|
||||
toast ({ title: label ('shared:messages.postFailed'),
|
||||
description: label ('shared:messages.checkInputModern') })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,16 +84,14 @@ const WikiNewPage: FC<Props> = ({ user }) => {
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>{`新規 Wiki ページ | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
<PageHead title={message ('newPage')}/>
|
||||
<div className="max-w-xl mx-auto p-4 space-y-4">
|
||||
<h1 className="text-2xl font-bold mb-2">新規 Wiki ページ</h1>
|
||||
<h1 className="text-2xl font-bold mb-2">{label ('newPage')}</h1>
|
||||
<FieldError messages={baseErrors}/>
|
||||
|
||||
{/* タイトル */}
|
||||
{/* TODO: タグ補完 */}
|
||||
<FormField label="タイトル" messages={fieldErrors.title}>
|
||||
<FormField label={label ('shared:labels.title')} messages={fieldErrors.title}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<input type="text"
|
||||
value={title}
|
||||
@@ -100,7 +102,7 @@ const WikiNewPage: FC<Props> = ({ user }) => {
|
||||
</FormField>
|
||||
|
||||
{/* 本文 */}
|
||||
<FormField label="本文" messages={fieldErrors.body}>
|
||||
<FormField label={label ('shared:labels.body')} messages={fieldErrors.body}>
|
||||
{() => (
|
||||
<MdEditor value={body}
|
||||
style={{ height: '500px' }}
|
||||
@@ -112,7 +114,7 @@ const WikiNewPage: FC<Props> = ({ user }) => {
|
||||
{/* 送信 */}
|
||||
<button onClick={handleSubmit}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
|
||||
追加
|
||||
{label ('shared:actions.add')}
|
||||
</button>
|
||||
</div>
|
||||
</MainArea>)
|
||||
|
||||
@@ -16,8 +16,8 @@ describe ('Wiki resource identity', () => {
|
||||
vi.mocked (apiGet).mockResolvedValueOnce ([buildWikiPage ({ title: '虹夏' })])
|
||||
renderWithProviders (<WikiSearchPage/>)
|
||||
expect (await screen.findByRole ('link', { name: '虹夏' }))
|
||||
.toHaveAttribute ('href', '/wiki/%E8%99%B9%E5%A4%8F')
|
||||
expect (apiGet).toHaveBeenCalledWith ('/wiki', { params: { title: '' } })
|
||||
.toHaveAttribute ('href', '/ja/wiki/%E8%99%B9%E5%A4%8F')
|
||||
expect (apiGet).toHaveBeenCalledWith ('/wiki', { locale: 'ja', params: { title: '' } })
|
||||
})
|
||||
|
||||
it ('searches by title on submit', async () => {
|
||||
@@ -28,7 +28,7 @@ describe ('Wiki resource identity', () => {
|
||||
fireEvent.change (screen.getAllByRole ('textbox')[0], { target: { value: '検索' } })
|
||||
fireEvent.click (screen.getByRole ('button', { name: '検索' }))
|
||||
await waitFor (() => expect (apiGet).toHaveBeenLastCalledWith (
|
||||
'/wiki', { params: { title: '検索' } }))
|
||||
'/wiki', { locale: 'ja', params: { title: '検索' } }))
|
||||
expect (await screen.findByRole ('link', { name: '検索結果' })).toBeInTheDocument ()
|
||||
})
|
||||
|
||||
@@ -49,7 +49,7 @@ describe ('Wiki resource identity', () => {
|
||||
|
||||
await screen.findByRole ('link', { name: '説明' })
|
||||
expect (screen.getByRole ('link', { name: 'タググループ:一覧' }))
|
||||
.toHaveAttribute ('href', `/wiki/${ encodeURIComponent (group.title) }`)
|
||||
.toHaveAttribute ('href', `/ja/wiki/${ encodeURIComponent (group.title) }`)
|
||||
await waitFor (() => {
|
||||
expect (errors.mock.calls.some (args => args.some (value =>
|
||||
String (value).includes ('same key')))).toBe (false)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { apiGet } from '@/lib/api'
|
||||
import { wikiKeys } from '@/lib/queryKeys'
|
||||
import { dateString, inputClass } from '@/lib/utils'
|
||||
|
||||
import type { FormEvent , FC } from 'react'
|
||||
@@ -15,36 +18,37 @@ import type { WikiPage } from '@/types'
|
||||
|
||||
|
||||
const WikiSearchPage: FC = () => {
|
||||
const { message, label } = useUITranslation ('shared')
|
||||
const locale = useLocale ()
|
||||
const [title, setTitle] = useState ('')
|
||||
const [text, setText] = useState ('')
|
||||
const [results, setResults] = useState<WikiPage[]> ([])
|
||||
|
||||
const search = async () => {
|
||||
setResults (await apiGet ('/wiki', { params: { title } }))
|
||||
}
|
||||
const [submittedTitle, setSubmittedTitle] = useState ('')
|
||||
const { data: results = [], refetch } = useQuery ({
|
||||
queryKey: wikiKeys.index ({ title: submittedTitle }, locale),
|
||||
queryFn: () => apiGet<WikiPage[]> ('/wiki', {
|
||||
locale, params: { title: submittedTitle } }) })
|
||||
|
||||
const handleSearch = (ev: FormEvent) => {
|
||||
ev.preventDefault ()
|
||||
search ()
|
||||
if (submittedTitle === title)
|
||||
void refetch ()
|
||||
else
|
||||
setSubmittedTitle (title)
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
void (async () => {
|
||||
setResults (await apiGet ('/wiki', { params: { title: '' } }))
|
||||
}) ()
|
||||
}, [])
|
||||
setSubmittedTitle ('')
|
||||
}, [locale])
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>Wiki | {SITE_TITLE}</title>
|
||||
</Helmet>
|
||||
<PageHead title={message ('navigation:wiki')}/>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<PageTitle>Wiki</PageTitle>
|
||||
<PageTitle>{label ('navigation:wiki')}</PageTitle>
|
||||
<form onSubmit={handleSearch} className="space-y-2">
|
||||
{/* タイトル */}
|
||||
<FormField label="タイトル">
|
||||
<FormField label={label ('labels.title')}>
|
||||
{({ invalid }) => (
|
||||
<input type="text"
|
||||
value={title}
|
||||
@@ -53,7 +57,7 @@ const WikiSearchPage: FC = () => {
|
||||
</FormField>
|
||||
|
||||
{/* 内容 */}
|
||||
<FormField label="内容">
|
||||
<FormField label={label ('labels.content')}>
|
||||
{({ invalid }) => (
|
||||
<input type="text"
|
||||
value={text}
|
||||
@@ -65,7 +69,7 @@ const WikiSearchPage: FC = () => {
|
||||
<div className="py-3">
|
||||
<button type="submit"
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||
検索
|
||||
{label ('common:search')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -75,8 +79,8 @@ const WikiSearchPage: FC = () => {
|
||||
<table className="table-auto w-full border-collapse">
|
||||
<thead className="border-b-2 border-black dark:border-white">
|
||||
<tr>
|
||||
<th className="p-2 text-left">タイトル</th>
|
||||
<th className="p-2 text-left">最終更新</th>
|
||||
<th className="p-2 text-left">{label ('labels.title')}</th>
|
||||
<th className="p-2 text-left">{label ('labels.lastUpdated')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -87,7 +91,7 @@ const WikiSearchPage: FC = () => {
|
||||
<PrefetchLink to={`/wiki/${ encodeURIComponent (page.title) }`}>
|
||||
{page.title}
|
||||
</PrefetchLink>
|
||||
{page.deprecatedAt != null && <span>(廃止)</span>}
|
||||
{page.deprecatedAt != null && label ('labels.deprecatedSuffix')}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{dateString (page.updatedAt)}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする