このコミットが含まれているのは:
@@ -90,6 +90,8 @@ export const applyThumbnailWarnings = (rows: PostImportRow[]): PostImportRow[] =
|
||||
|
||||
export const processableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||
validatableImportRows (rows).filter (row => {
|
||||
if (row.metadataUrl == null)
|
||||
return false
|
||||
if (row.importStatus === 'created')
|
||||
return false
|
||||
if (row.importStatus === 'skipped')
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
clearPostImportSourceDraft,
|
||||
generatePostImportSessionId,
|
||||
hasThumbnailBaseValue,
|
||||
initialisePreviewRows,
|
||||
isCompletedReviewRow,
|
||||
isExistingSkipRow,
|
||||
isNonRecoverableFailedRow,
|
||||
@@ -62,7 +61,7 @@ import type { User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
type PreviewResponse = {
|
||||
type PostMetadataResponse = {
|
||||
url: string
|
||||
title?: string
|
||||
thumbnailBase?: string
|
||||
@@ -105,6 +104,47 @@ const isRepairRow = (row: PostImportRow): boolean =>
|
||||
|| (row.importStatus === 'pending'
|
||||
&& Object.keys (row.validationErrors).length > 0))
|
||||
|
||||
const DUPLICATE_URL_MESSAGE = 'URL が重複しています.'
|
||||
|
||||
const applyDuplicateUrlErrors = (
|
||||
rows: PostImportRow[],
|
||||
): PostImportRow[] => {
|
||||
const counts = rows.reduce<Record<string, number>> ((result, row) => {
|
||||
if (row.url !== '')
|
||||
result[row.url] = (result[row.url] ?? 0) + 1
|
||||
return result
|
||||
}, { })
|
||||
|
||||
return rows.map (row => {
|
||||
const urlErrors = (row.validationErrors.url ?? []).filter (
|
||||
message => message !== DUPLICATE_URL_MESSAGE)
|
||||
if ((counts[row.url] ?? 0) < 2)
|
||||
{
|
||||
const nextValidationErrors = {
|
||||
...row.validationErrors,
|
||||
url: urlErrors }
|
||||
const hasErrors = Object.values (nextValidationErrors).some (
|
||||
messages => messages.length > 0)
|
||||
return urlErrors.length === (row.validationErrors.url ?? []).length
|
||||
? row
|
||||
: {
|
||||
...row,
|
||||
validationErrors: nextValidationErrors,
|
||||
status: hasErrors ? 'error' : row.status }
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
skipReason: row.skipReason === 'manual' ? 'manual' : undefined,
|
||||
existingPostId: undefined,
|
||||
existingPost: undefined,
|
||||
validationErrors: {
|
||||
...row.validationErrors,
|
||||
url: [...new Set ([...urlErrors, DUPLICATE_URL_MESSAGE])] },
|
||||
status: 'error' }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const buildSession = (rows: PostImportRow[]): PostImportSession => ({
|
||||
version: 3,
|
||||
@@ -137,9 +177,12 @@ const buildDryRunFormData = (row: PostImportRow): FormData => {
|
||||
|
||||
const mergeDryRunRow = (
|
||||
currentRow: PostImportRow,
|
||||
result: PreviewResponse,
|
||||
): PostImportRow =>
|
||||
applyThumbnailWarning ({
|
||||
result: PostMetadataResponse,
|
||||
): PostImportRow => {
|
||||
const hasWarnings =
|
||||
Object.values (result.fieldWarnings ?? { }).some (messages => messages.length > 0)
|
||||
|| (result.baseWarnings?.length ?? 0) > 0
|
||||
return applyThumbnailWarning ({
|
||||
...currentRow,
|
||||
url: result.url,
|
||||
attributes: {
|
||||
@@ -157,8 +200,7 @@ const mergeDryRunRow = (
|
||||
validationErrors: { },
|
||||
importErrors: undefined,
|
||||
status:
|
||||
Object.keys (result.fieldWarnings ?? { }).length > 0
|
||||
|| (result.baseWarnings?.length ?? 0) > 0
|
||||
hasWarnings
|
||||
? 'warning'
|
||||
: 'ready',
|
||||
skipReason: result.existingPost?.id != null ? 'existing' : undefined,
|
||||
@@ -169,10 +211,11 @@ const mergeDryRunRow = (
|
||||
? 'created'
|
||||
: 'pending',
|
||||
recoverable: undefined })
|
||||
}
|
||||
|
||||
|
||||
const buildPreviewResetSnapshot = (
|
||||
preview: PreviewResponse,
|
||||
preview: PostMetadataResponse,
|
||||
): PostImportRow['resetSnapshot'] => ({
|
||||
url: preview.url,
|
||||
attributes: {
|
||||
@@ -246,7 +289,7 @@ const indexedBulkResults = (
|
||||
|
||||
const mergePreviewRow = (
|
||||
currentRow: PostImportRow,
|
||||
preview: PreviewResponse,
|
||||
preview: PostMetadataResponse,
|
||||
): PostImportRow => {
|
||||
const fieldWarnings = preview.fieldWarnings ?? { }
|
||||
const baseWarnings = preview.baseWarnings ?? []
|
||||
@@ -405,7 +448,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
if (serialised == null)
|
||||
return null
|
||||
if (persistSequenceRef.current !== sequence)
|
||||
return sessionRef.current
|
||||
return null
|
||||
|
||||
const saved = savePostImportSession (sessionId, nextSession)
|
||||
const loadedSession = loadPostImportSession (sessionId)
|
||||
@@ -447,8 +490,23 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
}
|
||||
|
||||
const refreshRows = async (baseSession: PostImportSession) => {
|
||||
setLoading (true)
|
||||
let nextIndex = 0
|
||||
const previews = new Map<number, PreviewResponse> ()
|
||||
const previewErrors = new Map<number, Record<string, string[]>> ()
|
||||
let workingRows = [...(sessionRef.current ?? baseSession).rows]
|
||||
|
||||
const mergeWorkingRow = (
|
||||
sourceRow: number,
|
||||
updater: (row: PostImportRow) => PostImportRow,
|
||||
) => {
|
||||
workingRows = workingRows.map (row =>
|
||||
row.sourceRow === sourceRow
|
||||
? updater (row)
|
||||
: row)
|
||||
const nextSession = buildSession (workingRows)
|
||||
sessionRef.current = nextSession
|
||||
setSession (nextSession)
|
||||
}
|
||||
const worker = async () => {
|
||||
while (active)
|
||||
{
|
||||
@@ -461,18 +519,19 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
if (baseRow.skipReason === 'manual'
|
||||
|| baseRow.importStatus === 'created'
|
||||
|| baseRow.importStatus === 'skipped'
|
||||
|| isNonRecoverableFailedRow (baseRow))
|
||||
|| isNonRecoverableFailedRow (baseRow)
|
||||
|| baseRow.metadataUrl != null)
|
||||
continue
|
||||
const controller = new AbortController ()
|
||||
controllers.add (controller)
|
||||
try
|
||||
{
|
||||
const preview = await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', {
|
||||
params: { url: baseRow.url },
|
||||
signal: controller.signal })
|
||||
if (!(active) || previewSequenceRef.current !== previewSequence)
|
||||
return
|
||||
previews.set (baseRow.sourceRow, preview)
|
||||
mergeWorkingRow (baseRow.sourceRow, row => mergePreviewRow (row, preview))
|
||||
}
|
||||
catch (requestError)
|
||||
{
|
||||
@@ -480,8 +539,25 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
return
|
||||
if (!(active) || previewSequenceRef.current !== previewSequence)
|
||||
return
|
||||
if (!(isApiError (requestError)))
|
||||
if (!(isApiError<{
|
||||
errors?: Record<string, string[]>
|
||||
baseErrors?: string[]
|
||||
}> (requestError)))
|
||||
continue
|
||||
if (requestError.response?.status === 422)
|
||||
{
|
||||
const rowErrors = {
|
||||
...(requestError.response.data.errors ?? { }),
|
||||
...(requestError.response.data.baseErrors?.length
|
||||
? {
|
||||
base: requestError.response.data.baseErrors }
|
||||
: { }) }
|
||||
previewErrors.set (baseRow.sourceRow, rowErrors)
|
||||
mergeWorkingRow (baseRow.sourceRow, row => ({
|
||||
...row,
|
||||
validationErrors: rowErrors,
|
||||
status: 'error' }))
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -493,20 +569,22 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
await Promise.all (
|
||||
Array.from ({ length: Math.min (4, baseSession.rows.length) }, () => worker ()))
|
||||
if (!(active) || previewSequenceRef.current !== previewSequence)
|
||||
return
|
||||
{
|
||||
setLoading (false)
|
||||
return
|
||||
}
|
||||
|
||||
const latestSession = sessionRef.current ?? baseSession
|
||||
const nextRows = latestSession.rows.map (row => {
|
||||
const preview = previews.get (row.sourceRow)
|
||||
if (preview == null
|
||||
|| row.skipReason === 'manual'
|
||||
|| row.importStatus === 'created'
|
||||
|| row.importStatus === 'skipped'
|
||||
|| isNonRecoverableFailedRow (row))
|
||||
const nextRows = applyDuplicateUrlErrors (workingRows.map (row => {
|
||||
const rowErrors = previewErrors.get (row.sourceRow)
|
||||
if (rowErrors == null)
|
||||
return row
|
||||
return mergePreviewRow (row, preview)
|
||||
})
|
||||
return {
|
||||
...row,
|
||||
validationErrors: rowErrors,
|
||||
status: 'error' as const }
|
||||
}))
|
||||
await persistSession (buildSession (nextRows))
|
||||
setLoading (false)
|
||||
}
|
||||
|
||||
const hydrate = async () => {
|
||||
@@ -528,89 +606,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
sessionRef.current = loaded
|
||||
setSession (loaded)
|
||||
|
||||
if (loadedSession == null && parsed?.shortcut)
|
||||
{
|
||||
try
|
||||
{
|
||||
const preview = await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
params: { url: parsed.source } })
|
||||
if (!(active))
|
||||
return
|
||||
const rows = [applyThumbnailWarning (initialisePreviewRows ([{
|
||||
sourceRow: 1,
|
||||
url: preview.url,
|
||||
attributes: {
|
||||
title: preview.title ?? '',
|
||||
thumbnailBase: preview.thumbnailBase ?? '',
|
||||
originalCreatedFrom: preview.originalCreatedFrom ?? '',
|
||||
originalCreatedBefore: preview.originalCreatedBefore ?? '',
|
||||
duration: preview.duration ?? '',
|
||||
videoMs: preview.videoMs ?? '',
|
||||
tags: preview.tags ?? '',
|
||||
parentPostIds: String (preview.parentPostIds ?? '') },
|
||||
fieldWarnings: preview.fieldWarnings ?? { },
|
||||
baseWarnings: preview.baseWarnings ?? [],
|
||||
validationErrors: { },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
thumbnailBase: 'automatic',
|
||||
originalCreatedFrom: 'automatic',
|
||||
originalCreatedBefore: 'automatic',
|
||||
duration: 'automatic',
|
||||
videoMs: 'automatic',
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: preview.tags ?? '',
|
||||
manual: '' },
|
||||
status:
|
||||
Object.keys (preview.fieldWarnings ?? { }).length > 0
|
||||
|| (preview.baseWarnings?.length ?? 0) > 0
|
||||
? 'warning'
|
||||
: 'ready',
|
||||
skipReason: preview.existingPost?.id != null ? 'existing' : undefined,
|
||||
existingPostId: preview.existingPost?.id,
|
||||
existingPost: preview.existingPost ?? undefined,
|
||||
resetSnapshot: {
|
||||
url: preview.url,
|
||||
attributes: {
|
||||
title: preview.title ?? '',
|
||||
thumbnailBase: preview.thumbnailBase ?? '',
|
||||
originalCreatedFrom: preview.originalCreatedFrom ?? '',
|
||||
originalCreatedBefore: preview.originalCreatedBefore ?? '',
|
||||
duration: preview.duration ?? '',
|
||||
videoMs: preview.videoMs ?? '',
|
||||
tags: preview.tags ?? '',
|
||||
parentPostIds: String (preview.parentPostIds ?? '') },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
thumbnailBase: 'automatic',
|
||||
originalCreatedFrom: 'automatic',
|
||||
originalCreatedBefore: 'automatic',
|
||||
duration: 'automatic',
|
||||
videoMs: 'automatic',
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: preview.tags ?? '',
|
||||
manual: '' },
|
||||
fieldWarnings: preview.fieldWarnings ?? { },
|
||||
baseWarnings: preview.baseWarnings ?? [],
|
||||
metadataUrl: preview.url } }])[0])]
|
||||
const persisted = await persistSession (buildSession (rows))
|
||||
if (persisted == null)
|
||||
return
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (active)
|
||||
navigate ('/posts/new', { replace: true })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
void refreshRows (loaded)
|
||||
}
|
||||
|
||||
@@ -649,6 +644,9 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
const existingRows = sortedRows.filter (row => isExistingSkipRow (row))
|
||||
const reviewRows = sortedRows.filter (row => !(isExistingSkipRow (row)))
|
||||
const busy = loading || loadingRow != null
|
||||
const canSubmit =
|
||||
rows.every (row => isCompletedReviewRow (row))
|
||||
|| processableImportRows (rows).length > 0
|
||||
|
||||
const toggleManualSkip = async (sourceRow: number, checked: boolean) => {
|
||||
if (busy)
|
||||
@@ -709,10 +707,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
urlChanged
|
||||
? mergePreviewRow (
|
||||
nextRow,
|
||||
await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
await apiGet<PostMetadataResponse> ('/posts/metadata', {
|
||||
params: { url: nextRow.url } }))
|
||||
: nextRow
|
||||
const dryRun = await apiPost<PreviewResponse> (
|
||||
const dryRun = await apiPost<PostMetadataResponse> (
|
||||
'/posts?dry=1',
|
||||
buildDryRunFormData (candidateRow))
|
||||
const latestSession = sessionRef.current
|
||||
@@ -971,6 +969,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
|
||||
<PostImportFooter
|
||||
loading={busy}
|
||||
canSubmit={canSubmit}
|
||||
creatableCount={counts.creatable}
|
||||
manualSkippedCount={counts.manualSkipped}
|
||||
existingSkippedCount={counts.existingSkipped}
|
||||
@@ -982,12 +981,14 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
|
||||
const PostImportFooter = (
|
||||
{ loading,
|
||||
canSubmit,
|
||||
creatableCount,
|
||||
manualSkippedCount,
|
||||
existingSkippedCount,
|
||||
pendingOrErrorCount,
|
||||
onBack,
|
||||
onSubmit }: { loading: boolean
|
||||
canSubmit: boolean
|
||||
creatableCount: number
|
||||
manualSkippedCount: number
|
||||
existingSkippedCount: number
|
||||
@@ -1013,7 +1014,7 @@ const PostImportFooter = (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={loading}>
|
||||
disabled={loading || !(canSubmit)}>
|
||||
{creatableCount > 1 ? '一括追加' : '追加'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import MainArea from '@/components/layout/MainArea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet, isApiError } from '@/lib/api'
|
||||
import {
|
||||
appendPostNewSessionId,
|
||||
serialisePostNewState,
|
||||
@@ -37,191 +36,72 @@ import type { User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
type PreviewResponse = {
|
||||
url: string
|
||||
title?: string
|
||||
thumbnailBase?: string
|
||||
originalCreatedFrom?: string
|
||||
originalCreatedBefore?: string
|
||||
duration?: string
|
||||
videoMs?: number
|
||||
tags?: string
|
||||
fieldWarnings?: Record<string, string[]>
|
||||
baseWarnings?: string[]
|
||||
validationErrors?: Record<string, string[]>
|
||||
existingPost?: {
|
||||
id: number
|
||||
title: string
|
||||
url: string
|
||||
thumbnailUrl?: string } | null }
|
||||
|
||||
const MAX_ROWS = 100
|
||||
const SOURCE_ERROR_ID = 'post-import-source-error'
|
||||
const SOURCE_ISSUES_ID = 'post-import-source-issues'
|
||||
|
||||
const urlIssuesFromRows = (rows: PostImportRow[], source: string) => {
|
||||
const sourceLines = source.split (/\r\n|\n|\r/)
|
||||
|
||||
return rows.flatMap (row =>
|
||||
(row.validationErrors.url ?? []).map (message => ({
|
||||
sourceRow: row.sourceRow,
|
||||
message,
|
||||
url: sourceLines[row.sourceRow - 1]?.trim () ?? row.url })))
|
||||
}
|
||||
|
||||
|
||||
const previewRowFromResult = (
|
||||
sourceRow: number,
|
||||
result: PreviewResponse,
|
||||
): PostImportRow => {
|
||||
const fieldWarnings = result.fieldWarnings ?? { }
|
||||
const baseWarnings = result.baseWarnings ?? []
|
||||
const validationErrors = result.validationErrors ?? { }
|
||||
const hasWarnings =
|
||||
Object.values (fieldWarnings).some (messages => messages.length > 0)
|
||||
|| baseWarnings.length > 0
|
||||
const hasErrors =
|
||||
Object.values (validationErrors).some (messages => messages.length > 0)
|
||||
const row: PostImportRow = {
|
||||
sourceRow,
|
||||
url: result.url,
|
||||
attributes: {
|
||||
title: result.title ?? '',
|
||||
thumbnailBase: result.thumbnailBase ?? '',
|
||||
originalCreatedFrom: result.originalCreatedFrom ?? '',
|
||||
originalCreatedBefore: result.originalCreatedBefore ?? '',
|
||||
duration: result.duration ?? '',
|
||||
videoMs: result.videoMs ?? '',
|
||||
tags: result.tags ?? '',
|
||||
parentPostIds: '' },
|
||||
fieldWarnings,
|
||||
baseWarnings,
|
||||
validationErrors,
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
thumbnailBase: 'automatic',
|
||||
originalCreatedFrom: 'automatic',
|
||||
originalCreatedBefore: 'automatic',
|
||||
duration: 'automatic',
|
||||
videoMs: 'automatic',
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: result.tags ?? '',
|
||||
manual: '' },
|
||||
status:
|
||||
hasErrors
|
||||
? 'error'
|
||||
: hasWarnings
|
||||
? 'warning'
|
||||
: 'ready',
|
||||
skipReason: result.existingPost?.id != null ? 'existing' : undefined,
|
||||
existingPostId: result.existingPost?.id,
|
||||
existingPost: result.existingPost ?? undefined,
|
||||
resetSnapshot: {
|
||||
url: result.url,
|
||||
attributes: {
|
||||
title: result.title ?? '',
|
||||
thumbnailBase: result.thumbnailBase ?? '',
|
||||
originalCreatedFrom: result.originalCreatedFrom ?? '',
|
||||
originalCreatedBefore: result.originalCreatedBefore ?? '',
|
||||
duration: result.duration ?? '',
|
||||
videoMs: result.videoMs ?? '',
|
||||
tags: result.tags ?? '',
|
||||
parentPostIds: '' },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
thumbnailBase: 'automatic',
|
||||
originalCreatedFrom: 'automatic',
|
||||
originalCreatedBefore: 'automatic',
|
||||
duration: 'automatic',
|
||||
videoMs: 'automatic',
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: result.tags ?? '',
|
||||
manual: '' },
|
||||
fieldWarnings,
|
||||
baseWarnings } }
|
||||
return row
|
||||
}
|
||||
|
||||
|
||||
const fetchPreviewRows = async (
|
||||
rows: Array<{ sourceRow: number
|
||||
url: string }>,
|
||||
signal: AbortSignal,
|
||||
): Promise<{
|
||||
rows: PostImportRow[]
|
||||
issues: Array<{ sourceRow: number
|
||||
message: string
|
||||
url: string }>
|
||||
}> => {
|
||||
const results: Array<PostImportRow | null> = new Array (rows.length).fill (null)
|
||||
const issues: Array<{ sourceRow: number
|
||||
message: string
|
||||
url: string }> = []
|
||||
let nextIndex = 0
|
||||
|
||||
const previewIssues = (
|
||||
sourceRow: number,
|
||||
url: string,
|
||||
requestError: unknown,
|
||||
): Array<{ sourceRow: number
|
||||
message: string
|
||||
url: string }> => {
|
||||
if (!(isApiError<{
|
||||
message?: string
|
||||
errors?: Record<string, string[]>
|
||||
baseErrors?: string[]
|
||||
}> (requestError)))
|
||||
{
|
||||
return [{
|
||||
sourceRow,
|
||||
message: '入力を確認してください.',
|
||||
url }]
|
||||
}
|
||||
|
||||
const fieldMessages = Object.values (requestError.response?.data?.errors ?? { }).flat ()
|
||||
const baseMessages = requestError.response?.data?.baseErrors ?? []
|
||||
const messages = [...new Set ([...fieldMessages, ...baseMessages])]
|
||||
return (messages.length > 0 ? messages : [requestError.response?.data?.message ?? '入力を確認してください.'])
|
||||
.map (message => ({
|
||||
sourceRow,
|
||||
message,
|
||||
url }))
|
||||
}
|
||||
|
||||
const worker = async () => {
|
||||
while (nextIndex < rows.length)
|
||||
{
|
||||
const index = nextIndex
|
||||
++nextIndex
|
||||
const row = rows[index]
|
||||
try
|
||||
{
|
||||
const result = await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
params: { url: row.url },
|
||||
signal })
|
||||
results[index] = previewRowFromResult (row.sourceRow, result)
|
||||
}
|
||||
catch (requestError)
|
||||
{
|
||||
if (signal.aborted)
|
||||
return
|
||||
issues.push (...previewIssues (row.sourceRow, row.url, requestError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all (
|
||||
Array.from ({ length: Math.min (4, rows.length) }, () => worker ()))
|
||||
return {
|
||||
rows: results.filter ((row): row is PostImportRow => row != null),
|
||||
issues }
|
||||
}
|
||||
const buildInitialRows = (source: string): PostImportRow[] =>
|
||||
source
|
||||
.split (/\r\n|\n|\r/)
|
||||
.map ((line, index) => ({
|
||||
sourceRow: index + 1,
|
||||
url: line.trim () }))
|
||||
.filter (row => row.url !== '')
|
||||
.map (row => ({
|
||||
sourceRow: row.sourceRow,
|
||||
url: row.url,
|
||||
attributes: {
|
||||
title: '',
|
||||
thumbnailBase: '',
|
||||
originalCreatedFrom: '',
|
||||
originalCreatedBefore: '',
|
||||
duration: '',
|
||||
videoMs: '',
|
||||
tags: '',
|
||||
parentPostIds: '' },
|
||||
fieldWarnings: { },
|
||||
baseWarnings: [],
|
||||
validationErrors: { },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
thumbnailBase: 'automatic',
|
||||
originalCreatedFrom: 'automatic',
|
||||
originalCreatedBefore: 'automatic',
|
||||
duration: 'automatic',
|
||||
videoMs: 'automatic',
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: '',
|
||||
manual: '' },
|
||||
status: 'ready' as const,
|
||||
resetSnapshot: {
|
||||
url: row.url,
|
||||
attributes: {
|
||||
title: '',
|
||||
thumbnailBase: '',
|
||||
originalCreatedFrom: '',
|
||||
originalCreatedBefore: '',
|
||||
duration: '',
|
||||
videoMs: '',
|
||||
tags: '',
|
||||
parentPostIds: '' },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
thumbnailBase: 'automatic',
|
||||
originalCreatedFrom: 'automatic',
|
||||
originalCreatedBefore: 'automatic',
|
||||
duration: 'automatic',
|
||||
videoMs: 'automatic',
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: '',
|
||||
manual: '' },
|
||||
fieldWarnings: { },
|
||||
baseWarnings: [] } }))
|
||||
|
||||
|
||||
const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
@@ -233,7 +113,6 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
const [sourceError, setSourceError] = useState<string | null> (null)
|
||||
const saveTimer = useRef<number | null> (null)
|
||||
const editedRef = useRef (false)
|
||||
const previewController = useRef<AbortController | null> (null)
|
||||
|
||||
const lineCount = countImportSourceLines (source)
|
||||
const messages = sourceError != null ? [sourceError] : []
|
||||
@@ -274,10 +153,6 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
}
|
||||
}, [source])
|
||||
|
||||
useEffect (() => () => {
|
||||
previewController.current?.abort ()
|
||||
}, [])
|
||||
|
||||
const preview = async () => {
|
||||
if (lineCount === 0)
|
||||
{
|
||||
@@ -299,42 +174,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
setSourceError (null)
|
||||
try
|
||||
{
|
||||
previewController.current?.abort ()
|
||||
const controller = new AbortController ()
|
||||
previewController.current = controller
|
||||
const previewed = await fetchPreviewRows (
|
||||
source
|
||||
.split (/\r\n|\n|\r/)
|
||||
.map ((line, index) => ({
|
||||
sourceRow: index + 1,
|
||||
url: line.trim () }))
|
||||
.filter (row => row.url !== ''),
|
||||
controller.signal)
|
||||
if (previewed.issues.length > 0)
|
||||
{
|
||||
setSourceIssues (previewed.issues)
|
||||
return
|
||||
}
|
||||
const urlIssues = urlIssuesFromRows (previewed.rows, source)
|
||||
const duplicateIssues = Object.entries (
|
||||
previewed.rows.reduce<Record<string, PostImportRow[]>> ((acc, row) => {
|
||||
acc[row.url] ||= []
|
||||
acc[row.url].push (row)
|
||||
return acc
|
||||
}, { }))
|
||||
.flatMap (([, rows]) =>
|
||||
rows.length < 2
|
||||
? []
|
||||
: rows.map (row => ({
|
||||
sourceRow: row.sourceRow,
|
||||
message: 'URL が重複しています.',
|
||||
url: row.url })))
|
||||
if (urlIssues.length > 0 || duplicateIssues.length > 0)
|
||||
{
|
||||
setSourceIssues ([...urlIssues, ...duplicateIssues])
|
||||
return
|
||||
}
|
||||
const nextRows = initialisePreviewRows (previewed.rows)
|
||||
const nextRows = initialisePreviewRows (buildInitialRows (source))
|
||||
const serialised = await serialisePostNewState (nextRows)
|
||||
if (serialised == null)
|
||||
return
|
||||
@@ -355,16 +195,9 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
|
||||
navigate (appendPostNewSessionId (serialised.path, sessionId))
|
||||
}
|
||||
catch (requestError)
|
||||
catch
|
||||
{
|
||||
const message =
|
||||
isApiError<{ message?: string, baseErrors?: string[] }> (requestError)
|
||||
? (requestError.response?.data?.message
|
||||
?? requestError.response?.data?.baseErrors?.[0])
|
||||
: undefined
|
||||
setSourceError (message ?? '入力を確認してください.')
|
||||
toast ({ title: '投稿情報の取得に失敗しました',
|
||||
description: message ?? '入力を確認してください.' })
|
||||
return
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
新しい課題から参照
ユーザをブロックする