import type { PostImportEditableDraft, PostImportResultRow, PostImportRow } from '@/lib/postImportTypes' const hasSkipReason = (row: PostImportRow): boolean => row.skipReason === 'existing' const hasValidationErrors = (row: PostImportRow): boolean => Object.keys (row.validationErrors ?? { }).length > 0 const isRecoverableRow = (row: PostImportRow): boolean => row.recoverable === true const isRepairableImportStatus = (row: PostImportRow): boolean => isRecoverableRow (row) && (row.importStatus === 'failed' || row.importStatus === 'pending') const buildResetSnapshot = (row: PostImportRow) => ({ url: row.url, attributes: { ...row.attributes }, provenance: { ...row.provenance }, tagSources: { automatic: row.tagSources?.automatic ?? '', manual: row.tagSources?.manual ?? '' }, fieldWarnings: Object.fromEntries ( Object.entries (row.fieldWarnings).map (([key, values]) => [key, [...values]])), baseWarnings: [...row.baseWarnings], metadataUrl: row.metadataUrl }) export const processableImportRows = (rows: PostImportRow[]): PostImportRow[] => rows.filter (row => { if (row.importStatus === 'created') return false if (row.importStatus === 'skipped') return false if (row.importStatus === 'failed') return false if (hasValidationErrors (row)) return false return row.importStatus == null || row.importStatus === 'pending' }) export const creatableImportRows = (rows: PostImportRow[]): PostImportRow[] => processableImportRows (rows).filter (row => !(hasSkipReason (row))) export const reviewSummaryCounts = (rows: PostImportRow[]) => ({ total: rows.length, submittable: rows.filter (row => creatableImportRows ([row]).length > 0 && !(hasValidationErrors (row))).length, skipPlanned: rows.filter (row => processableImportRows ([row]).length > 0 && hasSkipReason (row)).length }) export const resultSummaryCounts = (rows: PostImportRow[]) => rows.reduce ( (counts, row) => { if (row.importStatus === 'created') { ++counts.created return counts } if (row.importStatus === 'skipped') { ++counts.skipped return counts } if (row.importStatus === 'failed' || (row.recoverable === true && row.importStatus === 'pending')) ++counts.failed return counts }, { created: 0, skipped: 0, failed: 0 }) export const resultRepairMode = ( rows: PostImportRow[], ): 'all' | 'failed' => rows.some (row => hasValidationErrors (row) || isRepairableImportStatus (row)) ? 'failed' : 'all' export const canEditReviewRow = (row: PostImportRow): boolean => !(row.importStatus === 'created' || row.importStatus === 'skipped' || (row.importStatus === 'failed' && row.recoverable !== true)) export const canEditResultRow = (row: PostImportRow): boolean => row.recoverable === true && (row.importStatus === 'failed' || (row.importStatus === 'pending' && hasValidationErrors (row))) export const canRetryResultRow = (row: PostImportRow): boolean => row.recoverable === true && (row.importStatus === 'failed' || (row.importStatus === 'pending' && !(hasValidationErrors (row)))) export const resultRowMessages = (row: PostImportRow): string[] => [...new Set ([ ...Object.values (row.validationErrors ?? { }).flat (), ...Object.values (row.importErrors ?? { }).flat ()])] export const resultRowWarnings = (row: PostImportRow): string[] => [...new Set ([ ...Object.values (row.fieldWarnings ?? { }).flat (), ...row.baseWarnings])] export const buildNextEditedRow = ( editingRow: PostImportRow, draft: PostImportEditableDraft, urlChanged: boolean, ): PostImportRow => { const nextProvenance = { ...editingRow.provenance } const nextAttributes = { ...editingRow.attributes } const nextTagSources = { automatic: editingRow.tagSources?.automatic ?? '', manual: editingRow.tagSources?.manual ?? '' } const draftFields = [ ['title', draft.title], ['thumbnailBase', draft.thumbnailBase], ['originalCreatedFrom', draft.originalCreatedFrom], ['originalCreatedBefore', draft.originalCreatedBefore], ['duration', draft.duration], ['parentPostIds', draft.parentPostIds]] as const draftFields.forEach (([field, value]) => { nextAttributes[field] = value nextProvenance[field] = value !== String (editingRow.attributes[field] ?? '') ? 'manual' : (editingRow.provenance[field] ?? 'automatic') }) nextAttributes.tags = draft.tags if (draft.tags !== String (editingRow.attributes.tags ?? '')) { nextProvenance.tags = 'manual' nextTagSources.manual = draft.tags } else { nextProvenance.tags = editingRow.provenance.tags ?? 'automatic' nextTagSources.manual = editingRow.tagSources?.manual ?? '' } return { ...editingRow, url: draft.url, attributes: nextAttributes, provenance: { ...nextProvenance, url: urlChanged ? 'manual' : (editingRow.provenance.url ?? 'manual') }, tagSources: nextTagSources, importStatus: editingRow.importStatus === 'created' ? 'created' : 'pending', importErrors: undefined } } export const hasExactSourceRows = ( expected: number[], actual: Array<{ sourceRow: number }>, ): boolean => { if (expected.length !== actual.length) return false const expectedSorted = [...expected].sort ((a, b) => a - b) const actualSorted = actual.map (row => row.sourceRow).sort ((a, b) => a - b) return expectedSorted.every ((value, index) => value === actualSorted[index]) } export const replaceImportRow = ( rows: PostImportRow[], nextRow: PostImportRow, ): PostImportRow[] => rows.map (row => row.sourceRow === nextRow.sourceRow ? nextRow : row) export const mergeValidatedImportRow = ( rows: PostImportRow[], validated: PostImportRow, ): PostImportRow[] => { const current = rows.find (row => row.sourceRow === validated.sourceRow) if (current == null) return rows const [merged] = mergeValidatedImportRows ([current], [validated]) return merged == null ? rows : replaceImportRow (rows, merged) } export const mergeValidatedImportRows = ( current: PostImportRow[], validated: PostImportRow[], ): PostImportRow[] => { const validatedMap = new Map (validated.map (row => [row.sourceRow, row])) return current.map (previous => { if ( previous.importStatus === 'created' || previous.importStatus === 'skipped' || previous.importStatus === 'failed') return previous const row = validatedMap.get (previous.sourceRow) if (row == null) return previous const fieldWarnings = Object.keys (row.fieldWarnings).length > 0 || row.metadataUrl !== previous.metadataUrl ? { ...row.fieldWarnings } : { ...previous.fieldWarnings } for (const [field, origin] of Object.entries (row.provenance)) { if (origin === 'manual') delete fieldWarnings[field] } return { ...previous, url: row.url, attributes: row.attributes, provenance: row.provenance, tagSources: row.tagSources, skipReason: row.skipReason, existingPostId: row.existingPostId, fieldWarnings, baseWarnings: row.baseWarnings.length > 0 || row.metadataUrl !== previous.metadataUrl ? row.baseWarnings : previous.baseWarnings, validationErrors: row.validationErrors, status: row.status, metadataUrl: row.metadataUrl, resetSnapshot: row.metadataUrl !== previous.metadataUrl ? buildResetSnapshot (row) : previous.resetSnapshot } }) } export const mergeImportResults = ( rows: PostImportRow[], results: PostImportResultRow[], ): PostImportRow[] => { const resultMap = new Map (results.map (row => [row.sourceRow, row])) return rows.map (row => { const result = resultMap.get (row.sourceRow) if (result == null) return row switch (result.status) { case 'created': return { ...row, importStatus: 'created', recoverable: undefined, skipReason: undefined, createdPostId: result.post.id, existingPostId: undefined, fieldWarnings: result.fieldWarnings ?? row.fieldWarnings, baseWarnings: result.baseWarnings ?? row.baseWarnings, importErrors: result.errors } case 'skipped': return { ...row, importStatus: 'skipped', recoverable: undefined, skipReason: 'existing', createdPostId: undefined, existingPostId: result.existingPostId, fieldWarnings: result.fieldWarnings ?? row.fieldWarnings, baseWarnings: result.baseWarnings ?? row.baseWarnings, importErrors: result.errors } case 'failed': return { ...row, importStatus: 'failed', recoverable: result.recoverable === true ? true : undefined, skipReason: undefined, createdPostId: undefined, existingPostId: undefined, fieldWarnings: result.fieldWarnings ?? row.fieldWarnings, baseWarnings: result.baseWarnings ?? row.baseWarnings, importErrors: result.errors } } }) } export const retryImportRow = ( rows: PostImportRow[], sourceRow: number, ): PostImportRow[] => rows.map (row => row.sourceRow === sourceRow && row.importStatus === 'failed' && row.recoverable === true ? { ...row, importStatus: 'pending', importErrors: undefined } : row) export const initialisePreviewRows = (rows: PostImportRow[]): PostImportRow[] => rows.map (row => ({ ...row, resetSnapshot: buildResetSnapshot (row) }))