このコミットが含まれているのは:
@@ -193,6 +193,9 @@ const isValidSkipReason = (
|
||||
): value is PostImportSkipReason =>
|
||||
value === 'existing'
|
||||
|
||||
const isPositiveInteger = (value: unknown): value is number =>
|
||||
Number.isInteger (value) && Number (value) > 0
|
||||
|
||||
|
||||
const sanitiseRow = (value: unknown): PostImportRow | null => {
|
||||
if (!(isPlainObject (value)))
|
||||
@@ -211,6 +214,12 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
||||
return null
|
||||
if (value.skipReason != null && !(isValidSkipReason (value.skipReason)))
|
||||
return null
|
||||
if (value.skipReason === 'existing' && !(isPositiveInteger (value.existingPostId)))
|
||||
return null
|
||||
if (value.skipReason !== 'existing' && value.existingPostId != null)
|
||||
return null
|
||||
if (value.createdPostId != null && !(isPositiveInteger (value.createdPostId)))
|
||||
return null
|
||||
|
||||
const validationErrors = ensureStringListRecord (value.validationErrors)
|
||||
const fieldWarnings = ensureStringListRecord (value.fieldWarnings)
|
||||
@@ -250,13 +259,13 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
||||
provenance: value.provenance as Record<string, PostImportOrigin>,
|
||||
tagSources: value.tagSources as Record<PostImportOrigin, string> | undefined,
|
||||
status: value.status,
|
||||
skipReason: value.skipReason,
|
||||
existingPostId:
|
||||
Number.isInteger (value.existingPostId) ? Number (value.existingPostId) : undefined,
|
||||
metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined,
|
||||
createdPostId:
|
||||
Number.isInteger (value.createdPostId) ? Number (value.createdPostId) : undefined,
|
||||
importStatus: value.importStatus }
|
||||
skipReason: value.skipReason,
|
||||
existingPostId:
|
||||
isPositiveInteger (value.existingPostId) ? Number (value.existingPostId) : undefined,
|
||||
metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined,
|
||||
createdPostId:
|
||||
isPositiveInteger (value.createdPostId) ? Number (value.createdPostId) : undefined,
|
||||
importStatus: value.importStatus }
|
||||
}
|
||||
|
||||
|
||||
@@ -605,6 +614,9 @@ export const validateImportSource = (
|
||||
const hasSkipReason = (row: PostImportRow): boolean =>
|
||||
row.skipReason === 'existing'
|
||||
|
||||
const hasValidationErrors = (row: PostImportRow): boolean =>
|
||||
Object.keys (row.validationErrors ?? { }).length > 0
|
||||
|
||||
|
||||
export const submittableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||
rows.filter (row => {
|
||||
@@ -612,10 +624,12 @@ export const submittableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||
return false
|
||||
if (row.importStatus === 'skipped')
|
||||
return false
|
||||
if (hasSkipReason (row))
|
||||
if (hasSkipReason (row) && !(hasValidationErrors (row)))
|
||||
return false
|
||||
if (row.importStatus === 'failed')
|
||||
return false
|
||||
if (hasValidationErrors (row))
|
||||
return false
|
||||
return row.importStatus == null || row.importStatus === 'pending'
|
||||
})
|
||||
|
||||
@@ -624,9 +638,11 @@ export const reviewSummaryCounts = (rows: PostImportRow[]) => ({
|
||||
total: rows.length,
|
||||
submittable: rows.filter (row =>
|
||||
submittableImportRows ([row]).length > 0
|
||||
&& Object.keys (row.validationErrors ?? { }).length === 0).length,
|
||||
invalid: rows.filter (row => Object.keys (row.validationErrors ?? { }).length > 0).length,
|
||||
&& !(hasValidationErrors (row))).length,
|
||||
invalid: rows.filter (row => hasValidationErrors (row)).length,
|
||||
skipPlanned: rows.filter (row =>
|
||||
hasSkipReason (row) && row.importStatus !== 'created').length,
|
||||
hasSkipReason (row)
|
||||
&& !(hasValidationErrors (row))
|
||||
&& row.importStatus !== 'created').length,
|
||||
created: rows.filter (row => row.importStatus === 'created').length,
|
||||
failed: rows.filter (row => row.importStatus === 'failed').length })
|
||||
|
||||
@@ -16,9 +16,11 @@ import { canEditContent } from '@/lib/users'
|
||||
import { clearPostImportSourceDraft,
|
||||
loadPostImportSession,
|
||||
mergeImportResults,
|
||||
mergeValidatedImportRows,
|
||||
retryImportRow,
|
||||
savePostImportSession,
|
||||
type PostImportResultRow,
|
||||
type PostImportRow,
|
||||
type PostImportSession } from '@/lib/postImportSession'
|
||||
import Forbidden from '@/pages/Forbidden'
|
||||
|
||||
@@ -92,9 +94,31 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
||||
{
|
||||
const pendingRows = retryImportRow (session.rows, sourceRow)
|
||||
setSession ({ ...session, rows: pendingRows })
|
||||
const target = pendingRows.find (_1 => _1.sourceRow === sourceRow)
|
||||
const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', {
|
||||
rows: pendingRows
|
||||
.filter (row => row.importStatus !== 'created')
|
||||
.map (row => ({
|
||||
sourceRow: row.sourceRow,
|
||||
url: row.url,
|
||||
attributes: row.attributes,
|
||||
provenance: row.provenance,
|
||||
tagSources: row.tagSources,
|
||||
metadataUrl: row.metadataUrl })),
|
||||
changed_row: -1 })
|
||||
const validatedRows = mergeValidatedImportRows (pendingRows, validated.rows).map (row =>
|
||||
row.sourceRow === sourceRow
|
||||
&& Object.keys (row.validationErrors ?? { }).length > 0
|
||||
? { ...row, importStatus: 'failed' as const }
|
||||
: row)
|
||||
setSession (current =>
|
||||
current
|
||||
? { ...current, rows: validatedRows }
|
||||
: current)
|
||||
const target = validatedRows.find (_1 => _1.sourceRow === sourceRow)
|
||||
if (target == null)
|
||||
return
|
||||
if (Object.keys (target.validationErrors ?? { }).length > 0)
|
||||
return
|
||||
|
||||
const result = await apiPost<{
|
||||
created: number
|
||||
@@ -125,15 +149,15 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
||||
}
|
||||
|
||||
const openRepair = (sourceRow: number) => {
|
||||
if (session == null || sessionId == null)
|
||||
return
|
||||
|
||||
const nextSession = { ...session, repairMode: 'failed' as const }
|
||||
setSession (nextSession)
|
||||
if (sessionId)
|
||||
{
|
||||
const saved = savePostImportSession (sessionId, nextSession, message =>
|
||||
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
||||
if (!(saved))
|
||||
return
|
||||
}
|
||||
const saved = savePostImportSession (sessionId, nextSession, message =>
|
||||
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
||||
if (!(saved))
|
||||
return
|
||||
navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,7 @@ import type { User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
type PostFormField =
|
||||
'url'
|
||||
type PostFormField = 'url'
|
||||
| 'title'
|
||||
| 'tags'
|
||||
| 'parentPostIds'
|
||||
@@ -42,10 +41,8 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
|
||||
useValidationErrors<PostFormField> ()
|
||||
|
||||
const [originalCreatedBefore, setOriginalCreatedBefore] =
|
||||
useState<string | null> (null)
|
||||
const [originalCreatedFrom, setOriginalCreatedFrom] =
|
||||
useState<string | null> (null)
|
||||
const [originalCreatedBefore, setOriginalCreatedBefore] = useState<string | null> (null)
|
||||
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null)
|
||||
const [parentPostIds, setParentPostIds] = useState ('')
|
||||
const [tags, setTags] = useState ('')
|
||||
const [duration, setDuration] = useState ('')
|
||||
@@ -57,11 +54,9 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
const [url, setURL] = useState ('')
|
||||
|
||||
const thumbnailPreviewRef = useRef ('')
|
||||
const videoFlg =
|
||||
useMemo (() =>
|
||||
tags.split (/\s+/).some (
|
||||
tag => tag.replace (/\[.*\]$/, '') === '動画'),
|
||||
[tags])
|
||||
const videoFlg = useMemo (() =>
|
||||
tags.split (/\s+/).some (
|
||||
tag => tag.replace (/\[.*\]$/, '') === '動画'), [tags])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
clearValidationErrors ()
|
||||
@@ -82,8 +77,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
|
||||
try
|
||||
{
|
||||
await apiPost ('/posts', formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
await apiPost ('/posts', formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
toast ({ title: '投稿成功!' })
|
||||
navigate ('/posts')
|
||||
}
|
||||
@@ -98,8 +92,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
setTitleLoading (true)
|
||||
try
|
||||
{
|
||||
const data = await apiGet<{ title: string }> ('/preview/title',
|
||||
{ params: { url } })
|
||||
const data = await apiGet<{ title: string }> ('/preview/title', { params: { url } })
|
||||
setTitle (data.title || '')
|
||||
}
|
||||
finally
|
||||
@@ -116,13 +109,11 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
URL.revokeObjectURL (thumbnailPreviewRef.current)
|
||||
try
|
||||
{
|
||||
const data = await apiGet<Blob> ('/preview/thumbnail',
|
||||
{ params: { url },
|
||||
responseType: 'blob' })
|
||||
const data = await apiGet<Blob> ('/preview/thumbnail', { params: { url },
|
||||
responseType: 'blob' })
|
||||
const imageURL = URL.createObjectURL (data)
|
||||
setThumbnailPreview (imageURL)
|
||||
setThumbnailFile (new File ([data],
|
||||
'thumbnail.png',
|
||||
setThumbnailFile (new File ([data], 'thumbnail.png',
|
||||
{ type: data.type || 'image/png' }))
|
||||
}
|
||||
finally
|
||||
@@ -176,7 +167,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => void fetchTitle()}
|
||||
onClick={() => void fetchTitle ()}
|
||||
disabled={!(url) || titleLoading}>
|
||||
取得
|
||||
</Button>
|
||||
@@ -193,7 +184,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => void fetchThumbnail()}
|
||||
onClick={() => void fetchThumbnail ()}
|
||||
disabled={!(url) || thumbnailLoading}>
|
||||
取得
|
||||
</Button>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする