278 行
8.9 KiB
TypeScript
278 行
8.9 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
|
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
|
import FieldError from '@/components/common/FieldError'
|
|
import FieldWarning from '@/components/common/FieldWarning'
|
|
import FormField from '@/components/common/FormField'
|
|
import TextArea from '@/components/common/TextArea'
|
|
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
|
|
import { inputClass } from '@/lib/utils'
|
|
|
|
import type { FC } from 'react'
|
|
|
|
import type { PostImportRow } from '@/lib/postImportSession'
|
|
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
|
|
|
|
type Draft = {
|
|
url: string
|
|
title: string
|
|
thumbnailBase: string
|
|
originalCreatedFrom: string
|
|
originalCreatedBefore: string
|
|
duration: string
|
|
tags: string
|
|
parentPostIds: string }
|
|
|
|
type Props = {
|
|
row: PostImportRow
|
|
controls: DialogueFormControls
|
|
onSave: (args: { draft: Draft
|
|
resetRequested: boolean }) => Promise<{
|
|
saved: boolean
|
|
row: PostImportRow | null }> }
|
|
|
|
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 buildResetDraft = (row: PostImportRow): Draft => ({
|
|
url: row.resetSnapshot.url,
|
|
title: String (row.resetSnapshot.attributes.title ?? ''),
|
|
thumbnailBase: String (row.resetSnapshot.attributes.thumbnailBase ?? ''),
|
|
originalCreatedFrom: String (row.resetSnapshot.attributes.originalCreatedFrom ?? ''),
|
|
originalCreatedBefore: String (row.resetSnapshot.attributes.originalCreatedBefore ?? ''),
|
|
duration: String (row.resetSnapshot.attributes.duration ?? ''),
|
|
tags: String (row.resetSnapshot.attributes.tags ?? ''),
|
|
parentPostIds: String (row.resetSnapshot.attributes.parentPostIds ?? '') })
|
|
|
|
const groupedMessages = (...values: (string[] | undefined)[]): string[] =>
|
|
values.flatMap (value => value ?? [])
|
|
|
|
const sameDraft = (left: Draft, right: Draft): boolean =>
|
|
left.url === right.url
|
|
&& left.title === right.title
|
|
&& left.thumbnailBase === right.thumbnailBase
|
|
&& left.originalCreatedFrom === right.originalCreatedFrom
|
|
&& left.originalCreatedBefore === right.originalCreatedBefore
|
|
&& left.duration === right.duration
|
|
&& left.tags === right.tags
|
|
&& left.parentPostIds === right.parentPostIds
|
|
|
|
|
|
const PostImportRowForm: FC<Props> = (
|
|
{ row,
|
|
controls,
|
|
onSave },
|
|
) => {
|
|
const [draft, setDraft] = useState<Draft> (() => buildDraft (row))
|
|
const [messageRow, setMessageRow] = useState<PostImportRow | null> (null)
|
|
const [saving, setSaving] = useState (false)
|
|
const [resetRequested, setResetRequested] = useState (false)
|
|
|
|
useEffect (() => {
|
|
const nextDraft = buildDraft (row)
|
|
setDraft (nextDraft)
|
|
setMessageRow (null)
|
|
setResetRequested (false)
|
|
}, [row])
|
|
|
|
const displayRow = messageRow ?? row
|
|
const resetDraft = useMemo (
|
|
() => buildResetDraft (row),
|
|
[row])
|
|
const resetDisabled = saving || sameDraft (draft, resetDraft)
|
|
|
|
const update = <Key extends keyof Draft,> (
|
|
key: Key,
|
|
value: Draft[Key],
|
|
) => {
|
|
if (messageRow != null)
|
|
setMessageRow (null)
|
|
setDraft (current => ({ ...current, [key]: value }))
|
|
}
|
|
|
|
const reset = useCallback (async (): Promise<boolean> => {
|
|
if (resetDisabled)
|
|
return false
|
|
|
|
const confirmed = await controls.confirm ({
|
|
title: '変更をリセットしますか?',
|
|
description: '現在の URL に対する自動取得直後の内容へ戻します.',
|
|
confirmText: 'リセット',
|
|
cancelText: '取消',
|
|
variant: 'danger' })
|
|
if (!(confirmed))
|
|
return false
|
|
|
|
setDraft (resetDraft)
|
|
setResetRequested (true)
|
|
setMessageRow (null)
|
|
return false
|
|
}, [controls, resetDisabled, resetDraft])
|
|
|
|
const save = useCallback (async (): Promise<boolean> => {
|
|
setSaving (true)
|
|
try
|
|
{
|
|
const result = await onSave ({ draft, resetRequested })
|
|
if (result.saved)
|
|
return true
|
|
|
|
if (result.row != null)
|
|
setMessageRow (result.row)
|
|
return false
|
|
}
|
|
finally
|
|
{
|
|
setSaving (false)
|
|
}
|
|
}, [draft, onSave, resetRequested])
|
|
|
|
useEffect (() => {
|
|
controls.setActions ([{
|
|
label: '変更をリセット',
|
|
placement: 'start',
|
|
variant: 'danger',
|
|
disabled: resetDisabled,
|
|
onSelect: reset },
|
|
{
|
|
label: '編輯内容を保存',
|
|
disabled: saving,
|
|
onSelect: save }])
|
|
}, [controls, resetDisabled, reset, save, saving])
|
|
|
|
return (
|
|
<>
|
|
<div className="px-6 pb-6">
|
|
<div className="space-y-4">
|
|
<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">
|
|
<ThumbnailPreview
|
|
url={draft.thumbnailBase}
|
|
className="h-28 w-28"/>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<PostImportTextField
|
|
label="URL"
|
|
value={draft.url}
|
|
warnings={displayRow.fieldWarnings.url}
|
|
errors={groupedMessages (
|
|
displayRow.validationErrors.url,
|
|
displayRow.importErrors?.url)}
|
|
onChange={value => update ('url', value)}/>
|
|
<PostImportTextField
|
|
label="タイトル"
|
|
value={draft.title}
|
|
warnings={displayRow.fieldWarnings.title}
|
|
errors={groupedMessages (
|
|
displayRow.validationErrors.title,
|
|
displayRow.importErrors?.title)}
|
|
onChange={value => update ('title', value)}/>
|
|
<PostImportTextField
|
|
label="サムネール基底 URL"
|
|
value={draft.thumbnailBase}
|
|
warnings={displayRow.fieldWarnings.thumbnailBase}
|
|
errors={groupedMessages (
|
|
displayRow.validationErrors.thumbnailBase,
|
|
displayRow.importErrors?.thumbnailBase)}
|
|
onChange={value => update ('thumbnailBase', value)}/>
|
|
<PostOriginalCreatedTimeField
|
|
originalCreatedFrom={draft.originalCreatedFrom || null}
|
|
setOriginalCreatedFrom={value => update ('originalCreatedFrom', value ?? '')}
|
|
originalCreatedBefore={draft.originalCreatedBefore || null}
|
|
setOriginalCreatedBefore={value => update ('originalCreatedBefore', value ?? '')}
|
|
errors={groupedMessages (
|
|
displayRow.validationErrors.originalCreatedAt,
|
|
displayRow.validationErrors.originalCreatedFrom,
|
|
displayRow.validationErrors.originalCreatedBefore,
|
|
displayRow.importErrors?.originalCreatedAt,
|
|
displayRow.importErrors?.originalCreatedFrom,
|
|
displayRow.importErrors?.originalCreatedBefore)}/>
|
|
<PostImportTextField
|
|
label="動画時間"
|
|
value={draft.duration}
|
|
errors={groupedMessages (
|
|
displayRow.validationErrors.duration,
|
|
displayRow.validationErrors.videoMs,
|
|
displayRow.importErrors?.duration,
|
|
displayRow.importErrors?.videoMs)}
|
|
onChange={value => update ('duration', value)}/>
|
|
<PostImportAreaField
|
|
label="タグ"
|
|
value={draft.tags}
|
|
warnings={displayRow.fieldWarnings.tags}
|
|
errors={groupedMessages (
|
|
displayRow.validationErrors.tags,
|
|
displayRow.importErrors?.tags)}
|
|
onChange={value => update ('tags', value)}/>
|
|
<PostImportTextField
|
|
label="親投稿"
|
|
value={draft.parentPostIds}
|
|
errors={groupedMessages (
|
|
displayRow.validationErrors.parentPostIds,
|
|
displayRow.importErrors?.parentPostIds)}
|
|
onChange={value => update ('parentPostIds', value)}/>
|
|
<FieldWarning messages={displayRow.baseWarnings}/>
|
|
<FieldError messages={displayRow.validationErrors.base}/>
|
|
<FieldError messages={displayRow.importErrors?.base}/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>)
|
|
}
|
|
|
|
const PostImportTextField = (
|
|
{ label, value, warnings, errors, onChange }: {
|
|
label: string
|
|
value: string
|
|
warnings?: string[]
|
|
errors?: string[]
|
|
onChange: (value: string) => void },
|
|
) => (
|
|
<FormField label={label} 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 PostImportAreaField = (
|
|
{ label, value, warnings, errors, onChange }: {
|
|
label: string
|
|
value: string
|
|
warnings?: string[]
|
|
errors?: string[]
|
|
onChange: (value: string) => void },
|
|
) => (
|
|
<FormField label={label} messages={errors}>
|
|
{({ describedBy, invalid }) => (
|
|
<>
|
|
<TextArea
|
|
value={value}
|
|
rows={4}
|
|
onChange={ev => onChange (ev.target.value)}
|
|
aria-describedby={describedBy}
|
|
invalid={invalid}
|
|
className="h-auto"/>
|
|
<FieldWarning messages={warnings}/>
|
|
</>)}
|
|
</FormField>)
|
|
|
|
export default PostImportRowForm
|
|
export { buildDraft }
|
|
export type { Draft as PostImportRowDraft }
|