このコミットが含まれているのは:
@@ -226,6 +226,7 @@ class Post < ApplicationRecord
|
|||||||
end
|
end
|
||||||
unless minute_precision_time?(time)
|
unless minute_precision_time?(time)
|
||||||
errors.add field, ORIGINAL_CREATED_MINUTE_PRECISION_MESSAGE
|
errors.add field, ORIGINAL_CREATED_MINUTE_PRECISION_MESSAGE
|
||||||
|
return nil
|
||||||
end
|
end
|
||||||
time
|
time
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -242,4 +242,61 @@ RSpec.describe Post, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'original created datetime validation' do
|
||||||
|
it 'adds only the minute-precision error for second precision values' do
|
||||||
|
post = described_class.new(
|
||||||
|
title: 'title',
|
||||||
|
url: 'https://example.com/post',
|
||||||
|
original_created_from: '2024-01-01T12:34:30',
|
||||||
|
original_created_before: '2024-01-01T12:35'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(post).to be_invalid
|
||||||
|
expect(post.errors[:original_created_from]).to eq(
|
||||||
|
[described_class::ORIGINAL_CREATED_MINUTE_PRECISION_MESSAGE]
|
||||||
|
)
|
||||||
|
expect(post.errors[:original_created_at]).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds only the minute-precision error for fractional-second values' do
|
||||||
|
post = described_class.new(
|
||||||
|
title: 'title',
|
||||||
|
url: 'https://example.com/post',
|
||||||
|
original_created_from: '2024-01-01T12:34:00.123',
|
||||||
|
original_created_before: '2024-01-01T12:35'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(post).to be_invalid
|
||||||
|
expect(post.errors[:original_created_from]).to eq(
|
||||||
|
[described_class::ORIGINAL_CREATED_MINUTE_PRECISION_MESSAGE]
|
||||||
|
)
|
||||||
|
expect(post.errors[:original_created_at]).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'checks range rules only for valid minute-precision endpoints' do
|
||||||
|
post = described_class.new(
|
||||||
|
title: 'title',
|
||||||
|
url: 'https://example.com/post',
|
||||||
|
original_created_from: '2024-01-01T12:34',
|
||||||
|
original_created_before: '2024-01-01T12:34'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(post).to be_invalid
|
||||||
|
expect(post.errors[:original_created_at]).to eq(
|
||||||
|
[described_class::ORIGINAL_CREATED_ORDER_MESSAGE]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts a one-minute range at minute precision' do
|
||||||
|
post = described_class.new(
|
||||||
|
title: 'title',
|
||||||
|
url: 'https://example.com/post',
|
||||||
|
original_created_from: '2024-01-01T12:34',
|
||||||
|
original_created_before: '2024-01-01T12:35'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(post).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import type { FC } from 'react'
|
|||||||
import type { PostImportRow } from '@/lib/postImportSession'
|
import type { PostImportRow } from '@/lib/postImportSession'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
row: PostImportRow
|
row: PostImportRow
|
||||||
onEdit: () => void }
|
onEdit: () => void
|
||||||
|
editDisabled?: boolean }
|
||||||
|
|
||||||
const summaryWarning = (row: PostImportRow): string | null =>
|
const summaryWarning = (row: PostImportRow): string | null =>
|
||||||
Object.values (row.fieldWarnings ?? { }).flat ()[0]
|
Object.values (row.fieldWarnings ?? { }).flat ()[0]
|
||||||
@@ -24,7 +25,7 @@ const summaryDate = (row: PostImportRow): string =>
|
|||||||
row.attributes.originalCreatedBefore?.toString () ?? null)
|
row.attributes.originalCreatedBefore?.toString () ?? null)
|
||||||
|
|
||||||
|
|
||||||
const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
const PostImportRowSummary: FC<Props> = ({ row, onEdit, editDisabled }) => {
|
||||||
const warning = summaryWarning (row)
|
const warning = summaryWarning (row)
|
||||||
const displayStatus = displayPostImportStatus (row)
|
const displayStatus = displayPostImportStatus (row)
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
disabled={row.importStatus === 'created'}>
|
disabled={row.importStatus === 'created' || editDisabled === true}>
|
||||||
編輯
|
編輯
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +110,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
disabled={row.importStatus === 'created'}>
|
disabled={row.importStatus === 'created' || editDisabled === true}>
|
||||||
編輯
|
編輯
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,4 +67,40 @@ describe ('PostImportResultPage', () => {
|
|||||||
expect (screen.getByText ('サムネール画像を取得できませんでした.'))
|
expect (screen.getByText ('サムネール画像を取得できませんでした.'))
|
||||||
.toBeInTheDocument ()
|
.toBeInTheDocument ()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it ('keeps recoverable pending rows actionable and hides actions for hard failures', () => {
|
||||||
|
savePostImportSession ('result-pending', {
|
||||||
|
source: '',
|
||||||
|
repairMode: 'failed',
|
||||||
|
rows: [
|
||||||
|
buildPostImportRow ({
|
||||||
|
sourceRow: 1,
|
||||||
|
importStatus: 'pending',
|
||||||
|
recoverable: true,
|
||||||
|
validationErrors: { title: ['invalid'] } }),
|
||||||
|
buildPostImportRow ({
|
||||||
|
sourceRow: 2,
|
||||||
|
importStatus: 'pending',
|
||||||
|
recoverable: true }),
|
||||||
|
buildPostImportRow ({
|
||||||
|
sourceRow: 3,
|
||||||
|
importStatus: 'failed',
|
||||||
|
importErrors: { base: ['hard failed'] } })] })
|
||||||
|
|
||||||
|
render (
|
||||||
|
<HelmetProvider>
|
||||||
|
<MemoryRouter initialEntries={['/posts/import/result-pending/result']}>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/posts/import/:sessionId/result"
|
||||||
|
element={<PostImportResultPage user={buildUser ()}/>}/>
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
</HelmetProvider>)
|
||||||
|
|
||||||
|
expect (screen.getAllByRole ('button', { name: '編輯' })).toHaveLength (1)
|
||||||
|
expect (screen.getAllByRole ('button', { name: '再試行' })).toHaveLength (1)
|
||||||
|
expect (screen.getByText ('invalid')).toBeInTheDocument ()
|
||||||
|
expect (screen.getByText ('hard failed')).toBeInTheDocument ()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ import { canEditContent } from '@/lib/users'
|
|||||||
import { clearPostImportSourceDraft,
|
import { clearPostImportSourceDraft,
|
||||||
initialisePreviewRows,
|
initialisePreviewRows,
|
||||||
loadPostImportSession,
|
loadPostImportSession,
|
||||||
mergeValidatedImportRow,
|
|
||||||
mergeImportResults,
|
mergeImportResults,
|
||||||
mergeValidatedImportRows,
|
mergeValidatedImportRow,
|
||||||
replaceImportRow,
|
replaceImportRow,
|
||||||
resultRepairMode,
|
resultRepairMode,
|
||||||
resultRowMessages,
|
resultRowMessages,
|
||||||
@@ -127,10 +126,6 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
() => resultSummaryCounts (session?.rows ?? []),
|
() => resultSummaryCounts (session?.rows ?? []),
|
||||||
[session])
|
[session])
|
||||||
|
|
||||||
const updateSessionRows = (nextRows: PostImportRow[]) =>
|
|
||||||
setSession (current =>
|
|
||||||
current != null ? { ...current, rows: nextRows } : current)
|
|
||||||
|
|
||||||
const saveDraft = async (
|
const saveDraft = async (
|
||||||
row: PostImportRow,
|
row: PostImportRow,
|
||||||
{ draft, resetRequested }: {
|
{ draft, resetRequested }: {
|
||||||
@@ -177,9 +172,31 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
changed_row: urlChanged ? baseRow.sourceRow : -1 })
|
changed_row: urlChanged ? baseRow.sourceRow : -1 })
|
||||||
const validatedRows = initialisePreviewRows (validated.rows)
|
const validatedRows = initialisePreviewRows (validated.rows)
|
||||||
const target = validatedRows.find (_1 => _1.sourceRow === baseRow.sourceRow)
|
const target = validatedRows.find (_1 => _1.sourceRow === baseRow.sourceRow)
|
||||||
if (target != null && Object.keys (target.validationErrors).length > 0)
|
const latestSession = sessionRef.current
|
||||||
return { saved: false, row: target }
|
if (latestSession == null)
|
||||||
updateSessionRows (mergeValidatedImportRows (nextRows, validatedRows))
|
return { saved: false, row: null }
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
const restoredRows = replaceImportRow (latestSession.rows, row)
|
||||||
|
const restoredSession = {
|
||||||
|
...latestSession,
|
||||||
|
rows: restoredRows,
|
||||||
|
repairMode: resultRepairMode (restoredRows) }
|
||||||
|
sessionRef.current = restoredSession
|
||||||
|
setSession (restoredSession)
|
||||||
|
toast ({ title: '行の再検証結果が不完全でした' })
|
||||||
|
return { saved: false, row: null }
|
||||||
|
}
|
||||||
|
const rows = mergeValidatedImportRow (latestSession.rows, target)
|
||||||
|
const nextSession = {
|
||||||
|
...latestSession,
|
||||||
|
rows,
|
||||||
|
repairMode: resultRepairMode (rows) }
|
||||||
|
sessionRef.current = nextSession
|
||||||
|
setSession (nextSession)
|
||||||
|
const mergedTarget = rows.find (_1 => _1.sourceRow === baseRow.sourceRow) ?? target
|
||||||
|
if (Object.keys (mergedTarget.validationErrors).length > 0)
|
||||||
|
return { saved: false, row: mergedTarget }
|
||||||
return { saved: true, row: null }
|
return { saved: true, row: null }
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -225,7 +242,7 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const retry = async (sourceRow: number) => {
|
const retry = async (sourceRow: number) => {
|
||||||
if (sessionId == null)
|
if (session == null || sessionId == null)
|
||||||
return
|
return
|
||||||
|
|
||||||
const initialSession = sessionRef.current
|
const initialSession = sessionRef.current
|
||||||
@@ -259,7 +276,18 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
const validatedTarget = initialisePreviewRows (validated.rows)
|
const validatedTarget = initialisePreviewRows (validated.rows)
|
||||||
.find (_1 => _1.sourceRow === sourceRow)
|
.find (_1 => _1.sourceRow === sourceRow)
|
||||||
if (validatedTarget == null)
|
if (validatedTarget == null)
|
||||||
return
|
{
|
||||||
|
const latest = sessionRef.current ?? pendingSession
|
||||||
|
const restoredRows = replaceImportRow (latest.rows, originalRow)
|
||||||
|
const restoredSession = {
|
||||||
|
...latest,
|
||||||
|
rows: restoredRows,
|
||||||
|
repairMode: resultRepairMode (restoredRows) }
|
||||||
|
sessionRef.current = restoredSession
|
||||||
|
setSession (restoredSession)
|
||||||
|
toast ({ title: '再検証結果が不完全でした' })
|
||||||
|
return
|
||||||
|
}
|
||||||
const latestAfterValidate = sessionRef.current ?? pendingSession
|
const latestAfterValidate = sessionRef.current ?? pendingSession
|
||||||
const validatedRows = mergeValidatedImportRow (
|
const validatedRows = mergeValidatedImportRow (
|
||||||
latestAfterValidate.rows,
|
latestAfterValidate.rows,
|
||||||
@@ -272,7 +300,17 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
setSession (nextSession)
|
setSession (nextSession)
|
||||||
const target = validatedRows.find (_1 => _1.sourceRow === sourceRow)
|
const target = validatedRows.find (_1 => _1.sourceRow === sourceRow)
|
||||||
if (target == null)
|
if (target == null)
|
||||||
return
|
{
|
||||||
|
const restoredRows = replaceImportRow (latestAfterValidate.rows, originalRow)
|
||||||
|
const restoredSession = {
|
||||||
|
...latestAfterValidate,
|
||||||
|
rows: restoredRows,
|
||||||
|
repairMode: resultRepairMode (restoredRows) }
|
||||||
|
sessionRef.current = restoredSession
|
||||||
|
setSession (restoredSession)
|
||||||
|
toast ({ title: '再検証結果が不完全でした' })
|
||||||
|
return
|
||||||
|
}
|
||||||
if (Object.keys (target.validationErrors ?? { }).length > 0)
|
if (Object.keys (target.validationErrors ?? { }).length > 0)
|
||||||
{
|
{
|
||||||
const saved = savePostImportSession (sessionId, nextSession, message =>
|
const saved = savePostImportSession (sessionId, nextSession, message =>
|
||||||
@@ -350,10 +388,11 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openRepair = (sourceRow: number) => {
|
const openRepair = (sourceRow: number) => {
|
||||||
if (session == null || sessionId == null)
|
const currentSession = sessionRef.current
|
||||||
|
if (currentSession == null || sessionId == null)
|
||||||
return
|
return
|
||||||
|
|
||||||
const nextSession = { ...session, repairMode: 'failed' as const }
|
const nextSession = { ...currentSession, repairMode: 'failed' as const }
|
||||||
sessionRef.current = nextSession
|
sessionRef.current = nextSession
|
||||||
setSession (nextSession)
|
setSession (nextSession)
|
||||||
const saved = savePostImportSession (sessionId, nextSession, message =>
|
const saved = savePostImportSession (sessionId, nextSession, message =>
|
||||||
@@ -460,7 +499,11 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const nextSession = { ...session, repairMode: 'all' as const }
|
const currentSession = sessionRef.current
|
||||||
|
if (currentSession == null)
|
||||||
|
return
|
||||||
|
const nextSession = { ...currentSession, repairMode: 'all' as const }
|
||||||
|
sessionRef.current = nextSession
|
||||||
setSession (nextSession)
|
setSession (nextSession)
|
||||||
const saved = savePostImportSession (sessionId, nextSession, message =>
|
const saved = savePostImportSession (sessionId, nextSession, message =>
|
||||||
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import PostImportReviewPage from '@/pages/posts/PostImportReviewPage'
|
|||||||
import { buildUser } from '@/test/factories'
|
import { buildUser } from '@/test/factories'
|
||||||
import { buildPostImportRow } from '@/test/postImportFactories'
|
import { buildPostImportRow } from '@/test/postImportFactories'
|
||||||
|
|
||||||
|
import type { PostImportRow } from '@/lib/postImportSession'
|
||||||
|
|
||||||
const api = vi.hoisted (() => ({
|
const api = vi.hoisted (() => ({
|
||||||
apiPost: vi.fn (),
|
apiPost: vi.fn (),
|
||||||
}))
|
}))
|
||||||
@@ -71,4 +73,37 @@ describe ('PostImportReviewPage', () => {
|
|||||||
})
|
})
|
||||||
expect (dialogue.form).not.toHaveBeenCalled ()
|
expect (dialogue.form).not.toHaveBeenCalled ()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it ('disables row editing and navigation while batch import is running', async () => {
|
||||||
|
let resolveValidation: ((value: { rows: PostImportRow[] }) => void) | null = null
|
||||||
|
savePostImportSession ('review-loading', {
|
||||||
|
source: 'https://example.com/post',
|
||||||
|
repairMode: 'all',
|
||||||
|
rows: [buildPostImportRow ({ sourceRow: 1 })] })
|
||||||
|
api.apiPost.mockImplementationOnce (() =>
|
||||||
|
new Promise<{ rows: PostImportRow[] }> (resolve => {
|
||||||
|
resolveValidation = resolve
|
||||||
|
}))
|
||||||
|
|
||||||
|
render (
|
||||||
|
<HelmetProvider>
|
||||||
|
<MemoryRouter initialEntries={['/posts/import/review-loading/review']}>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/posts/import/:sessionId/review"
|
||||||
|
element={<PostImportReviewPage user={buildUser ()}/>}/>
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
</HelmetProvider>)
|
||||||
|
|
||||||
|
fireEvent.click (screen.getByRole ('button', { name: '取込実行' }))
|
||||||
|
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (screen.getByRole ('button', { name: '編輯' })).toBeDisabled ()
|
||||||
|
expect (screen.getByRole ('button', { name: 'URL リスト入力へ戻る' })).toBeDisabled ()
|
||||||
|
expect (screen.getByRole ('button', { name: '取込実行' })).toBeDisabled ()
|
||||||
|
})
|
||||||
|
|
||||||
|
resolveValidation?.({ rows: [buildPostImportRow ({ sourceRow: 1 })] })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import { loadPostImportSession,
|
|||||||
creatableImportRows,
|
creatableImportRows,
|
||||||
initialisePreviewRows,
|
initialisePreviewRows,
|
||||||
mergeImportResults,
|
mergeImportResults,
|
||||||
|
mergeValidatedImportRow,
|
||||||
mergeValidatedImportRows,
|
mergeValidatedImportRows,
|
||||||
processableImportRows,
|
processableImportRows,
|
||||||
|
replaceImportRow,
|
||||||
resultRepairMode,
|
resultRepairMode,
|
||||||
reviewSummaryCounts,
|
reviewSummaryCounts,
|
||||||
savePostImportSession } from '@/lib/postImportSession'
|
savePostImportSession } from '@/lib/postImportSession'
|
||||||
@@ -94,10 +96,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
|
element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
|
||||||
}, [editingRow, session?.repairMode])
|
}, [editingRow, session?.repairMode])
|
||||||
|
|
||||||
const updateSessionRows = (nextRows: PostImportRow[]) =>
|
|
||||||
setSession (current =>
|
|
||||||
current != null ? { ...current, rows: nextRows } : current)
|
|
||||||
|
|
||||||
const saveDraft = async (
|
const saveDraft = async (
|
||||||
row: PostImportRow,
|
row: PostImportRow,
|
||||||
{ draft, resetRequested }: {
|
{ draft, resetRequested }: {
|
||||||
@@ -143,10 +141,32 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
metadataUrl: row.metadataUrl })),
|
metadataUrl: row.metadataUrl })),
|
||||||
changed_row: urlChanged ? baseRow.sourceRow : -1 })
|
changed_row: urlChanged ? baseRow.sourceRow : -1 })
|
||||||
const validatedRows = initialisePreviewRows (validated.rows)
|
const validatedRows = initialisePreviewRows (validated.rows)
|
||||||
const target = validatedRows.find (row => row.sourceRow === baseRow.sourceRow)
|
const target = validatedRows.find (_1 => _1.sourceRow === baseRow.sourceRow)
|
||||||
if (target != null && Object.keys (target.validationErrors).length > 0)
|
const latestSession = sessionRef.current
|
||||||
return { saved: false, row: target }
|
if (latestSession == null)
|
||||||
updateSessionRows (mergeValidatedImportRows (nextRows, validatedRows))
|
return { saved: false, row: null }
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
const restoredRows = replaceImportRow (latestSession.rows, row)
|
||||||
|
const restoredSession = {
|
||||||
|
...latestSession,
|
||||||
|
rows: restoredRows,
|
||||||
|
repairMode: resultRepairMode (restoredRows) }
|
||||||
|
sessionRef.current = restoredSession
|
||||||
|
setSession (restoredSession)
|
||||||
|
toast ({ title: '行の再検証結果が不完全でした' })
|
||||||
|
return { saved: false, row: null }
|
||||||
|
}
|
||||||
|
const rows = mergeValidatedImportRow (latestSession.rows, target)
|
||||||
|
const nextSession = {
|
||||||
|
...latestSession,
|
||||||
|
rows,
|
||||||
|
repairMode: resultRepairMode (rows) }
|
||||||
|
sessionRef.current = nextSession
|
||||||
|
setSession (nextSession)
|
||||||
|
const mergedTarget = rows.find (_1 => _1.sourceRow === baseRow.sourceRow) ?? target
|
||||||
|
if (Object.keys (mergedTarget.validationErrors).length > 0)
|
||||||
|
return { saved: false, row: mergedTarget }
|
||||||
return { saved: true, row: null }
|
return { saved: true, row: null }
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -211,11 +231,15 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
metadataUrl: row.metadataUrl })),
|
metadataUrl: row.metadataUrl })),
|
||||||
changed_row: -1 })
|
changed_row: -1 })
|
||||||
const validatedRows = initialisePreviewRows (validated.rows)
|
const validatedRows = initialisePreviewRows (validated.rows)
|
||||||
const mergedRows = mergeValidatedImportRows (currentSession.rows, validatedRows)
|
const latestAfterValidate = sessionRef.current ?? currentSession
|
||||||
|
const mergedRows = mergeValidatedImportRows (latestAfterValidate.rows, validatedRows)
|
||||||
const firstInvalid = mergedRows.find (row => Object.keys (row.validationErrors).length > 0)
|
const firstInvalid = mergedRows.find (row => Object.keys (row.validationErrors).length > 0)
|
||||||
if (firstInvalid != null)
|
if (firstInvalid != null)
|
||||||
{
|
{
|
||||||
const nextSession = { ...currentSession, rows: mergedRows }
|
const nextSession = {
|
||||||
|
...latestAfterValidate,
|
||||||
|
rows: mergedRows,
|
||||||
|
repairMode: resultRepairMode (mergedRows) }
|
||||||
sessionRef.current = nextSession
|
sessionRef.current = nextSession
|
||||||
setSession (nextSession)
|
setSession (nextSession)
|
||||||
void editRow (firstInvalid)
|
void editRow (firstInvalid)
|
||||||
@@ -234,7 +258,11 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
provenance: row.provenance,
|
provenance: row.provenance,
|
||||||
tagSources: row.tagSources,
|
tagSources: row.tagSources,
|
||||||
metadataUrl: row.metadataUrl })) })
|
metadataUrl: row.metadataUrl })) })
|
||||||
const mergedResults = mergeImportResults (mergedRows, result.rows)
|
const latestAfterImport = sessionRef.current ?? {
|
||||||
|
...latestAfterValidate,
|
||||||
|
rows: mergedRows,
|
||||||
|
repairMode: resultRepairMode (mergedRows) }
|
||||||
|
const mergedResults = mergeImportResults (latestAfterImport.rows, result.rows)
|
||||||
const recoverableRows = result.rows.filter (row =>
|
const recoverableRows = result.rows.filter (row =>
|
||||||
row.status === 'failed'
|
row.status === 'failed'
|
||||||
&& row.recoverable
|
&& row.recoverable
|
||||||
@@ -251,7 +279,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
importErrors: undefined }
|
importErrors: undefined }
|
||||||
})
|
})
|
||||||
const nextSession = {
|
const nextSession = {
|
||||||
...currentSession,
|
...latestAfterImport,
|
||||||
rows: nextRows,
|
rows: nextRows,
|
||||||
repairMode: resultRepairMode (nextRows) }
|
repairMode: resultRepairMode (nextRows) }
|
||||||
sessionRef.current = nextSession
|
sessionRef.current = nextSession
|
||||||
@@ -304,6 +332,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
|
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
|
||||||
<PostImportRowSummary
|
<PostImportRowSummary
|
||||||
row={row}
|
row={row}
|
||||||
|
editDisabled={loading}
|
||||||
onEdit={() => void editRow (row)}/>
|
onEdit={() => void editRow (row)}/>
|
||||||
</div>))}
|
</div>))}
|
||||||
</div>
|
</div>
|
||||||
@@ -344,7 +373,7 @@ const PostImportFooter = (
|
|||||||
<span>スキップ予定 {skipPlannedCount}件</span>
|
<span>スキップ予定 {skipPlannedCount}件</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
<Button type="button" variant="outline" onClick={onBack}>
|
<Button type="button" variant="outline" onClick={onBack} disabled={loading}>
|
||||||
URL リスト入力へ戻る
|
URL リスト入力へ戻る
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする