Reviewed-on: #433 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
@@ -8,7 +8,7 @@ import App from '@/App'
|
||||
import { helpFixture } from '@/test/helpFixtures'
|
||||
|
||||
vi.hoisted (() => {
|
||||
window.history.replaceState (null, '', '/help/changes')
|
||||
window.history.replaceState (null, '', '/ja/help/changes')
|
||||
})
|
||||
|
||||
vi.mock ('@/lib/help', async () => (await import ('@/test/helpFixtures')).helpFixture.api)
|
||||
@@ -21,7 +21,10 @@ vi.mock ('@/lib/useClientBehaviourSettings', () => ({
|
||||
|
||||
|
||||
describe ('Help global edition history', () => {
|
||||
beforeEach (() => helpFixture.reset ())
|
||||
beforeEach (() => {
|
||||
helpFixture.reset ()
|
||||
vi.mocked (apiGet).mockResolvedValue ({ locales: [{ code: 'ja', name: '日本語' }] })
|
||||
})
|
||||
|
||||
it ('routes /help/changes to published editions in date and numeric order', async () => {
|
||||
const client = new QueryClient ({ defaultOptions: { queries: { retry: false } } })
|
||||
@@ -31,12 +34,12 @@ describe ('Help global edition history', () => {
|
||||
</QueryClientProvider>)
|
||||
const links = await screen.findAllByRole ('link')
|
||||
const editions = links.map (link => link.getAttribute ('href'))
|
||||
.filter (href => href?.startsWith ('/help/') && href.includes ('edition='))
|
||||
.filter (href => href?.startsWith ('/ja/help/') && href.includes ('edition='))
|
||||
expect (editions).toEqual ([
|
||||
`/help/${encodeURIComponent ('設定 手順')}?edition=2026-10-03`,
|
||||
'/help/guide?edition=2026-10-02.10', '/help/guide?edition=2026-10-02.3',
|
||||
'/help/guide?edition=2026-10-02.2', '/help/guide?edition=2026-10-02',
|
||||
'/help/guide?edition=2026-10-01'])
|
||||
`/ja/help/${encodeURIComponent ('設定 手順')}?edition=2026-10-03`,
|
||||
'/ja/help/guide?edition=2026-10-02.10', '/ja/help/guide?edition=2026-10-02.3',
|
||||
'/ja/help/guide?edition=2026-10-02.2', '/ja/help/guide?edition=2026-10-02',
|
||||
'/ja/help/guide?edition=2026-10-01'])
|
||||
expect (container).toHaveTextContent ('旧ホーム')
|
||||
expect (screen.getAllByRole ('columnheader').map (cell => cell.textContent))
|
||||
.toEqual (['差分', 'タイトル', '有効開始年月日'])
|
||||
@@ -45,11 +48,11 @@ describe ('Help global edition history', () => {
|
||||
expect (container.textContent).not.toMatch (/\d{1,2}:\d{2}/)
|
||||
expect (screen.getAllByRole ('link', { name: '差分' }).map (link =>
|
||||
link.getAttribute ('href'))).toEqual ([
|
||||
'/help/guide/diff?from=2026-10-02.3&to=2026-10-02.10',
|
||||
'/help/guide/diff?from=2026-10-02.2&to=2026-10-02.3',
|
||||
'/help/guide/diff?from=2026-10-02&to=2026-10-02.2',
|
||||
'/help/guide/diff?from=2026-10-01&to=2026-10-02'])
|
||||
expect (apiGet).not.toHaveBeenCalled ()
|
||||
expect (window.location.pathname).toBe ('/help/changes')
|
||||
'/ja/help/guide/diff?from=2026-10-02.3&to=2026-10-02.10',
|
||||
'/ja/help/guide/diff?from=2026-10-02.2&to=2026-10-02.3',
|
||||
'/ja/help/guide/diff?from=2026-10-02&to=2026-10-02.2',
|
||||
'/ja/help/guide/diff?from=2026-10-01&to=2026-10-02'])
|
||||
expect (apiGet).toHaveBeenCalledWith ('/locales')
|
||||
expect (window.location.pathname).toBe ('/ja/help/changes')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import Pagination from '@/components/common/Pagination'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { getHelpChanges, helpEditionDate } from '@/lib/help'
|
||||
import { localeLanguage } from '@/lib/locales'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import { helpKeys } from '@/lib/queryKeys'
|
||||
import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
|
||||
import { dateString } from '@/lib/utils'
|
||||
import { localisedDateString } from '@/lib/utils'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
|
||||
const HelpChangesPage: FC = () => {
|
||||
const { message, label } = useUITranslation ('help')
|
||||
const locale = useLocale ()
|
||||
const language = localeLanguage (locale)
|
||||
const [searchParams, setSearchParams] = useSearchParams ()
|
||||
const identifier = searchParams.get ('identifier') ?? ''
|
||||
const pageValue = searchParams.get ('page') ?? '1'
|
||||
@@ -27,8 +32,9 @@ const HelpChangesPage: FC = () => {
|
||||
const limit = LIST_LIMIT_OPTIONS.find (
|
||||
value => String (value) === searchParams.get ('limit')) ?? 20
|
||||
const { data: changes = [], isLoading, error } = useQuery ({
|
||||
queryKey: helpKeys.changes ('ja'),
|
||||
queryFn: () => getHelpChanges ('ja'),
|
||||
queryKey: helpKeys.changes (language),
|
||||
queryFn: () => getHelpChanges (language),
|
||||
select: entries => entries.filter (entry => entry.language === language),
|
||||
staleTime: Infinity })
|
||||
|
||||
const results = useMemo (() => identifier
|
||||
@@ -47,13 +53,11 @@ const HelpChangesPage: FC = () => {
|
||||
|
||||
return (
|
||||
<MainArea className="min-w-0 max-w-full">
|
||||
<Helmet>
|
||||
<title>{`ヘルプ改定履歴 | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
<PageHead title={message ('history')}/>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<PageTitle>ヘルプ改定履歴</PageTitle>
|
||||
<PageTitle>{label ('history')}</PageTitle>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>件数</span>
|
||||
<span>{label ('common:countLabel')}</span>
|
||||
<select value={limit} onChange={ev => handleLimit (ev.target.value)}
|
||||
className="rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-foreground">
|
||||
@@ -64,9 +68,9 @@ const HelpChangesPage: FC = () => {
|
||||
</div>
|
||||
|
||||
{isLoading
|
||||
? <p className="mt-4">読み込み中...</p>
|
||||
? <p className="mt-4">{label ('shared:messages.loading')}</p>
|
||||
: (error
|
||||
? <p className="mt-4 text-red-600" role="alert">{error.message}</p>
|
||||
? <p className="mt-4 text-red-600" role="alert">{label ('common:fetchError')}</p>
|
||||
: (
|
||||
<div className="mt-4">
|
||||
<div className="min-w-0 max-w-full overflow-x-auto">
|
||||
@@ -74,9 +78,9 @@ const HelpChangesPage: FC = () => {
|
||||
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">有効開始年月日</th>
|
||||
<th className="p-2 text-left">{label ('shared:actions.diff')}</th>
|
||||
<th className="p-2 text-left">{label ('shared:labels.title')}</th>
|
||||
<th className="p-2 text-left">{label ('effectiveSince')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -94,7 +98,7 @@ const HelpChangesPage: FC = () => {
|
||||
<td className="p-2">
|
||||
{entry.pred != null && (
|
||||
<PrefetchLink to={diffTo}>
|
||||
差分
|
||||
{label ('shared:actions.diff')}
|
||||
</PrefetchLink>)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
@@ -103,7 +107,7 @@ const HelpChangesPage: FC = () => {
|
||||
</PrefetchLink>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{updatedAt != null && dateString (updatedAt, 'hour')}
|
||||
{updatedAt != null && localisedDateString (updatedAt, locale, 'hour')}
|
||||
</td>
|
||||
</tr>)
|
||||
})}
|
||||
|
||||
@@ -17,19 +17,19 @@ const Navigation = () => {
|
||||
return (
|
||||
<>
|
||||
<output data-testid="location">{location.pathname}{location.search}</output>
|
||||
<button onClick={() => navigate ('/help/設定%20手順')}>switch article</button>
|
||||
<button onClick={() => navigate ('/help/guide?edition=2026-10-02.3')}>
|
||||
<button onClick={() => navigate ('/ja/help/設定%20手順')}>switch article</button>
|
||||
<button onClick={() => navigate ('/ja/help/guide?edition=2026-10-02.3')}>
|
||||
switch edition
|
||||
</button>
|
||||
</>)
|
||||
}
|
||||
|
||||
|
||||
const renderDetail = (route = '/help/guide') => renderWithProviders (
|
||||
const renderDetail = (route = '/ja/help/guide') => renderWithProviders (
|
||||
<>
|
||||
<Navigation/>
|
||||
<Routes>
|
||||
<Route path="/help/:identifier" element={<HelpDetailPage/>}/>
|
||||
<Route path="/ja/help/:identifier" element={<HelpDetailPage/>}/>
|
||||
</Routes>
|
||||
</>, { route })
|
||||
|
||||
@@ -39,7 +39,7 @@ describe ('Help details', () => {
|
||||
|
||||
it.each (['2026-10-02', '2026-10-02.2', '2026-10-02.3', '2026-10-02.10']) (
|
||||
'displays a date without inventing a time for edition %s', async edition => {
|
||||
const { container } = renderDetail (`/help/guide?edition=${ edition }`)
|
||||
const { container } = renderDetail (`/ja/help/guide?edition=${ edition }`)
|
||||
await screen.findByRole ('heading')
|
||||
expect (container).toHaveTextContent ('10月2日')
|
||||
expect (container).not.toHaveTextContent ('Invalid Date')
|
||||
@@ -61,31 +61,31 @@ describe ('Help details', () => {
|
||||
})
|
||||
|
||||
it ('uses the historical H1 in both page and document titles', async () => {
|
||||
renderDetail ('/help/guide?edition=2026-10-01')
|
||||
renderDetail ('/ja/help/guide?edition=2026-10-01')
|
||||
expect (await screen.findByRole ('heading', { name: '旧ホーム' })).toBeInTheDocument ()
|
||||
expect (screen.getByText ('過去限定本文')).toBeInTheDocument ()
|
||||
await waitFor (() => expect (document.title).toContain ('旧ホーム ヘルプ'))
|
||||
expect (screen.getByText ('(最古)')).toBeInTheDocument ()
|
||||
expect (screen.getByRole ('link', { name: '新 >' }))
|
||||
.toHaveAttribute ('href', '/help/guide?edition=2026-10-02')
|
||||
.toHaveAttribute ('href', '/ja/help/guide?edition=2026-10-02')
|
||||
})
|
||||
|
||||
it ('navigates .2, .3 and .10 in numeric edition order', async () => {
|
||||
renderDetail ('/help/guide?edition=2026-10-02.3')
|
||||
renderDetail ('/ja/help/guide?edition=2026-10-02.3')
|
||||
await screen.findByRole ('heading', { name: 'ホーム第三版' })
|
||||
expect (screen.getByRole ('link', { name: '< 古' }))
|
||||
.toHaveAttribute ('href', '/help/guide?edition=2026-10-02.2')
|
||||
.toHaveAttribute ('href', '/ja/help/guide?edition=2026-10-02.2')
|
||||
const next = screen.getByRole ('link', { name: '新 >' })
|
||||
expect (next).toHaveAttribute ('href', '/help/guide?edition=2026-10-02.10')
|
||||
expect (next).toHaveAttribute ('href', '/ja/help/guide?edition=2026-10-02.10')
|
||||
fireEvent.click (next)
|
||||
await screen.findByRole ('heading', { name: 'ホーム第十版' })
|
||||
expect (screen.getByText ('(最新)')).toBeInTheDocument ()
|
||||
expect (screen.queryByText ('第三版本文')).not.toBeInTheDocument ()
|
||||
})
|
||||
|
||||
it.each (['/help/missing', '/help/guide?edition=missing',
|
||||
'/help/guide?edition=2026-02-30', '/help/guide?edition=2026-10-02.2.extra',
|
||||
'/help/guide?edition=2026-10-02.11']) (
|
||||
it.each (['/ja/help/missing', '/ja/help/guide?edition=missing',
|
||||
'/ja/help/guide?edition=2026-02-30', '/ja/help/guide?edition=2026-10-02.2.extra',
|
||||
'/ja/help/guide?edition=2026-10-02.11']) (
|
||||
'shows NotFound without redirecting %s', async route => {
|
||||
renderDetail (route)
|
||||
expect (await screen.findByText ('404')).toBeInTheDocument ()
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { getHelpArticle,
|
||||
getHelpEditionContent,
|
||||
helpEditionDate,
|
||||
parseHelpEdition } from '@/lib/help'
|
||||
parseHelpEdition,
|
||||
resolveHelpLanguage } from '@/lib/help'
|
||||
import { localeLanguage } from '@/lib/locales'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import { dateString } from '@/lib/utils'
|
||||
import { helpKeys } from '@/lib/queryKeys'
|
||||
import { currentRenderContext } from '@/lib/renderContext'
|
||||
import { LocalisedDocument } from '@/mdx-components'
|
||||
import NotFound from '@/pages/NotFound'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
|
||||
const HelpDetailPage: FC = () => {
|
||||
const { message, label } = useUITranslation ('help')
|
||||
const locale = useLocale ()
|
||||
const { identifier = '' } = useParams ()
|
||||
const location = useLocation ()
|
||||
|
||||
// Locale 対応が入るまでは ja 固定
|
||||
const language = 'ja'
|
||||
|
||||
const { data: article, isLoading: articleLoading } = useQuery ({
|
||||
queryKey: helpKeys.article (identifier),
|
||||
queryFn: () => getHelpArticle (identifier),
|
||||
staleTime: Infinity })
|
||||
|
||||
const requestedLanguage = localeLanguage (locale)
|
||||
const language = resolveHelpLanguage (article, requestedLanguage)
|
||||
|
||||
const query = new URLSearchParams (location.search)
|
||||
const requestedEdition = query.get ('edition')
|
||||
|
||||
const languageMeta = article?.languages[language]
|
||||
const languageMeta = language == null ? null : article?.languages[language]
|
||||
|
||||
const edition = requestedEdition ?? languageMeta?.currentEdition
|
||||
const editionInfo = parseHelpEdition (edition ?? '')
|
||||
@@ -48,8 +55,8 @@ const HelpDetailPage: FC = () => {
|
||||
|
||||
const { data: current, isSuccess, isError } = useQuery ({
|
||||
enabled: edition != null && editionInfo != null && editions.includes (edition),
|
||||
queryKey: helpKeys.edition (identifier, language, edition ?? ''),
|
||||
queryFn: () => getHelpEditionContent (identifier, language, edition ?? ''),
|
||||
queryKey: helpKeys.edition (identifier, language ?? requestedLanguage, edition ?? ''),
|
||||
queryFn: () => getHelpEditionContent (identifier, language ?? requestedLanguage, edition ?? ''),
|
||||
staleTime: Infinity })
|
||||
|
||||
if (!articleLoading
|
||||
@@ -64,12 +71,19 @@ const HelpDetailPage: FC = () => {
|
||||
return <NotFound/>
|
||||
|
||||
const Content = current?.content
|
||||
const origin = currentRenderContext ()?.origin
|
||||
const canonical = origin == null ? undefined : new URL (
|
||||
`/${locale}/help/${encodeURIComponent (identifier)}`, origin).href
|
||||
const description = current?.body.replace (/!?(\[([^\]]*)\])\([^)]*\)/g, '$2')
|
||||
.replace (/[#*`<>]/g, '')
|
||||
.replace (/\s+/g, language === 'ain' ? '\u3000' : ' ').trim ().slice (0, 180)
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>{current?.title || identifier} ヘルプ | {SITE_TITLE}</title>
|
||||
</Helmet>
|
||||
<MainArea className="min-w-0">
|
||||
<PageHead title={message ('articleTitle', { title: current?.title || identifier })}
|
||||
canonical={canonical}
|
||||
description={description == null ? undefined : {
|
||||
text: description, language: language === 'ain' ? 'ain-Kana' : language ?? 'ja' }}/>
|
||||
|
||||
{requestedEdition && (
|
||||
<div className="text-sm flex gap-3 items-center justify-center
|
||||
@@ -78,9 +92,9 @@ const HelpDetailPage: FC = () => {
|
||||
? (
|
||||
<PrefetchLink
|
||||
to={`/help/${ encodeURIComponent (identifier) }?edition=${ pred }`}>
|
||||
< 古
|
||||
{label ('shared:actions.old')}
|
||||
</PrefetchLink>)
|
||||
: '(最古)'}
|
||||
: label ('shared:actions.oldest')}
|
||||
|
||||
<span>
|
||||
{editionDate != null && dateString (editionDate, 'hour')}
|
||||
@@ -91,13 +105,19 @@ const HelpDetailPage: FC = () => {
|
||||
? (
|
||||
<PrefetchLink
|
||||
to={`/help/${ encodeURIComponent (identifier) }?edition=${ succ }`}>
|
||||
新 >
|
||||
{label ('shared:actions.new')}
|
||||
</PrefetchLink>)
|
||||
: '(最新)'}
|
||||
: label ('shared:actions.latest')}
|
||||
</div>)}
|
||||
|
||||
<article className="prose dark:prose-invert mx-auto p-4">
|
||||
{Content ? <Content/> : <div>Loading...</div>}
|
||||
<article lang={language ?? requestedLanguage}
|
||||
className="prose dark:prose-invert mx-auto min-w-0 p-4">
|
||||
{Content
|
||||
? (
|
||||
<LocalisedDocument language={language ?? requestedLanguage}>
|
||||
<Content/>
|
||||
</LocalisedDocument>)
|
||||
: <div>{label ('common:loading')}</div>}
|
||||
</article>
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { getHelpDiff } from '@/lib/help'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { getHelpArticle, getHelpDiff, resolveHelpLanguage } from '@/lib/help'
|
||||
import { localeLanguage } from '@/lib/locales'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import { helpKeys } from '@/lib/queryKeys'
|
||||
import { cn } from '@/lib/utils'
|
||||
import NotFound from '@/pages/NotFound'
|
||||
@@ -17,20 +19,29 @@ const markers = { added: '+ ', removed: '- ', unchanged: ' ' }
|
||||
|
||||
|
||||
const HelpDiffPage: FC = () => {
|
||||
const { message, label } = useUITranslation ('help')
|
||||
const locale = useLocale ()
|
||||
const { identifier = '' } = useParams ()
|
||||
const [searchParams] = useSearchParams ()
|
||||
const from = searchParams.get ('from') ?? ''
|
||||
const to = searchParams.get ('to') ?? ''
|
||||
const { data: article } = useQuery ({
|
||||
queryKey: helpKeys.article (identifier),
|
||||
queryFn: () => getHelpArticle (identifier),
|
||||
staleTime: Infinity })
|
||||
const requestedLanguage = localeLanguage (locale)
|
||||
const language = resolveHelpLanguage (article, requestedLanguage)
|
||||
const validParams = Boolean (identifier && from && to)
|
||||
&& searchParams.getAll ('from').length === 1
|
||||
&& searchParams.getAll ('to').length === 1
|
||||
const { data: diff, isLoading, isError } = useQuery ({
|
||||
enabled: validParams,
|
||||
queryKey: helpKeys.diff (identifier, 'ja', from, to),
|
||||
queryFn: () => getHelpDiff (identifier, 'ja', from, to),
|
||||
enabled: validParams && language != null,
|
||||
queryKey: helpKeys.diff (identifier, requestedLanguage, from, to),
|
||||
queryFn: () => getHelpDiff (identifier, requestedLanguage, from, to),
|
||||
staleTime: Infinity })
|
||||
|
||||
if (!(validParams) || isError || (!(isLoading) && diff == null))
|
||||
if (!(validParams) || (article != null && language == null)
|
||||
|| isError || (!(isLoading) && diff == null))
|
||||
return <NotFound/>
|
||||
|
||||
const title = `${ diff?.to.title ?? identifier } (${ from } → ${ to })`
|
||||
@@ -38,9 +49,7 @@ const HelpDiffPage: FC = () => {
|
||||
|
||||
return (
|
||||
<MainArea className="min-w-0 max-w-full">
|
||||
<Helmet>
|
||||
<title>{`ヘルプ 差分: ${ title } | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
<PageHead title={message ('diffTitle', { title })}/>
|
||||
<PageTitle>{title}</PageTitle>
|
||||
|
||||
{diff
|
||||
@@ -55,7 +64,7 @@ const HelpDiffPage: FC = () => {
|
||||
{diff.to.title} ({to})
|
||||
</PrefetchLink>
|
||||
</div>
|
||||
<pre className="mt-4 overflow-x-auto whitespace-pre">
|
||||
<pre lang={requestedLanguage} className="mt-4 overflow-x-auto whitespace-pre">
|
||||
<code>
|
||||
{diff.lines.map ((line, index) => {
|
||||
const colour = cn (
|
||||
@@ -73,7 +82,7 @@ const HelpDiffPage: FC = () => {
|
||||
</code>
|
||||
</pre>
|
||||
</>)
|
||||
: <div>Loading...</div>}
|
||||
: <div>{label ('common:loading')}</div>}
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ describe ('Help search', () => {
|
||||
it ('initially lists each current article once with encoded detail links', async () => {
|
||||
renderWithProviders (<HelpListPage/>, { route: '/help' })
|
||||
expect (await screen.findByRole ('link', { name: 'ホーム第十版' }))
|
||||
.toHaveAttribute ('href', '/help/guide')
|
||||
.toHaveAttribute ('href', '/ja/help/guide')
|
||||
expect (screen.getByRole ('link', { name: '設定' }))
|
||||
.toHaveAttribute ('href', `/help/${encodeURIComponent ('設定 手順')}`)
|
||||
.toHaveAttribute ('href', `/ja/help/${encodeURIComponent ('設定 手順')}`)
|
||||
expect (screen.getByText ('2 件')).toBeInTheDocument ()
|
||||
expect (screen.queryByText ('旧ホーム')).not.toBeInTheDocument ()
|
||||
expect (screen.queryByText ('未公開ホーム')).not.toBeInTheDocument ()
|
||||
@@ -82,7 +82,7 @@ describe ('Help search', () => {
|
||||
renderWithProviders (<HelpListPage/>, { route: `/help?${ params }` })
|
||||
await screen.findByText (`${ found ? 1 : 0 } 件`)
|
||||
if (found)
|
||||
expect (screen.getByRole ('link', { name: title })).toHaveAttribute ('href', '/help/guide')
|
||||
expect (screen.getByRole ('link', { name: title })).toHaveAttribute ('href', '/ja/help/guide')
|
||||
else
|
||||
expect (screen.queryByRole ('link', { name: title })).not.toBeInTheDocument ()
|
||||
expect (entry).toEqual ({ identifier: 'guide', edition: '2026-10-02.10',
|
||||
@@ -99,7 +99,7 @@ describe ('Help search', () => {
|
||||
route: `/help?order=title:${ direction }` })
|
||||
await screen.findByText ('2 件')
|
||||
const expected = direction === 'asc'
|
||||
? ['/help/beta', '/help/alpha'] : ['/help/alpha', '/help/beta']
|
||||
? ['/ja/help/beta', '/ja/help/alpha'] : ['/ja/help/alpha', '/ja/help/beta']
|
||||
expect (Array.from (container.querySelectorAll ('tbody a'), link =>
|
||||
link.getAttribute ('href'))).toEqual (expected)
|
||||
})
|
||||
@@ -113,7 +113,7 @@ describe ('Help search', () => {
|
||||
route: `/help?order=title:${ direction }` })
|
||||
await screen.findByText ('2 件')
|
||||
expect (Array.from (container.querySelectorAll ('tbody a'), link =>
|
||||
link.getAttribute ('href'))).toEqual (['/help/alpha', '/help/beta'])
|
||||
link.getAttribute ('href'))).toEqual (['/ja/help/alpha', '/ja/help/beta'])
|
||||
})
|
||||
|
||||
it.each (['asc', 'desc']) ('ignores title case before identifier ties in order %s',
|
||||
@@ -125,6 +125,6 @@ describe ('Help search', () => {
|
||||
route: `/help?order=title:${ direction }` })
|
||||
await screen.findByText ('2 件')
|
||||
expect (Array.from (container.querySelectorAll ('tbody a'), link =>
|
||||
link.getAttribute ('href'))).toEqual (['/help/alpha', '/help/beta'])
|
||||
link.getAttribute ('href'))).toEqual (['/ja/help/alpha', '/ja/help/beta'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
@@ -9,12 +8,15 @@ import FormField from '@/components/common/FormField'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import Pagination from '@/components/common/Pagination'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
import { compareHelpEditions,
|
||||
compareHelpTitles,
|
||||
getHelpSearchEntries,
|
||||
helpEditionDate,
|
||||
normaliseHelpSearchText } from '@/lib/help'
|
||||
import { localeLanguage } from '@/lib/locales'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import { helpKeys } from '@/lib/queryKeys'
|
||||
import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
|
||||
import { dateString, inputClass } from '@/lib/utils'
|
||||
@@ -30,6 +32,8 @@ const defaultDirection: Record<HelpOrderField, 'asc' | 'desc'> = {
|
||||
|
||||
|
||||
const HelpSearchPage: FC = () => {
|
||||
const { message, label } = useUITranslation ('help')
|
||||
const language = localeLanguage (useLocale ())
|
||||
const [searchParams, setSearchParams] = useSearchParams ()
|
||||
const pageValue = searchParams.get ('page') ?? '1'
|
||||
const pageNumber = Number (pageValue)
|
||||
@@ -49,8 +53,9 @@ const HelpSearchPage: FC = () => {
|
||||
const [title, setTitle] = useState (conditions.title)
|
||||
const [text, setText] = useState (conditions.text)
|
||||
const { data: entries = [], isLoading: loading, error } = useQuery ({
|
||||
queryKey: helpKeys.index ('ja'),
|
||||
queryFn: () => getHelpSearchEntries ('ja'),
|
||||
queryKey: helpKeys.index (language),
|
||||
queryFn: () => getHelpSearchEntries (language),
|
||||
select: entries => entries.filter (entry => entry.language === language),
|
||||
staleTime: Infinity })
|
||||
|
||||
useEffect (() => {
|
||||
@@ -107,15 +112,13 @@ const HelpSearchPage: FC = () => {
|
||||
|
||||
return (
|
||||
<MainArea className="min-w-0 max-w-full">
|
||||
<Helmet>
|
||||
<title>{`ヘルプ | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
<PageHead title={message ('title')}/>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<PageTitle>ヘルプ検索</PageTitle>
|
||||
<PageTitle>{label ('search')}</PageTitle>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>件数</span>
|
||||
<span>{label ('common:countLabel')}</span>
|
||||
<select value={limit} onChange={ev => handleLimit (ev.target.value)}
|
||||
className="rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-foreground">
|
||||
@@ -125,19 +128,19 @@ const HelpSearchPage: FC = () => {
|
||||
</label>
|
||||
</div>
|
||||
<form onSubmit={handleSearch} className="space-y-2">
|
||||
<FormField label="タイトル">
|
||||
<FormField label={label ('shared:labels.title')}>
|
||||
{({ invalid }) => (
|
||||
<input type="text"
|
||||
aria-label="タイトル"
|
||||
aria-label={message ('shared:labels.title').text}
|
||||
value={title}
|
||||
onChange={e => setTitle (e.target.value)}
|
||||
className={inputClass (invalid)}/>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="内容">
|
||||
<FormField label={label ('shared:labels.content')}>
|
||||
{({ invalid }) => (
|
||||
<input type="text"
|
||||
aria-label="内容"
|
||||
aria-label={message ('shared:labels.content').text}
|
||||
value={text}
|
||||
onChange={e => setText (e.target.value)}
|
||||
className={inputClass (invalid)}/>)}
|
||||
@@ -146,19 +149,21 @@ const HelpSearchPage: 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>
|
||||
</div>
|
||||
|
||||
{loading
|
||||
? <p className="mt-4">読み込み中...</p>
|
||||
? <p className="mt-4">{label ('shared:messages.loading')}</p>
|
||||
: (error
|
||||
? <p className="mt-4 text-red-600" role="alert">{error.message}</p>
|
||||
: (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2 text-sm" aria-live="polite">{results.length} 件</p>
|
||||
<p className="mb-2 text-sm" aria-live="polite">
|
||||
{label ('shared:labels.resultsCount', { count: results.length })}
|
||||
</p>
|
||||
<div className="min-w-0 max-w-full overflow-x-auto">
|
||||
<table className="table-auto w-full min-w-[640px] whitespace-nowrap
|
||||
border-collapse text-gray-900 dark:text-gray-100">
|
||||
@@ -167,14 +172,14 @@ const HelpSearchPage: FC = () => {
|
||||
<th className="p-2 text-left">
|
||||
<SortHeader<HelpOrderField>
|
||||
by="title"
|
||||
label="タイトル"
|
||||
label={label ('shared:labels.title')}
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left">
|
||||
<SortHeader<HelpOrderField>
|
||||
by="updated_at"
|
||||
label="有効開始年月日"
|
||||
label={label ('effectiveSince')}
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
@@ -203,7 +208,7 @@ const HelpSearchPage: FC = () => {
|
||||
</table>
|
||||
</div>
|
||||
{totalPages > 0 && <Pagination page={page} totalPages={totalPages}/>}
|
||||
{results.length === 0 && <p className="mt-4">該当する記事がありません.</p>}
|
||||
{results.length === 0 && <p className="mt-4">{label ('empty')}</p>}
|
||||
</div>))}
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする