|
|
|
@@ -76,6 +76,7 @@ type PostMetadataResponse = {
|
|
|
|
|
fieldWarnings?: Record<string, string[]>
|
|
|
|
|
baseWarnings?: string[]
|
|
|
|
|
validationErrors?: Record<string, string[]>
|
|
|
|
|
existingPostId?: number
|
|
|
|
|
existingPost?: {
|
|
|
|
|
id: number
|
|
|
|
|
title: string
|
|
|
|
@@ -85,6 +86,7 @@ type PostMetadataResponse = {
|
|
|
|
|
type BulkApiRow = {
|
|
|
|
|
status: 'created' | 'skipped' | 'failed'
|
|
|
|
|
post?: { id: number }
|
|
|
|
|
existingPostId?: number
|
|
|
|
|
existingPost?: {
|
|
|
|
|
id: number
|
|
|
|
|
title: string
|
|
|
|
@@ -114,20 +116,20 @@ const rowMetadataPending = (row: PostImportRow): boolean =>
|
|
|
|
|
const rowOperationBusy = (
|
|
|
|
|
row: PostImportRow,
|
|
|
|
|
submitting: boolean,
|
|
|
|
|
loadingRow: number | null,
|
|
|
|
|
busyRowIds: Set<number>,
|
|
|
|
|
): boolean =>
|
|
|
|
|
submitting
|
|
|
|
|
|| loadingRow === row.sourceRow
|
|
|
|
|
|| busyRowIds.has (row.sourceRow)
|
|
|
|
|
|| rowMetadataPending (row)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const rowSkipBusy = (
|
|
|
|
|
row: PostImportRow,
|
|
|
|
|
submitting: boolean,
|
|
|
|
|
loadingRow: number | null,
|
|
|
|
|
busyRowIds: Set<number>,
|
|
|
|
|
): boolean =>
|
|
|
|
|
submitting
|
|
|
|
|
|| loadingRow === row.sourceRow
|
|
|
|
|
|| busyRowIds.has (row.sourceRow)
|
|
|
|
|
|| (rowMetadataPending (row) && !(isManualSkipRow (row)))
|
|
|
|
|
|
|
|
|
|
const shouldFetchMetadata = (row: PostImportRow): boolean =>
|
|
|
|
@@ -242,8 +244,11 @@ const mergeDryRunRow = (
|
|
|
|
|
hasWarnings
|
|
|
|
|
? 'warning'
|
|
|
|
|
: 'ready',
|
|
|
|
|
skipReason: result.existingPost?.id != null ? 'existing' : undefined,
|
|
|
|
|
existingPostId: result.existingPost?.id,
|
|
|
|
|
skipReason:
|
|
|
|
|
result.existingPostId != null || result.existingPost?.id != null
|
|
|
|
|
? 'existing'
|
|
|
|
|
: undefined,
|
|
|
|
|
existingPostId: result.existingPostId ?? result.existingPost?.id,
|
|
|
|
|
existingPost: result.existingPost ?? undefined,
|
|
|
|
|
importStatus:
|
|
|
|
|
currentRow.importStatus === 'created'
|
|
|
|
@@ -306,12 +311,16 @@ const indexedBulkResults = (
|
|
|
|
|
baseWarnings: result.baseWarnings,
|
|
|
|
|
errors }
|
|
|
|
|
}
|
|
|
|
|
if (result?.status === 'skipped' && result.existingPost != null)
|
|
|
|
|
if (result?.status === 'skipped')
|
|
|
|
|
{
|
|
|
|
|
return {
|
|
|
|
|
sourceRow: row.sourceRow,
|
|
|
|
|
status: 'skipped',
|
|
|
|
|
existingPostId: result.existingPost.id,
|
|
|
|
|
existingPostId:
|
|
|
|
|
result.existingPostId
|
|
|
|
|
?? result.existingPost?.id
|
|
|
|
|
?? row.existingPostId
|
|
|
|
|
?? row.sourceRow,
|
|
|
|
|
existingPost: result.existingPost,
|
|
|
|
|
fieldWarnings: result.fieldWarnings,
|
|
|
|
|
baseWarnings: result.baseWarnings,
|
|
|
|
@@ -350,10 +359,10 @@ const mergePreviewRow = (
|
|
|
|
|
fieldWarnings,
|
|
|
|
|
baseWarnings,
|
|
|
|
|
validationErrors,
|
|
|
|
|
existingPostId: preview.existingPost?.id,
|
|
|
|
|
existingPostId: preview.existingPostId ?? preview.existingPost?.id,
|
|
|
|
|
existingPost: preview.existingPost ?? undefined,
|
|
|
|
|
skipReason:
|
|
|
|
|
preview.existingPost?.id != null
|
|
|
|
|
preview.existingPostId != null || preview.existingPost?.id != null
|
|
|
|
|
? 'existing'
|
|
|
|
|
: currentRow.skipReason === 'manual'
|
|
|
|
|
? 'manual'
|
|
|
|
@@ -468,7 +477,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
const [session, setSession] = useState<PostImportSession | null> (null)
|
|
|
|
|
const [metadataLoading, setMetadataLoading] = useState (false)
|
|
|
|
|
const [submitting, setSubmitting] = useState (false)
|
|
|
|
|
const [loadingRow, setLoadingRow] = useState<number | null> (null)
|
|
|
|
|
const [busyRowIds, setBusyRowIds] = useState<Set<number>> (new Set ())
|
|
|
|
|
const [editingRow, setEditingRow] = useState<PostImportRow | null> (null)
|
|
|
|
|
const [showExistingRows, setShowExistingRows] = useState (false)
|
|
|
|
|
const sessionRef = useRef<PostImportSession | null> (null)
|
|
|
|
@@ -476,18 +485,31 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
const persistedSearchRef = useRef<string | null> (null)
|
|
|
|
|
const persistSequenceRef = useRef (0)
|
|
|
|
|
const previewSequenceRef = useRef (0)
|
|
|
|
|
const metadataSequenceRef = useRef (0)
|
|
|
|
|
|
|
|
|
|
const commitSessionState = (
|
|
|
|
|
nextSession: PostImportSession,
|
|
|
|
|
): PostImportSession => {
|
|
|
|
|
): {
|
|
|
|
|
session: PostImportSession
|
|
|
|
|
sessionRoundTripSucceeded: boolean } => {
|
|
|
|
|
const sessionId =
|
|
|
|
|
sessionIdRef.current ?? generatePostImportSessionId ()
|
|
|
|
|
sessionIdRef.current = sessionId
|
|
|
|
|
const persistedSession = buildSession (nextSession.rows)
|
|
|
|
|
savePostImportSession (sessionId, persistedSession)
|
|
|
|
|
const saved = savePostImportSession (sessionId, persistedSession, showStorageError)
|
|
|
|
|
const loadedSession =
|
|
|
|
|
saved
|
|
|
|
|
? loadPostImportSession (sessionId, showStorageError)
|
|
|
|
|
: null
|
|
|
|
|
sessionRef.current = persistedSession
|
|
|
|
|
setSession (persistedSession)
|
|
|
|
|
return persistedSession
|
|
|
|
|
return {
|
|
|
|
|
session: persistedSession,
|
|
|
|
|
sessionRoundTripSucceeded:
|
|
|
|
|
saved
|
|
|
|
|
&& serialisedPostImportRowsEqual (
|
|
|
|
|
loadedSession?.rows ?? [],
|
|
|
|
|
persistedSession.rows) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const syncSessionUrl = async (
|
|
|
|
@@ -503,7 +525,16 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
if (persistSequenceRef.current !== sequence)
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
const persistedSession = commitSessionState (nextSession)
|
|
|
|
|
const { session: persistedSession,
|
|
|
|
|
sessionRoundTripSucceeded } = commitSessionState (nextSession)
|
|
|
|
|
const canNavigate =
|
|
|
|
|
serialised.complete
|
|
|
|
|
|| sessionRoundTripSucceeded
|
|
|
|
|
if (!(canNavigate))
|
|
|
|
|
{
|
|
|
|
|
showStorageError ('ブラウザへ保存できませんでした.')
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const path = appendPostNewSessionId (serialised.path, sessionId)
|
|
|
|
|
persistedSearchRef.current = (new URL (path, window.location.origin)).search
|
|
|
|
@@ -530,6 +561,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
useEffect (() => {
|
|
|
|
|
let active = true
|
|
|
|
|
const previewSequence = ++previewSequenceRef.current
|
|
|
|
|
metadataSequenceRef.current = previewSequence
|
|
|
|
|
const controllers = new Set<AbortController> ()
|
|
|
|
|
|
|
|
|
|
if (sessionRef.current != null
|
|
|
|
@@ -637,7 +669,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
if (active && previewSequenceRef.current === previewSequence)
|
|
|
|
|
if (active && metadataSequenceRef.current === previewSequence)
|
|
|
|
|
setMetadataLoading (false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -651,6 +683,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
return
|
|
|
|
|
if (parsed == null && loadedSession == null)
|
|
|
|
|
{
|
|
|
|
|
setMetadataLoading (false)
|
|
|
|
|
navigate ('/posts/new', { replace: true })
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
@@ -709,7 +742,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
const currentRow = currentRowBySource (sourceRow, currentSession)
|
|
|
|
|
if (currentRow == null)
|
|
|
|
|
return
|
|
|
|
|
if (rowSkipBusy (currentRow, submitting, loadingRow))
|
|
|
|
|
if (rowSkipBusy (currentRow, submitting, busyRowIds))
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
const nextRows = currentSession.rows.map (row => {
|
|
|
|
@@ -720,9 +753,12 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
return row
|
|
|
|
|
return {
|
|
|
|
|
...row,
|
|
|
|
|
skipReason: checked ? 'manual' : undefined,
|
|
|
|
|
existingPostId: checked ? undefined : row.existingPostId,
|
|
|
|
|
existingPost: checked ? undefined : row.existingPost }
|
|
|
|
|
skipReason:
|
|
|
|
|
checked
|
|
|
|
|
? 'manual'
|
|
|
|
|
: row.existingPostId != null
|
|
|
|
|
? 'existing'
|
|
|
|
|
: undefined }
|
|
|
|
|
})
|
|
|
|
|
const nextSession = buildSession (applyDuplicateUrlErrors (nextRows))
|
|
|
|
|
if (metadataLoading)
|
|
|
|
@@ -787,7 +823,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
replaceImportRow (latestSession.rows, mergedRow))
|
|
|
|
|
const nextSession =
|
|
|
|
|
metadataLoading
|
|
|
|
|
? commitSessionState (buildSession (mergedRows))
|
|
|
|
|
? commitSessionState (buildSession (mergedRows)).session
|
|
|
|
|
: await syncSessionUrl (buildSession (mergedRows))
|
|
|
|
|
if (nextSession == null)
|
|
|
|
|
return { saved: false, row: null }
|
|
|
|
@@ -852,7 +888,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
return
|
|
|
|
|
if (!(canEditReviewRow (currentRow)))
|
|
|
|
|
return
|
|
|
|
|
if (rowOperationBusy (currentRow, submitting, loadingRow))
|
|
|
|
|
if (rowOperationBusy (currentRow, submitting, busyRowIds))
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
setEditingRow (currentRow)
|
|
|
|
@@ -877,16 +913,16 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
const originalRow = initialSession.rows.find (row => row.sourceRow === sourceRow)
|
|
|
|
|
if (originalRow == null)
|
|
|
|
|
return
|
|
|
|
|
if (rowOperationBusy (originalRow, submitting, loadingRow))
|
|
|
|
|
if (rowOperationBusy (originalRow, submitting, busyRowIds))
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
setLoadingRow (sourceRow)
|
|
|
|
|
setBusyRowIds (current => new Set ([...current, sourceRow]))
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
const pendingRows = retryImportRow (initialSession.rows, sourceRow)
|
|
|
|
|
const pendingSession =
|
|
|
|
|
metadataLoading
|
|
|
|
|
? commitSessionState (buildSession (pendingRows))
|
|
|
|
|
? commitSessionState (buildSession (pendingRows)).session
|
|
|
|
|
: await syncSessionUrl (buildSession (pendingRows))
|
|
|
|
|
if (pendingSession == null)
|
|
|
|
|
return
|
|
|
|
@@ -915,7 +951,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
const nextRows = applyDuplicateUrlErrors (
|
|
|
|
|
mergeImportResults (latestAfterImport.rows, indexedResults))
|
|
|
|
|
.map (row => applyThumbnailWarning (row))
|
|
|
|
|
const persisted = await syncSessionUrl (buildSession (nextRows))
|
|
|
|
|
const persisted =
|
|
|
|
|
metadataLoading
|
|
|
|
|
? commitSessionState (buildSession (nextRows)).session
|
|
|
|
|
: await syncSessionUrl (buildSession (nextRows))
|
|
|
|
|
if (persisted != null
|
|
|
|
|
&& persisted.rows.every (row => isCompletedReviewRow (row)))
|
|
|
|
|
finishImport ()
|
|
|
|
@@ -932,13 +971,20 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
setLoadingRow (null)
|
|
|
|
|
setBusyRowIds (current => {
|
|
|
|
|
const next = new Set (current)
|
|
|
|
|
next.delete (sourceRow)
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const submit = async () => {
|
|
|
|
|
const currentSession = sessionRef.current
|
|
|
|
|
if (currentSession == null || metadataLoading || submitting || loadingRow != null)
|
|
|
|
|
if (currentSession == null
|
|
|
|
|
|| metadataLoading
|
|
|
|
|
|| submitting
|
|
|
|
|
|| busyRowIds.size > 0)
|
|
|
|
|
return
|
|
|
|
|
if (currentSession.rows.every (row => isCompletedReviewRow (row)))
|
|
|
|
|
{
|
|
|
|
@@ -1043,10 +1089,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
row={row}
|
|
|
|
|
displayNumber={index + 1}
|
|
|
|
|
showSkipToggle={true}
|
|
|
|
|
editDisabled={rowOperationBusy (row, submitting, loadingRow)}
|
|
|
|
|
retryDisabled={rowOperationBusy (row, submitting, loadingRow)}
|
|
|
|
|
editDisabled={rowOperationBusy (row, submitting, busyRowIds)}
|
|
|
|
|
retryDisabled={rowOperationBusy (row, submitting, busyRowIds)}
|
|
|
|
|
skipDisabled={
|
|
|
|
|
rowSkipBusy (row, submitting, loadingRow)
|
|
|
|
|
rowSkipBusy (row, submitting, busyRowIds)
|
|
|
|
|
|| isExistingSkipRow (row)
|
|
|
|
|
|| row.importStatus === 'created'}
|
|
|
|
|
onEdit={() => editRow (row)}
|
|
|
|
@@ -1061,6 +1107,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
<PostImportFooter
|
|
|
|
|
metadataLoading={metadataLoading}
|
|
|
|
|
submitting={submitting}
|
|
|
|
|
busyRowsPresent={busyRowIds.size > 0}
|
|
|
|
|
canSubmit={canSubmit}
|
|
|
|
|
creatableCount={counts.creatable}
|
|
|
|
|
manualSkippedCount={counts.manualSkipped}
|
|
|
|
@@ -1074,6 +1121,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|
|
|
|
const PostImportFooter = (
|
|
|
|
|
{ metadataLoading,
|
|
|
|
|
submitting,
|
|
|
|
|
busyRowsPresent,
|
|
|
|
|
canSubmit,
|
|
|
|
|
creatableCount,
|
|
|
|
|
manualSkippedCount,
|
|
|
|
@@ -1082,6 +1130,7 @@ const PostImportFooter = (
|
|
|
|
|
onBack,
|
|
|
|
|
onSubmit }: { metadataLoading: boolean
|
|
|
|
|
submitting: boolean
|
|
|
|
|
busyRowsPresent: boolean
|
|
|
|
|
canSubmit: boolean
|
|
|
|
|
creatableCount: number
|
|
|
|
|
manualSkippedCount: number
|
|
|
|
@@ -1101,18 +1150,20 @@ const PostImportFooter = (
|
|
|
|
|
<span>既存投稿による自動スキップ {existingSkippedCount}件</span>
|
|
|
|
|
<span>登録不可/未処理 {pendingOrErrorCount}件</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col gap-2 sm:flex-row">
|
|
|
|
|
<div className="flex flex-col gap-2 md:flex-row">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="w-full md:w-auto"
|
|
|
|
|
onClick={onBack}
|
|
|
|
|
disabled={submitting}>
|
|
|
|
|
URL リスト入力へ戻る
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
className="w-full md:w-auto"
|
|
|
|
|
onClick={onSubmit}
|
|
|
|
|
disabled={metadataLoading || submitting || !(canSubmit)}>
|
|
|
|
|
disabled={metadataLoading || submitting || busyRowsPresent || !(canSubmit)}>
|
|
|
|
|
{creatableCount > 1 ? '一括追加' : '追加'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|