このコミットが含まれているのは:
2026-07-13 01:17:11 +09:00
コミット be68841bd3
9個のファイルの変更265行の追加217行の削除
+1 -1
ファイルの表示
@@ -215,7 +215,7 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
type="button"
variant="outline"
onClick={() => openRepair (row.sourceRow)}>
</Button>
<Button
type="button"
+68 -55
ファイルの表示
@@ -4,7 +4,6 @@ import { useNavigate,
useParams,
useSearchParams } from 'react-router-dom'
import FieldError from '@/components/common/FieldError'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PostImportRowDialog from '@/components/posts/import/PostImportRowDialog'
@@ -98,7 +97,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
return
const element = document.getElementById (`post-import-row-${ editingRow.sourceRow }`)
element?.scrollIntoView ({ block: 'center', behaviour: 'smooth' })
element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
}, [editingRow, session?.repairMode])
const updateSessionRows = (nextRows: PostImportRow[]) =>
@@ -233,7 +232,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
<MainArea>
<div className="mx-auto max-w-4xl space-y-4 p-4">
<PageTitle>稿</PageTitle>
<FieldError messages={['取込状態が見つかりません.']}/>
<div className="text-red-700 dark:text-red-300"></div>
<Button type="button" onClick={() => navigate ('/posts/import')}>
URL
</Button>
@@ -242,65 +241,46 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
}
return (
<MainArea>
<>
<Helmet>
<title>{`投稿インポート確認 | ${ SITE_TITLE }`}</title>
</Helmet>
<div className="mx-auto flex max-w-6xl flex-col gap-4 p-4 pb-8">
<PageTitle>稿</PageTitle>
<MainArea className="min-h-0">
<div className="mx-auto max-w-6xl space-y-4 p-4">
<PageTitle>稿</PageTitle>
<div className="rounded-lg border bg-white p-4 dark:border-neutral-700
dark:bg-neutral-900">
<div className="flex flex-wrap items-center gap-2">
<SummaryChip label="全件" value={counts.total}/>
<SummaryChip label="登録対象" value={counts.submittable}/>
<SummaryChip label="要修正" value={counts.invalid}/>
<SummaryChip label="スキップ予定" value={counts.skipPlanned}/>
<SummaryChip label="登録済み" value={counts.created}/>
<SummaryChip label="失敗" value={counts.failed}/>
<div className="rounded-lg border bg-white p-4 dark:border-neutral-700
dark:bg-neutral-900">
<div className="flex flex-wrap items-center gap-2">
<SummaryChip label="全件" value={counts.total}/>
<SummaryChip label="登録対象" value={counts.submittable}/>
<SummaryChip label="要修正" value={counts.invalid}/>
<SummaryChip label="スキップ予定" value={counts.skipPlanned}/>
<SummaryChip label="登録済み" value={counts.created}/>
<SummaryChip label="失敗" value={counts.failed}/>
</div>
</div>
<div className="space-y-3">
{reviewRows.map (row => (
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
<PostImportRowSummary
row={row}
onEdit={() => {
setSearchParams ({ edit: String (row.sourceRow) })
}}/>
</div>))}
</div>
</div>
</MainArea>
<div className="flex-1 space-y-3 pb-24">
{reviewRows.map (row => (
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
<PostImportRowSummary
row={row}
onEdit={() => {
setSearchParams ({ edit: String (row.sourceRow) })
}}/>
</div>))}
</div>
</div>
<div
className="sticky bottom-0 border-t bg-white/95 p-4 backdrop-blur
dark:border-neutral-700 dark:bg-neutral-950/95">
<div className="mx-auto flex max-w-6xl flex-col gap-3 md:flex-row
md:items-center md:justify-between">
<div className="flex flex-wrap items-center gap-2 text-sm">
<PostImportStatusBadge value="pending"/>
<span> {submittable.length} </span>
<PostImportStatusBadge value="error"/>
<span> {counts.invalid} </span>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<Button
type="button"
variant="outline"
onClick={() => navigate ('/posts/import')}>
URL
</Button>
<Button
type="button"
onClick={submit}
disabled={loading || submittable.length === 0}>
稿
</Button>
</div>
</div>
</div>
<PostImportFooter
loading={loading}
invalidCount={counts.invalid}
submittableCount={submittable.length}
onBack={() => navigate ('/posts/import')}
onSubmit={submit}/>
<PostImportRowDialog
open={editingRow != null}
@@ -311,9 +291,42 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
setSearchParams ({ })
}}
onSave={saveDraft}/>
</MainArea>)
</>)
}
const PostImportFooter = (
{ loading, invalidCount, submittableCount, onBack, onSubmit }: {
loading: boolean
invalidCount: number
submittableCount: number
onBack: () => void
onSubmit: () => void },
) => (
<div
className="shrink-0 border-t bg-white/95 p-4 backdrop-blur
dark:border-neutral-700 dark:bg-neutral-950/95">
<div className="mx-auto flex max-w-6xl flex-col gap-3 md:flex-row
md:items-center md:justify-between">
<div className="flex flex-wrap items-center gap-2 text-sm">
<PostImportStatusBadge value="pending"/>
<span> {submittableCount} </span>
<PostImportStatusBadge value="error"/>
<span> {invalidCount} </span>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<Button type="button" variant="outline" onClick={onBack}>
URL
</Button>
<Button
type="button"
onClick={onSubmit}
disabled={loading || submittableCount === 0}>
稿
</Button>
</div>
</div>
</div>)
const SummaryChip = ({ label, value }: { label: string
value: number }) => (
<div className="rounded-full border border-border bg-background px-3 py-1 text-sm">
+26 -17
ファイルの表示
@@ -39,15 +39,12 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
const [source, setSource] = useState (draft.source)
const [loading, setLoading] = useState (false)
const [error, setError] = useState<string | null> (null)
const [touched, setTouched] = useState (false)
const [sourceIssues, setSourceIssues] = useState<ReturnType<typeof validateImportSource>> ([])
const [sourceError, setSourceError] = useState<string | null> (null)
const saveTimer = useRef<number | null> (null)
const lineCount = countImportSourceLines (source)
const issues = useMemo (() => validateImportSource (source), [source])
const messages = [
...(((touched) && lineCount === 0) ? ['URL を入力してください.'] : []),
...(error ? [error] : [])]
const messages = sourceError ? [sourceError] : []
useEffect (() => {
cleanupExpiredPostImportSessions (message =>
@@ -68,12 +65,24 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
}, [source])
const preview = async () => {
setTouched (true)
if (lineCount === 0 || issues.length > 0)
return
if (lineCount === 0)
{
setSourceIssues ([])
setSourceError ('URL を入力してください.')
return
}
const issues = validateImportSource (source)
if (issues.length > 0)
{
setSourceIssues (issues)
setSourceError (null)
return
}
setLoading (true)
setError (null)
setSourceIssues ([])
setSourceError (null)
try
{
const data = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/preview', {
@@ -95,7 +104,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
? requestError.response?.data?.message
?? requestError.response?.data?.baseErrors?.[0]
: undefined
setError (message ?? '入力を確認してください.')
setSourceError (message ?? '入力を確認してください.')
toast ({
title: '投稿情報の取得に失敗しました',
description: message ?? '入力を確認してください.' })
@@ -126,11 +135,10 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
value={source}
rows={14}
className={inputClass (
messages.length > 0 || issues.length > 0,
messages.length > 0 || sourceIssues.length > 0,
'font-mono text-sm',
)}
onBlur={() => {
setTouched (true)
savePostImportSourceDraft (source, message =>
toast ({
title: '入力内容を保存できませんでした',
@@ -138,12 +146,13 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
}}
onChange={ev => {
setSource (ev.target.value)
setError (null)
setSourceError (null)
setSourceIssues ([])
}}/>)}
</FormField>
<FieldError messages={messages}/>
<div className="space-y-2">
{issues.map (issue => (
{sourceIssues.map (issue => (
<div
key={`${ issue.sourceRow }-${ issue.message }-${ issue.url }`}
className="rounded-md border border-red-300 bg-red-50 p-3 text-sm
@@ -163,8 +172,8 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
<Button
type="button"
onClick={preview}
disabled={loading || lineCount === 0 || issues.length > 0}>
稿
disabled={loading}>
</Button>
</div>
</div>