ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

172 lines
4.8 KiB

  1. import { useEffect, useState } from 'react'
  2. import PostFormTagsArea from '@/components/PostFormTagsArea'
  3. import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
  4. import Label from '@/components/common/Label'
  5. import { useDialogue } from '@/components/dialogues/DialogueProvider'
  6. import { Button } from '@/components/ui/button'
  7. import { toast } from '@/components/ui/use-toast'
  8. import { isApiError } from '@/lib/api'
  9. import { updatePost } from '@/lib/posts'
  10. import { msToTime } from '@/lib/utils'
  11. import type { FC, FormEvent } from 'react'
  12. import type { Post, Tag } from '@/types'
  13. const tagsToStr = (tags: Tag[]): string => {
  14. const result: Tag[] = []
  15. const walk = (tag: Tag) => {
  16. const { children, ...rest } = tag
  17. result.push (rest)
  18. children?.forEach (walk)
  19. }
  20. tags.filter (t => t.category !== 'nico').forEach (walk)
  21. return [...(new Set (result.map (t => `${ t.name }${ t.sections.map (s => `[${ msToTime (s.beginMs) }-${ msToTime (s.endMs) }]`).join ('') }`)))].join (' ')
  22. }
  23. type Props = { post: Post
  24. onSave: (newPost: Post) => void }
  25. const PostEditForm: FC<Props> = ({ post, onSave }) => {
  26. const [disabled, setDisabled] = useState (false)
  27. const [originalCreatedBefore, setOriginalCreatedBefore] =
  28. useState<string | null> (post.originalCreatedBefore)
  29. const [originalCreatedFrom, setOriginalCreatedFrom] =
  30. useState<string | null> (post.originalCreatedFrom)
  31. const [parentPostIds, setParentPostIds] =
  32. useState ((post.parentPosts ?? []).map (p => p.id).join (' '))
  33. const [tags, setTags] = useState<string> ('')
  34. const [title, setTitle] = useState (post.title)
  35. const dialogue = useDialogue ()
  36. const update = async (...args: Parameters<typeof updatePost>) => {
  37. try
  38. {
  39. const data = await updatePost (...args)
  40. onSave ({ ...post,
  41. versionNo: data.versionNo,
  42. title: data.title,
  43. tags: data.tags,
  44. parentPosts: data.parentPosts,
  45. childPosts: data.childPosts,
  46. siblingPosts: data.siblingPosts,
  47. originalCreatedFrom: data.originalCreatedFrom,
  48. originalCreatedBefore: data.originalCreatedBefore } as Post)
  49. toast ({ description: '更新しました.' })
  50. }
  51. catch (e)
  52. {
  53. const response = isApiError<{ mergeable?: boolean }> (e) ? e.response : undefined
  54. if (response?.status !== 409)
  55. {
  56. toast ({ description: '更新はできなかったよ……' })
  57. return
  58. }
  59. const action = await dialogue.choice ({
  60. title: '競合が発生しました.',
  61. description: (
  62. <div>
  63. <p>ほかの耕作員が先に更新してゐます.</p>
  64. <p>現在の変更をどう扱ひますか?</p>
  65. </div>),
  66. choices: [...(response?.data?.mergeable ? [{ value: 'merge', label: '差分をマージ' }] : []),
  67. { value: 'overwrite', label: '強制上書き', variant: 'danger' }] })
  68. if (action === 'merge')
  69. {
  70. // TODO: 差分 UI
  71. await update ({ id: post.id, title, tags, parentPostIds,
  72. originalCreatedFrom, originalCreatedBefore },
  73. { baseVersionNo: post.versionNo, merge: true })
  74. return
  75. }
  76. if (action === 'overwrite')
  77. {
  78. await update ({ id: post.id, title, tags, parentPostIds,
  79. originalCreatedFrom, originalCreatedBefore },
  80. { baseVersionNo: post.versionNo, force: true })
  81. return
  82. }
  83. }
  84. }
  85. const handleSubmit = async (e: FormEvent) => {
  86. e.preventDefault ()
  87. setDisabled (true)
  88. try
  89. {
  90. await update ({ id: post.id, title, tags, parentPostIds,
  91. originalCreatedFrom, originalCreatedBefore },
  92. { baseVersionNo: post.versionNo })
  93. }
  94. finally
  95. {
  96. setDisabled (false)
  97. }
  98. }
  99. useEffect (() => {
  100. setTags(tagsToStr (post.tags))
  101. }, [post])
  102. return (
  103. <form onSubmit={handleSubmit} className="max-w-xl pt-2 space-y-4">
  104. {/* タイトル */}
  105. <div>
  106. <Label>タイトル</Label>
  107. <input
  108. type="text"
  109. disabled={disabled}
  110. className="w-full border rounded p-2"
  111. value={title ?? ''}
  112. onChange={ev => setTitle (ev.target.value)}/>
  113. </div>
  114. {/* 親投稿 */}
  115. <div>
  116. <Label>親投稿</Label>
  117. <input
  118. type="text"
  119. disabled={disabled}
  120. value={parentPostIds}
  121. onChange={e => setParentPostIds (e.target.value)}
  122. className="w-full border p-2 rounded"/>
  123. </div>
  124. {/* タグ */}
  125. <PostFormTagsArea
  126. disabled={disabled}
  127. tags={tags}
  128. setTags={setTags}/>
  129. {/* オリジナルの作成日時 */}
  130. <PostOriginalCreatedTimeField
  131. disabled={disabled}
  132. originalCreatedFrom={originalCreatedFrom}
  133. setOriginalCreatedFrom={setOriginalCreatedFrom}
  134. originalCreatedBefore={originalCreatedBefore}
  135. setOriginalCreatedBefore={setOriginalCreatedBefore}/>
  136. {/* 送信 */}
  137. <Button
  138. type="submit"
  139. disabled={disabled}>
  140. 更新
  141. </Button>
  142. </form>)
  143. }
  144. export default PostEditForm