import { useEffect, useMemo, useState } from 'react' import { Helmet } from 'react-helmet-async' import { useNavigate, useParams } from 'react-router-dom' import FieldError from '@/components/common/FieldError' import PageTitle from '@/components/common/PageTitle' import PrefetchLink from '@/components/PrefetchLink' import MainArea from '@/components/layout/MainArea' import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge' import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' import { SITE_TITLE } from '@/config' import { apiPost } from '@/lib/api' import { canEditContent } from '@/lib/users' import { clearPostImportSourceDraft, initialisePreviewRows, loadPostImportSession, mergeImportResults, mergeValidatedImportRows, resultSummaryCounts, retryImportRow, savePostImportSession } from '@/lib/postImportSession' import Forbidden from '@/pages/Forbidden' import type { FC } from 'react' import type { PostImportResultRow, PostImportRow } from '@/lib/postImportSession' import type { PostImportSession } from '@/lib/postImportSession' import type { User } from '@/types' type Props = { user: User | null } const rowMessages = (row: PostImportRow): string[] => Object.values (row.importErrors ?? { }).flat () const PostImportResultPage: FC = ({ user }) => { const editable = canEditContent (user) const navigate = useNavigate () const { sessionId } = useParams () const [session, setSession] = useState (null) const [missing, setMissing] = useState (false) const [loadingRow, setLoadingRow] = useState (null) useEffect (() => { if (sessionId == null) return const loaded = loadPostImportSession (sessionId, message => toast ({ title: '取込状態を復元できませんでした', description: message })) setSession (loaded) setMissing (loaded == null) }, [sessionId]) useEffect (() => { if (sessionId == null || session == null) return savePostImportSession (sessionId, session, message => toast ({ title: '取込状態を保存できませんでした', description: message })) }, [session, sessionId]) const counts = useMemo ( () => resultSummaryCounts (session?.rows ?? []), [session]) const retry = async (sourceRow: number) => { if (session == null || sessionId == null) return setLoadingRow (sourceRow) try { const pendingRows = retryImportRow (session.rows, sourceRow) setSession ({ ...session, rows: pendingRows }) const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', { rows: pendingRows .filter (row => row.importStatus !== 'created') .map (row => ({ sourceRow: row.sourceRow, url: row.url, attributes: row.attributes, provenance: row.provenance, tagSources: row.tagSources, metadataUrl: row.metadataUrl })), changed_row: -1 }) const validatedRows = mergeValidatedImportRows ( pendingRows, initialisePreviewRows (validated.rows)) const nextSession = { ...session, rows: validatedRows, repairMode: 'failed' as const } setSession (nextSession) const target = validatedRows.find (_1 => _1.sourceRow === sourceRow) if (target == null) return if (Object.keys (target.validationErrors ?? { }).length > 0) { const saved = savePostImportSession (sessionId, nextSession, message => toast ({ title: '取込状態を保存できませんでした', description: message })) if (saved) navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`) return } const result = await apiPost<{ created: number skipped: number failed: number rows: PostImportResultRow[] }> ('/posts/import', { rows: [{ sourceRow: target.sourceRow, url: target.url, attributes: target.attributes, provenance: target.provenance, tagSources: target.tagSources, metadataUrl: target.metadataUrl }] }) const mergedRows = mergeImportResults (nextSession.rows, result.rows) const recoverableRows = result.rows.filter (row => row.status === 'failed' && row.recoverable && Object.keys (row.errors ?? { }).length > 0) const nextRows = mergedRows.map ((row): PostImportRow => { const recoverable = recoverableRows.find (_1 => _1.sourceRow === row.sourceRow) if (recoverable == null) return row return { ...row, importStatus: 'pending', validationErrors: recoverable.errors ?? { }, importErrors: undefined } }) const recoverableTarget = nextRows.find (_1 => _1.sourceRow === sourceRow) const resultSession = { ...nextSession, rows: nextRows, repairMode: recoverableTarget == null ? 'all' as const : 'failed' as const } setSession (resultSession) if (recoverableTarget != null && Object.keys (recoverableTarget.validationErrors).length > 0) { const saved = savePostImportSession (sessionId, resultSession, message => toast ({ title: '取込状態を保存できませんでした', description: message })) if (saved) navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`) return } } catch { setSession (session) toast ({ title: '再試行に失敗しました' }) } finally { setLoadingRow (null) } } const openRepair = (sourceRow: number) => { if (session == null || sessionId == null) return const nextSession = { ...session, repairMode: 'failed' as const } setSession (nextSession) const saved = savePostImportSession (sessionId, nextSession, message => toast ({ title: '取込状態を保存できませんでした', description: message })) if (!(saved)) return navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`) } if (!(editable)) return if (missing || sessionId == null || session == null) { return (
投稿インポート結果
) } return ( {`投稿インポート結果 | ${ SITE_TITLE }`}
登録結果
登録成功 {counts.created}件 スキップ {counts.skipped}件 失敗 {counts.failed}件
{session.rows.map (row => { const displayStatus = displayPostImportStatus (row) const canEdit = row.importStatus === 'failed' const canRetry = row.importStatus === 'failed' && Object.keys (row.validationErrors).length === 0 return (
行 {row.sourceRow} {displayStatus != null && }
{String (row.attributes.title ?? '') || row.url}
{row.url}
{(row.createdPostId != null || row.existingPostId != null) && ( )} {canEdit && ( )} {canRetry && ( )}
)})}
) } export default PostImportResultPage