import { useEffect, useMemo, useState } from 'react' import PostFormTagsArea from '@/components/PostFormTagsArea' import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' import FieldError from '@/components/common/FieldError' import FormField from '@/components/common/FormField' import { useDialogue } from '@/components/dialogues/DialogueProvider' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' import { isApiError } from '@/lib/api' import { updatePost } from '@/lib/posts' import { inputClass, msToTime } from '@/lib/utils' import { useValidationErrors } from '@/lib/useValidationErrors' import type { FC, FormEvent } from 'react' import type { Post, TagWithSections } from '@/types' type PostFormField = 'parentPostIds' | 'tags' | 'videoMs' | 'originalCreatedAt' const videoMsToDurationValue = (videoMs: number | null): string => videoMs == null ? '' : String (videoMs / 1_000) const tagsToStr = (tags: TagWithSections[]): string => { const result: Omit[] = [] const walk = (tag: TagWithSections) => { const { children, ...rest } = tag result.push (rest) children.forEach (walk) } tags.filter (t => t.category !== 'nico').forEach (walk) return [...(new Set (result.map (t => `${ t.name }${ t.sections .map (s => `[${ msToTime (s.beginMs) }-${ s.endMs == null ? '' : msToTime (s.endMs) }]`) .join ('') }`)))].join (' ') } type Props = { post: Post onSave: (newPost: Post) => void } const PostEditForm: FC = ({ post, onSave }) => { const [disabled, setDisabled] = useState (false) const [duration, setDuration] = useState (videoMsToDurationValue (post.videoMs)) const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } = useValidationErrors () const [originalCreatedBefore, setOriginalCreatedBefore] = useState (post.originalCreatedBefore) const [originalCreatedFrom, setOriginalCreatedFrom] = useState (post.originalCreatedFrom) const [parentPostIds, setParentPostIds] = useState ((post.parentPosts ?? []).map (p => p.id).join (' ')) const [tags, setTags] = useState ('') const [title, setTitle] = useState (post.title) const videoFlg = useMemo (() => tags.split (/\s+/).some (tag => tag.replace (/\[.*\]$/, '') === '動画'), [tags]) const dialogue = useDialogue () const update = async (...args: Parameters) => { clearValidationErrors () try { const data = await updatePost (...args) onSave ({ ...post, versionNo: data.versionNo, title: data.title, videoMs: data.videoMs, tags: data.tags, parentPosts: data.parentPosts, childPosts: data.childPosts, siblingPosts: data.siblingPosts, originalCreatedFrom: data.originalCreatedFrom, originalCreatedBefore: data.originalCreatedBefore } as Post) toast ({ description: '更新しました.' }) } catch (e) { const response = isApiError<{ mergeable?: boolean }> (e) ? e.response : undefined if (response?.status !== 409) { if (applyValidationError (e)) { toast ({ description: '更新はできなかったよ……' }) return } toast ({ title: '失敗……', description: '入力を確認してください.' }) return } const action = await dialogue.choice ({ title: '競合が発生しました.', description: (

ほかの耕作員が先に更新してゐます.

現在の変更をどう扱ひますか?

), choices: [...(response?.data?.mergeable ? [{ value: 'merge', label: '差分をマージ' }] : []), { value: 'overwrite', label: '強制上書き', variant: 'danger' }] }) if (action === 'merge') { // TODO: 差分 UI await update ({ id: post.id, title, tags, parentPostIds, duration: videoFlg ? duration : null, originalCreatedFrom, originalCreatedBefore }, { baseVersionNo: post.versionNo, merge: true }) return } if (action === 'overwrite') { await update ({ id: post.id, title, tags, parentPostIds, duration: videoFlg ? duration : null, originalCreatedFrom, originalCreatedBefore }, { baseVersionNo: post.versionNo, force: true }) return } } } const handleSubmit = async (e: FormEvent) => { e.preventDefault () setDisabled (true) try { await update ({ id: post.id, title, tags, parentPostIds, duration: videoFlg ? duration : null, originalCreatedFrom, originalCreatedBefore }, { baseVersionNo: post.versionNo }) } finally { setDisabled (false) } } useEffect (() => { setTags (tagsToStr (post.tags)) setDuration (videoMsToDurationValue (post.videoMs)) }, [post]) return (
{/* タイトル */} {({ invalid }) => ( setTitle (e.target.value)}/>)} {/* 親投稿 */} {({ describedBy, invalid }) => ( setParentPostIds (e.target.value)} aria-describedby={describedBy} aria-invalid={invalid} className={inputClass (invalid)}/>)} {/* タグ */} {/* オリジナルの作成日時 */} {/* 動画時間 */} {videoFlg && ( {({ invalid }) => ( setDuration (e.target.value)}/>)} )} {/* 送信 */} ) } export default PostEditForm