このコミットが含まれているのは:
@@ -1,258 +0,0 @@
|
||||
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import FieldWarning from '@/components/common/FieldWarning'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
|
||||
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 { PostImportResetSnapshot,
|
||||
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
|
||||
messageRow?: PostImportRow | null
|
||||
saving: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: (
|
||||
payload: { draft: Draft
|
||||
resetRequested: boolean
|
||||
resetSnapshot: PostImportResetSnapshot },
|
||||
) => Promise<boolean> }
|
||||
|
||||
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 buildResetDraft = (row: PostImportRow): Draft => ({
|
||||
url: row.resetSnapshot.url,
|
||||
title: String (row.resetSnapshot.attributes.title ?? ''),
|
||||
thumbnailBase: String (row.resetSnapshot.attributes.thumbnailBase ?? ''),
|
||||
originalCreatedFrom: String (row.resetSnapshot.attributes.originalCreatedFrom ?? ''),
|
||||
originalCreatedBefore: String (row.resetSnapshot.attributes.originalCreatedBefore ?? ''),
|
||||
duration: String (row.resetSnapshot.attributes.duration ?? ''),
|
||||
tags: String (row.resetSnapshot.attributes.tags ?? ''),
|
||||
parentPostIds: String (row.resetSnapshot.attributes.parentPostIds ?? '') })
|
||||
|
||||
const groupedMessages = (
|
||||
...values: Array<string[] | undefined>
|
||||
): string[] =>
|
||||
values.flatMap (value => value ?? [])
|
||||
|
||||
|
||||
const PostImportRowDialog: FC<Props> = (
|
||||
{ open, row, messageRow, saving, onOpenChange, onSave },
|
||||
) => {
|
||||
const [draft, setDraft] = useState<Draft | null> (null)
|
||||
const [resetRequested, setResetRequested] = useState (false)
|
||||
|
||||
useEffect (() => {
|
||||
if (open && row != null)
|
||||
{
|
||||
setDraft (buildDraft (row))
|
||||
setResetRequested (false)
|
||||
}
|
||||
}, [open, row])
|
||||
|
||||
if (row == null || draft == null)
|
||||
return null
|
||||
|
||||
const displayRow = messageRow ?? row
|
||||
|
||||
const update = <Key extends keyof Draft,> (
|
||||
key: Key,
|
||||
value: Draft[Key],
|
||||
) => {
|
||||
setDraft (current =>
|
||||
current != null ? { ...current, [key]: value } : current)
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
if (await onSave ({
|
||||
draft,
|
||||
resetRequested,
|
||||
resetSnapshot: row.resetSnapshot }))
|
||||
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 pb-4 pl-14 pr-6 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>
|
||||
|
||||
<div className="space-y-4">
|
||||
<DialogTextField
|
||||
label="URL"
|
||||
value={draft.url}
|
||||
warnings={displayRow.fieldWarnings.url}
|
||||
errors={groupedMessages (displayRow.validationErrors.url,
|
||||
displayRow.importErrors?.url)}
|
||||
onChange={value => update ('url', value)}/>
|
||||
<DialogTextField
|
||||
label="タイトル"
|
||||
value={draft.title}
|
||||
warnings={displayRow.fieldWarnings.title}
|
||||
errors={groupedMessages (displayRow.validationErrors.title,
|
||||
displayRow.importErrors?.title)}
|
||||
onChange={value => update ('title', value)}/>
|
||||
<DialogTextField
|
||||
label="サムネール基底 URL"
|
||||
value={draft.thumbnailBase}
|
||||
warnings={displayRow.fieldWarnings.thumbnailBase}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.thumbnailBase,
|
||||
displayRow.importErrors?.thumbnailBase)}
|
||||
onChange={value => update ('thumbnailBase', value)}/>
|
||||
<PostOriginalCreatedTimeField
|
||||
originalCreatedFrom={draft.originalCreatedFrom || null}
|
||||
setOriginalCreatedFrom={value => update ('originalCreatedFrom', value ?? '')}
|
||||
originalCreatedBefore={draft.originalCreatedBefore || null}
|
||||
setOriginalCreatedBefore={value => update ('originalCreatedBefore', value ?? '')}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.originalCreatedAt,
|
||||
displayRow.validationErrors.originalCreatedFrom,
|
||||
displayRow.validationErrors.originalCreatedBefore,
|
||||
displayRow.importErrors?.originalCreatedAt,
|
||||
displayRow.importErrors?.originalCreatedFrom,
|
||||
displayRow.importErrors?.originalCreatedBefore)}/>
|
||||
<DialogTextField
|
||||
label="動画時間"
|
||||
value={draft.duration}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.duration,
|
||||
displayRow.validationErrors.videoMs,
|
||||
displayRow.importErrors?.duration,
|
||||
displayRow.importErrors?.videoMs)}
|
||||
onChange={value => update ('duration', value)}/>
|
||||
<DialogAreaField
|
||||
label="タグ"
|
||||
value={draft.tags}
|
||||
warnings={displayRow.fieldWarnings.tags}
|
||||
errors={groupedMessages (displayRow.validationErrors.tags,
|
||||
displayRow.importErrors?.tags)}
|
||||
onChange={value => update ('tags', value)}/>
|
||||
<DialogTextField
|
||||
label="親投稿"
|
||||
value={draft.parentPostIds}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.parentPostIds,
|
||||
displayRow.importErrors?.parentPostIds)}
|
||||
onChange={value => update ('parentPostIds', value)}/>
|
||||
<FieldWarning messages={displayRow.baseWarnings}/>
|
||||
<FieldError messages={displayRow.validationErrors.base}/>
|
||||
<FieldError messages={displayRow.importErrors?.base}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="shrink-0 px-6 pb-6 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDraft (buildResetDraft (row))
|
||||
setResetRequested (true)
|
||||
}}>
|
||||
変更をリセット
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange (false)}>
|
||||
取消し
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={save}
|
||||
disabled={saving}>
|
||||
編輯内容を保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>)
|
||||
}
|
||||
|
||||
const DialogTextField = (
|
||||
{ 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 DialogAreaField = (
|
||||
{ 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}
|
||||
aria-invalid={invalid}
|
||||
className={inputClass (invalid)}/>
|
||||
<FieldWarning messages={warnings}/>
|
||||
</>)}
|
||||
</FormField>)
|
||||
|
||||
export default PostImportRowDialog
|
||||
@@ -0,0 +1,263 @@
|
||||
import { 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 ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useDialogue } from '@/lib/dialogues/useDialogue'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { PostImportResetSnapshot,
|
||||
PostImportRow } from '@/lib/postImportSession'
|
||||
|
||||
type Draft = {
|
||||
url: string
|
||||
title: string
|
||||
thumbnailBase: string
|
||||
originalCreatedFrom: string
|
||||
originalCreatedBefore: string
|
||||
duration: string
|
||||
tags: string
|
||||
parentPostIds: string }
|
||||
|
||||
type Props = {
|
||||
row: PostImportRow
|
||||
messageRow?: PostImportRow | null
|
||||
saving: boolean
|
||||
onCancel: () => void
|
||||
onSave: (
|
||||
payload: { draft: Draft
|
||||
resetRequested: boolean
|
||||
resetSnapshot: PostImportResetSnapshot },
|
||||
) => Promise<boolean> }
|
||||
|
||||
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 buildResetDraft = (row: PostImportRow): Draft => ({
|
||||
url: row.resetSnapshot.url,
|
||||
title: String (row.resetSnapshot.attributes.title ?? ''),
|
||||
thumbnailBase: String (row.resetSnapshot.attributes.thumbnailBase ?? ''),
|
||||
originalCreatedFrom: String (row.resetSnapshot.attributes.originalCreatedFrom ?? ''),
|
||||
originalCreatedBefore: String (row.resetSnapshot.attributes.originalCreatedBefore ?? ''),
|
||||
duration: String (row.resetSnapshot.attributes.duration ?? ''),
|
||||
tags: String (row.resetSnapshot.attributes.tags ?? ''),
|
||||
parentPostIds: String (row.resetSnapshot.attributes.parentPostIds ?? '') })
|
||||
|
||||
const groupedMessages = (...values: (string[] | undefined)[]): string[] =>
|
||||
values.flatMap (value => value ?? [])
|
||||
|
||||
const sameDraft = (left: Draft, right: Draft): boolean =>
|
||||
left.url === right.url
|
||||
&& left.title === right.title
|
||||
&& left.thumbnailBase === right.thumbnailBase
|
||||
&& left.originalCreatedFrom === right.originalCreatedFrom
|
||||
&& left.originalCreatedBefore === right.originalCreatedBefore
|
||||
&& left.duration === right.duration
|
||||
&& left.tags === right.tags
|
||||
&& left.parentPostIds === right.parentPostIds
|
||||
|
||||
|
||||
const PostImportRowForm: FC<Props> = (
|
||||
{ row, messageRow, saving, onCancel, onSave },
|
||||
) => {
|
||||
const dialogue = useDialogue ()
|
||||
|
||||
const [draft, setDraft] = useState<Draft> (() => buildDraft (row))
|
||||
const [resetRequested, setResetRequested] = useState (false)
|
||||
|
||||
const displayRow = messageRow ?? row
|
||||
const resetDraft = buildResetDraft (row)
|
||||
const resetDisabled = saving || sameDraft (draft, resetDraft)
|
||||
|
||||
const update = <Key extends keyof Draft,> (
|
||||
key: Key,
|
||||
value: Draft[Key],
|
||||
) => {
|
||||
setDraft (current => ({ ...current, [key]: value }))
|
||||
}
|
||||
|
||||
const reset = async () => {
|
||||
if (resetDisabled)
|
||||
return
|
||||
|
||||
const confirmed = await dialogue.confirm ({
|
||||
title: '変更をリセットしますか?',
|
||||
description: '現在の URL に対する自動取得直後の内容へ戻します.',
|
||||
confirmText: 'リセット',
|
||||
cancelText: '取消',
|
||||
variant: 'danger' })
|
||||
if (!(confirmed))
|
||||
return
|
||||
|
||||
setDraft (resetDraft)
|
||||
setResetRequested (true)
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
if (await onSave ({
|
||||
draft,
|
||||
resetRequested,
|
||||
resetSnapshot: row.resetSnapshot }))
|
||||
onCancel ()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<div className="space-y-4">
|
||||
<PostImportTextField
|
||||
label="URL"
|
||||
value={draft.url}
|
||||
warnings={displayRow.fieldWarnings.url}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.url,
|
||||
displayRow.importErrors?.url)}
|
||||
onChange={value => update ('url', value)}/>
|
||||
<PostImportTextField
|
||||
label="タイトル"
|
||||
value={draft.title}
|
||||
warnings={displayRow.fieldWarnings.title}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.title,
|
||||
displayRow.importErrors?.title)}
|
||||
onChange={value => update ('title', value)}/>
|
||||
<PostImportTextField
|
||||
label="サムネール基底 URL"
|
||||
value={draft.thumbnailBase}
|
||||
warnings={displayRow.fieldWarnings.thumbnailBase}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.thumbnailBase,
|
||||
displayRow.importErrors?.thumbnailBase)}
|
||||
onChange={value => update ('thumbnailBase', value)}/>
|
||||
<PostOriginalCreatedTimeField
|
||||
originalCreatedFrom={draft.originalCreatedFrom || null}
|
||||
setOriginalCreatedFrom={value => update ('originalCreatedFrom', value ?? '')}
|
||||
originalCreatedBefore={draft.originalCreatedBefore || null}
|
||||
setOriginalCreatedBefore={value => update ('originalCreatedBefore', value ?? '')}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.originalCreatedAt,
|
||||
displayRow.validationErrors.originalCreatedFrom,
|
||||
displayRow.validationErrors.originalCreatedBefore,
|
||||
displayRow.importErrors?.originalCreatedAt,
|
||||
displayRow.importErrors?.originalCreatedFrom,
|
||||
displayRow.importErrors?.originalCreatedBefore)}/>
|
||||
<PostImportTextField
|
||||
label="動画時間"
|
||||
value={draft.duration}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.duration,
|
||||
displayRow.validationErrors.videoMs,
|
||||
displayRow.importErrors?.duration,
|
||||
displayRow.importErrors?.videoMs)}
|
||||
onChange={value => update ('duration', value)}/>
|
||||
<PostImportAreaField
|
||||
label="タグ"
|
||||
value={draft.tags}
|
||||
warnings={displayRow.fieldWarnings.tags}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.tags,
|
||||
displayRow.importErrors?.tags)}
|
||||
onChange={value => update ('tags', value)}/>
|
||||
<PostImportTextField
|
||||
label="親投稿"
|
||||
value={draft.parentPostIds}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.parentPostIds,
|
||||
displayRow.importErrors?.parentPostIds)}
|
||||
onChange={value => update ('parentPostIds', value)}/>
|
||||
<FieldWarning messages={displayRow.baseWarnings}/>
|
||||
<FieldError messages={displayRow.validationErrors.base}/>
|
||||
<FieldError messages={displayRow.importErrors?.base}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col-reverse gap-2 px-6 pb-6 pt-4 sm:flex-row sm:justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => reset ()}
|
||||
disabled={resetDisabled}>
|
||||
変更をリセット
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={saving}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => save ()}
|
||||
disabled={saving}>
|
||||
編輯内容を保存
|
||||
</Button>
|
||||
</div>
|
||||
</>)
|
||||
}
|
||||
|
||||
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}
|
||||
aria-invalid={invalid}
|
||||
className={inputClass (invalid)}/>
|
||||
<FieldWarning messages={warnings}/>
|
||||
</>)}
|
||||
</FormField>)
|
||||
|
||||
export default PostImportRowForm
|
||||
export type { Draft as PostImportRowDraft }
|
||||
新しい課題から参照
ユーザをブロックする