ファイル
btrc-hub/frontend/src/components/posts/import/PostImportRowDialog.tsx
T
2026-07-13 01:31:45 +09:00

263 行
8.3 KiB
TypeScript
Raw Blame 履歴

このファイルには曖昧(ambiguous)なUnicode文字が含まれてゐます
このファイルには,他の文字と見間違える可能性があるUnicode文字が含まれてゐます. それが意図的なものと考えられる場合は,この警告を無視して構ゐません. それらの文字を表示するにはエスケープボタンを使用します.
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
import FieldError from '@/components/common/FieldError'
import FieldWarning from '@/components/common/FieldWarning'
import FormField from '@/components/common/FormField'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { Button } from '@/components/ui/button'
import { Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle } from '@/components/ui/dialog'
import { inputClass } from '@/lib/utils'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import type { PostImportOrigin,
PostImportRow } from '@/lib/postImportSession'
type Draft = {
url: string
title: string
thumbnailBase: string
originalCreatedFrom: string
originalCreatedBefore: string
duration: string
tags: string
parentPostIds: string }
type Props = {
open: boolean
row: PostImportRow | null
saving: boolean
onOpenChange: (open: boolean) => void
onSave: (draft: Draft) => Promise<boolean> }
const originOf = (
row: PostImportRow,
field: string,
): PostImportOrigin =>
row.provenance[field] ?? 'automatic'
const originalCreatedOrigin = (row: PostImportRow): PostImportOrigin =>
originOf (row, 'originalCreatedFrom') === 'manual'
|| originOf (row, 'originalCreatedBefore') === 'manual'
? 'manual'
: 'automatic'
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 groupedMessages = (
...values: Array<string[] | undefined>
): string[] =>
values.flatMap (value => value ?? [])
const PostImportRowDialog: FC<Props> = (
{ open, row, saving, onOpenChange, onSave },
) => {
const [draft, setDraft] = useState<Draft | null> (null)
useEffect (() => {
if (open && row)
setDraft (buildDraft (row))
}, [open, row])
if (!(row) || !(draft))
return null
const update = <Key extends keyof Draft,> (
key: Key,
value: Draft[Key],
) => {
setDraft (current =>
current ? { ...current, [key]: value } : current)
}
const save = async () => {
if (await onSave (draft))
onOpenChange (false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[calc(100dvh-1rem)] max-w-3xl flex-col
overflow-hidden p-0">
<DialogHeader className="shrink-0 px-6 pb-4 pt-7">
<DialogTitle>稿</DialogTitle>
<DialogDescription>
稿 {row.sourceRow}
</DialogDescription>
</DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-6">
<div className="grid gap-6 md:grid-cols-[7rem_minmax(0,1fr)]">
<div className="space-y-3">
<ThumbnailPreview
url={draft.thumbnailBase}
className="h-28 w-28"/>
<div className="flex flex-wrap gap-2">
<PostImportStatusBadge value={effectivePostImportStatus (row)}/>
</div>
</div>
<div className="space-y-4">
<DialogTextField
label="URL"
value={draft.url}
origin={originOf (row, 'url')}
warnings={row.fieldWarnings.url}
errors={groupedMessages (row.validationErrors.url, row.importErrors?.url)}
onChange={value => update ('url', value)}/>
<DialogTextField
label="タイトル"
value={draft.title}
origin={originOf (row, 'title')}
warnings={row.fieldWarnings.title}
errors={groupedMessages (row.validationErrors.title, row.importErrors?.title)}
onChange={value => update ('title', value)}/>
<DialogTextField
label="サムネール基底 URL"
value={draft.thumbnailBase}
origin={originOf (row, 'thumbnailBase')}
warnings={row.fieldWarnings.thumbnailBase}
errors={groupedMessages (
row.validationErrors.thumbnailBase,
row.importErrors?.thumbnailBase,
)}
onChange={value => update ('thumbnailBase', value)}/>
<PostOriginalCreatedTimeField
labelAddon={<PostImportStatusBadge value={originalCreatedOrigin (row)}/>}
originalCreatedFrom={draft.originalCreatedFrom || null}
setOriginalCreatedFrom={value => update ('originalCreatedFrom', value ?? '')}
originalCreatedBefore={draft.originalCreatedBefore || null}
setOriginalCreatedBefore={value => update ('originalCreatedBefore', value ?? '')}
errors={groupedMessages (
row.validationErrors.originalCreatedAt,
row.validationErrors.originalCreatedFrom,
row.validationErrors.originalCreatedBefore,
row.importErrors?.originalCreatedAt,
row.importErrors?.originalCreatedFrom,
row.importErrors?.originalCreatedBefore,
)}/>
<DialogTextField
label="動画時間"
value={draft.duration}
origin={originOf (row, 'duration')}
errors={groupedMessages (
row.validationErrors.duration,
row.validationErrors.videoMs,
row.importErrors?.duration,
row.importErrors?.videoMs,
)}
onChange={value => update ('duration', value)}/>
<DialogAreaField
label="タグ"
value={draft.tags}
origin={originOf (row, 'tags')}
warnings={row.fieldWarnings.tags}
errors={groupedMessages (row.validationErrors.tags, row.importErrors?.tags)}
onChange={value => update ('tags', value)}/>
<DialogTextField
label="親投稿"
value={draft.parentPostIds}
origin={originOf (row, 'parentPostIds')}
errors={groupedMessages (
row.validationErrors.parentPostIds,
row.importErrors?.parentPostIds,
)}
onChange={value => update ('parentPostIds', value)}/>
<FieldWarning messages={row.baseWarnings}/>
<FieldError messages={row.validationErrors.base}/>
<FieldError messages={row.importErrors?.base}/>
</div>
</div>
</div>
<DialogFooter className="shrink-0 px-6 pb-6 pt-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange (false)}>
</Button>
<Button
type="button"
onClick={save}
disabled={saving}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>)
}
const DialogTextField = (
{ label, value, origin, warnings, errors, onChange }: {
label: string
value: string
origin: PostImportOrigin
warnings?: string[]
errors?: string[]
onChange: (value: string) => void },
) => (
<FormField label={<DialogLabel label={label} origin={origin}/>} 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 DialogAreaField = (
{ label, value, origin, warnings, errors, onChange }: {
label: string
value: string
origin: PostImportOrigin
warnings?: string[]
errors?: string[]
onChange: (value: string) => void },
) => (
<FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
{({ describedBy, invalid }) => (
<>
<textarea
value={value}
rows={4}
onChange={ev => onChange (ev.target.value)}
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}/>
<FieldWarning messages={warnings}/>
</>)}
</FormField>)
const DialogLabel = (
{ label, origin }: {
label: string
origin: PostImportOrigin },
) => (
<span className="flex items-center gap-2">
<span>{label}</span>
<PostImportStatusBadge value={origin}/>
</span>)
export default PostImportRowDialog