このコミットが含まれているのは:
@@ -11,13 +11,20 @@ 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 { apiPost, isApiError } from '@/lib/api'
|
||||
import { serialisePostNewState } from '@/lib/postNewQueryState'
|
||||
import { apiGet, isApiError } from '@/lib/api'
|
||||
import {
|
||||
appendPostNewSessionId,
|
||||
serialisePostNewState,
|
||||
} from '@/lib/postNewQueryState'
|
||||
import { canEditContent } from '@/lib/users'
|
||||
import { countImportSourceLines,
|
||||
generatePostImportSessionId,
|
||||
cleanupExpiredPostImportSessions,
|
||||
initialisePreviewRows,
|
||||
loadPostImportSession,
|
||||
loadPostImportSourceDraft,
|
||||
resultRepairMode,
|
||||
savePostImportSession,
|
||||
savePostImportSourceDraft,
|
||||
validateImportSource } from '@/lib/postImportSession'
|
||||
import Forbidden from '@/pages/Forbidden'
|
||||
@@ -29,6 +36,19 @@ 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[]
|
||||
existingPost?: { id: number } | null }
|
||||
|
||||
const MAX_ROWS = 100
|
||||
const SOURCE_ERROR_ID = 'post-import-source-error'
|
||||
const SOURCE_ISSUES_ID = 'post-import-source-issues'
|
||||
@@ -44,6 +64,96 @@ const urlIssuesFromRows = (rows: PostImportRow[], source: string) => {
|
||||
}
|
||||
|
||||
|
||||
const previewRowFromResult = (
|
||||
sourceRow: number,
|
||||
result: PreviewResponse,
|
||||
): PostImportRow => {
|
||||
const fieldWarnings = result.fieldWarnings ?? { }
|
||||
const baseWarnings = result.baseWarnings ?? []
|
||||
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:
|
||||
Object.keys (fieldWarnings).length > 0 || baseWarnings.length > 0
|
||||
? 'warning'
|
||||
: 'ready',
|
||||
skipReason: result.existingPost?.id != null ? 'existing' : undefined,
|
||||
existingPostId: result.existingPost?.id,
|
||||
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 (urls: string[]): Promise<PostImportRow[]> => {
|
||||
const results: PostImportRow[] = new Array (urls.length)
|
||||
let nextIndex = 0
|
||||
const worker = async () => {
|
||||
while (nextIndex < urls.length)
|
||||
{
|
||||
const index = nextIndex
|
||||
++nextIndex
|
||||
const result = await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
params: { url: urls[index] } })
|
||||
results[index] = previewRowFromResult (index + 1, result)
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all (
|
||||
Array.from ({ length: Math.min (4, urls.length) }, () => worker ()))
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
const editable = canEditContent (user)
|
||||
const navigate = useNavigate ()
|
||||
@@ -114,17 +224,30 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
setSourceError (null)
|
||||
try
|
||||
{
|
||||
const data = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/preview', { source })
|
||||
const urlIssues = urlIssuesFromRows (data.rows, source)
|
||||
const urls = source.split (/\r\n|\n|\r/).map (line => line.trim ()).filter (line => line !== '')
|
||||
const rows = await fetchPreviewRows (urls)
|
||||
const urlIssues = urlIssuesFromRows (rows, source)
|
||||
if (urlIssues.length > 0)
|
||||
{
|
||||
setSourceIssues (urlIssues)
|
||||
return
|
||||
}
|
||||
const nextRows = initialisePreviewRows (data.rows)
|
||||
const nextRows = initialisePreviewRows (rows)
|
||||
const serialised = await serialisePostNewState (nextRows)
|
||||
if (serialised != null)
|
||||
navigate (serialised.path)
|
||||
if (serialised == null)
|
||||
return
|
||||
|
||||
const sessionId = generatePostImportSessionId ()
|
||||
const session = {
|
||||
source,
|
||||
rows: nextRows,
|
||||
repairMode: resultRepairMode (nextRows) }
|
||||
savePostImportSession (sessionId, session)
|
||||
const sessionLoaded = loadPostImportSession (sessionId)
|
||||
if (!(serialised.complete) && sessionLoaded?.rows.length !== nextRows.length)
|
||||
return
|
||||
|
||||
navigate (appendPostNewSessionId (serialised.path, sessionId))
|
||||
}
|
||||
catch (requestError)
|
||||
{
|
||||
|
||||
新しい課題から参照
ユーザをブロックする