313 行
10 KiB
TypeScript
313 行
10 KiB
TypeScript
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) }))
|