|
|
|
@@ -1,7 +1,6 @@
|
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
|
import { Helmet } from 'react-helmet-async'
|
|
|
|
|
import { useNavigate,
|
|
|
|
|
useParams } from 'react-router-dom'
|
|
|
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
|
|
|
|
|
|
|
|
|
import FieldError from '@/components/common/FieldError'
|
|
|
|
|
import PageTitle from '@/components/common/PageTitle'
|
|
|
|
@@ -26,9 +25,8 @@ import Forbidden from '@/pages/Forbidden'
|
|
|
|
|
|
|
|
|
|
import type { FC } from 'react'
|
|
|
|
|
|
|
|
|
|
import type { PostImportResultRow,
|
|
|
|
|
PostImportRow,
|
|
|
|
|
PostImportSession } from '@/lib/postImportSession'
|
|
|
|
|
import type { PostImportResultRow, PostImportRow } from '@/lib/postImportSession'
|
|
|
|
|
import type { PostImportSession } from '@/lib/postImportSession'
|
|
|
|
|
import type { User } from '@/types'
|
|
|
|
|
|
|
|
|
|
type Props = { user: User | null }
|
|
|
|
@@ -38,184 +36,184 @@ const rowMessages = (row: PostImportRow): string[] =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const PostImportResultPage: FC<Props> = ({ user }) => {
|
|
|
|
|
const editable = canEditContent (user)
|
|
|
|
|
const navigate = useNavigate ()
|
|
|
|
|
const { sessionId } = useParams ()
|
|
|
|
|
const editable = canEditContent (user)
|
|
|
|
|
const navigate = useNavigate ()
|
|
|
|
|
const { sessionId } = useParams ()
|
|
|
|
|
|
|
|
|
|
const [session, setSession] = useState<PostImportSession | null> (null)
|
|
|
|
|
const [missing, setMissing] = useState (false)
|
|
|
|
|
const [loadingRow, setLoadingRow] = useState<number | null> (null)
|
|
|
|
|
const [session, setSession] = useState<PostImportSession | null> (null)
|
|
|
|
|
const [missing, setMissing] = useState (false)
|
|
|
|
|
const [loadingRow, setLoadingRow] = useState<number | null> (null)
|
|
|
|
|
|
|
|
|
|
useEffect (() => {
|
|
|
|
|
if (sessionId == null)
|
|
|
|
|
return
|
|
|
|
|
useEffect (() => {
|
|
|
|
|
if (sessionId == null)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
const loaded = loadPostImportSession (sessionId, message =>
|
|
|
|
|
toast ({ title: '取込状態を復元できませんでした', description: message }))
|
|
|
|
|
setSession (loaded)
|
|
|
|
|
setMissing (loaded == null)
|
|
|
|
|
}, [sessionId])
|
|
|
|
|
const loaded = loadPostImportSession (sessionId, message =>
|
|
|
|
|
toast ({ title: '取込状態を復元できませんでした', description: message }))
|
|
|
|
|
setSession (loaded)
|
|
|
|
|
setMissing (loaded == null)
|
|
|
|
|
}, [sessionId])
|
|
|
|
|
|
|
|
|
|
useEffect (() => {
|
|
|
|
|
if (sessionId == null || session == null)
|
|
|
|
|
return
|
|
|
|
|
useEffect (() => {
|
|
|
|
|
if (sessionId == null || session == null)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
savePostImportSession (sessionId, session, message =>
|
|
|
|
|
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
|
|
|
|
}, [session, sessionId])
|
|
|
|
|
savePostImportSession (sessionId, session, message =>
|
|
|
|
|
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
|
|
|
|
}, [session, sessionId])
|
|
|
|
|
|
|
|
|
|
const counts = useMemo (
|
|
|
|
|
() => resultSummaryCounts (session?.rows ?? []),
|
|
|
|
|
[session])
|
|
|
|
|
const counts = useMemo (
|
|
|
|
|
() => resultSummaryCounts (session?.rows ?? []),
|
|
|
|
|
[session])
|
|
|
|
|
|
|
|
|
|
const retry = async (sourceRow: number) => {
|
|
|
|
|
if (session == null || sessionId == null)
|
|
|
|
|
return
|
|
|
|
|
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 => {
|
|
|
|
|
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 <Forbidden/>
|
|
|
|
|
|
|
|
|
|
if (missing || sessionId == null || session == null)
|
|
|
|
|
setLoadingRow (sourceRow)
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return (
|
|
|
|
|
<MainArea>
|
|
|
|
|
<div className="mx-auto max-w-4xl space-y-4 p-4">
|
|
|
|
|
<PageTitle>投稿インポート結果</PageTitle>
|
|
|
|
|
<FieldError messages={['取込状態が見つかりません.']}/>
|
|
|
|
|
<Button type="button" onClick={() => navigate ('/posts/import')}>
|
|
|
|
|
URL リスト入力へ戻る
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</MainArea>)
|
|
|
|
|
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 => {
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<MainArea>
|
|
|
|
|
<Helmet>
|
|
|
|
|
<title>{`投稿インポート結果 | ${ SITE_TITLE }`}</title>
|
|
|
|
|
</Helmet>
|
|
|
|
|
const openRepair = (sourceRow: number) => {
|
|
|
|
|
if (session == null || sessionId == null)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
<div className="mx-auto max-w-5xl space-y-4 p-4">
|
|
|
|
|
<PageTitle>登録結果</PageTitle>
|
|
|
|
|
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 }`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<div className="text-sm text-neutral-700 dark:text-neutral-200">
|
|
|
|
|
登録成功 {counts.created}件 スキップ {counts.skipped}件 失敗 {counts.failed}件
|
|
|
|
|
if (!(editable))
|
|
|
|
|
return <Forbidden/>
|
|
|
|
|
|
|
|
|
|
if (missing || sessionId == null || session == null)
|
|
|
|
|
{
|
|
|
|
|
return (
|
|
|
|
|
<MainArea>
|
|
|
|
|
<div className="mx-auto max-w-4xl space-y-4 p-4">
|
|
|
|
|
<PageTitle>投稿インポート結果</PageTitle>
|
|
|
|
|
<FieldError messages={['取込状態が見つかりません.']}/>
|
|
|
|
|
<Button type="button" onClick={() => navigate ('/posts/import')}>
|
|
|
|
|
URL リスト入力へ戻る
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</MainArea>)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{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 (
|
|
|
|
|
<div
|
|
|
|
|
key={row.sourceRow}
|
|
|
|
|
className="rounded-lg border p-4 transition-shadow hover:shadow-sm">
|
|
|
|
|
return (
|
|
|
|
|
<MainArea>
|
|
|
|
|
<Helmet>
|
|
|
|
|
<title>{`投稿インポート結果 | ${ SITE_TITLE }`}</title>
|
|
|
|
|
</Helmet>
|
|
|
|
|
|
|
|
|
|
<div className="mx-auto max-w-5xl space-y-4 p-4">
|
|
|
|
|
<PageTitle>登録結果</PageTitle>
|
|
|
|
|
|
|
|
|
|
<div className="text-sm text-neutral-700 dark:text-neutral-200">
|
|
|
|
|
登録成功 {counts.created}件 スキップ {counts.skipped}件 失敗 {counts.failed}件
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{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 (
|
|
|
|
|
<div
|
|
|
|
|
key={row.sourceRow}
|
|
|
|
|
className="rounded-lg border p-4 transition-shadow hover:shadow-sm">
|
|
|
|
|
<div className="flex flex-col gap-3 md:flex-row md:items-start
|
|
|
|
|
md:justify-between">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
@@ -248,14 +246,12 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|
|
|
|
編輯
|
|
|
|
|
</Button>)}
|
|
|
|
|
{canRetry && (
|
|
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => retry (row.sourceRow)}
|
|
|
|
|
disabled={loadingRow === row.sourceRow}>
|
|
|
|
|
再試行
|
|
|
|
|
</Button>
|
|
|
|
|
</>)}
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => retry (row.sourceRow)}
|
|
|
|
|
disabled={loadingRow === row.sourceRow}>
|
|
|
|
|
再試行
|
|
|
|
|
</Button>)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>)})}
|
|
|
|
|