From d0914fce3b141076f510747f395e5e5ce0c90955 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Wed, 15 Jul 2026 22:51:01 +0900 Subject: [PATCH] #399 --- .../components/dialogues/DialogueProvider.tsx | 295 +++++++++++------- .../posts/import/PostImportRowForm.tsx | 12 +- frontend/src/lib/dialogues/useDialogue.ts | 3 +- .../src/pages/posts/PostImportResultPage.tsx | 8 +- .../src/pages/posts/PostImportReviewPage.tsx | 19 +- .../src/pages/posts/PostImportSourcePage.tsx | 2 +- 6 files changed, 199 insertions(+), 140 deletions(-) diff --git a/frontend/src/components/dialogues/DialogueProvider.tsx b/frontend/src/components/dialogues/DialogueProvider.tsx index 4357b19..3598d42 100644 --- a/frontend/src/components/dialogues/DialogueProvider.tsx +++ b/frontend/src/components/dialogues/DialogueProvider.tsx @@ -7,7 +7,7 @@ import { Dialog, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { DialogueContext, useDialogue } from '@/lib/dialogues/useDialogue' +import { DialogueContext } from '@/lib/dialogues/useDialogue' import type { FC, ReactNode } from 'react' import type { AlertOptions, @@ -42,20 +42,24 @@ type Props = { children: ReactNode } const DialogueProvider: FC = ({ children }) => { - const [stack, setStack] = useState ([]) + const [queue, setQueue] = useState ([]) const [pendingIds, setPendingIds] = useState ([]) const [formActions, setFormActions] = useState> ({ }) const formControls = useRef> ({ }) + const [nestedConfirm, setNestedConfirm] = useState<{ + parentId: number + options: ConfirmOptions + resolve: (value: boolean) => void } | null> (null) const push = useCallback ((request: Omit) => { const id = nextDialogueId ++nextDialogueId - setStack (current => [...current, { ...request, id } as DialogueRequest]) + setQueue (current => [...current, { ...request, id } as DialogueRequest]) }, []) const closeRequest = useCallback ((id: number, result?: unknown) => { - setStack (current => { + setQueue (current => { const active = current.find (request => request.id === id) if (active == null) @@ -96,6 +100,23 @@ const DialogueProvider: FC = ({ children }) => { }, []) + const openNestedConfirm = useCallback ( + (parentId: number, options: ConfirmOptions) => + new Promise (resolve => { + setNestedConfirm ({ parentId, options, resolve }) + }), + []) + + const closeNestedConfirm = useCallback ((result: boolean) => { + setNestedConfirm (current => { + if (current == null) + return current + + current.resolve (result) + return null + }) + }, []) + const api = useMemo (() => ({ confirm: options => new Promise (resolve => { push ({ kind: 'confirm', options, resolve }) @@ -131,138 +152,172 @@ const DialogueProvider: FC = ({ children }) => { }, [closeRequest, pendingIds]) + const active = queue[0] + return ( {children} - {stack.map ((request, index) => { - const isTop = index === stack.length - 1 - const pending = pendingIds.includes (request.id) - const controls = - request.kind === 'form' - ? (formControls.current[request.id] ??= { - close: () => closeRequest (request.id), - setActions: actions => setRequestActions (request.id, actions) }) - : null - const requestActions = formActions[request.id] ?? [] - - return ( - { - if (!(open) && isTop && !(pending)) - closeRequest (request.id, request.kind !== 'confirm' && null) + {active && ( + { + const blocked = + nestedConfirm?.parentId === active.id + || pendingIds.includes (active.id) + if (!(open) && !(blocked)) + closeRequest (active.id, active.kind !== 'confirm' && null) + }}> + { + if (nestedConfirm?.parentId === active.id || pendingIds.includes (active.id)) + event.preventDefault () + }} + onPointerDownOutside={event => { + if (nestedConfirm?.parentId === active.id || pendingIds.includes (active.id)) + event.preventDefault () }}> - { - if (pending) - event.preventDefault () - }} - onPointerDownOutside={event => { - if (pending) - event.preventDefault () - }}> - {request.kind === 'form' - ? ( - <> - - {request.options.title} + {active.kind === 'form' + ? ( + <> + + {active.options.title} - {request.options.description && ( - -
{request.options.description}
-
)} -
+ {active.options.description && ( + +
{active.options.description}
+
)} +
-
- {controls && request.options.body (controls)} -
+
+ {active.options.body ( + formControls.current[active.id] ??= { + close: () => closeRequest (active.id), + setActions: actions => setRequestActions (active.id, actions), + confirm: options => openNestedConfirm (active.id, options) })} +
- - + + - {requestActions.map (action => ( + {(formActions[active.id] ?? []).map (action => ( + ))} + + ) + : ( + <> + + {active.options.title} + + {active.options.description && ( + +
{active.options.description}
+
)} +
+ + + {active.kind === 'confirm' && ( + <> ))} - - ) - : ( - <> - - {request.options.title} + variant="outline" + onClick={() => closeRequest (active.id, false)}> + {active.options.cancelText ?? '取消'} + - {request.options.description && ( - -
{request.options.description}
-
)} -
+ + )} - - {request.kind === 'confirm' && ( - <> - + {active.kind === 'alert' && ( + )} - - )} + {active.kind === 'choice' && ( + <> + - {request.kind === 'alert' && ( - )} + {active.options.choices.map (choice => ( + ))} + )} + + )} +
+
)} - {request.kind === 'choice' && ( - <> - + {nestedConfirm && ( + { + if (!(open)) + closeNestedConfirm (false) + }}> + + + {nestedConfirm.options.title} - {request.options.choices.map (choice => ( - ))} - )} - - )} - - ) - })} + {nestedConfirm.options.description && ( + +
{nestedConfirm.options.description}
+
)} + + + + + + + + +
)}
) } export { useDialogue } diff --git a/frontend/src/components/posts/import/PostImportRowForm.tsx b/frontend/src/components/posts/import/PostImportRowForm.tsx index bb7a71c..31c068b 100644 --- a/frontend/src/components/posts/import/PostImportRowForm.tsx +++ b/frontend/src/components/posts/import/PostImportRowForm.tsx @@ -1,11 +1,10 @@ -import { useCallback, useEffect, useState } from 'react' +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 ThumbnailPreview from '@/components/posts/import/ThumbnailPreview' -import { useDialogue } from '@/lib/dialogues/useDialogue' import { inputClass } from '@/lib/utils' import type { FC } from 'react' @@ -70,7 +69,6 @@ const PostImportRowForm: FC = ( controls, onSave }, ) => { - const dialogue = useDialogue () const [draft, setDraft] = useState (() => buildDraft (row)) const [messageRow, setMessageRow] = useState (null) const [saving, setSaving] = useState (false) @@ -84,7 +82,9 @@ const PostImportRowForm: FC = ( }, [row]) const displayRow = messageRow ?? row - const resetDraft = buildResetDraft (row) + const resetDraft = useMemo ( + () => buildResetDraft (row), + [row]) const resetDisabled = saving || sameDraft (draft, resetDraft) const update = ( @@ -100,7 +100,7 @@ const PostImportRowForm: FC = ( if (resetDisabled) return false - const confirmed = await dialogue.confirm ({ + const confirmed = await controls.confirm ({ title: '変更をリセットしますか?', description: '現在の URL に対する自動取得直後の内容へ戻します.', confirmText: 'リセット', @@ -113,7 +113,7 @@ const PostImportRowForm: FC = ( setResetRequested (true) setMessageRow (null) return false - }, [dialogue, resetDisabled, resetDraft]) + }, [controls, resetDisabled, resetDraft]) const save = useCallback (async (): Promise => { setSaving (true) diff --git a/frontend/src/lib/dialogues/useDialogue.ts b/frontend/src/lib/dialogues/useDialogue.ts index 01b0db3..c712a9a 100644 --- a/frontend/src/lib/dialogues/useDialogue.ts +++ b/frontend/src/lib/dialogues/useDialogue.ts @@ -31,7 +31,8 @@ type DialogueFormAction = { type DialogueFormControls = { close: () => void - setActions: (actions: DialogueFormAction[]) => void } + setActions: (actions: DialogueFormAction[]) => void + confirm: (options: ConfirmOptions) => Promise } type DialogueFormOptions = { title: string description?: ReactNode diff --git a/frontend/src/pages/posts/PostImportResultPage.tsx b/frontend/src/pages/posts/PostImportResultPage.tsx index d5f29b0..a4509b6 100644 --- a/frontend/src/pages/posts/PostImportResultPage.tsx +++ b/frontend/src/pages/posts/PostImportResultPage.tsx @@ -21,14 +21,14 @@ import { clearPostImportSourceDraft, mergeValidatedImportRows, resultSummaryCounts, retryImportRow, - savePostImportSession, - type PostImportResultRow, - type PostImportRow, - type PostImportSession } from '@/lib/postImportSession' + savePostImportSession } from '@/lib/postImportSession' import Forbidden from '@/pages/Forbidden' import type { FC } from 'react' +import type { PostImportResultRow, + PostImportRow, + PostImportSession } from '@/lib/postImportSession' import type { User } from '@/types' type Props = { user: User | null } diff --git a/frontend/src/pages/posts/PostImportReviewPage.tsx b/frontend/src/pages/posts/PostImportReviewPage.tsx index 8deac39..2342ac6 100644 --- a/frontend/src/pages/posts/PostImportReviewPage.tsx +++ b/frontend/src/pages/posts/PostImportReviewPage.tsx @@ -156,12 +156,19 @@ const PostImportReviewPage: FC = ({ user }) => { toast ({ title: '行の再検証に失敗しました' }) return { saved: false, row: null } } - finally - { - } } const openEditingDialogue = async (row: PostImportRow) => { + const saveRowDraft = ( + { draft, resetRequested }: { + draft: PostImportRowDraft + resetRequested: boolean }, + ) => + saveDraft ({ + draft, + resetRequested, + resetSnapshot: row.resetSnapshot }) + await dialogue.form ({ title: '投稿を編輯', description: `投稿 ${ row.sourceRow } の内容を確認し、必要な項目を編輯してください.`, @@ -171,11 +178,7 @@ const PostImportReviewPage: FC = ({ user }) => { - saveDraft ({ - draft, - resetRequested, - resetSnapshot: row.resetSnapshot })}/>) }) + onSave={saveRowDraft}/>) }) setSearchParams ({ }) } diff --git a/frontend/src/pages/posts/PostImportSourcePage.tsx b/frontend/src/pages/posts/PostImportSourcePage.tsx index 08a0269..9b07796 100644 --- a/frontend/src/pages/posts/PostImportSourcePage.tsx +++ b/frontend/src/pages/posts/PostImportSourcePage.tsx @@ -25,7 +25,7 @@ import Forbidden from '@/pages/Forbidden' import type { FC } from 'react' -import type { PostImportRow } from '@/lib/postImportSeession' +import type { PostImportRow } from '@/lib/postImportSession' import type { User } from '@/types' type Props = { user: User | null }