広場投稿追加画面の刷新 (#399) #413

マージ済み
みてるぞ が 103 個のコミットを feature/399 から main へマージ 2026-07-19 00:03:11 +09:00
4個のファイルの変更331行の追加232行の削除
コミット 0ac7332458 の変更だけを表示してゐます - すべてのコミットを表示
+158 -77
ファイルの表示
@@ -1,4 +1,4 @@
import { createContext, useCallback, useContext, useMemo, useState } from 'react' import { useCallback, useMemo, useRef, useState } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Dialog, import { Dialog,
@@ -7,29 +7,16 @@ import { Dialog,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle } from '@/components/ui/dialog' DialogTitle } from '@/components/ui/dialog'
import { DialogueContext, useDialogue } from '@/lib/dialogues/useDialogue'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
import type { AlertOptions,
type DialogueVariant = 'default' | 'danger' ChoiceOptions,
ConfirmOptions,
type ConfirmOptions = { title: string DialogueAPI,
description?: ReactNode DialogueFormAction,
confirmText?: string DialogueFormControls,
cancelText?: string DialogueFormOptions } from '@/lib/dialogues/useDialogue'
variant?: DialogueVariant }
type AlertOptions = { title: string
description?: ReactNode
okText?: string }
type Choice<T extends string> = { value: T
label: string
variant?: DialogueVariant }
type ChoiceOptions<T extends string> = { title: string
description?: ReactNode
choices: Choice<T>[]
cancelText?: string }
type DialogueRequest = type DialogueRequest =
| { id: number | { id: number
@@ -44,13 +31,10 @@ type DialogueRequest =
kind: 'choice' kind: 'choice'
options: ChoiceOptions<string> options: ChoiceOptions<string>
resolve: (value: string | null) => void } resolve: (value: string | null) => void }
| { id: number
type DialogueAPI = kind: 'form'
{ confirm: (options: ConfirmOptions) => Promise<boolean> options: DialogueFormOptions
alert: (options: AlertOptions) => Promise<void> resolve: () => void }
choice: <T extends string> (options: ChoiceOptions<T>) => Promise<T | null> }
const DialogueContext = createContext<DialogueAPI | null> (null)
let nextDialogueId = 1 let nextDialogueId = 1
@@ -58,21 +42,24 @@ type Props = { children: ReactNode }
const DialogueProvider: FC<Props> = ({ children }) => { const DialogueProvider: FC<Props> = ({ children }) => {
const [queue, setQueue] = useState<DialogueRequest[]> ([]) const [stack, setStack] = useState<DialogueRequest[]> ([])
const [pendingIds, setPendingIds] = useState<number[]> ([])
const [formActions, setFormActions] = useState<Record<number, DialogueFormAction[]>> ({ })
const formControls = useRef<Record<number, DialogueFormControls>> ({ })
const push = useCallback ((request: Omit<DialogueRequest, 'id'>) => { const push = useCallback ((request: Omit<DialogueRequest, 'id'>) => {
const id = nextDialogueId const id = nextDialogueId
++nextDialogueId ++nextDialogueId
setQueue (q => [...q, { ...request, id } as DialogueRequest]) setStack (current => [...current, { ...request, id } as DialogueRequest])
}, []) }, [])
const closeActive = useCallback ((result?: unknown) => { const closeRequest = useCallback ((id: number, result?: unknown) => {
setQueue (q => { setStack (current => {
const [active, ...rest] = q const active = current.find (request => request.id === id)
if (!(active)) if (active == null)
return rest return current
switch (active.kind) switch (active.kind)
{ {
@@ -87,12 +74,28 @@ const DialogueProvider: FC<Props> = ({ children }) => {
case 'choice': case 'choice':
active.resolve ((result ?? null) as string | null) active.resolve ((result ?? null) as string | null)
break break
case 'form':
active.resolve ()
break
} }
return current.filter (request => request.id !== id)
})
setPendingIds (current => current.filter (_1 => _1 !== id))
setFormActions (current => {
const { [id]: _, ...rest } = current
return rest return rest
}) })
delete formControls.current[id]
}, []) }, [])
const setRequestActions = useCallback (
(id: number, actions: DialogueFormAction[]) => {
setFormActions (current => ({ ...current, [id]: actions }))
},
[])
const api = useMemo<DialogueAPI> (() => ({ const api = useMemo<DialogueAPI> (() => ({
confirm: options => new Promise<boolean> (resolve => { confirm: options => new Promise<boolean> (resolve => {
push ({ kind: 'confirm', options, resolve }) push ({ kind: 'confirm', options, resolve })
@@ -103,86 +106,164 @@ const DialogueProvider: FC<Props> = ({ children }) => {
choice: options => new Promise (resolve => { choice: options => new Promise (resolve => {
push ({ kind: 'choice', push ({ kind: 'choice',
options: options as ChoiceOptions<string>, options: options as ChoiceOptions<string>,
resolve: resolve as (value: string | null) => void })}) }), [push]) resolve: resolve as (value: string | null) => void })
}),
form: options => new Promise<void> (resolve => {
push ({ kind: 'form', options, resolve })
}) }), [push])
const active = queue[0] const handleFormAction = useCallback (
async (id: number, action: DialogueFormAction) => {
if (pendingIds.includes (id))
return
setPendingIds (current => [...current, id])
try
{
const shouldClose = await action.onSelect ()
if (shouldClose !== false)
closeRequest (id)
}
finally
{
setPendingIds (current => current.filter (_1 => _1 !== id))
}
},
[closeRequest, pendingIds])
return ( return (
<DialogueContext.Provider value={api}> <DialogueContext.Provider value={api}>
{children} {children}
<Dialog {stack.map ((request, index) => {
open={Boolean (active)} const isTop = index === stack.length - 1
onOpenChange={open => { const pending = pendingIds.includes (request.id)
if (!(open)) const controls =
closeActive (active?.kind !== 'confirm' && null) request.kind === 'form'
}}> ? (formControls.current[request.id] ??= {
{active && ( close: () => closeRequest (request.id),
<DialogContent className="px-6 pb-6 pt-7"> setActions: actions => setRequestActions (request.id, actions) })
<DialogHeader className="pl-8"> : null
<DialogTitle>{active.options.title}</DialogTitle> const requestActions = formActions[request.id] ?? []
{active.options.description && ( return (
<Dialog
key={request.id}
open
onOpenChange={open => {
if (!(open) && isTop && !(pending))
closeRequest (request.id, request.kind !== 'confirm' && null)
}}>
<DialogContent
className={
request.kind === 'form'
? `flex max-h-[calc(100dvh-1rem)] ${ request.options.size === 'large'
? 'max-w-3xl'
: 'max-w-lg' } flex-col overflow-hidden gap-0`
: 'px-6 pb-6 pt-7'
}
onEscapeKeyDown={event => {
if (pending)
event.preventDefault ()
}}
onPointerDownOutside={event => {
if (pending)
event.preventDefault ()
}}>
{request.kind === 'form'
? (
<>
<DialogHeader className="shrink-0 pl-8 pr-0 pt-1 text-left">
<DialogTitle>{request.options.title}</DialogTitle>
{request.options.description && (
<DialogDescription asChild> <DialogDescription asChild>
<div>{active.options.description}</div> <div>{request.options.description}</div>
</DialogDescription>)}
</DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
{controls && request.options.body (controls)}
</div>
<DialogFooter className="shrink-0 pt-4">
<Button
variant="outline"
onClick={() => closeRequest (request.id)}
disabled={pending}>
{request.options.cancelText ?? '取消'}
</Button>
{requestActions.map (action => (
<Button
key={action.label}
variant={action.variant === 'danger'
? 'destructive'
: 'default'}
onClick={() => void handleFormAction (request.id, action)}
disabled={pending || action.disabled}>
{action.label}
</Button>))}
</DialogFooter>
</>)
: (
<>
<DialogHeader className="pl-8">
<DialogTitle>{request.options.title}</DialogTitle>
{request.options.description && (
<DialogDescription asChild>
<div>{request.options.description}</div>
</DialogDescription>)} </DialogDescription>)}
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
{active.kind === 'confirm' && ( {request.kind === 'confirm' && (
<> <>
<Button <Button
variant="outline" variant="outline"
onClick={() => closeActive (false)}> onClick={() => closeRequest (request.id, false)}>
{active.options.cancelText ?? '取消'} {request.options.cancelText ?? '取消'}
</Button> </Button>
<Button <Button
variant={(active.options.variant === 'danger') variant={(request.options.variant === 'danger')
? 'destructive' ? 'destructive'
: 'default'} : 'default'}
onClick={() => closeActive (true)}> onClick={() => closeRequest (request.id, true)}>
{active.options.confirmText ?? '確定'} {request.options.confirmText ?? '確定'}
</Button> </Button>
</>)} </>)}
{active.kind === 'alert' && ( {request.kind === 'alert' && (
<Button onClick={() => closeActive ()}> <Button onClick={() => closeRequest (request.id)}>
{active.options.okText ?? '確定'} {request.options.okText ?? '確定'}
</Button>)} </Button>)}
{active.kind === 'choice' && ( {request.kind === 'choice' && (
<> <>
<Button <Button
variant="outline" variant="outline"
onClick={() => closeActive (null)}> onClick={() => closeRequest (request.id, null)}>
{active.options.cancelText ?? '取消'} {request.options.cancelText ?? '取消'}
</Button> </Button>
{active.options.choices.map (choice => ( {request.options.choices.map (choice => (
<Button <Button
key={choice.value} key={choice.value}
variant={(choice.variant === 'danger') variant={(choice.variant === 'danger')
? 'destructive' ? 'destructive'
: 'default'} : 'default'}
onClick={() => closeActive (choice.value)}> onClick={() => closeRequest (request.id, choice.value)}>
{choice.label} {choice.label}
</Button>))} </Button>))}
</>)} </>)}
</DialogFooter> </DialogFooter>
</DialogContent>)} </>)}
</Dialog> </DialogContent>
</Dialog>)
})}
</DialogueContext.Provider>) </DialogueContext.Provider>)
} }
export { useDialogue }
export const useDialogue = () => {
const dialogue = useContext (DialogueContext)
if (!(dialogue))
throw new Error ('useDialogue must be used inside DialogueProvider')
return dialogue
}
export default DialogueProvider export default DialogueProvider
+53 -40
ファイルの表示
@@ -1,17 +1,17 @@
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
import FieldError from '@/components/common/FieldError' import FieldError from '@/components/common/FieldError'
import FieldWarning from '@/components/common/FieldWarning' import FieldWarning from '@/components/common/FieldWarning'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview' import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
import { Button } from '@/components/ui/button'
import { useDialogue } from '@/lib/dialogues/useDialogue' import { useDialogue } from '@/lib/dialogues/useDialogue'
import { inputClass } from '@/lib/utils' import { inputClass } from '@/lib/utils'
import type { Dispatch, FC, SetStateAction } from 'react' import type { FC } from 'react'
import type { PostImportRow } from '@/lib/postImportSession' import type { PostImportRow } from '@/lib/postImportSession'
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
type Draft = { type Draft = {
url: string url: string
@@ -25,10 +25,11 @@ type Draft = {
type Props = { type Props = {
row: PostImportRow row: PostImportRow
messageRow?: PostImportRow | null controls: DialogueFormControls
saving: boolean onSave: (args: { draft: Draft
onDraftChange: Dispatch<SetStateAction<Draft | null>> resetRequested: boolean }) => Promise<{
onResetRequestedChange: Dispatch<SetStateAction<boolean>> } saved: boolean
row: PostImportRow | null }> }
const buildDraft = (row: PostImportRow): Draft => ({ const buildDraft = (row: PostImportRow): Draft => ({
url: row.url, url: row.url,
@@ -66,22 +67,21 @@ const sameDraft = (left: Draft, right: Draft): boolean =>
const PostImportRowForm: FC<Props> = ( const PostImportRowForm: FC<Props> = (
{ row, { row,
messageRow, controls,
saving, onSave },
onDraftChange,
onResetRequestedChange },
) => { ) => {
const dialogue = useDialogue () const dialogue = useDialogue ()
const [draft, setDraft] = useState<Draft> (() => buildDraft (row)) const [draft, setDraft] = useState<Draft> (() => buildDraft (row))
const [messageRow, setMessageRow] = useState<PostImportRow | null> (null)
const [saving, setSaving] = useState (false)
const [resetRequested, setResetRequested] = useState (false) const [resetRequested, setResetRequested] = useState (false)
useEffect (() => { useEffect (() => {
const nextDraft = buildDraft (row) const nextDraft = buildDraft (row)
setDraft (nextDraft) setDraft (nextDraft)
setMessageRow (null)
setResetRequested (false) setResetRequested (false)
onDraftChange (nextDraft) }, [row])
onResetRequestedChange (false)
}, [onDraftChange, onResetRequestedChange, row])
const displayRow = messageRow ?? row const displayRow = messageRow ?? row
const resetDraft = buildResetDraft (row) const resetDraft = buildResetDraft (row)
@@ -91,16 +91,14 @@ const PostImportRowForm: FC<Props> = (
key: Key, key: Key,
value: Draft[Key], value: Draft[Key],
) => { ) => {
setDraft (current => { if (messageRow != null)
const next = { ...current, [key]: value } setMessageRow (null)
onDraftChange (next) setDraft (current => ({ ...current, [key]: value }))
return next
})
} }
const reset = async () => { const reset = useCallback (async (): Promise<boolean> => {
if (resetDisabled) if (resetDisabled)
return return false
const confirmed = await dialogue.confirm ({ const confirmed = await dialogue.confirm ({
title: '変更をリセットしますか?', title: '変更をリセットしますか?',
@@ -109,22 +107,48 @@ const PostImportRowForm: FC<Props> = (
cancelText: '取消', cancelText: '取消',
variant: 'danger' }) variant: 'danger' })
if (!(confirmed)) if (!(confirmed))
return return false
setDraft (resetDraft) setDraft (resetDraft)
setResetRequested (true) setResetRequested (true)
onDraftChange (resetDraft) setMessageRow (null)
onResetRequestedChange (true) return false
}, [dialogue, resetDisabled, resetDraft])
const save = useCallback (async (): Promise<boolean> => {
setSaving (true)
try
{
const result = await onSave ({ draft, resetRequested })
if (result.saved)
return true
if (result.row != null)
setMessageRow (result.row)
return false
} }
finally
{
setSaving (false)
}
}, [draft, onSave, resetRequested])
useEffect (() => {
controls.setActions ([{
label: '変更をリセット',
variant: 'danger',
disabled: resetDisabled,
onSelect: reset },
{
label: '編輯内容を保存',
disabled: saving,
onSelect: save }])
}, [controls, resetDisabled, reset, save, saving])
return ( return (
<> <>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-6"> <div className="px-6 pb-6">
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-muted-foreground">
稿 {row.sourceRow}
</p>
<div className="grid gap-6 md:grid-cols-[7rem_minmax(0,1fr)]"> <div className="grid gap-6 md:grid-cols-[7rem_minmax(0,1fr)]">
<div className="space-y-3"> <div className="space-y-3">
<ThumbnailPreview <ThumbnailPreview
@@ -196,16 +220,6 @@ const PostImportRowForm: FC<Props> = (
<FieldWarning messages={displayRow.baseWarnings}/> <FieldWarning messages={displayRow.baseWarnings}/>
<FieldError messages={displayRow.validationErrors.base}/> <FieldError messages={displayRow.validationErrors.base}/>
<FieldError messages={displayRow.importErrors?.base}/> <FieldError messages={displayRow.importErrors?.base}/>
<div className="pt-2">
<Button
type="button"
variant="destructive"
onClick={() => reset ()}
disabled={resetDisabled}>
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -259,4 +273,3 @@ const PostImportAreaField = (
export default PostImportRowForm export default PostImportRowForm
export { buildDraft } export { buildDraft }
export type { Draft as PostImportRowDraft } export type { Draft as PostImportRowDraft }
export type { Draft as PostImportRowDraft }
+67 -2
ファイルの表示
@@ -1,5 +1,70 @@
import { useDialogue as useDialogueFromProvider } from '@/components/dialogues/DialogueProvider' import { createContext, useContext } from 'react'
export const useDialogue = () => useDialogueFromProvider () import type { ReactNode } from 'react'
type DialogueVariant = 'default' | 'danger'
type ConfirmOptions = { title: string
description?: ReactNode
confirmText?: string
cancelText?: string
variant?: DialogueVariant }
type AlertOptions = { title: string
description?: ReactNode
okText?: string }
type Choice<T extends string> = { value: T
label: string
variant?: DialogueVariant }
type ChoiceOptions<T extends string> = { title: string
description?: ReactNode
choices: Choice<T>[]
cancelText?: string }
type DialogueFormAction = {
label: string
variant?: DialogueVariant
disabled?: boolean
onSelect: () => Promise<boolean | void> | boolean | void }
type DialogueFormControls = {
close: () => void
setActions: (actions: DialogueFormAction[]) => void }
type DialogueFormOptions = { title: string
description?: ReactNode
body: (controls: DialogueFormControls) => ReactNode
cancelText?: string
size?: 'default' | 'large' }
type DialogueAPI =
{ confirm: (options: ConfirmOptions) => Promise<boolean>
alert: (options: AlertOptions) => Promise<void>
choice: <T extends string> (options: ChoiceOptions<T>) => Promise<T | null>
form: (options: DialogueFormOptions) => Promise<void> }
const DialogueContext = createContext<DialogueAPI | null> (null)
const useDialogue = () => {
const dialogue = useContext (DialogueContext)
if (dialogue == null)
throw new Error ('useDialogue must be used inside DialogueProvider')
return dialogue
}
export { DialogueContext, useDialogue }
export default useDialogue export default useDialogue
export type {
AlertOptions,
Choice,
ChoiceOptions,
ConfirmOptions,
DialogueAPI,
DialogueFormAction,
DialogueFormControls,
DialogueFormOptions,
DialogueVariant }
+16 -76
ファイルの表示
@@ -1,10 +1,10 @@
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async' import { Helmet } from 'react-helmet-async'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import PageTitle from '@/components/common/PageTitle' import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import PostImportRowForm, { buildDraft } from '@/components/posts/import/PostImportRowForm' import PostImportRowForm from '@/components/posts/import/PostImportRowForm'
import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary' import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
@@ -43,12 +43,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const [session, setSession] = useState<PostImportSession | null> (null) const [session, setSession] = useState<PostImportSession | null> (null)
const [loading, setLoading] = useState (false) const [loading, setLoading] = useState (false)
const [missing, setMissing] = useState (false) const [missing, setMissing] = useState (false)
const [savingRow, setSavingRow] = useState<number | null> (null)
const [dialogueMessageRow, setDialogueMessageRow] = useState<PostImportRow | null> (null)
const [dialogueDraft, setDialogueDraft] = useState<PostImportRowDraft | null> (null)
const [dialogueResetRequested, setDialogueResetRequested] = useState (false)
const dialogueDraftRef = useRef<PostImportRowDraft | null> (null)
const dialogueResetRequestedRef = useRef (false)
useEffect (() => { useEffect (() => {
if (sessionId == null) if (sessionId == null)
@@ -68,14 +62,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
toast ({ title: '取込状態を保存できませんでした', description: message })) toast ({ title: '取込状態を保存できませんでした', description: message }))
}, [session, sessionId]) }, [session, sessionId])
useEffect (() => {
dialogueDraftRef.current = dialogueDraft
}, [dialogueDraft])
useEffect (() => {
dialogueResetRequestedRef.current = dialogueResetRequested
}, [dialogueResetRequested])
const rows = session?.rows ?? [] const rows = session?.rows ?? []
const editingSourceRow = Number (searchParams.get ('edit') ?? '') const editingSourceRow = Number (searchParams.get ('edit') ?? '')
const editingRow = const editingRow =
@@ -107,21 +93,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
element?.scrollIntoView ({ block: 'center', behavior: 'smooth' }) element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
}, [editingRow, session?.repairMode]) }, [editingRow, session?.repairMode])
useEffect (() => {
if (editingRow == null)
{
setDialogueMessageRow (null)
setDialogueDraft (null)
setDialogueResetRequested (false)
return
}
if (dialogueMessageRow?.sourceRow !== editingRow.sourceRow)
setDialogueMessageRow (null)
setDialogueDraft (current =>
current != null ? current : buildDraft (editingRow))
setDialogueResetRequested (false)
}, [editingRow, dialogueMessageRow])
const updateSessionRows = (nextRows: PostImportRow[]) => const updateSessionRows = (nextRows: PostImportRow[]) =>
setSession (current => setSession (current =>
current != null ? { ...current, rows: nextRows } : current) current != null ? { ...current, rows: nextRows } : current)
@@ -155,7 +126,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const urlChanged = draft.url !== baseRow.url const urlChanged = draft.url !== baseRow.url
const nextRow = buildNextEditedRow (baseRow, draft, urlChanged) const nextRow = buildNextEditedRow (baseRow, draft, urlChanged)
setSavingRow (editingRow.sourceRow)
const nextRows = session.rows.map (row => const nextRows = session.rows.map (row =>
row.sourceRow === editingRow.sourceRow row.sourceRow === editingRow.sourceRow
? nextRow ? nextRow
@@ -176,12 +146,8 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const validatedRows = initialisePreviewRows (validated.rows) const validatedRows = initialisePreviewRows (validated.rows)
const target = validatedRows.find (row => row.sourceRow === editingRow.sourceRow) const target = validatedRows.find (row => row.sourceRow === editingRow.sourceRow)
if (target != null && Object.keys (target.validationErrors).length > 0) if (target != null && Object.keys (target.validationErrors).length > 0)
{
setDialogueMessageRow (target)
return { saved: false, row: target } return { saved: false, row: target }
}
updateSessionRows (mergeValidatedImportRows (nextRows, validatedRows)) updateSessionRows (mergeValidatedImportRows (nextRows, validatedRows))
setDialogueMessageRow (null)
setSearchParams ({ }) setSearchParams ({ })
return { saved: true, row: null } return { saved: true, row: null }
} }
@@ -192,48 +158,25 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
} }
finally finally
{ {
setSavingRow (null)
} }
} }
const openEditingDialogue = async (row: PostImportRow) => { const openEditingDialogue = async (row: PostImportRow) => {
let currentRow = row await dialogue.form ({
while (true)
{
const initialDraft = buildDraft (currentRow)
setDialogueDraft (current => current != null ? current : initialDraft)
setDialogueResetRequested (false)
const action = await dialogue.choice ({
title: '投稿を編輯', title: '投稿を編輯',
description: <PostImportRowForm description: `投稿 ${ row.sourceRow } の内容を確認し、必要な項目を編輯してください.`,
row={currentRow}
messageRow={dialogueMessageRow?.sourceRow === currentRow.sourceRow
? dialogueMessageRow
: null}
saving={savingRow === currentRow.sourceRow}
onDraftChange={setDialogueDraft}
onResetRequestedChange={setDialogueResetRequested}/>,
cancelText: '取消', cancelText: '取消',
choices: [{ value: 'save', label: '編輯内容を保存' }] as const }) size: 'large',
body: controls => (
if (action !== 'save') <PostImportRowForm
{ row={row}
setSearchParams ({ }) controls={controls}
return onSave={({ draft, resetRequested }) =>
} saveDraft ({
const draft = dialogueDraftRef.current ?? initialDraft
const result = await saveDraft ({
draft, draft,
resetRequested: dialogueResetRequestedRef.current, resetRequested,
resetSnapshot: currentRow.resetSnapshot }) resetSnapshot: row.resetSnapshot })}/>) })
if (result.saved) setSearchParams ({ })
return
currentRow = result.row ?? currentRow
}
} }
useEffect (() => { useEffect (() => {
@@ -267,7 +210,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
if (firstInvalid != null) if (firstInvalid != null)
{ {
setSession ({ ...session, rows: mergedRows }) setSession ({ ...session, rows: mergedRows })
setDialogueMessageRow (firstInvalid)
setSearchParams ({ edit: String (firstInvalid.sourceRow) }) setSearchParams ({ edit: String (firstInvalid.sourceRow) })
return return
} }
@@ -308,7 +250,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
rows: nextRows, rows: nextRows,
repairMode: 'failed' as const } repairMode: 'failed' as const }
setSession (repairSession) setSession (repairSession)
setDialogueMessageRow (firstRecoverable)
const saved = savePostImportSession (sessionId, repairSession, message => const saved = savePostImportSession (sessionId, repairSession, message =>
toast ({ title: '取込状態を保存できませんでした', description: message })) toast ({ title: '取込状態を保存できませんでした', description: message }))
if (saved) if (saved)
@@ -358,7 +299,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
<MainArea className="min-h-0"> <MainArea className="min-h-0">
<div className="mx-auto max-w-6xl space-y-4 p-4"> <div className="mx-auto max-w-6xl space-y-4 p-4">
<PageTitle>稿</PageTitle> <PageTitle>稿</PageTitle>
<div className="space-y-3"> <div className="space-y-3">
{reviewRows.map (row => ( {reviewRows.map (row => (
@@ -380,7 +321,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
skipPlannedCount={counts.skipPlanned} skipPlannedCount={counts.skipPlanned}
onBack={() => navigate ('/posts/import')} onBack={() => navigate ('/posts/import')}
onSubmit={submit}/> onSubmit={submit}/>
</>) </>)
} }
@@ -415,7 +355,7 @@ const PostImportFooter = (
type="button" type="button"
onClick={onSubmit} onClick={onSubmit}
disabled={loading || processableCount === 0}> disabled={loading || processableCount === 0}>
</Button> </Button>
</div> </div>
</div> </div>