このコミットが含まれているのは:
2026-07-16 20:05:09 +09:00
コミット 90d8d3ff08
27個のファイルの変更725行の追加257行の削除
+5 -5
ファイルの表示
@@ -19,8 +19,8 @@ const toastApi = vi.hoisted (() => ({
vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
vi.mock ('@/components/dialogues/DialogueProvider', () => ({
useDialogue: () => ({
vi.mock ('@/lib/dialogues/useDialogue', () => ({
default: () => ({
choice: vi.fn (),
}),
}))
@@ -78,15 +78,15 @@ describe ('PostEditForm', () => {
render (<PostEditForm post={post} onSave={vi.fn ()}/>)
expect (screen.getByRole ('spinbutton')).toHaveValue (180.5)
expect (screen.getByPlaceholderText ('例: 2 / 2.5 / 1:23')).toHaveValue ('180.5')
const tags = screen.getAllByRole ('textbox')[2]
fireEvent.change (tags, { target: { value: 'general-tag' } })
expect (screen.queryByRole ('spinbutton')).not.toBeInTheDocument ()
expect (screen.queryByPlaceholderText ('例: 2 / 2.5 / 1:23')).not.toBeInTheDocument ()
fireEvent.change (tags, {
target: { value: '動画 general-tag' },
})
expect (screen.getByRole ('spinbutton')).toHaveValue (180.5)
expect (screen.getByPlaceholderText ('例: 2 / 2.5 / 1:23')).toHaveValue ('180.5')
})
})
+22 -40
ファイルの表示
@@ -1,15 +1,16 @@
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 PostDurationField from '@/components/posts/PostDurationField'
import PostTagsField from '@/components/posts/PostTagsField'
import PostTextField from '@/components/posts/PostTextField'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { isApiError } from '@/lib/api'
import useDialogue from '@/lib/dialogues/useDialogue'
import { updatePost } from '@/lib/posts'
import { inputClass, msToTime } from '@/lib/utils'
import { msToTime } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
import type { FC, FormEvent } from 'react'
@@ -157,32 +158,20 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => {
<form onSubmit={handleSubmit} className="max-w-xl pt-2 space-y-4">
<FieldError messages={baseErrors}/>
{/* タイトル */}
<FormField label="タイトル">
{({ invalid }) => (
<input
type="text"
disabled={disabled}
className={inputClass (invalid)}
value={title ?? ''}
onChange={e => setTitle (e.target.value)}/>)}
</FormField>
<PostTextField
label="タイトル"
value={title ?? ''}
disabled={disabled}
onChange={setTitle}/>
{/* 親投稿 */}
<FormField label="親投稿" messages={fieldErrors.parentPostIds}>
{({ describedBy, invalid }) => (
<input
type="text"
disabled={disabled}
value={parentPostIds}
onChange={e => setParentPostIds (e.target.value)}
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}/>)}
</FormField>
<PostTextField
label="親投稿"
value={parentPostIds}
disabled={disabled}
errors={fieldErrors.parentPostIds}
onChange={setParentPostIds}/>
{/* タグ */}
<PostFormTagsArea
<PostTagsField
disabled={disabled}
tags={tags}
setTags={setTags}
@@ -197,19 +186,12 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => {
setOriginalCreatedBefore={setOriginalCreatedBefore}
errors={fieldErrors.originalCreatedAt}/>
{/* 動画時間 */}
{videoFlg && (
<FormField label="動画時間" messages={fieldErrors.videoMs}>
{({ invalid }) => (
<input
type="number"
min="0.001"
step="0.001"
disabled={disabled}
className={inputClass (invalid)}
value={duration}
onChange={e => setDuration (e.target.value)}/>)}
</FormField>)}
<PostDurationField
value={duration}
disabled={disabled}
errors={fieldErrors.videoMs}
onChange={setDuration}/>)}
{/* 送信 */}
<Button type="submit" disabled={disabled}>
+28
ファイルの表示
@@ -0,0 +1,28 @@
import PostTextField from '@/components/posts/PostTextField'
import type { FC } from 'react'
type Props = {
value: string
onChange: (value: string) => void
errors?: string[]
disabled?: boolean }
const PostDurationField: FC<Props> = (
{ value,
onChange,
errors,
disabled },
) => (
<PostTextField
label="動画時間"
value={value}
onChange={onChange}
errors={errors}
disabled={disabled}
type="text"
placeholder="例: 2 / 2.5 / 1:23"/>
)
export default PostDurationField
+29
ファイルの表示
@@ -0,0 +1,29 @@
import PostFormTagsArea from '@/components/PostFormTagsArea'
import FieldWarning from '@/components/common/FieldWarning'
import type { ComponentPropsWithoutRef, FC } from 'react'
type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange'> & {
tags: string
setTags: (tags: string) => void
warnings?: string[]
errors?: string[] }
const PostTagsField: FC<Props> = (
{ tags,
setTags,
warnings,
errors,
...rest },
) => (
<div className="space-y-2">
<PostFormTagsArea
{...rest}
tags={tags}
setTags={setTags}
errors={errors}/>
<FieldWarning messages={warnings}/>
</div>)
export default PostTagsField
+49
ファイルの表示
@@ -0,0 +1,49 @@
import FieldWarning from '@/components/common/FieldWarning'
import FormField from '@/components/common/FormField'
import { inputClass } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
type Props = {
label: string
value: string
onChange: (value: string) => void
warnings?: string[]
errors?: string[]
disabled?: boolean
type?: string
placeholder?: string
className?: string
after?: ReactNode }
const PostTextField: FC<Props> = (
{ label,
value,
onChange,
warnings,
errors,
disabled,
type = 'text',
placeholder,
className,
after },
) => (
<FormField label={label} messages={errors}>
{({ describedBy, invalid }) => (
<>
<input
type={type}
value={value}
disabled={disabled}
placeholder={placeholder}
onChange={ev => onChange (ev.target.value)}
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid, className)}/>
<FieldWarning messages={warnings}/>
{after}
</>)}
</FormField>)
export default PostTextField
@@ -2,14 +2,15 @@ import { useEffect, useState } from 'react'
import type { FC } from 'react'
type Props = {
url: string
alt?: string
className?: string }
url: string
alt?: string
className?: string }
const ThumbnailPreview: FC<Props> = ({ url, alt = 'サムネール', className = 'h-16 w-16' }) => {
const PostThumbnailPreview: FC<Props> = (
{ url, alt = 'サムネール', className = 'h-16 w-16' },
) => {
const [failed, setFailed] = useState (false)
useEffect (() => {
@@ -46,4 +47,4 @@ const ThumbnailPreview: FC<Props> = ({ url, alt = 'サムネール', className =
onError={() => setFailed (true)}/>)
}
export default ThumbnailPreview
export default PostThumbnailPreview
+14 -58
ファイルの表示
@@ -3,10 +3,10 @@ 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 PostDurationField from '@/components/posts/PostDurationField'
import PostTagsField from '@/components/posts/PostTagsField'
import PostTextField from '@/components/posts/PostTextField'
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
import type { FC } from 'react'
@@ -153,13 +153,13 @@ const PostImportRowForm: FC<Props> = (
<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
<PostThumbnailPreview
url={draft.thumbnailBase}
className="h-28 w-28"/>
</div>
<div className="space-y-4">
<PostImportTextField
<PostTextField
label="URL"
value={draft.url}
warnings={displayRow.fieldWarnings.url}
@@ -167,7 +167,7 @@ const PostImportRowForm: FC<Props> = (
displayRow.validationErrors.url,
displayRow.importErrors?.url)}
onChange={value => update ('url', value)}/>
<PostImportTextField
<PostTextField
label="タイトル"
value={draft.title}
warnings={displayRow.fieldWarnings.title}
@@ -175,7 +175,7 @@ const PostImportRowForm: FC<Props> = (
displayRow.validationErrors.title,
displayRow.importErrors?.title)}
onChange={value => update ('title', value)}/>
<PostImportTextField
<PostTextField
label="サムネール基底 URL"
value={draft.thumbnailBase}
warnings={displayRow.fieldWarnings.thumbnailBase}
@@ -195,8 +195,7 @@ const PostImportRowForm: FC<Props> = (
displayRow.importErrors?.originalCreatedAt,
displayRow.importErrors?.originalCreatedFrom,
displayRow.importErrors?.originalCreatedBefore)}/>
<PostImportTextField
label="動画時間"
<PostDurationField
value={draft.duration}
errors={groupedMessages (
displayRow.validationErrors.duration,
@@ -204,15 +203,15 @@ const PostImportRowForm: FC<Props> = (
displayRow.importErrors?.duration,
displayRow.importErrors?.videoMs)}
onChange={value => update ('duration', value)}/>
<PostImportAreaField
label="タグ"
value={draft.tags}
<PostTagsField
tags={draft.tags}
setTags={value => update ('tags', value)}
warnings={displayRow.fieldWarnings.tags}
errors={groupedMessages (
displayRow.validationErrors.tags,
displayRow.importErrors?.tags)}
onChange={value => update ('tags', value)}/>
<PostImportTextField
rows={4}/>
<PostTextField
label="親投稿"
value={draft.parentPostIds}
errors={groupedMessages (
@@ -229,49 +228,6 @@ const PostImportRowForm: FC<Props> = (
</>)
}
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 }
+7 -7
ファイルの表示
@@ -1,9 +1,9 @@
import { Button } from '@/components/ui/button'
import { cn, originalCreatedAtString } from '@/lib/utils'
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { canEditReviewRow } from '@/lib/postImportSession'
import { cn, originalCreatedAtString } from '@/lib/utils'
import type { FC } from 'react'
@@ -39,7 +39,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit, editDisabled }) => {
<div className="space-y-1">
<div className="text-sm font-medium">#{row.sourceRow}</div>
</div>
<ThumbnailPreview
<PostThumbnailPreview
url={String (row.attributes.thumbnailBase ?? '')}
className="h-16 w-16"/>
<div className="min-w-0 space-y-1">
@@ -69,7 +69,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit, editDisabled }) => {
type="button"
variant="outline"
onClick={onEdit}
disabled={row.importStatus === 'created' || editDisabled === true}>
disabled={!(canEditReviewRow (row)) || editDisabled === true}>
</Button>
</div>
@@ -80,7 +80,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit, editDisabled }) => {
'space-y-3 rounded-lg border p-4 md:hidden',
'transition-shadow hover:shadow-sm')}>
<div className="flex items-start gap-3">
<ThumbnailPreview
<PostThumbnailPreview
url={String (row.attributes.thumbnailBase ?? '')}
className="h-20 w-20 shrink-0"/>
<div className="min-w-0 flex-1 space-y-2">
@@ -110,7 +110,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit, editDisabled }) => {
type="button"
variant="outline"
onClick={onEdit}
disabled={row.importStatus === 'created' || editDisabled === true}>
disabled={!(canEditReviewRow (row)) || editDisabled === true}>
</Button>
</div>