【再掲】タグの局所記載 (#351) (#380)

#353 を再開できなかったので.

Reviewed-on: #380
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #380 でマージされました.
このコミットが含まれているのは:
2026-07-03 03:23:36 +09:00
committed by みてるぞ
コミット fdf652951a
32個のファイルの変更920行の追加63行の削除
+5 -1
ファイルの表示
@@ -91,9 +91,13 @@ const PostHistoryPage: FC = () => {
.filter (p => p.type !== 'removed')
.map (p => p.id)
.join (' ')
const duration =
change.videoMs.current == null
? null
: String (change.videoMs.current / 1_000)
const originalCreatedFrom = change.originalCreatedFrom.current
const originalCreatedBefore = change.originalCreatedBefore.current
await updatePost ({ id, title, tags, parentPostIds,
await updatePost ({ id, title, tags, duration, parentPostIds,
originalCreatedFrom, originalCreatedBefore },
{ force: true })
+18
ファイルの表示
@@ -62,6 +62,24 @@ describe ('PostNewPage', () => {
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '投稿成功!' })
})
it ('preserves duration while the video tag is temporarily removed', () => {
api.apiGet.mockResolvedValue ([])
renderWithProviders (<PostNewPage user={buildUser ({ role: 'member' })}/>)
const tags = screen.getAllByRole ('textbox')[3]
fireEvent.change (tags, { target: { value: '動画' } })
fireEvent.change (screen.getByRole ('spinbutton'), { target: { value: '180.5' } })
fireEvent.change (tags, { target: { value: 'general-tag' } })
expect (screen.queryByRole ('spinbutton')).not.toBeInTheDocument ()
fireEvent.change (tags, {
target: { value: '動画 general-tag' },
})
expect (screen.getByRole ('spinbutton')).toHaveValue (180.5)
})
it ('shows 422 validation errors for post fields', async () => {
api.apiGet.mockResolvedValue ([])
api.isApiError.mockReturnValue (true)
+22 -2
ファイルの表示
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState, useRef } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useNavigate } from 'react-router-dom'
@@ -25,7 +25,7 @@ import type { User } from '@/types'
type Props = { user: User | null }
type PostFormField =
'url' | 'title' | 'tags' | 'parentPostIds' | 'originalCreatedAt' | 'thumbnail'
'url' | 'title' | 'tags' | 'parentPostIds' | 'videoMs' | 'originalCreatedAt' | 'thumbnail'
const PostNewPage: FC<Props> = ({ user }) => {
@@ -40,6 +40,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null)
const [parentPostIds, setParentPostIds] = useState ('')
const [tags, setTags] = useState ('')
const [duration, setDuration] = useState ('')
const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true)
const [thumbnailFile, setThumbnailFile] = useState<File | null> (null)
const [thumbnailLoading, setThumbnailLoading] = useState (false)
@@ -51,6 +52,9 @@ const PostNewPage: FC<Props> = ({ user }) => {
const previousURLRef = useRef ('')
const thumbnailPreviewRef = useRef ('')
const videoFlg =
useMemo (() => tags.split (/\s+/).some (tag => tag.replace (/\[.*\]$/, '') === '動画'),
[tags])
const handleSubmit = async () => {
clearValidationErrors ()
@@ -60,6 +64,8 @@ const PostNewPage: FC<Props> = ({ user }) => {
formData.append ('url', url)
formData.append ('tags', tags)
formData.append ('parent_post_ids', parentPostIds)
if (videoFlg && duration !== '')
formData.append ('duration', duration)
if (thumbnailFile)
formData.append ('thumbnail', thumbnailFile)
if (originalCreatedFrom)
@@ -233,6 +239,20 @@ const PostNewPage: FC<Props> = ({ user }) => {
setOriginalCreatedBefore={setOriginalCreatedBefore}
errors={fieldErrors.originalCreatedAt}/>
{/* 動画時間 */}
{(videoFlg &&
<FormField label="動画時間" messages={fieldErrors.videoMs}>
{({ invalid }) => (
<input
type="number"
min="0.001"
step="0.001"
value={duration}
onChange={e => setDuration (e.target.value)}
aria-invalid={invalid}
className={inputClass (invalid)}/>)}
</FormField>)}
{/* 送信 */}
<Button onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"