|
|
|
@@ -1,4 +1,7 @@
|
|
|
|
|
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
|
|
|
|
import FieldError from '@/components/common/FieldError'
|
|
|
|
|
import FieldWarning from '@/components/common/FieldWarning'
|
|
|
|
|
import FormField from '@/components/common/FormField'
|
|
|
|
|
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
|
|
|
|
|
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
|
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
@@ -49,9 +52,10 @@ const buildDraft = (row: PostImportRow): Draft => ({
|
|
|
|
|
tags: String (row.attributes.tags ?? ''),
|
|
|
|
|
parentPostIds: String (row.attributes.parentPostIds ?? '') })
|
|
|
|
|
|
|
|
|
|
const warningClass =
|
|
|
|
|
'mt-1 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm '
|
|
|
|
|
+ 'text-amber-700 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200'
|
|
|
|
|
const groupedMessages = (
|
|
|
|
|
...values: Array<string[] | undefined>
|
|
|
|
|
): string[] =>
|
|
|
|
|
values.flatMap (value => value ?? [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const PostImportRowDialog: FC<Props> = (
|
|
|
|
@@ -82,109 +86,101 @@ const PostImportRowDialog: FC<Props> = (
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<DialogContent className="max-w-3xl px-6 pb-6 pt-7">
|
|
|
|
|
<DialogHeader className="pl-8">
|
|
|
|
|
<DialogTitle>投稿 {row.sourceRow} を編集</DialogTitle>
|
|
|
|
|
<DialogContent className="flex max-h-[calc(100dvh-1rem)] max-w-3xl flex-col
|
|
|
|
|
overflow-hidden p-0">
|
|
|
|
|
<DialogHeader className="shrink-0 px-6 pb-4 pt-7">
|
|
|
|
|
<DialogTitle>投稿を編輯</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
自動取得結果を確認し、必要な項目だけ修正してください.
|
|
|
|
|
投稿 {row.sourceRow} の内容を確認し、必要な項目を編輯してください.
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-6 md:grid-cols-[7rem_minmax(0,1fr)]">
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<ThumbnailPreview
|
|
|
|
|
url={draft.thumbnailBase}
|
|
|
|
|
className="h-28 w-28"/>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<PostImportStatusBadge value={row.status}/>
|
|
|
|
|
<PostImportStatusBadge value={row.importStatus ?? 'pending'}/>
|
|
|
|
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-6">
|
|
|
|
|
<div className="grid gap-6 md:grid-cols-[7rem_minmax(0,1fr)]">
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<ThumbnailPreview
|
|
|
|
|
url={draft.thumbnailBase}
|
|
|
|
|
className="h-28 w-28"/>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<PostImportStatusBadge value={row.status}/>
|
|
|
|
|
<PostImportStatusBadge value={row.importStatus ?? 'pending'}/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<DialogField
|
|
|
|
|
label="URL"
|
|
|
|
|
value={draft.url}
|
|
|
|
|
origin={originOf (row, 'url')}
|
|
|
|
|
warnings={row.fieldWarnings.url}
|
|
|
|
|
errors={[
|
|
|
|
|
...(row.validationErrors.url ?? []),
|
|
|
|
|
...(row.importErrors?.url ?? [])]}
|
|
|
|
|
onChange={value => update ('url', value)}/>
|
|
|
|
|
<DialogField
|
|
|
|
|
label="タイトル"
|
|
|
|
|
value={draft.title}
|
|
|
|
|
origin={originOf (row, 'title')}
|
|
|
|
|
warnings={row.fieldWarnings.title}
|
|
|
|
|
errors={[
|
|
|
|
|
...(row.validationErrors.title ?? []),
|
|
|
|
|
...(row.importErrors?.title ?? [])]}
|
|
|
|
|
onChange={value => update ('title', value)}/>
|
|
|
|
|
<DialogField
|
|
|
|
|
label="サムネール基底 URL"
|
|
|
|
|
value={draft.thumbnailBase}
|
|
|
|
|
origin={originOf (row, 'thumbnailBase')}
|
|
|
|
|
warnings={row.fieldWarnings.thumbnailBase}
|
|
|
|
|
errors={[
|
|
|
|
|
...(row.validationErrors.thumbnailBase ?? []),
|
|
|
|
|
...(row.importErrors?.thumbnailBase ?? [])]}
|
|
|
|
|
onChange={value => update ('thumbnailBase', value)}/>
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
|
|
|
<DialogField
|
|
|
|
|
label="作成日時 From"
|
|
|
|
|
value={draft.originalCreatedFrom}
|
|
|
|
|
origin={originOf (row, 'originalCreatedFrom')}
|
|
|
|
|
errors={[
|
|
|
|
|
...(row.validationErrors.originalCreatedFrom ?? []),
|
|
|
|
|
...(row.importErrors?.originalCreatedFrom ?? [])]}
|
|
|
|
|
onChange={value => update ('originalCreatedFrom', value)}/>
|
|
|
|
|
<DialogField
|
|
|
|
|
label="作成日時 Before"
|
|
|
|
|
value={draft.originalCreatedBefore}
|
|
|
|
|
origin={originOf (row, 'originalCreatedBefore')}
|
|
|
|
|
errors={[
|
|
|
|
|
...(row.validationErrors.originalCreatedBefore ?? []),
|
|
|
|
|
...(row.importErrors?.originalCreatedBefore ?? [])]}
|
|
|
|
|
onChange={value => update ('originalCreatedBefore', value)}/>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<DialogTextField
|
|
|
|
|
label="URL"
|
|
|
|
|
value={draft.url}
|
|
|
|
|
origin={originOf (row, 'url')}
|
|
|
|
|
warnings={row.fieldWarnings.url}
|
|
|
|
|
errors={groupedMessages (row.validationErrors.url, row.importErrors?.url)}
|
|
|
|
|
onChange={value => update ('url', value)}/>
|
|
|
|
|
<DialogTextField
|
|
|
|
|
label="タイトル"
|
|
|
|
|
value={draft.title}
|
|
|
|
|
origin={originOf (row, 'title')}
|
|
|
|
|
warnings={row.fieldWarnings.title}
|
|
|
|
|
errors={groupedMessages (row.validationErrors.title, row.importErrors?.title)}
|
|
|
|
|
onChange={value => update ('title', value)}/>
|
|
|
|
|
<DialogTextField
|
|
|
|
|
label="サムネール基底 URL"
|
|
|
|
|
value={draft.thumbnailBase}
|
|
|
|
|
origin={originOf (row, 'thumbnailBase')}
|
|
|
|
|
warnings={row.fieldWarnings.thumbnailBase}
|
|
|
|
|
errors={groupedMessages (
|
|
|
|
|
row.validationErrors.thumbnailBase,
|
|
|
|
|
row.importErrors?.thumbnailBase,
|
|
|
|
|
)}
|
|
|
|
|
onChange={value => update ('thumbnailBase', value)}/>
|
|
|
|
|
<PostOriginalCreatedTimeField
|
|
|
|
|
labelAddon={<PostImportStatusBadge value={originOf (row, 'originalCreatedFrom')}/>}
|
|
|
|
|
originalCreatedFrom={draft.originalCreatedFrom || null}
|
|
|
|
|
setOriginalCreatedFrom={value => update ('originalCreatedFrom', value ?? '')}
|
|
|
|
|
originalCreatedBefore={draft.originalCreatedBefore || null}
|
|
|
|
|
setOriginalCreatedBefore={value => update ('originalCreatedBefore', value ?? '')}
|
|
|
|
|
errors={groupedMessages (
|
|
|
|
|
row.validationErrors.originalCreatedAt,
|
|
|
|
|
row.validationErrors.originalCreatedFrom,
|
|
|
|
|
row.validationErrors.originalCreatedBefore,
|
|
|
|
|
row.importErrors?.originalCreatedAt,
|
|
|
|
|
row.importErrors?.originalCreatedFrom,
|
|
|
|
|
row.importErrors?.originalCreatedBefore,
|
|
|
|
|
)}/>
|
|
|
|
|
<DialogTextField
|
|
|
|
|
label="動画時間"
|
|
|
|
|
value={draft.duration}
|
|
|
|
|
origin={originOf (row, 'duration')}
|
|
|
|
|
errors={groupedMessages (
|
|
|
|
|
row.validationErrors.duration,
|
|
|
|
|
row.validationErrors.videoMs,
|
|
|
|
|
row.importErrors?.duration,
|
|
|
|
|
row.importErrors?.videoMs,
|
|
|
|
|
)}
|
|
|
|
|
onChange={value => update ('duration', value)}/>
|
|
|
|
|
<DialogAreaField
|
|
|
|
|
label="タグ"
|
|
|
|
|
value={draft.tags}
|
|
|
|
|
origin={originOf (row, 'tags')}
|
|
|
|
|
warnings={row.fieldWarnings.tags}
|
|
|
|
|
errors={groupedMessages (row.validationErrors.tags, row.importErrors?.tags)}
|
|
|
|
|
onChange={value => update ('tags', value)}/>
|
|
|
|
|
<DialogTextField
|
|
|
|
|
label="親投稿"
|
|
|
|
|
value={draft.parentPostIds}
|
|
|
|
|
origin={originOf (row, 'parentPostIds')}
|
|
|
|
|
errors={groupedMessages (
|
|
|
|
|
row.validationErrors.parentPostIds,
|
|
|
|
|
row.importErrors?.parentPostIds,
|
|
|
|
|
)}
|
|
|
|
|
onChange={value => update ('parentPostIds', value)}/>
|
|
|
|
|
<FieldWarning messages={row.baseWarnings}/>
|
|
|
|
|
<FieldError messages={row.validationErrors.base}/>
|
|
|
|
|
<FieldError messages={row.importErrors?.base}/>
|
|
|
|
|
</div>
|
|
|
|
|
<DialogField
|
|
|
|
|
label="動画時間"
|
|
|
|
|
value={draft.duration}
|
|
|
|
|
origin={originOf (row, 'duration')}
|
|
|
|
|
errors={[
|
|
|
|
|
...(row.validationErrors.duration ?? []),
|
|
|
|
|
...(row.validationErrors.videoMs ?? []),
|
|
|
|
|
...(row.importErrors?.duration ?? []),
|
|
|
|
|
...(row.importErrors?.videoMs ?? [])]}
|
|
|
|
|
onChange={value => update ('duration', value)}/>
|
|
|
|
|
<DialogArea
|
|
|
|
|
label="タグ"
|
|
|
|
|
value={draft.tags}
|
|
|
|
|
origin={originOf (row, 'tags')}
|
|
|
|
|
warnings={row.fieldWarnings.tags}
|
|
|
|
|
errors={[
|
|
|
|
|
...(row.validationErrors.tags ?? []),
|
|
|
|
|
...(row.importErrors?.tags ?? [])]}
|
|
|
|
|
onChange={value => update ('tags', value)}/>
|
|
|
|
|
<DialogField
|
|
|
|
|
label="親投稿"
|
|
|
|
|
value={draft.parentPostIds}
|
|
|
|
|
origin={originOf (row, 'parentPostIds')}
|
|
|
|
|
errors={[
|
|
|
|
|
...(row.validationErrors.parentPostIds ?? []),
|
|
|
|
|
...(row.importErrors?.parentPostIds ?? [])]}
|
|
|
|
|
onChange={value => update ('parentPostIds', value)}/>
|
|
|
|
|
{row.baseWarnings.length > 0 && (
|
|
|
|
|
<div className={warningClass}>
|
|
|
|
|
{row.baseWarnings.map ((message, index) => (
|
|
|
|
|
<div key={`${ message }-${ index }`}>{message}</div>))}
|
|
|
|
|
</div>)}
|
|
|
|
|
<FieldError messages={row.validationErrors.base}/>
|
|
|
|
|
<FieldError messages={row.importErrors?.base}/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<DialogFooter className="shrink-0 px-6 pb-6 pt-4">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
@@ -195,14 +191,14 @@ const PostImportRowDialog: FC<Props> = (
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={save}
|
|
|
|
|
disabled={saving}>
|
|
|
|
|
保存
|
|
|
|
|
編輯内容を保存
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DialogField = (
|
|
|
|
|
const DialogTextField = (
|
|
|
|
|
{ label, value, origin, warnings, errors, onChange }: {
|
|
|
|
|
label: string
|
|
|
|
|
value: string
|
|
|
|
@@ -211,24 +207,20 @@ const DialogField = (
|
|
|
|
|
errors?: string[]
|
|
|
|
|
onChange: (value: string) => void },
|
|
|
|
|
) => (
|
|
|
|
|
<label className="block space-y-1">
|
|
|
|
|
<span className="flex items-center gap-2 text-sm font-medium">
|
|
|
|
|
{label}
|
|
|
|
|
<PostImportStatusBadge value={origin}/>
|
|
|
|
|
</span>
|
|
|
|
|
<input
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={ev => onChange (ev.target.value)}
|
|
|
|
|
className={inputClass (false)}/>
|
|
|
|
|
{warnings && warnings.length > 0 && (
|
|
|
|
|
<div className={warningClass}>
|
|
|
|
|
{warnings.map ((message, index) => (
|
|
|
|
|
<div key={`${ message }-${ index }`}>{message}</div>))}
|
|
|
|
|
</div>)}
|
|
|
|
|
<FieldError messages={errors}/>
|
|
|
|
|
</label>)
|
|
|
|
|
<FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
|
|
|
|
|
{({ describedBy, invalid }) => (
|
|
|
|
|
<>
|
|
|
|
|
<input
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={ev => onChange (ev.target.value)}
|
|
|
|
|
aria-describedby={describedBy}
|
|
|
|
|
aria-invalid={invalid}
|
|
|
|
|
className={inputClass (invalid)}/>
|
|
|
|
|
<FieldWarning messages={warnings}/>
|
|
|
|
|
</>)}
|
|
|
|
|
</FormField>)
|
|
|
|
|
|
|
|
|
|
const DialogArea = (
|
|
|
|
|
const DialogAreaField = (
|
|
|
|
|
{ label, value, origin, warnings, errors, onChange }: {
|
|
|
|
|
label: string
|
|
|
|
|
value: string
|
|
|
|
@@ -237,22 +229,28 @@ const DialogArea = (
|
|
|
|
|
errors?: string[]
|
|
|
|
|
onChange: (value: string) => void },
|
|
|
|
|
) => (
|
|
|
|
|
<label className="block space-y-1">
|
|
|
|
|
<span className="flex items-center gap-2 text-sm font-medium">
|
|
|
|
|
{label}
|
|
|
|
|
<PostImportStatusBadge value={origin}/>
|
|
|
|
|
</span>
|
|
|
|
|
<textarea
|
|
|
|
|
value={value}
|
|
|
|
|
rows={4}
|
|
|
|
|
onChange={ev => onChange (ev.target.value)}
|
|
|
|
|
className={inputClass (false)}/>
|
|
|
|
|
{warnings && warnings.length > 0 && (
|
|
|
|
|
<div className={warningClass}>
|
|
|
|
|
{warnings.map ((message, index) => (
|
|
|
|
|
<div key={`${ message }-${ index }`}>{message}</div>))}
|
|
|
|
|
</div>)}
|
|
|
|
|
<FieldError messages={errors}/>
|
|
|
|
|
</label>)
|
|
|
|
|
<FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
|
|
|
|
|
{({ describedBy, invalid }) => (
|
|
|
|
|
<>
|
|
|
|
|
<textarea
|
|
|
|
|
value={value}
|
|
|
|
|
rows={4}
|
|
|
|
|
onChange={ev => onChange (ev.target.value)}
|
|
|
|
|
aria-describedby={describedBy}
|
|
|
|
|
aria-invalid={invalid}
|
|
|
|
|
className={inputClass (invalid)}/>
|
|
|
|
|
<FieldWarning messages={warnings}/>
|
|
|
|
|
</>)}
|
|
|
|
|
</FormField>)
|
|
|
|
|
|
|
|
|
|
const DialogLabel = (
|
|
|
|
|
{ label, origin }: {
|
|
|
|
|
label: string
|
|
|
|
|
origin: PostImportOrigin },
|
|
|
|
|
) => (
|
|
|
|
|
<span className="flex items-center gap-2">
|
|
|
|
|
<span>{label}</span>
|
|
|
|
|
<PostImportStatusBadge value={origin}/>
|
|
|
|
|
</span>)
|
|
|
|
|
|
|
|
|
|
export default PostImportRowDialog
|
|
|
|
|