このコミットが含まれているのは:
@@ -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
|
||||
{
|
||||
|
||||
新しい課題から参照
ユーザをブロックする