このコミットが含まれているのは:
@@ -3,8 +3,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import FieldWarning from '@/components/common/FieldWarning'
|
||||
import PostCreationDataFields from '@/components/posts/PostCreationDataFields'
|
||||
import PostDurationField from '@/components/posts/PostDurationField'
|
||||
import PostTextField from '@/components/posts/PostTextField'
|
||||
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
|
||||
import { hasThumbnailBaseValue, hasVideoTag } from '@/lib/postImportRows'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
@@ -21,6 +23,8 @@ type Props = {
|
||||
saved: boolean
|
||||
row: PostImportRow | null }> }
|
||||
|
||||
const THUMBNAIL_MISSING_WARNING = 'サムネールなし'
|
||||
|
||||
const buildDraft = (row: PostImportRow): Draft => ({
|
||||
url: row.url,
|
||||
title: String (row.attributes.title ?? ''),
|
||||
@@ -29,6 +33,7 @@ const buildDraft = (row: PostImportRow): Draft => ({
|
||||
originalCreatedBefore: String (row.attributes.originalCreatedBefore ?? ''),
|
||||
tags: String (row.attributes.tags ?? ''),
|
||||
parentPostIds: String (row.attributes.parentPostIds ?? ''),
|
||||
duration: String (row.attributes.duration ?? ''),
|
||||
thumbnailFile: row.thumbnailFile })
|
||||
|
||||
const buildResetDraft = (row: PostImportRow): Draft => ({
|
||||
@@ -39,6 +44,7 @@ const buildResetDraft = (row: PostImportRow): Draft => ({
|
||||
originalCreatedBefore: String (row.resetSnapshot.attributes.originalCreatedBefore ?? ''),
|
||||
tags: String (row.resetSnapshot.attributes.tags ?? ''),
|
||||
parentPostIds: String (row.resetSnapshot.attributes.parentPostIds ?? ''),
|
||||
duration: String (row.resetSnapshot.attributes.duration ?? ''),
|
||||
thumbnailFile: undefined })
|
||||
|
||||
const groupedMessages = (...values: (string[] | undefined)[]): string[] =>
|
||||
@@ -52,6 +58,8 @@ const sameDraft = (left: Draft, right: Draft): boolean =>
|
||||
&& left.originalCreatedBefore === right.originalCreatedBefore
|
||||
&& left.tags === right.tags
|
||||
&& left.parentPostIds === right.parentPostIds
|
||||
&& left.duration === right.duration
|
||||
&& left.thumbnailFile === right.thumbnailFile
|
||||
|
||||
const sameProvenance = (
|
||||
current: PostImportRow['provenance'],
|
||||
@@ -73,6 +81,17 @@ const sameWarnings = (
|
||||
JSON.stringify (current.fieldWarnings) === JSON.stringify (reset.fieldWarnings)
|
||||
&& JSON.stringify (current.baseWarnings) === JSON.stringify (reset.baseWarnings)
|
||||
|
||||
const thumbnailWarnings = (
|
||||
messages: string[] | undefined,
|
||||
thumbnailBase: string,
|
||||
thumbnailFile: File | undefined,
|
||||
): string[] => {
|
||||
const others = (messages ?? []).filter (message => message !== THUMBNAIL_MISSING_WARNING)
|
||||
return hasThumbnailBaseValue (thumbnailBase) || thumbnailFile != null
|
||||
? others
|
||||
: [...new Set ([...others, THUMBNAIL_MISSING_WARNING])]
|
||||
}
|
||||
|
||||
|
||||
const PostImportRowForm: FC<Props> = (
|
||||
{ row,
|
||||
@@ -85,8 +104,6 @@ const PostImportRowForm: FC<Props> = (
|
||||
const [resetRequested, setResetRequested] = useState (false)
|
||||
const [committedThumbnailBase, setCommittedThumbnailBase] = useState (
|
||||
() => String (row.attributes.thumbnailBase ?? ''))
|
||||
const [thumbnailObjectUrl, setThumbnailObjectUrl] = useState<string | undefined> (
|
||||
() => row.thumbnailObjectUrl)
|
||||
|
||||
useEffect (() => {
|
||||
const nextDraft = buildDraft (row)
|
||||
@@ -94,18 +111,17 @@ const PostImportRowForm: FC<Props> = (
|
||||
setMessageRow (null)
|
||||
setResetRequested (false)
|
||||
setCommittedThumbnailBase (String (row.attributes.thumbnailBase ?? ''))
|
||||
setThumbnailObjectUrl (row.thumbnailObjectUrl)
|
||||
}, [row])
|
||||
|
||||
useEffect (() => () => {
|
||||
if (thumbnailObjectUrl != null)
|
||||
URL.revokeObjectURL (thumbnailObjectUrl)
|
||||
}, [thumbnailObjectUrl])
|
||||
|
||||
const displayRow = messageRow ?? row
|
||||
const resetDraft = useMemo (
|
||||
() => buildResetDraft (row),
|
||||
[row])
|
||||
const durationVisible = hasVideoTag (draft.tags)
|
||||
const currentThumbnailWarnings = thumbnailWarnings (
|
||||
displayRow.fieldWarnings.thumbnailBase,
|
||||
draft.thumbnailBase,
|
||||
draft.thumbnailFile)
|
||||
const resetDisabled =
|
||||
saving
|
||||
|| (sameDraft (draft, resetDraft)
|
||||
@@ -129,7 +145,6 @@ const PostImportRowForm: FC<Props> = (
|
||||
|
||||
const confirmed = await controls.confirm ({
|
||||
title: '変更をリセットしますか?',
|
||||
description: '現在の URL に対する自動取得直後の内容へ戻します.',
|
||||
confirmText: 'リセット',
|
||||
cancelText: '取消',
|
||||
variant: 'danger' })
|
||||
@@ -140,11 +155,8 @@ const PostImportRowForm: FC<Props> = (
|
||||
setResetRequested (true)
|
||||
setMessageRow (null)
|
||||
setCommittedThumbnailBase (resetDraft.thumbnailBase)
|
||||
if (thumbnailObjectUrl != null)
|
||||
URL.revokeObjectURL (thumbnailObjectUrl)
|
||||
setThumbnailObjectUrl (undefined)
|
||||
return false
|
||||
}, [controls, resetDisabled, resetDraft, thumbnailObjectUrl])
|
||||
}, [controls, resetDisabled, resetDraft])
|
||||
|
||||
const save = useCallback (async (): Promise<boolean> => {
|
||||
setSaving (true)
|
||||
@@ -184,7 +196,11 @@ const PostImportRowForm: FC<Props> = (
|
||||
<div className="grid gap-6 md:grid-cols-[7rem_minmax(0,1fr)]">
|
||||
<div className="space-y-3 md:sticky md:top-0 md:self-start">
|
||||
<PostImportThumbnailPreview
|
||||
url={committedThumbnailBase || thumbnailObjectUrl || ''}
|
||||
url={committedThumbnailBase}
|
||||
file={
|
||||
hasThumbnailBaseValue (committedThumbnailBase)
|
||||
? undefined
|
||||
: draft.thumbnailFile}
|
||||
className="h-28 w-28"/>
|
||||
</div>
|
||||
|
||||
@@ -204,40 +220,22 @@ const PostImportRowForm: FC<Props> = (
|
||||
label="サムネール"
|
||||
value={draft.thumbnailBase}
|
||||
disabled={saving}
|
||||
warnings={displayRow.fieldWarnings.thumbnailBase}
|
||||
warnings={currentThumbnailWarnings}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.thumbnailBase,
|
||||
displayRow.importErrors?.thumbnailBase)}
|
||||
onBlur={() => {
|
||||
if (draft.thumbnailBase !== committedThumbnailBase)
|
||||
if (draft.thumbnailBase.trim () !== committedThumbnailBase.trim ())
|
||||
setCommittedThumbnailBase (draft.thumbnailBase)
|
||||
}}
|
||||
onChange={value => {
|
||||
if (value && thumbnailObjectUrl != null)
|
||||
{
|
||||
URL.revokeObjectURL (thumbnailObjectUrl)
|
||||
setThumbnailObjectUrl (undefined)
|
||||
update ('thumbnailFile', undefined)
|
||||
}
|
||||
update ('thumbnailBase', value)
|
||||
}}/>
|
||||
{!(draft.thumbnailBase) && (
|
||||
onChange={value => update ('thumbnailBase', value)}/>
|
||||
{!(hasThumbnailBaseValue (draft.thumbnailBase)) && (
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
disabled={saving}
|
||||
onChange={event => {
|
||||
const file = event.target.files?.[0]
|
||||
if (thumbnailObjectUrl != null)
|
||||
URL.revokeObjectURL (thumbnailObjectUrl)
|
||||
if (file == null)
|
||||
{
|
||||
setThumbnailObjectUrl (undefined)
|
||||
update ('thumbnailFile', undefined)
|
||||
return
|
||||
}
|
||||
const objectUrl = URL.createObjectURL (file)
|
||||
setThumbnailObjectUrl (objectUrl)
|
||||
update ('thumbnailFile', file)
|
||||
}}/>)}
|
||||
</>}
|
||||
@@ -283,7 +281,18 @@ const PostImportRowForm: FC<Props> = (
|
||||
disabled: saving,
|
||||
errors: groupedMessages (
|
||||
displayRow.validationErrors.parentPostIds,
|
||||
displayRow.importErrors?.parentPostIds) } }}/>
|
||||
displayRow.importErrors?.parentPostIds) } }}
|
||||
extraFields={
|
||||
durationVisible
|
||||
? (
|
||||
<PostDurationField
|
||||
value={draft.duration}
|
||||
onChange={value => update ('duration', value)}
|
||||
disabled={saving}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.videoMs,
|
||||
displayRow.importErrors?.videoMs)}/>)
|
||||
: null}/>
|
||||
<FieldWarning messages={displayRow.baseWarnings}/>
|
||||
<FieldError messages={displayRow.validationErrors.base}/>
|
||||
<FieldError messages={displayRow.importErrors?.base}/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'
|
||||
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
|
||||
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
|
||||
import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus'
|
||||
import { hasVideoTag } from '@/lib/postImportSession'
|
||||
import { canEditReviewRow, canRetryResultRow } from '@/lib/postImportSession'
|
||||
import { cn, originalCreatedAtString } from '@/lib/utils'
|
||||
|
||||
@@ -54,6 +55,8 @@ const PostImportRowSummary: FC<Props> = (
|
||||
const retryAllowed = onRetry != null && canRetryResultRow (row)
|
||||
const skipChecked = row.skipReason === 'manual'
|
||||
const rowNumber = displayNumber ?? row.sourceRow
|
||||
const duration = String (row.attributes.duration ?? '')
|
||||
const showDuration = hasVideoTag (row.attributes.tags) && duration !== ''
|
||||
const skipControl = showSkipToggle
|
||||
? (
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
@@ -78,10 +81,11 @@ const PostImportRowSummary: FC<Props> = (
|
||||
</div>
|
||||
<PostImportThumbnailPreview
|
||||
url={String (row.attributes.thumbnailBase ?? '')}
|
||||
file={row.thumbnailFile}
|
||||
className="h-16 w-16"/>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="line-clamp-2 text-sm font-medium">
|
||||
{String (row.attributes.title ?? '') || '(タイトル未取得)'}
|
||||
{String (row.attributes.title ?? '')}
|
||||
</div>
|
||||
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
||||
{row.url}
|
||||
@@ -93,6 +97,10 @@ const PostImportRowSummary: FC<Props> = (
|
||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{summaryDate (row)}
|
||||
</div>
|
||||
{showDuration && (
|
||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
動画時間 {duration}
|
||||
</div>)}
|
||||
{warning && (
|
||||
<div className="text-xs text-amber-700 dark:text-amber-200">
|
||||
{warning}
|
||||
@@ -133,10 +141,11 @@ const PostImportRowSummary: FC<Props> = (
|
||||
<div className="flex items-start gap-3">
|
||||
<PostImportThumbnailPreview
|
||||
url={String (row.attributes.thumbnailBase ?? '')}
|
||||
file={row.thumbnailFile}
|
||||
className="h-20 w-20 shrink-0"/>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="line-clamp-2 text-sm font-medium">
|
||||
{String (row.attributes.title ?? '') || '(タイトル未取得)'}
|
||||
{String (row.attributes.title ?? '')}
|
||||
</div>
|
||||
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
||||
{row.url}
|
||||
@@ -151,6 +160,10 @@ const PostImportRowSummary: FC<Props> = (
|
||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{summaryDate (row)}
|
||||
</div>
|
||||
{showDuration && (
|
||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
動画時間 {duration}
|
||||
</div>)}
|
||||
{warning && (
|
||||
<div className="text-xs text-amber-700 dark:text-amber-200">
|
||||
{warning}
|
||||
|
||||
@@ -11,12 +11,16 @@ type Props = {
|
||||
const LABELS: Record<PostImportBadgeValue, string> = {
|
||||
ready: '登録可能',
|
||||
warning: '警告',
|
||||
skipped: 'スキップ' }
|
||||
skipped: 'スキップ',
|
||||
created: '登録済み',
|
||||
failed: '登録失敗' }
|
||||
|
||||
const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = {
|
||||
ready: 'success',
|
||||
warning: 'warning',
|
||||
skipped: 'neutral' }
|
||||
skipped: 'neutral',
|
||||
created: 'success',
|
||||
failed: 'warning' }
|
||||
|
||||
|
||||
const PostImportStatusBadge: FC<Props> = ({ value }) => (
|
||||
|
||||
@@ -4,17 +4,20 @@ import type { FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
file?: File
|
||||
alt?: string
|
||||
className?: string }
|
||||
|
||||
|
||||
const PostImportThumbnailPreview: FC<Props> = (
|
||||
{ url,
|
||||
file,
|
||||
alt = 'サムネール',
|
||||
className = 'h-16 w-16' },
|
||||
) => (
|
||||
<PostThumbnailPreview
|
||||
url={url}
|
||||
file={file}
|
||||
alt={alt}
|
||||
className={className}
|
||||
referrerPolicy="no-referrer"/>)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { PostImportRow } from '@/lib/postImportSession'
|
||||
|
||||
export type PostImportDisplayStatus = 'ready' | 'skipped' | 'warning'
|
||||
export type PostImportDisplayStatus =
|
||||
'ready'
|
||||
| 'skipped'
|
||||
| 'warning'
|
||||
| 'created'
|
||||
| 'failed'
|
||||
|
||||
export type PostImportBadgeValue = PostImportDisplayStatus
|
||||
|
||||
@@ -13,10 +18,11 @@ export const displayPostImportStatus = (
|
||||
): PostImportDisplayStatus | null =>
|
||||
(row.skipReason != null || row.importStatus === 'skipped')
|
||||
? 'skipped'
|
||||
: ((Object.keys (row.validationErrors ?? { }).length > 0
|
||||
|| row.importStatus === 'failed'
|
||||
|| row.importStatus === 'created')
|
||||
? null
|
||||
: ((hasWarnings (row) || row.status === 'warning')
|
||||
? 'warning'
|
||||
: 'ready'))
|
||||
: (row.importStatus === 'created')
|
||||
? 'created'
|
||||
: ((Object.keys (row.validationErrors ?? { }).length > 0
|
||||
|| row.importStatus === 'failed')
|
||||
? 'failed'
|
||||
: ((hasWarnings (row) || row.status === 'warning')
|
||||
? 'warning'
|
||||
: 'ready'))
|
||||
|
||||
新しい課題から参照
ユーザをブロックする