このコミットが含まれているのは:
@@ -16,6 +16,7 @@ import type { PostImportRow } from '@/lib/postImportSession'
|
||||
import type { DialogueFormAction, DialogueFormControls } from '@/lib/dialogues/useDialogue'
|
||||
|
||||
const api = vi.hoisted (() => ({
|
||||
apiGet: vi.fn (),
|
||||
apiPost: vi.fn (),
|
||||
}))
|
||||
|
||||
@@ -37,6 +38,9 @@ describe ('PostImportResultPage', () => {
|
||||
beforeEach (() => {
|
||||
sessionStorage.clear ()
|
||||
vi.clearAllMocks ()
|
||||
globalThis.URL.createObjectURL = vi.fn (() => 'blob:preview')
|
||||
globalThis.URL.revokeObjectURL = vi.fn ()
|
||||
api.apiGet.mockResolvedValue (new Blob (['img'], { type: 'image/png' }))
|
||||
})
|
||||
|
||||
it ('shows edit and retry only for recoverable failures and shows warnings', () => {
|
||||
@@ -86,6 +90,35 @@ describe ('PostImportResultPage', () => {
|
||||
)).toBeInTheDocument ()
|
||||
})
|
||||
|
||||
it ('shows hour-precision original-created ranges with the shared utility output', () => {
|
||||
const from = '2024-01-01T12:00:00Z'
|
||||
const before = '2024-01-01T13:00:00Z'
|
||||
savePostImportSession ('result-hour-range', {
|
||||
source: '',
|
||||
repairMode: 'all',
|
||||
rows: [buildPostImportRow ({
|
||||
sourceRow: 1,
|
||||
importStatus: 'created',
|
||||
createdPostId: 1,
|
||||
attributes: {
|
||||
title: 'created row',
|
||||
originalCreatedFrom: from,
|
||||
originalCreatedBefore: before } })] })
|
||||
|
||||
render (
|
||||
<HelmetProvider>
|
||||
<MemoryRouter initialEntries={['/posts/import/result-hour-range/result']}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/posts/import/:sessionId/result"
|
||||
element={<PostImportResultPage user={buildUser ()}/>}/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</HelmetProvider>)
|
||||
|
||||
expect (screen.getByText (originalCreatedAtString (from, before))).toBeInTheDocument ()
|
||||
})
|
||||
|
||||
it ('keeps recoverable pending rows actionable and hides actions for hard failures', () => {
|
||||
savePostImportSession ('result-pending', {
|
||||
source: '',
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { DialogueFormAction, DialogueFormControls } from '@/lib/dialogues/u
|
||||
import type { PostImportRow } from '@/lib/postImportSession'
|
||||
|
||||
const api = vi.hoisted (() => ({
|
||||
apiGet: vi.fn (),
|
||||
apiPost: vi.fn (),
|
||||
}))
|
||||
|
||||
@@ -33,6 +34,9 @@ describe ('PostImportReviewPage', () => {
|
||||
beforeEach (() => {
|
||||
sessionStorage.clear ()
|
||||
vi.clearAllMocks ()
|
||||
globalThis.URL.createObjectURL = vi.fn (() => 'blob:preview')
|
||||
globalThis.URL.revokeObjectURL = vi.fn ()
|
||||
api.apiGet.mockResolvedValue (new Blob (['img'], { type: 'image/png' }))
|
||||
})
|
||||
|
||||
it ('navigates to the result route after an initial recoverable failure', async () => {
|
||||
@@ -257,4 +261,35 @@ describe ('PostImportReviewPage', () => {
|
||||
.toBe ('edited title')
|
||||
})
|
||||
})
|
||||
|
||||
it ('sorts recoverable pending validation rows to the top in repair mode', () => {
|
||||
savePostImportSession ('review-repair-sort', {
|
||||
source: '',
|
||||
repairMode: 'failed',
|
||||
rows: [
|
||||
buildPostImportRow ({
|
||||
sourceRow: 2,
|
||||
attributes: { title: 'normal row' } }),
|
||||
buildPostImportRow ({
|
||||
sourceRow: 1,
|
||||
attributes: { title: 'repair row' },
|
||||
importStatus: 'pending',
|
||||
recoverable: true,
|
||||
validationErrors: { title: ['invalid'] } })] })
|
||||
|
||||
const { container } = render (
|
||||
<HelmetProvider>
|
||||
<MemoryRouter initialEntries={['/posts/import/review-repair-sort/review']}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/posts/import/:sessionId/review"
|
||||
element={<PostImportReviewPage user={buildUser ()}/>}/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</HelmetProvider>)
|
||||
|
||||
const titles = Array.from (container.querySelectorAll ('.line-clamp-2')).map (
|
||||
node => node.textContent)
|
||||
expect (titles[0]).toBe ('repair row')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -38,6 +38,12 @@ import type { User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
const isRepairRow = (row: PostImportRow): boolean =>
|
||||
row.recoverable === true
|
||||
&& (row.importStatus === 'failed'
|
||||
|| (row.importStatus === 'pending'
|
||||
&& Object.keys (row.validationErrors).length > 0))
|
||||
|
||||
|
||||
const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
const editable = canEditContent (user)
|
||||
@@ -85,9 +91,9 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
session?.repairMode === 'failed'
|
||||
? (
|
||||
[...rows].sort ((a, b) => {
|
||||
const aFailed = a.importStatus === 'failed' ? 0 : 1
|
||||
const bFailed = b.importStatus === 'failed' ? 0 : 1
|
||||
return aFailed - bFailed || a.sourceRow - b.sourceRow
|
||||
const aRepair = isRepairRow (a) ? 0 : 1
|
||||
const bRepair = isRepairRow (b) ? 0 : 1
|
||||
return aRepair - bRepair || a.sourceRow - b.sourceRow
|
||||
}))
|
||||
: rows
|
||||
|
||||
@@ -228,21 +234,19 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
setLoading (true)
|
||||
try
|
||||
{
|
||||
const validatableRows =
|
||||
currentSession.rows.filter (row => row.importStatus !== 'created')
|
||||
const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', {
|
||||
rows:
|
||||
currentSession.rows
|
||||
.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 })),
|
||||
validatableRows.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 = initialisePreviewRows (validated.rows)
|
||||
const expectedSourceRows = currentSession.rows
|
||||
.filter (row => row.importStatus !== 'created')
|
||||
.map (row => row.sourceRow)
|
||||
const expectedSourceRows = validatableRows.map (row => row.sourceRow)
|
||||
if (!(hasExactSourceRows (expectedSourceRows, validatedRows)))
|
||||
{
|
||||
toast ({ title: '再検証結果が不完全でした' })
|
||||
|
||||
@@ -134,4 +134,29 @@ describe ('PostNewPage', () => {
|
||||
expect (formData.get ('original_created_from')).toBe (
|
||||
toMinutePrecisionIsoUtc ('2024-01-01T12:34'))
|
||||
})
|
||||
|
||||
it ('shows deduplicated original-created endpoint errors on the shared datetime field', async () => {
|
||||
api.apiGet.mockResolvedValue ([])
|
||||
api.isApiError.mockReturnValue (true)
|
||||
api.apiPost.mockRejectedValueOnce ({
|
||||
response: {
|
||||
status: 422,
|
||||
data: {
|
||||
type: 'validation_error',
|
||||
errors: {
|
||||
original_created_at: ['日時を確認してください.'],
|
||||
original_created_from: ['日時を確認してください.'],
|
||||
original_created_before: ['終了を確認してください.'] },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
renderWithProviders (<PostNewPage user={buildUser ({ role: 'member' })}/>)
|
||||
|
||||
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
|
||||
|
||||
expect (await screen.findByText ('日時を確認してください.')).toBeInTheDocument ()
|
||||
expect (screen.getByText ('終了を確認してください.')).toBeInTheDocument ()
|
||||
expect (screen.getAllByText ('日時を確認してください.')).toHaveLength (1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,7 +27,11 @@ type Props = { user: User | null }
|
||||
|
||||
type PostFormField =
|
||||
'url' | 'title' | 'tags' | 'parentPostIds'
|
||||
| 'videoMs' | 'originalCreatedAt' | 'thumbnail'
|
||||
| 'videoMs' | 'originalCreatedAt' | 'originalCreatedFrom'
|
||||
| 'originalCreatedBefore' | 'thumbnail'
|
||||
|
||||
const groupedMessages = (...values: (string[] | undefined)[]): string[] =>
|
||||
[...new Set (values.flatMap (value => value ?? []))]
|
||||
|
||||
|
||||
const PostNewPage: FC<Props> = ({ user }) => {
|
||||
@@ -216,7 +220,10 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
setOriginalCreatedFrom={setOriginalCreatedFrom}
|
||||
originalCreatedBefore={originalCreatedBefore}
|
||||
setOriginalCreatedBefore={setOriginalCreatedBefore}
|
||||
errors={fieldErrors.originalCreatedAt}/>
|
||||
errors={groupedMessages (
|
||||
fieldErrors.originalCreatedAt,
|
||||
fieldErrors.originalCreatedFrom,
|
||||
fieldErrors.originalCreatedBefore)}/>
|
||||
|
||||
{videoFlg && (
|
||||
<PostDurationField
|
||||
|
||||
新しい課題から参照
ユーザをブロックする