263 行
8.3 KiB
TypeScript
263 行
8.3 KiB
TypeScript
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 { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle } from '@/components/ui/dialog'
|
||
import { inputClass } from '@/lib/utils'
|
||
|
||
import type { FC } from 'react'
|
||
import { useEffect, useState } from 'react'
|
||
|
||
import type { PostImportOrigin,
|
||
PostImportRow } from '@/lib/postImportSession'
|
||
|
||
type Draft = {
|
||
url: string
|
||
title: string
|
||
thumbnailBase: string
|
||
originalCreatedFrom: string
|
||
originalCreatedBefore: string
|
||
duration: string
|
||
tags: string
|
||
parentPostIds: string }
|
||
|
||
type Props = {
|
||
open: boolean
|
||
row: PostImportRow | null
|
||
saving: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
onSave: (draft: Draft) => Promise<boolean> }
|
||
|
||
const originOf = (
|
||
row: PostImportRow,
|
||
field: string,
|
||
): PostImportOrigin =>
|
||
row.provenance[field] ?? 'automatic'
|
||
|
||
const originalCreatedOrigin = (row: PostImportRow): PostImportOrigin =>
|
||
originOf (row, 'originalCreatedFrom') === 'manual'
|
||
|| originOf (row, 'originalCreatedBefore') === 'manual'
|
||
? 'manual'
|
||
: 'automatic'
|
||
|
||
const buildDraft = (row: PostImportRow): Draft => ({
|
||
url: row.url,
|
||
title: String (row.attributes.title ?? ''),
|
||
thumbnailBase: String (row.attributes.thumbnailBase ?? ''),
|
||
originalCreatedFrom: String (row.attributes.originalCreatedFrom ?? ''),
|
||
originalCreatedBefore: String (row.attributes.originalCreatedBefore ?? ''),
|
||
duration: String (row.attributes.duration ?? ''),
|
||
tags: String (row.attributes.tags ?? ''),
|
||
parentPostIds: String (row.attributes.parentPostIds ?? '') })
|
||
|
||
const groupedMessages = (
|
||
...values: Array<string[] | undefined>
|
||
): string[] =>
|
||
values.flatMap (value => value ?? [])
|
||
|
||
|
||
const PostImportRowDialog: FC<Props> = (
|
||
{ open, row, saving, onOpenChange, onSave },
|
||
) => {
|
||
const [draft, setDraft] = useState<Draft | null> (null)
|
||
|
||
useEffect (() => {
|
||
if (open && row)
|
||
setDraft (buildDraft (row))
|
||
}, [open, row])
|
||
|
||
if (!(row) || !(draft))
|
||
return null
|
||
|
||
const update = <Key extends keyof Draft,> (
|
||
key: Key,
|
||
value: Draft[Key],
|
||
) => {
|
||
setDraft (current =>
|
||
current ? { ...current, [key]: value } : current)
|
||
}
|
||
|
||
const save = async () => {
|
||
if (await onSave (draft))
|
||
onOpenChange (false)
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<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="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={effectivePostImportStatus (row)}/>
|
||
</div>
|
||
</div>
|
||
|
||
<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={originalCreatedOrigin (row)}/>}
|
||
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>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter className="shrink-0 px-6 pb-6 pt-4">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => onOpenChange (false)}>
|
||
取消し
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
onClick={save}
|
||
disabled={saving}>
|
||
編輯内容を保存
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>)
|
||
}
|
||
|
||
const DialogTextField = (
|
||
{ label, value, origin, warnings, errors, onChange }: {
|
||
label: string
|
||
value: string
|
||
origin: PostImportOrigin
|
||
warnings?: string[]
|
||
errors?: string[]
|
||
onChange: (value: string) => void },
|
||
) => (
|
||
<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 DialogAreaField = (
|
||
{ label, value, origin, warnings, errors, onChange }: {
|
||
label: string
|
||
value: string
|
||
origin: PostImportOrigin
|
||
warnings?: string[]
|
||
errors?: string[]
|
||
onChange: (value: string) => void },
|
||
) => (
|
||
<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
|