11'use client'
22
33import { type ReactNode , useState } from 'react'
4+ import { getErrorMessage } from '@sim/utils/errors'
45import type { IconName } from '@/platform/design-system/catalog/icon-geometry'
56import { Button } from '@/platform/design-system/components/button/button'
7+ import styles from '@/platform/design-system/components/modal/modal.module.css'
68import { ModalBody , ModalFooter } from '@/platform/design-system/components/modal/modal-parts'
79import { OverlayDialog } from '@/platform/design-system/components/modal/overlay-dialog'
810
@@ -11,11 +13,19 @@ interface ModalStep {
1113 content : ReactNode
1214}
1315
16+ /**
17+ * A synchronous confirm closes the dialog in the same update. A returned promise holds it open as
18+ * pending, closes it on success, and on rejection keeps the draft and shows the error's message.
19+ */
20+ type ConfirmHandler < Args extends unknown [ ] > =
21+ | ( ( ...args : Args ) => void )
22+ | ( ( ...args : Args ) => Promise < void > )
23+
1424interface ModalActions {
1525 title : string
1626 confirmLabel : string
1727 confirmDisabled ?: boolean
18- onConfirm ?: ( values : FormData ) => void
28+ onConfirm ?: ConfirmHandler < [ values : FormData ] >
1929}
2030type ModalTrigger =
2131 | { triggerLabel : string ; triggerIcon ?: IconName ; open ?: never ; onOpenChange ?: never }
@@ -27,7 +37,7 @@ type ModalTrigger =
2737 }
2838
2939interface MultiStepModalProps extends Omit < ModalActions , 'onConfirm' > {
30- onConfirm ?: ( ) => void
40+ onConfirm ?: ConfirmHandler < [ ] >
3141 triggerLabel : string
3242 steps : readonly [ ModalStep , ...ModalStep [ ] ]
3343}
@@ -57,15 +67,42 @@ function ModalSurface({
5767} : ModalSurfaceProps ) {
5868 const [ internal , setInternal ] = useState ( false )
5969 const [ index , setIndex ] = useState ( 0 )
70+ const [ pending , setPending ] = useState ( false )
71+ const [ error , setError ] = useState < string | null > ( null )
6072 const open = controlled ?? internal
6173 const step = steps [ index ]
6274 const last = index === steps . length - 1
6375
76+ if ( ! open && error ) setError ( null )
77+
6478 function close ( ) {
6579 setIndex ( 0 )
6680 if ( onOpenChange ) onOpenChange ( false )
6781 else setInternal ( false )
6882 }
83+
84+ async function confirm ( values : FormData ) {
85+ setError ( null )
86+ try {
87+ const result = onConfirm ?.( values )
88+ if ( ! ( result instanceof Promise ) ) {
89+ close ( )
90+ return
91+ }
92+ setPending ( true )
93+ await result
94+ close ( )
95+ } catch ( failure ) {
96+ setError ( getErrorMessage ( failure , 'Something went wrong. Please try again.' ) )
97+ } finally {
98+ setPending ( false )
99+ }
100+ }
101+
102+ /** The work is already underway and its outcome belongs here, so nothing dismisses it mid-flight. */
103+ function dismiss ( ) {
104+ if ( ! pending ) close ( )
105+ }
69106 return (
70107 < >
71108 { triggerLabel && (
@@ -79,29 +116,34 @@ function ModalSurface({
79116 description = { step . description }
80117 step = { index }
81118 presentation = { presentation }
82- onClose = { close }
119+ busy = { pending }
120+ onClose = { dismiss }
83121 onSubmit = { ( event ) => {
84122 event . preventDefault ( )
85- if ( last && confirmDisabled ) return
123+ if ( pending || ( last && confirmDisabled ) ) return
86124 if ( ! last ) {
87125 setIndex ( index + 1 )
88126 return
89127 }
90- const values = new FormData ( event . currentTarget )
91- close ( )
92- onConfirm ?.( values )
128+ void confirm ( new FormData ( event . currentTarget ) )
93129 } }
94130 >
95131 { steps . map ( ( entry , i ) => (
96132 < ModalBody key = { i } inactive = { i !== index } label = { entry . description || title } >
97133 { entry . content }
98134 </ ModalBody >
99135 ) ) }
136+ { error && (
137+ < p className = { styles . error } data-type = 'T052' role = 'alert' >
138+ { error }
139+ </ p >
140+ ) }
100141 < ModalFooter
101142 confirmLabel = { last ? confirmLabel : 'Continue' }
102- onPrevious = { ( ) => ( index === 0 ? close ( ) : setIndex ( index - 1 ) ) }
143+ onPrevious = { ( ) => ( index === 0 ? dismiss ( ) : setIndex ( index - 1 ) ) }
103144 previousStep = { index > 0 }
104- disabled = { last && confirmDisabled }
145+ disabled = { pending || ( last && confirmDisabled ) }
146+ cancelDisabled = { pending }
105147 destructive = { destructive }
106148 progress = { steps . length > 1 ? `Step ${ index + 1 } of ${ steps . length } ` : undefined }
107149 />
0 commit comments