このコミットが含まれているのは:
2026-07-18 02:08:20 +09:00
コミット 9552081133
9個のファイルの変更351行の追加151行の削除
+188 -103
ファイルの表示
@@ -30,8 +30,10 @@ import {
clearPostImportSourceDraft,
generatePostImportSessionId,
hasThumbnailBaseValue,
hasErrorMessages,
isCompletedReviewRow,
isExistingSkipRow,
isManualSkipRow,
isNonRecoverableFailedRow,
mergeImportResults,
processableImportRows,
@@ -41,7 +43,7 @@ import {
reviewSummaryCounts,
retryImportRow,
loadPostImportSession,
serialisedPostImportRowsEqual,
compactMessageRecord,
savePostImportSession,
} from '@/lib/postImportSession'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
@@ -102,15 +104,48 @@ const isRepairRow = (row: PostImportRow): boolean =>
row.recoverable === true
&& (row.importStatus === 'failed'
|| (row.importStatus === 'pending'
&& Object.keys (row.validationErrors).length > 0))
&& hasErrorMessages (row.validationErrors)))
const DUPLICATE_URL_MESSAGE = 'URL が重複しています.'
const rowMetadataPending = (row: PostImportRow): boolean =>
row.status === 'pending'
const rowOperationBusy = (
row: PostImportRow,
submitting: boolean,
loadingRow: number | null,
): boolean =>
submitting
|| loadingRow === row.sourceRow
|| rowMetadataPending (row)
const rowSkipBusy = (
row: PostImportRow,
submitting: boolean,
loadingRow: number | null,
): boolean =>
submitting
|| loadingRow === row.sourceRow
|| (rowMetadataPending (row) && !(isManualSkipRow (row)))
const shouldFetchMetadata = (row: PostImportRow): boolean =>
row.status === 'pending'
&& row.skipReason !== 'manual'
&& row.importStatus !== 'created'
&& row.importStatus !== 'skipped'
&& !(isNonRecoverableFailedRow (row))
&& row.metadataUrl == null
const applyDuplicateUrlErrors = (
rows: PostImportRow[],
): PostImportRow[] => {
const counts = rows.reduce<Record<string, number>> ((result, row) => {
if (row.url !== '')
if (row.url !== ''
&& !(isManualSkipRow (row))
&& row.importStatus !== 'created'
&& row.importStatus !== 'skipped')
result[row.url] = (result[row.url] ?? 0) + 1
return result
}, { })
@@ -120,27 +155,30 @@ const applyDuplicateUrlErrors = (
message => message !== DUPLICATE_URL_MESSAGE)
if ((counts[row.url] ?? 0) < 2)
{
const nextValidationErrors = {
const nextValidationErrors = compactMessageRecord ({
...row.validationErrors,
url: urlErrors }
const hasErrors = Object.values (nextValidationErrors).some (
messages => messages.length > 0)
url: urlErrors })
const hasErrors = hasErrorMessages (nextValidationErrors)
return urlErrors.length === (row.validationErrors.url ?? []).length
? row
: {
...row,
validationErrors: nextValidationErrors,
skipReason:
row.skipReason === 'manual'
? 'manual'
: row.existingPostId != null
? 'existing'
: undefined,
status: hasErrors ? 'error' : row.status }
}
return {
...row,
skipReason: row.skipReason === 'manual' ? 'manual' : undefined,
existingPostId: undefined,
existingPost: undefined,
validationErrors: {
validationErrors: compactMessageRecord ({
...row.validationErrors,
url: [...new Set ([...urlErrors, DUPLICATE_URL_MESSAGE])] },
url: [...new Set ([...urlErrors, DUPLICATE_URL_MESSAGE])] }),
status: 'error' }
})
}
@@ -179,8 +217,9 @@ const mergeDryRunRow = (
currentRow: PostImportRow,
result: PostMetadataResponse,
): PostImportRow => {
const fieldWarnings = compactMessageRecord (result.fieldWarnings ?? { })
const hasWarnings =
Object.values (result.fieldWarnings ?? { }).some (messages => messages.length > 0)
Object.values (fieldWarnings).some (messages => messages.length > 0)
|| (result.baseWarnings?.length ?? 0) > 0
return applyThumbnailWarning ({
...currentRow,
@@ -195,7 +234,7 @@ const mergeDryRunRow = (
videoMs: result.videoMs ?? '',
tags: result.tags ?? '',
parentPostIds: String (result.parentPostIds ?? currentRow.attributes.parentPostIds ?? '') },
fieldWarnings: result.fieldWarnings ?? { },
fieldWarnings,
baseWarnings: result.baseWarnings ?? [],
validationErrors: { },
importErrors: undefined,
@@ -291,9 +330,9 @@ const mergePreviewRow = (
currentRow: PostImportRow,
preview: PostMetadataResponse,
): PostImportRow => {
const fieldWarnings = preview.fieldWarnings ?? { }
const fieldWarnings = compactMessageRecord (preview.fieldWarnings ?? { })
const baseWarnings = preview.baseWarnings ?? []
const validationErrors = preview.validationErrors ?? { }
const validationErrors = compactMessageRecord (preview.validationErrors ?? { })
const metadataChanged = currentRow.metadataUrl !== preview.url
const hasWarnings =
Object.values (fieldWarnings).some (messages => messages.length > 0)
@@ -438,7 +477,20 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const persistSequenceRef = useRef (0)
const previewSequenceRef = useRef (0)
const persistSession = async (
const commitSessionState = (
nextSession: PostImportSession,
): PostImportSession => {
const sessionId =
sessionIdRef.current ?? generatePostImportSessionId ()
sessionIdRef.current = sessionId
const persistedSession = buildSession (nextSession.rows)
savePostImportSession (sessionId, persistedSession)
sessionRef.current = persistedSession
setSession (persistedSession)
return persistedSession
}
const syncSessionUrl = async (
nextSession: PostImportSession,
): Promise<PostImportSession | null> => {
const sessionId =
@@ -451,26 +503,21 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
if (persistSequenceRef.current !== sequence)
return null
const saved = savePostImportSession (sessionId, nextSession)
const loadedSession = loadPostImportSession (sessionId)
const canRecoverFromUrlOnly = serialised.complete
if (!(canRecoverFromUrlOnly)
&& (!(saved))
&& !(serialisedPostImportRowsEqual (loadedSession?.rows ?? [], nextSession.rows)))
return null
if (saved
&& !(serialisedPostImportRowsEqual (loadedSession?.rows ?? [], nextSession.rows)))
return null
const persistedSession = commitSessionState (nextSession)
const persistedSession = buildSession (nextSession.rows)
const path = appendPostNewSessionId (serialised.path, sessionId)
persistedSearchRef.current = (new URL (path, window.location.origin)).search
sessionRef.current = persistedSession
setSession (persistedSession)
navigate (path, { replace: true })
return persistedSession
}
const currentRowBySource = (
sourceRow: number,
fallback?: PostImportSession,
): PostImportRow | null =>
(sessionRef.current?.rows ?? fallback?.rows ?? []).find (
row => row.sourceRow === sourceRow) ?? null
const finishImport = () => {
const sessionId = sessionIdRef.current
if (sessionId != null)
@@ -485,7 +532,9 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const previewSequence = ++previewSequenceRef.current
const controllers = new Set<AbortController> ()
if (sessionRef.current != null && persistedSearchRef.current === location.search)
if (sessionRef.current != null
&& persistedSearchRef.current === location.search
&& !(sessionRef.current.rows.some (row => shouldFetchMetadata (row))))
return () => {
previewSequenceRef.current = previewSequence
}
@@ -496,20 +545,16 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
try
{
let nextIndex = 0
const previewErrors = new Map<number, Record<string, string[]>> ()
let workingRows = [...(sessionRef.current ?? baseSession).rows]
const mergeWorkingRow = (
const mergeCurrentRow = (
sourceRow: number,
updater: (row: PostImportRow) => PostImportRow,
) => {
workingRows = workingRows.map (row =>
const latestRows = sessionRef.current?.rows ?? baseSession.rows
const nextRows = applyDuplicateUrlErrors (latestRows.map (row =>
row.sourceRow === sourceRow
? updater (row)
: row)
const nextSession = buildSession (workingRows)
sessionRef.current = nextSession
setSession (nextSession)
: row))
commitSessionState (buildSession (nextRows))
}
const worker = async () => {
while (active)
@@ -520,22 +565,25 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
return
const baseRow = baseSession.rows[index]
if (baseRow.skipReason === 'manual'
|| baseRow.importStatus === 'created'
|| baseRow.importStatus === 'skipped'
|| isNonRecoverableFailedRow (baseRow)
|| baseRow.metadataUrl != null)
const currentRow = currentRowBySource (baseRow.sourceRow, baseSession)
if (currentRow == null || !(shouldFetchMetadata (currentRow)))
continue
const controller = new AbortController ()
controllers.add (controller)
try
{
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', {
params: { url: baseRow.url },
params: { url: currentRow.url },
signal: controller.signal })
if (!(active) || previewSequenceRef.current !== previewSequence)
return
mergeWorkingRow (baseRow.sourceRow, row => mergePreviewRow (row, preview))
mergeCurrentRow (currentRow.sourceRow, row =>
row.importStatus === 'created'
|| row.importStatus === 'skipped'
|| isNonRecoverableFailedRow (row)
|| row.skipReason === 'manual'
? row
: mergePreviewRow (row, preview))
}
catch (requestError)
{
@@ -547,21 +595,29 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
errors?: Record<string, string[]>
baseErrors?: string[]
}> (requestError)))
continue
{
mergeCurrentRow (currentRow.sourceRow, row => ({
...row,
status: 'error' }))
continue
}
if (requestError.response?.status === 422)
{
const rowErrors = {
const rowErrors = compactMessageRecord ({
...(requestError.response.data.errors ?? { }),
...(requestError.response.data.baseErrors?.length
? {
base: requestError.response.data.baseErrors }
: { }) }
previewErrors.set (baseRow.sourceRow, rowErrors)
mergeWorkingRow (baseRow.sourceRow, row => ({
: { }) })
mergeCurrentRow (currentRow.sourceRow, row => ({
...row,
validationErrors: rowErrors,
status: 'error' }))
}
else
mergeCurrentRow (currentRow.sourceRow, row => ({
...row,
status: 'error' }))
}
finally
{
@@ -575,20 +631,13 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
if (!(active) || previewSequenceRef.current !== previewSequence)
return
const nextRows = applyDuplicateUrlErrors (workingRows.map (row => {
const rowErrors = previewErrors.get (row.sourceRow)
if (rowErrors == null)
return row
return {
...row,
validationErrors: rowErrors,
status: 'error' as const }
}))
await persistSession (buildSession (nextRows))
const latestRows = sessionRef.current?.rows ?? baseSession.rows
const nextRows = applyDuplicateUrlErrors (latestRows)
await syncSessionUrl (buildSession (nextRows))
}
finally
{
if (active)
if (active && previewSequenceRef.current === previewSequence)
setMetadataLoading (false)
}
}
@@ -649,18 +698,19 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
: rows
const existingRows = sortedRows.filter (row => isExistingSkipRow (row))
const reviewRows = sortedRows.filter (row => !(isExistingSkipRow (row)))
const busy = metadataLoading || submitting || loadingRow != null
const canSubmit =
rows.every (row => isCompletedReviewRow (row))
|| processableImportRows (rows).length > 0
const toggleManualSkip = async (sourceRow: number, checked: boolean) => {
if (busy)
return
const currentSession = sessionRef.current
if (currentSession == null)
return
const currentRow = currentRowBySource (sourceRow, currentSession)
if (currentRow == null)
return
if (rowSkipBusy (currentRow, submitting, loadingRow))
return
const nextRows = currentSession.rows.map (row => {
if (row.sourceRow !== sourceRow)
@@ -674,7 +724,13 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
existingPostId: checked ? undefined : row.existingPostId,
existingPost: checked ? undefined : row.existingPost }
})
await persistSession (buildSession (nextRows))
const nextSession = buildSession (applyDuplicateUrlErrors (nextRows))
if (metadataLoading)
{
commitSessionState (nextSession)
return
}
await syncSessionUrl (nextSession)
}
const saveDraft = async (
@@ -687,23 +743,26 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const currentSession = sessionRef.current
if (currentSession == null)
return { saved: false, row: null }
if (!(canEditReviewRow (row)))
const currentRow = currentRowBySource (row.sourceRow, currentSession)
if (currentRow == null)
return { saved: false, row: null }
if (!(canEditReviewRow (currentRow)))
return { saved: false, row: null }
const baseRow =
resetRequested
? { ...row,
url: row.resetSnapshot.url,
attributes: { ...row.resetSnapshot.attributes },
provenance: { ...row.resetSnapshot.provenance },
tagSources: { ...row.resetSnapshot.tagSources },
? { ...currentRow,
url: currentRow.resetSnapshot.url,
attributes: { ...currentRow.resetSnapshot.attributes },
provenance: { ...currentRow.resetSnapshot.provenance },
tagSources: { ...currentRow.resetSnapshot.tagSources },
fieldWarnings: Object.fromEntries (
Object.entries (row.resetSnapshot.fieldWarnings)
Object.entries (currentRow.resetSnapshot.fieldWarnings)
.map (([key, values]) => [key, [...values]])),
baseWarnings: [...row.resetSnapshot.baseWarnings],
metadataUrl: row.resetSnapshot.metadataUrl,
baseWarnings: [...currentRow.resetSnapshot.baseWarnings],
metadataUrl: currentRow.resetSnapshot.metadataUrl,
thumbnailFile: undefined }
: row
: currentRow
const urlChanged = draft.url !== baseRow.url
const nextRow = buildNextEditedRow (baseRow, draft, urlChanged)
let candidateRow = nextRow
@@ -724,15 +783,19 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
return { saved: false, row: null }
const mergedRow = mergeDryRunRow (candidateRow, dryRun)
const mergedRows = replaceImportRow (latestSession.rows, mergedRow)
const nextSession = await persistSession (buildSession (mergedRows))
const mergedRows = applyDuplicateUrlErrors (
replaceImportRow (latestSession.rows, mergedRow))
const nextSession =
metadataLoading
? commitSessionState (buildSession (mergedRows))
: await syncSessionUrl (buildSession (mergedRows))
if (nextSession == null)
return { saved: false, row: null }
const mergedTarget = nextSession.rows.find (
currentRow => currentRow.sourceRow === baseRow.sourceRow)
if (mergedTarget == null)
return { saved: false, row: null }
if (Object.keys (mergedTarget.validationErrors).length > 0)
if (hasErrorMessages (mergedTarget.validationErrors))
return { saved: false, row: mergedTarget }
return { saved: true, row: null }
}
@@ -746,7 +809,12 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
{
const errorRow = {
...candidateRow,
validationErrors: requestError.response.data.errors ?? { },
validationErrors: compactMessageRecord ({
...(requestError.response.data.errors ?? { }),
...(requestError.response.data.baseErrors?.length
? {
base: requestError.response.data.baseErrors }
: { }) }),
baseWarnings: [],
fieldWarnings: { },
status: 'error' as const }
@@ -778,39 +846,48 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
}
const editRow = async (row: PostImportRow) => {
setEditingRow (row)
const currentRow = sessionRef.current?.rows.find (
current => current.sourceRow === row.sourceRow)
if (currentRow == null)
return
if (!(canEditReviewRow (currentRow)))
return
if (rowOperationBusy (currentRow, submitting, loadingRow))
return
setEditingRow (currentRow)
try
{
await openEditingDialogue (row)
await openEditingDialogue (currentRow)
}
finally
{
setEditingRow (current =>
current?.sourceRow === row.sourceRow
current?.sourceRow === currentRow.sourceRow
? null
: current)
}
}
const retry = async (sourceRow: number) => {
if (busy)
return
const initialSession = sessionRef.current
if (initialSession == null)
return
setLoadingRow (sourceRow)
const originalRow = initialSession.rows.find (row => row.sourceRow === sourceRow)
if (originalRow == null)
{
setLoadingRow (null)
return
}
return
if (rowOperationBusy (originalRow, submitting, loadingRow))
return
setLoadingRow (sourceRow)
try
{
const pendingRows = retryImportRow (initialSession.rows, sourceRow)
const pendingSession = await persistSession (buildSession (pendingRows))
const pendingSession =
metadataLoading
? commitSessionState (buildSession (pendingRows))
: await syncSessionUrl (buildSession (pendingRows))
if (pendingSession == null)
return
@@ -825,16 +902,20 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
{
const latest = sessionRef.current ?? pendingSession
const restoredRows = replaceImportRow (latest.rows, originalRow)
await persistSession (buildSession (restoredRows))
if (metadataLoading)
commitSessionState (buildSession (restoredRows))
else
await syncSessionUrl (buildSession (restoredRows))
toast ({ title: '登録結果が不完全でした' })
return
}
const indexedResults = indexedBulkResults ([target], result.results)
const latestAfterImport = sessionRef.current ?? pendingSession
const nextRows = mergeImportResults (latestAfterImport.rows, indexedResults)
const nextRows = applyDuplicateUrlErrors (
mergeImportResults (latestAfterImport.rows, indexedResults))
.map (row => applyThumbnailWarning (row))
const persisted = await persistSession (buildSession (nextRows))
const persisted = await syncSessionUrl (buildSession (nextRows))
if (persisted != null
&& persisted.rows.every (row => isCompletedReviewRow (row)))
finishImport ()
@@ -843,7 +924,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
{
const latest = sessionRef.current ?? initialSession
const restoredRows = replaceImportRow (latest.rows, originalRow)
await persistSession (buildSession (restoredRows))
if (metadataLoading)
commitSessionState (buildSession (restoredRows))
else
await syncSessionUrl (buildSession (restoredRows))
toast ({ title: '再試行に失敗しました' })
}
finally
@@ -854,7 +938,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const submit = async () => {
const currentSession = sessionRef.current
if (currentSession == null || busy)
if (currentSession == null || metadataLoading || submitting || loadingRow != null)
return
if (currentSession.rows.every (row => isCompletedReviewRow (row)))
{
@@ -880,9 +964,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const indexedResults = indexedBulkResults (processingRows, result.results)
const latestAfterImport = sessionRef.current ?? buildSession (currentSession.rows)
const nextRows = mergeImportResults (latestAfterImport.rows, indexedResults)
const nextRows = applyDuplicateUrlErrors (
mergeImportResults (latestAfterImport.rows, indexedResults))
.map (row => applyThumbnailWarning (row))
const persisted = await persistSession (buildSession (nextRows))
const persisted = await syncSessionUrl (buildSession (nextRows))
if (persisted != null
&& persisted.rows.every (row => isCompletedReviewRow (row)))
finishImport ()
@@ -958,10 +1043,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
row={row}
displayNumber={index + 1}
showSkipToggle={true}
editDisabled={busy}
retryDisabled={busy}
editDisabled={rowOperationBusy (row, submitting, loadingRow)}
retryDisabled={rowOperationBusy (row, submitting, loadingRow)}
skipDisabled={
busy
rowSkipBusy (row, submitting, loadingRow)
|| isExistingSkipRow (row)
|| row.importStatus === 'created'}
onEdit={() => editRow (row)}