このコミットが含まれているのは:
@@ -1,9 +1,13 @@
|
||||
import { useQueries } from '@tanstack/react-query'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
|
||||
import PostImportRowForm from '@/components/posts/import/PostImportRowForm'
|
||||
import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -11,39 +15,53 @@ import { toast } from '@/components/ui/use-toast'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiPost } from '@/lib/api'
|
||||
import useDialogue from '@/lib/dialogues/useDialogue'
|
||||
import { parsePostNewState, serialisePostNewState } from '@/lib/postNewQueryState'
|
||||
import { clearPostImportSourceDraft,
|
||||
buildNextEditedRow,
|
||||
canEditReviewRow,
|
||||
canRetryResultRow,
|
||||
hasExactSourceRows,
|
||||
initialisePreviewRows,
|
||||
isCompletedReviewRow,
|
||||
isExistingSkipRow,
|
||||
isNonRecoverableFailedRow,
|
||||
mergeImportResults,
|
||||
mergeValidatedImportRow,
|
||||
mergeValidatedImportRows,
|
||||
processableImportRows,
|
||||
replaceImportRow,
|
||||
resultRepairMode,
|
||||
resultRowMessages,
|
||||
reviewSummaryCounts,
|
||||
retryImportRow,
|
||||
validatableImportRows } from '@/lib/postImportSession'
|
||||
import { fetchPost } from '@/lib/posts'
|
||||
import {
|
||||
parsePostNewState,
|
||||
serialisePostNewState,
|
||||
} from '@/lib/postNewQueryState'
|
||||
import {
|
||||
buildNextEditedRow,
|
||||
canEditReviewRow,
|
||||
canRetryResultRow,
|
||||
clearPostImportSourceDraft,
|
||||
hasExactSourceRows,
|
||||
initialisePreviewRows,
|
||||
isCompletedReviewRow,
|
||||
isExistingSkipRow,
|
||||
isNonRecoverableFailedRow,
|
||||
mergeImportResults,
|
||||
mergeValidatedImportRow,
|
||||
mergeValidatedImportRows,
|
||||
processableImportRows,
|
||||
replaceImportRow,
|
||||
resultRepairMode,
|
||||
resultRowMessages,
|
||||
reviewSummaryCounts,
|
||||
retryImportRow,
|
||||
validatableImportRows,
|
||||
} from '@/lib/postImportSession'
|
||||
import { postsKeys } from '@/lib/queryKeys'
|
||||
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
|
||||
import { canEditContent } from '@/lib/users'
|
||||
import Forbidden from '@/pages/Forbidden'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { PostImportRowDraft } from '@/components/posts/import/PostImportRowForm'
|
||||
import type { PostImportResultRow,
|
||||
PostImportRow,
|
||||
PostImportSession } from '@/lib/postImportSession'
|
||||
import type { User } from '@/types'
|
||||
import type {
|
||||
PostImportResultRow,
|
||||
PostImportRow,
|
||||
PostImportSession,
|
||||
} from '@/lib/postImportSession'
|
||||
import type { Post, User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
type ExistingSkippedRowsProps = {
|
||||
open: boolean
|
||||
rows: PostImportRow[] }
|
||||
|
||||
const isRepairRow = (row: PostImportRow): boolean =>
|
||||
row.recoverable === true
|
||||
&& (row.importStatus === 'failed'
|
||||
@@ -51,11 +69,87 @@ const isRepairRow = (row: PostImportRow): boolean =>
|
||||
&& Object.keys (row.validationErrors).length > 0))
|
||||
|
||||
|
||||
const buildSession = (rows: PostImportRow[]): PostImportSession => ({
|
||||
version: 2,
|
||||
savedAt: new Date ().toISOString (),
|
||||
source: rows.map (row => row.url).join ('\n'),
|
||||
rows,
|
||||
repairMode: resultRepairMode (rows) })
|
||||
|
||||
|
||||
const ExistingSkippedRows: FC<ExistingSkippedRowsProps> = ({ open, rows }) => {
|
||||
const existingPostIds = useMemo (
|
||||
() =>
|
||||
open
|
||||
? Array.from (
|
||||
new Set (
|
||||
rows
|
||||
.map (row => row.existingPostId)
|
||||
.filter ((postId): postId is number => postId != null),
|
||||
),
|
||||
)
|
||||
: [],
|
||||
[open, rows],
|
||||
)
|
||||
const posts = useQueries ({
|
||||
queries: existingPostIds.map (postId => ({
|
||||
enabled: open,
|
||||
queryKey: postsKeys.show (String (postId)),
|
||||
queryFn: () => fetchPost (String (postId)) })),
|
||||
})
|
||||
const postsById = new Map<number, Post> (
|
||||
posts
|
||||
.map (query => query.data)
|
||||
.filter ((post): post is Post => post != null)
|
||||
.map (post => [post.id, post]),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="mt-3 max-h-72 overflow-y-auto rounded border">
|
||||
<div className="divide-y">
|
||||
{existingPostIds.map (postId => {
|
||||
const post = postsById.get (postId)
|
||||
if (post == null)
|
||||
return <div key={postId} className="h-14"/>
|
||||
|
||||
return (
|
||||
<PrefetchLink
|
||||
key={postId}
|
||||
to={`/posts/${ post.id }`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3">
|
||||
<PostThumbnailPreview
|
||||
url={post.thumbnail ?? ''}
|
||||
className="h-10 w-10 shrink-0"/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium">
|
||||
{post.title ?? ''}
|
||||
</div>
|
||||
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
||||
{post.url}
|
||||
</div>
|
||||
</div>
|
||||
</PrefetchLink>)
|
||||
})}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
const editable = canEditContent (user)
|
||||
const dialogue = useDialogue ()
|
||||
const location = useLocation ()
|
||||
const navigate = useNavigate ()
|
||||
const behaviourSettings = useClientBehaviourSettings ()
|
||||
const animationMode = behaviourSettings.animation ?? 'normal'
|
||||
const existingRowsTransition =
|
||||
animationMode === 'off'
|
||||
? { duration: 0 }
|
||||
: animationMode === 'reduced'
|
||||
? { duration: .08, ease: 'linear' as const }
|
||||
: { duration: .2, ease: 'easeOut' as const }
|
||||
|
||||
const [session, setSession] = useState<PostImportSession | null> (null)
|
||||
const [loading, setLoading] = useState (false)
|
||||
@@ -64,45 +158,72 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
const [showExistingRows, setShowExistingRows] = useState (false)
|
||||
const sessionRef = useRef<PostImportSession | null> (null)
|
||||
const persistedSearchRef = useRef<string | null> (null)
|
||||
const persistSequenceRef = useRef (0)
|
||||
|
||||
const persistSession = async (
|
||||
nextSession: PostImportSession,
|
||||
): Promise<PostImportSession | null> => {
|
||||
const sequence = ++persistSequenceRef.current
|
||||
const serialised = await serialisePostNewState (nextSession.rows)
|
||||
if (serialised == null)
|
||||
return null
|
||||
if (persistSequenceRef.current !== sequence)
|
||||
return sessionRef.current
|
||||
|
||||
const persistedSession = buildSession (serialised.rows)
|
||||
persistedSearchRef.current =
|
||||
(new URL (serialised.path, window.location.origin)).search
|
||||
sessionRef.current = persistedSession
|
||||
setSession (persistedSession)
|
||||
navigate (serialised.path, { replace: true })
|
||||
return persistedSession
|
||||
}
|
||||
|
||||
const finishImport = () => {
|
||||
clearPostImportSourceDraft (message =>
|
||||
toast ({ title: '入力内容を削除できませんでした', description: message }))
|
||||
navigate ('/posts')
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
if (sessionRef.current != null && persistedSearchRef.current === location.search)
|
||||
return
|
||||
let active = true
|
||||
|
||||
const parsed = parsePostNewState (location.search)
|
||||
if (parsed == null)
|
||||
{
|
||||
navigate ('/posts/new', { replace: true })
|
||||
return
|
||||
if (sessionRef.current != null && persistedSearchRef.current === location.search)
|
||||
return () => {
|
||||
}
|
||||
const loaded = {
|
||||
version: 2,
|
||||
savedAt: new Date ().toISOString (),
|
||||
source: parsed.source,
|
||||
rows: parsed.rows,
|
||||
repairMode: parsed.repairMode }
|
||||
persistedSearchRef.current = location.search
|
||||
sessionRef.current = loaded
|
||||
setSession (loaded)
|
||||
|
||||
const hydrate = async () => {
|
||||
const parsed = await parsePostNewState (location.search)
|
||||
if (!(active))
|
||||
return
|
||||
if (parsed == null)
|
||||
{
|
||||
navigate ('/posts/new', { replace: true })
|
||||
return
|
||||
}
|
||||
|
||||
const loaded = buildSession (parsed.rows)
|
||||
persistedSearchRef.current = location.search
|
||||
sessionRef.current = loaded
|
||||
setSession (loaded)
|
||||
|
||||
if (parsed.shortcut)
|
||||
{
|
||||
try
|
||||
{
|
||||
const preview = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/preview', {
|
||||
source: parsed.source })
|
||||
if (!(active))
|
||||
return
|
||||
const rows = initialisePreviewRows (preview.rows)
|
||||
const nextSession = {
|
||||
...loaded,
|
||||
source: parsed.source,
|
||||
rows,
|
||||
repairMode: resultRepairMode (rows) }
|
||||
persistSession (nextSession)
|
||||
const persisted = await persistSession (buildSession (rows))
|
||||
if (persisted == null)
|
||||
return
|
||||
}
|
||||
catch
|
||||
{
|
||||
navigate ('/posts/new', { replace: true })
|
||||
if (active)
|
||||
navigate ('/posts/new', { replace: true })
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -122,17 +243,18 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
tagSources: row.tagSources,
|
||||
metadataUrl: row.metadataUrl })),
|
||||
changed_row: 'all' })
|
||||
if (!(active))
|
||||
return
|
||||
|
||||
const validatedRows = initialisePreviewRows (validated.rows)
|
||||
if (!(hasExactSourceRows (requestedRows.map (row => row.sourceRow), validatedRows)))
|
||||
return
|
||||
|
||||
const latest = sessionRef.current ?? loaded
|
||||
const rows = mergeValidatedImportRows (latest.rows, validatedRows)
|
||||
const nextSession = {
|
||||
...latest,
|
||||
rows,
|
||||
repairMode: resultRepairMode (rows) }
|
||||
sessionRef.current = nextSession
|
||||
setSession (nextSession)
|
||||
const persisted = await persistSession (buildSession (rows))
|
||||
if (persisted == null)
|
||||
return
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -140,12 +262,32 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
}
|
||||
|
||||
void hydrate ()
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [location.search, navigate])
|
||||
|
||||
useEffect (() => {
|
||||
sessionRef.current = session
|
||||
if (session != null)
|
||||
sessionRef.current = session
|
||||
}, [session])
|
||||
|
||||
useEffect (() => {
|
||||
if (session == null || session.rows.length === 0)
|
||||
return
|
||||
if (session.rows.every (row => isCompletedReviewRow (row)))
|
||||
finishImport ()
|
||||
}, [session])
|
||||
|
||||
useEffect (() => {
|
||||
if (editingRow == null || session?.repairMode !== 'failed')
|
||||
return
|
||||
|
||||
const element = document.getElementById (`post-import-row-${ editingRow.sourceRow }`)
|
||||
element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
|
||||
}, [editingRow, session?.repairMode])
|
||||
|
||||
const rows = session?.rows ?? []
|
||||
const counts = useMemo (() => reviewSummaryCounts (rows), [rows])
|
||||
const sortedRows =
|
||||
@@ -161,25 +303,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
const reviewRows = sortedRows.filter (row => !(isExistingSkipRow (row)))
|
||||
const busy = loading || loadingRow != null
|
||||
|
||||
const persistSession = (nextSession: PostImportSession) => {
|
||||
const source = nextSession.rows.map (row => row.url).join ('\n')
|
||||
const persistedSession = {
|
||||
...nextSession,
|
||||
source }
|
||||
const nextPath = serialisePostNewState (persistedSession.rows)
|
||||
persistedSearchRef.current = nextPath.replace ('/posts/new', '')
|
||||
sessionRef.current = persistedSession
|
||||
setSession (persistedSession)
|
||||
navigate (nextPath, { replace: true })
|
||||
}
|
||||
|
||||
const finishImport = () => {
|
||||
clearPostImportSourceDraft (message =>
|
||||
toast ({ title: '入力内容を削除できませんでした', description: message }))
|
||||
navigate ('/posts')
|
||||
}
|
||||
|
||||
const toggleManualSkip = (sourceRow: number, checked: boolean) => {
|
||||
const toggleManualSkip = async (sourceRow: number, checked: boolean) => {
|
||||
if (busy)
|
||||
return
|
||||
|
||||
@@ -189,33 +313,19 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
|
||||
const nextRows = currentSession.rows.map (row => {
|
||||
if (row.sourceRow !== sourceRow)
|
||||
return row
|
||||
return row
|
||||
if (isExistingSkipRow (row)
|
||||
|| row.importStatus === 'created'
|
||||
|| isNonRecoverableFailedRow (row))
|
||||
return row
|
||||
return row
|
||||
return {
|
||||
...row,
|
||||
skipReason: checked ? 'manual' : undefined,
|
||||
existingPostId: checked ? undefined : row.existingPostId }
|
||||
})
|
||||
const nextSession = {
|
||||
...currentSession,
|
||||
rows: nextRows,
|
||||
repairMode: resultRepairMode (nextRows) }
|
||||
persistSession (nextSession)
|
||||
if (nextRows.every (row => isCompletedReviewRow (row)))
|
||||
finishImport ()
|
||||
await persistSession (buildSession (nextRows))
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
if (editingRow == null || session?.repairMode !== 'failed')
|
||||
return
|
||||
|
||||
const element = document.getElementById (`post-import-row-${ editingRow.sourceRow }`)
|
||||
element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
|
||||
}, [editingRow, session?.repairMode])
|
||||
|
||||
const saveDraft = async (
|
||||
row: PostImportRow,
|
||||
{ draft, resetRequested }: {
|
||||
@@ -270,22 +380,18 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
if (target == null)
|
||||
{
|
||||
const restoredRows = replaceImportRow (latestSession.rows, row)
|
||||
persistSession ({
|
||||
...latestSession,
|
||||
rows: restoredRows,
|
||||
repairMode: resultRepairMode (restoredRows) })
|
||||
await persistSession (buildSession (restoredRows))
|
||||
toast ({ title: '行の再検証結果が不完全でした' })
|
||||
return { saved: false, row: null }
|
||||
}
|
||||
|
||||
const editedRows = replaceImportRow (latestSession.rows, nextRow)
|
||||
const mergedRows = mergeValidatedImportRow (editedRows, target)
|
||||
const nextSession = {
|
||||
...latestSession,
|
||||
rows: mergedRows,
|
||||
repairMode: resultRepairMode (mergedRows) }
|
||||
persistSession (nextSession)
|
||||
const nextSession = await persistSession (buildSession (mergedRows))
|
||||
if (nextSession == null)
|
||||
return { saved: false, row: null }
|
||||
const mergedTarget =
|
||||
mergedRows.find (mergedRow => mergedRow.sourceRow === baseRow.sourceRow)
|
||||
nextSession.rows.find (mergedRow => mergedRow.sourceRow === baseRow.sourceRow)
|
||||
?? target
|
||||
if (Object.keys (mergedTarget.validationErrors).length > 0)
|
||||
return { saved: false, row: mergedTarget }
|
||||
@@ -351,9 +457,11 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
try
|
||||
{
|
||||
const pendingRows = retryImportRow (initialSession.rows, sourceRow)
|
||||
const pendingSession = { ...initialSession, rows: pendingRows }
|
||||
persistSession (pendingSession)
|
||||
const requestedRows = validatableImportRows (pendingRows)
|
||||
const pendingSession = await persistSession (buildSession (pendingRows))
|
||||
if (pendingSession == null)
|
||||
return
|
||||
|
||||
const requestedRows = validatableImportRows (pendingSession.rows)
|
||||
const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', {
|
||||
rows: requestedRows.map (row => ({
|
||||
sourceRow: row.sourceRow,
|
||||
@@ -368,48 +476,39 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
{
|
||||
const latest = sessionRef.current ?? pendingSession
|
||||
const restoredRows = replaceImportRow (latest.rows, originalRow)
|
||||
persistSession ({
|
||||
...latest,
|
||||
rows: restoredRows,
|
||||
repairMode: resultRepairMode (restoredRows) })
|
||||
await persistSession (buildSession (restoredRows))
|
||||
toast ({ title: '再検証結果が不完全でした' })
|
||||
return
|
||||
}
|
||||
|
||||
const validatedTarget = validatedRows.find (row => row.sourceRow === sourceRow)
|
||||
if (validatedTarget == null)
|
||||
{
|
||||
const latest = sessionRef.current ?? pendingSession
|
||||
const restoredRows = replaceImportRow (latest.rows, originalRow)
|
||||
persistSession ({
|
||||
...latest,
|
||||
rows: restoredRows,
|
||||
repairMode: resultRepairMode (restoredRows) })
|
||||
await persistSession (buildSession (restoredRows))
|
||||
toast ({ title: '再検証結果が不完全でした' })
|
||||
return
|
||||
}
|
||||
|
||||
const latestAfterValidate = sessionRef.current ?? pendingSession
|
||||
const mergedValidatedRows = mergeValidatedImportRow (
|
||||
latestAfterValidate.rows,
|
||||
validatedTarget)
|
||||
const nextSession = {
|
||||
...latestAfterValidate,
|
||||
rows: mergedValidatedRows,
|
||||
repairMode: resultRepairMode (mergedValidatedRows) }
|
||||
persistSession (nextSession)
|
||||
const target = mergedValidatedRows.find (row => row.sourceRow === sourceRow)
|
||||
const nextSession = await persistSession (buildSession (mergedValidatedRows))
|
||||
if (nextSession == null)
|
||||
return
|
||||
const target = nextSession.rows.find (row => row.sourceRow === sourceRow)
|
||||
if (target == null)
|
||||
{
|
||||
const restoredRows = replaceImportRow (latestAfterValidate.rows, originalRow)
|
||||
persistSession ({
|
||||
...latestAfterValidate,
|
||||
rows: restoredRows,
|
||||
repairMode: resultRepairMode (restoredRows) })
|
||||
await persistSession (buildSession (restoredRows))
|
||||
toast ({ title: '再検証結果が不完全でした' })
|
||||
return
|
||||
}
|
||||
if (Object.keys (target.validationErrors ?? { }).length > 0)
|
||||
{
|
||||
void editRow (target)
|
||||
editRow (target)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -429,13 +528,11 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
{
|
||||
const latest = sessionRef.current ?? nextSession
|
||||
const restoredRows = replaceImportRow (latest.rows, originalRow)
|
||||
persistSession ({
|
||||
...latest,
|
||||
rows: restoredRows,
|
||||
repairMode: resultRepairMode (restoredRows) })
|
||||
await persistSession (buildSession (restoredRows))
|
||||
toast ({ title: '登録結果が不完全でした' })
|
||||
return
|
||||
}
|
||||
|
||||
const latestAfterImport = sessionRef.current ?? nextSession
|
||||
const mergedRows = mergeImportResults (latestAfterImport.rows, result.rows)
|
||||
const recoverableRows = result.rows.filter (row =>
|
||||
@@ -454,25 +551,13 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
validationErrors: recoverable.errors ?? { },
|
||||
importErrors: undefined }
|
||||
})
|
||||
const resultSession = {
|
||||
...latestAfterImport,
|
||||
rows: nextRows,
|
||||
repairMode: resultRepairMode (nextRows) }
|
||||
persistSession (resultSession)
|
||||
if (nextRows.every (row => isCompletedReviewRow (row)))
|
||||
{
|
||||
finishImport ()
|
||||
return
|
||||
}
|
||||
await persistSession (buildSession (nextRows))
|
||||
}
|
||||
catch
|
||||
{
|
||||
const latest = sessionRef.current ?? initialSession
|
||||
const restoredRows = replaceImportRow (latest.rows, originalRow)
|
||||
persistSession ({
|
||||
...latest,
|
||||
rows: restoredRows,
|
||||
repairMode: resultRepairMode (restoredRows) })
|
||||
await persistSession (buildSession (restoredRows))
|
||||
toast ({ title: '再試行に失敗しました' })
|
||||
}
|
||||
finally
|
||||
@@ -485,7 +570,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
const currentSession = sessionRef.current
|
||||
if (currentSession == null || busy)
|
||||
return
|
||||
if (rows.every (row => isCompletedReviewRow (row)))
|
||||
if (currentSession.rows.every (row => isCompletedReviewRow (row)))
|
||||
{
|
||||
finishImport ()
|
||||
return
|
||||
@@ -517,50 +602,49 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
toast ({ title: '再検証結果が不完全でした' })
|
||||
return
|
||||
}
|
||||
|
||||
const latestAfterValidate = sessionRef.current ?? currentSession
|
||||
const mergedRows = mergeValidatedImportRows (latestAfterValidate.rows, validatedRows)
|
||||
const firstInvalid = mergedRows.find (row => Object.keys (row.validationErrors).length > 0)
|
||||
const persistedValidated = await persistSession (buildSession (mergedRows))
|
||||
if (persistedValidated == null)
|
||||
return
|
||||
const currentRows = persistedValidated.rows
|
||||
const firstInvalid = currentRows.find (
|
||||
row => Object.keys (row.validationErrors).length > 0)
|
||||
if (firstInvalid != null)
|
||||
{
|
||||
persistSession ({
|
||||
...latestAfterValidate,
|
||||
rows: mergedRows,
|
||||
repairMode: resultRepairMode (mergedRows) })
|
||||
void editRow (firstInvalid)
|
||||
editRow (firstInvalid)
|
||||
return
|
||||
}
|
||||
const validatedSession = {
|
||||
...latestAfterValidate,
|
||||
rows: mergedRows,
|
||||
repairMode: resultRepairMode (mergedRows) }
|
||||
persistSession (validatedSession)
|
||||
|
||||
const result = await apiPost<{
|
||||
created: number
|
||||
skipped: number
|
||||
failed: number
|
||||
rows: PostImportResultRow[] }> ('/posts/import', {
|
||||
rows: processableImportRows (mergedRows).map (row => ({
|
||||
rows: processableImportRows (currentRows).map (row => ({
|
||||
sourceRow: row.sourceRow,
|
||||
url: row.url,
|
||||
attributes: row.attributes,
|
||||
provenance: row.provenance,
|
||||
tagSources: row.tagSources,
|
||||
metadataUrl: row.metadataUrl })) })
|
||||
const expectedImportRows = processableImportRows (mergedRows).map (row => row.sourceRow)
|
||||
const expectedImportRows = processableImportRows (currentRows).map (row => row.sourceRow)
|
||||
if (!(hasExactSourceRows (expectedImportRows, result.rows)))
|
||||
{
|
||||
toast ({ title: '登録結果が不完全でした' })
|
||||
return
|
||||
}
|
||||
const latestAfterImport = sessionRef.current ?? validatedSession
|
||||
|
||||
const latestAfterImport = sessionRef.current ?? buildSession (currentRows)
|
||||
const mergedResults = mergeImportResults (latestAfterImport.rows, result.rows)
|
||||
const recoverableRows = result.rows.filter (row =>
|
||||
row.status === 'failed'
|
||||
&& row.recoverable
|
||||
&& Object.keys (row.errors ?? { }).length > 0)
|
||||
const nextRows = mergedResults.map ((row): PostImportRow => {
|
||||
const recoverable = recoverableRows.find (failedRow => failedRow.sourceRow === row.sourceRow)
|
||||
const recoverable = recoverableRows.find (
|
||||
failedRow => failedRow.sourceRow === row.sourceRow)
|
||||
if (recoverable == null)
|
||||
return row
|
||||
return {
|
||||
@@ -570,15 +654,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
validationErrors: recoverable.errors ?? { },
|
||||
importErrors: undefined }
|
||||
})
|
||||
const nextSession = {
|
||||
...latestAfterImport,
|
||||
rows: nextRows,
|
||||
repairMode: resultRepairMode (nextRows) }
|
||||
persistSession (nextSession)
|
||||
if (nextRows.every (row => isCompletedReviewRow (row)))
|
||||
{
|
||||
finishImport ()
|
||||
}
|
||||
await persistSession (buildSession (nextRows))
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -614,25 +690,30 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
onClick={() => setShowExistingRows (current => !(current))}>
|
||||
既存投稿による自動スキップ {counts.existingSkipped}件
|
||||
</button>
|
||||
{showExistingRows && (
|
||||
<div className="mt-3 space-y-3">
|
||||
{existingRows.map (row => (
|
||||
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
|
||||
<PostImportRowSummary
|
||||
row={row}
|
||||
showActions={false}
|
||||
showSkipToggle={false}
|
||||
rowMessages={resultRowMessages (row)}/>
|
||||
</div>))}
|
||||
</div>)}
|
||||
<AnimatePresence initial={false}>
|
||||
{showExistingRows && (
|
||||
<motion.div
|
||||
initial={
|
||||
animationMode === 'off'
|
||||
? false
|
||||
: { height: 0, opacity: 0 }
|
||||
}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={existingRowsTransition}
|
||||
className="overflow-hidden">
|
||||
<ExistingSkippedRows open={showExistingRows} rows={existingRows}/>
|
||||
</motion.div>)}
|
||||
</AnimatePresence>
|
||||
</div>)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{reviewRows.map (row => (
|
||||
{reviewRows.map ((row, index) => (
|
||||
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
|
||||
<PostImportRowSummary
|
||||
row={row}
|
||||
showSkipToggle={!(isExistingSkipRow (row))}
|
||||
displayNumber={index + 1}
|
||||
showSkipToggle={true}
|
||||
editDisabled={busy}
|
||||
retryDisabled={busy}
|
||||
skipDisabled={
|
||||
@@ -640,9 +721,9 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
|| isExistingSkipRow (row)
|
||||
|| row.importStatus === 'created'
|
||||
|| isNonRecoverableFailedRow (row)}
|
||||
onEdit={() => void editRow (row)}
|
||||
onEdit={() => editRow (row)}
|
||||
onToggleSkip={checked => toggleManualSkip (row.sourceRow, checked)}
|
||||
onRetry={canRetryResultRow (row) ? () => void retry (row.sourceRow) : undefined}
|
||||
onRetry={canRetryResultRow (row) ? () => retry (row.sourceRow) : undefined}
|
||||
rowMessages={resultRowMessages (row)}/>
|
||||
</div>))}
|
||||
</div>
|
||||
@@ -656,7 +737,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
existingSkippedCount={counts.existingSkipped}
|
||||
pendingOrErrorCount={counts.pendingOrError}
|
||||
onBack={() => navigate (-1)}
|
||||
onSubmit={submit}/>
|
||||
onSubmit={() => submit ()}/>
|
||||
</>)
|
||||
}
|
||||
|
||||
@@ -667,13 +748,13 @@ const PostImportFooter = (
|
||||
existingSkippedCount,
|
||||
pendingOrErrorCount,
|
||||
onBack,
|
||||
onSubmit }: { loading: boolean
|
||||
creatableCount: number
|
||||
manualSkippedCount: number
|
||||
onSubmit }: { loading: boolean
|
||||
creatableCount: number
|
||||
manualSkippedCount: number
|
||||
existingSkippedCount: number
|
||||
pendingOrErrorCount: number
|
||||
onBack: () => void
|
||||
onSubmit: () => void },
|
||||
pendingOrErrorCount: number
|
||||
onBack: () => void
|
||||
onSubmit: () => void },
|
||||
) => (
|
||||
<div
|
||||
className="shrink-0 border-t bg-white/95 p-4 backdrop-blur
|
||||
|
||||
@@ -122,7 +122,9 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
return
|
||||
}
|
||||
const nextRows = initialisePreviewRows (data.rows)
|
||||
navigate (serialisePostNewState (nextRows))
|
||||
const serialised = await serialisePostNewState (nextRows)
|
||||
if (serialised != null)
|
||||
navigate (serialised.path)
|
||||
}
|
||||
catch (requestError)
|
||||
{
|
||||
|
||||
新しい課題から参照
ユーザをブロックする