このコミットが含まれているのは:
2026-07-15 22:04:47 +09:00
コミット 5f1d619139
8個のファイルの変更433行の追加354行の削除
+14
ファイルの表示
@@ -410,6 +410,20 @@ Good:
- For user-facing Japanese kanji spelling, do not normalize to
《当用漢字による書きかえ》; prefer original forms such as `編輯`.
- For user-facing Japanese ellipses, prefer `……` over ASCII `...`.
- Frontend dialogue work must use `@/lib/dialogues/useDialogue` as the
feature-facing entrypoint.
- Reuse the existing common dialogue API and common dialogue component
instead of building feature-local overlay, close button, header, footer,
focus handling, outside click handling, Escape handling, or confirmation
flows.
- Do not import `@/components/ui/dialog` directly in feature code to build a
one-off dialogue, and do not evade this rule with aliases such as
`Dialog as Dialogue`.
- Keep business-specific form content in feature code, and keep the visual
and behavioural dialogue shell in common code.
- Use British spelling `Dialogue` for project-defined dialogue identifiers.
Keep an exact third-party API spelling only at the external boundary where
compatibility requires it.
### Frontend TypeScript and TSX style
+16
ファイルの表示
@@ -108,6 +108,22 @@ pass or the remaining failure is clearly blocked.
third-party request outside the Rails API.
- For blob responses, pass `responseType: 'blob'` so the wrapper does not camelCase the body.
## Dialogues
- Feature code must use `@/lib/dialogues/useDialogue` as the entrypoint for
dialogue work.
- Reuse the existing common dialogue API and common dialogue component.
- Do not import `@/components/ui/dialog` directly in feature code to build a
bespoke dialogue, and do not evade this rule with aliases such as
`Dialog as Dialogue`.
- Keep business-specific form content in feature code, and keep the visual
and behavioural dialogue shell in common code.
- Reuse the common confirmation flow from `useDialogue` instead of building a
feature-local confirmation dialogue.
- Use British spelling `Dialogue` for project-defined dialogue identifiers.
Keep an exact third-party API spelling only at the external boundary where
compatibility requires it.
## Imports and aliases
- The `@` alias points to `frontend/src`.
+54
ファイルの表示
@@ -0,0 +1,54 @@
import { Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle } from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
type Props = {
open: boolean
onOpenChange: (open: boolean) => void
title: ReactNode
description?: ReactNode
className?: string
bodyClassName?: string
footerClassName?: string
footer?: ReactNode
children: ReactNode }
const CommonDialogue: FC<Props> = (
{ open,
onOpenChange,
title,
description,
className,
bodyClassName,
footerClassName,
footer,
children },
) => (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn ('px-6 pb-6 pt-7', className)}>
<DialogHeader className="pl-8">
<DialogTitle>{title}</DialogTitle>
{description != null && (
<DialogDescription asChild>
<div>{description}</div>
</DialogDescription>)}
</DialogHeader>
<div className={bodyClassName}>{children}</div>
{footer != null && (
<DialogFooter className={footerClassName}>
{footer}
</DialogFooter>)}
</DialogContent>
</Dialog>)
export default CommonDialogue
+52 -64
ファイルの表示
@@ -1,12 +1,7 @@
import { createContext, useCallback, useContext, useMemo, useState } from 'react'
import CommonDialogue from '@/components/dialogues/CommonDialogue'
import { Button } from '@/components/ui/button'
import { Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle } from '@/components/ui/dialog'
import type { FC, ReactNode } from 'react'
@@ -111,67 +106,60 @@ const DialogueProvider: FC<Props> = ({ children }) => {
<DialogueContext.Provider value={api}>
{children}
<Dialog
open={Boolean (active)}
onOpenChange={open => {
if (!(open))
closeActive (active?.kind !== 'confirm' && null)
}}>
{active && (
<DialogContent className="px-6 pb-6 pt-7">
<DialogHeader className="pl-8">
<DialogTitle>{active.options.title}</DialogTitle>
{active.options.description && (
<DialogDescription asChild>
<div>{active.options.description}</div>
</DialogDescription>)}
</DialogHeader>
<DialogFooter>
{active.kind === 'confirm' && (
<>
<Button
variant="outline"
onClick={() => closeActive (false)}>
{active.options.cancelText ?? '取消'}
</Button>
<Button
variant={(active.options.variant === 'danger')
? 'destructive'
: 'default'}
onClick={() => closeActive (true)}>
{active.options.confirmText ?? '確定'}
</Button>
</>)}
{active.kind === 'alert' && (
<Button onClick={() => closeActive ()}>
{active.options.okText ?? '確定'}
</Button>)}
{active.kind === 'choice' && (
<>
<Button
variant="outline"
onClick={() => closeActive (null)}>
{active.options.cancelText ?? '取消'}
</Button>
{active.options.choices.map (choice => (
{active && (
<CommonDialogue
open={Boolean (active)}
onOpenChange={open => {
if (!(open))
closeActive (active.kind !== 'confirm' && null)
}}
title={active.options.title}
description={active.options.description}
bodyClassName="hidden"
footer={
<>
{active.kind === 'confirm' && (
<>
<Button
key={choice.value}
variant={(choice.variant === 'danger')
variant="outline"
onClick={() => closeActive (false)}>
{active.options.cancelText ?? '取消'}
</Button>
<Button
variant={(active.options.variant === 'danger')
? 'destructive'
: 'default'}
onClick={() => closeActive (choice.value)}>
{choice.label}
</Button>))}
</>)}
</DialogFooter>
</DialogContent>)}
</Dialog>
onClick={() => closeActive (true)}>
{active.options.confirmText ?? '確定'}
</Button>
</>)}
{active.kind === 'alert' && (
<Button onClick={() => closeActive ()}>
{active.options.okText ?? '確定'}
</Button>)}
{active.kind === 'choice' && (
<>
<Button
variant="outline"
onClick={() => closeActive (null)}>
{active.options.cancelText ?? '取消'}
</Button>
{active.options.choices.map (choice => (
<Button
key={choice.value}
variant={(choice.variant === 'danger')
? 'destructive'
: 'default'}
onClick={() => closeActive (choice.value)}>
{choice.label}
</Button>))}
</>)}
</>}/>
)}
</DialogueContext.Provider>)
}
-258
ファイルの表示
@@ -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
+263
ファイルの表示
@@ -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 }
+1
ファイルの表示
@@ -0,0 +1 @@
export { useDialogue } from '@/components/dialogues/DialogueProvider'
+33 -32
ファイルの表示
@@ -2,9 +2,10 @@ import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import CommonDialogue from '@/components/dialogues/CommonDialogue'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PostImportRowDialog from '@/components/posts/import/PostImportRowDialog'
import PostImportRowForm from '@/components/posts/import/PostImportRowForm'
import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
@@ -23,6 +24,7 @@ import Forbidden from '@/pages/Forbidden'
import type { FC } from 'react'
import type { PostImportRowDraft } from '@/components/posts/import/PostImportRowForm'
import type { PostImportResultRow,
PostImportRow,
PostImportSession } from '@/lib/postImportSession'
@@ -30,16 +32,6 @@ import type { User } from '@/types'
type Props = { user: User | null }
type Draft = {
url: string
title: string
thumbnailBase: string
originalCreatedFrom: string
originalCreatedBefore: string
duration: string
tags: string
parentPostIds: string }
const PostImportReviewPage: FC<Props> = ({ user }) => {
const editable = canEditContent (user)
@@ -51,7 +43,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const [loading, setLoading] = useState (false)
const [missing, setMissing] = useState (false)
const [savingRow, setSavingRow] = useState<number | null> (null)
const [dialogMessageRow, setDialogMessageRow] = useState<PostImportRow | null> (null)
const [dialogueMessageRow, setDialogueMessageRow] = useState<PostImportRow | null> (null)
useEffect (() => {
if (sessionId == null)
@@ -105,12 +97,12 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
useEffect (() => {
if (editingRow == null)
{
setDialogMessageRow (null)
setDialogueMessageRow (null)
return
}
if (dialogMessageRow?.sourceRow !== editingRow.sourceRow)
setDialogMessageRow (null)
}, [editingRow, dialogMessageRow])
if (dialogueMessageRow?.sourceRow !== editingRow.sourceRow)
setDialogueMessageRow (null)
}, [editingRow, dialogueMessageRow])
const updateSessionRows = (nextRows: PostImportRow[]) =>
setSession (current =>
@@ -118,7 +110,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const saveDraft = async (
{ draft, resetRequested, resetSnapshot }: {
draft: Draft
draft: PostImportRowDraft
resetRequested: boolean
resetSnapshot: PostImportRow['resetSnapshot'] },
): Promise<boolean> => {
@@ -166,11 +158,11 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const target = validatedRows.find (row => row.sourceRow === editingRow.sourceRow)
if (target != null && Object.keys (target.validationErrors).length > 0)
{
setDialogMessageRow (target)
setDialogueMessageRow (target)
return false
}
updateSessionRows (mergeValidatedImportRows (nextRows, validatedRows))
setDialogMessageRow (null)
setDialogueMessageRow (null)
setSearchParams ({ })
return true
}
@@ -209,7 +201,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
if (firstInvalid != null)
{
setSession ({ ...session, rows: mergedRows })
setDialogMessageRow (firstInvalid)
setDialogueMessageRow (firstInvalid)
setSearchParams ({ edit: String (firstInvalid.sourceRow) })
return
}
@@ -250,7 +242,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
rows: nextRows,
repairMode: 'failed' as const }
setSession (repairSession)
setDialogMessageRow (firstRecoverable)
setDialogueMessageRow (firstRecoverable)
const saved = savePostImportSession (sessionId, repairSession, message =>
toast ({ title: '取込状態を保存できませんでした', description: message }))
if (saved)
@@ -323,16 +315,25 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
onBack={() => navigate ('/posts/import')}
onSubmit={submit}/>
<PostImportRowDialog
open={editingRow != null}
row={editingRow}
messageRow={dialogMessageRow}
saving={savingRow != null}
onOpenChange={open => {
if (!open)
setSearchParams ({ })
}}
onSave={saveDraft}/>
{editingRow != null && (
<CommonDialogue
open={true}
onOpenChange={open => {
if (!open)
setSearchParams ({ })
}}
title="投稿を編輯"
description={`投稿 ${ editingRow.sourceRow } の内容を確認し、必要な項目を編輯してください.`}
className="flex max-h-[calc(100dvh-1rem)] max-w-3xl flex-col overflow-hidden p-0"
bodyClassName="contents">
<PostImportRowForm
key={editingRow.sourceRow}
row={editingRow}
messageRow={dialogueMessageRow}
saving={savingRow != null}
onCancel={() => setSearchParams ({ })}
onSave={saveDraft}/>
</CommonDialogue>)}
</>)
}
@@ -375,7 +376,7 @@ const PostImportFooter = (
const buildNextEditedRow = (
editingRow: PostImportRow,
draft: Draft,
draft: PostImportRowDraft,
urlChanged: boolean,
): PostImportRow => {
const nextProvenance = { ...editingRow.provenance }