TextConfirmModal.tsx253 lines · main
1'use client'
2
3import { zodResolver } from '@hookform/resolvers/zod'
4import { Check, Copy } from 'lucide-react'
5// Required to avoid issue:
6// The inferred type of ConfirmationModal cannot be named without a reference to DialogProps
7import { Dialog as _RadixDialog } from 'radix-ui'
8import { forwardRef, ReactNode, useEffect, useState } from 'react'
9import { useForm } from 'react-hook-form'
10import {
11 Alert,
12 Button,
13 cn,
14 copyToClipboard,
15 Dialog,
16 DialogContent,
17 DialogSection,
18 DialogSectionSeparator,
19 DialogTitle,
20 Form,
21 FormControl,
22 FormDescription,
23 FormField,
24 FormItem,
25 FormLabel,
26 FormMessage,
27 Input,
28} from 'ui'
29import { DialogHeader } from 'ui/src/components/shadcn/ui/dialog'
30import { z } from 'zod'
31
32import { Admonition } from './../admonition'
33
34export interface TextConfirmModalProps {
35 loading: boolean
36 visible: boolean
37 title: string
38 size?: React.ComponentProps<typeof DialogContent>['size']
39 cancelLabel?: string
40 confirmLabel?: string
41 confirmPlaceholder: string
42 confirmString: string
43 text?: string | ReactNode
44 onConfirm: () => void
45 onCancel: () => void
46 variant?: React.ComponentProps<typeof Alert>['variant']
47 alert?: {
48 base?: React.ComponentProps<typeof Alert>
49 title?: string
50 description?: string | ReactNode
51 }
52 input?: React.ComponentProps<typeof Input>
53 label?: React.ComponentProps<typeof FormLabel>
54 formMessage?: React.ComponentProps<typeof FormMessage>
55 description?: React.ComponentProps<typeof FormDescription>
56 blockDeleteButton?: boolean
57 errorMessage?: string
58 enableCopy?: boolean
59}
60
61export const TextConfirmModal = forwardRef<
62 React.ElementRef<typeof DialogContent>,
63 React.ComponentPropsWithoutRef<typeof Dialog> & TextConfirmModalProps
64>(
65 (
66 {
67 title,
68 size = 'small',
69 onConfirm,
70 visible,
71 onCancel,
72 loading,
73 cancelLabel = 'Cancel',
74 confirmLabel = 'Submit',
75 confirmPlaceholder,
76 confirmString,
77 alert,
78 input,
79 label,
80 description,
81 formMessage,
82 text,
83 children,
84 blockDeleteButton = true,
85 variant = 'default',
86 errorMessage = 'Value entered does not match',
87 enableCopy = false,
88 ...props
89 },
90 ref
91 ) => {
92 const [showCopied, setShowCopied] = useState(false)
93
94 const formSchema = z.object({
95 confirmValue: z.preprocess(
96 (val) => (typeof val === 'string' ? val.trim() : val),
97 z.literal(confirmString.trim(), {
98 errorMap: () => ({ message: errorMessage }),
99 })
100 ),
101 })
102
103 // 1. Define your form.
104 const form = useForm<z.infer<typeof formSchema>>({
105 resolver: zodResolver(formSchema as any),
106 reValidateMode: 'onChange',
107 defaultValues: {
108 confirmValue: '',
109 },
110 })
111
112 const isFormValid = form.formState.isValid
113
114 // 2. Define a submit handler.
115 function onSubmit(_values: z.infer<typeof formSchema>) {
116 // Do something with the form values.
117 // ✅ This will be type-safe and validated.
118 onConfirm()
119 }
120
121 useEffect(() => {
122 if (confirmString) form.reset()
123 }, [confirmString])
124
125 useEffect(() => {
126 if (!showCopied) return
127 const timer = setTimeout(() => setShowCopied(false), 2000)
128 return () => clearTimeout(timer)
129 }, [showCopied])
130
131 const { title: _alertBaseTitle, children: _alertBaseChildren, ...alertBase } = alert?.base ?? {}
132 const alertTitleProps = alert?.title ? { label: alert.title } : {}
133
134 return (
135 <Dialog
136 open={visible}
137 {...props}
138 onOpenChange={() => {
139 if (visible) {
140 onCancel()
141 }
142 }}
143 >
144 <DialogContent ref={ref} className="p-0 gap-0 pb-5 block!" size={size}>
145 <DialogHeader className={cn('border-b')} padding={'small'}>
146 <DialogTitle className="">{title}</DialogTitle>
147 </DialogHeader>
148 {alert && (
149 <Admonition
150 type={variant as 'default' | 'destructive' | 'warning'}
151 description={alert.description}
152 {...alertTitleProps}
153 className="border-x-0 rounded-none -mt-px"
154 {...alertBase}
155 />
156 )}
157 {children && (
158 <>
159 <DialogSection padding={'small'}>{children}</DialogSection>
160 <DialogSectionSeparator />
161 </>
162 )}
163 {/* // older prop from before refactor */}
164 {text !== undefined && (
165 <>
166 <DialogSection className="p-5" padding={'small'}>
167 <p className="text-foreground-light text-sm">{text}</p>
168 </DialogSection>
169 <DialogSectionSeparator />
170 </>
171 )}
172 <Form {...form}>
173 <form
174 autoComplete="off"
175 onSubmit={form.handleSubmit(onSubmit)}
176 className="px-5 flex flex-col gap-y-3 pt-3"
177 >
178 <FormField
179 control={form.control}
180 name="confirmValue"
181 render={({ field }) => (
182 <FormItem className="flex flex-col gap-y-2">
183 <FormLabel {...label} enableSelection={!enableCopy}>
184 Type{' '}
185 {enableCopy ? (
186 <Button
187 type="default"
188 className="h-[23px] px-1.5 py-0 border-muted text-sm whitespace-pre break-all"
189 iconRight={
190 showCopied ? <Check strokeWidth={2} className="text-brand" /> : <Copy />
191 }
192 onClick={() => {
193 setShowCopied(true)
194 copyToClipboard(confirmString)
195 }}
196 >
197 {confirmString}
198 </Button>
199 ) : (
200 <span className="text-foreground break-all whitespace-pre">
201 {confirmString}
202 </span>
203 )}{' '}
204 to confirm.
205 </FormLabel>
206 <FormControl>
207 <Input
208 autoComplete="off"
209 placeholder={confirmPlaceholder}
210 {...input}
211 {...field}
212 />
213 </FormControl>
214 {!!description && <FormDescription {...description} />}
215 <FormMessage {...formMessage} />
216 </FormItem>
217 )}
218 />
219 <div className="flex gap-2">
220 {!blockDeleteButton && (
221 <Button size="medium" block type="default" disabled={loading} onClick={onCancel}>
222 {cancelLabel}
223 </Button>
224 )}
225 <Button
226 block
227 size="medium"
228 type={
229 variant === 'destructive'
230 ? 'danger'
231 : variant === 'warning'
232 ? 'warning'
233 : 'primary'
234 }
235 htmlType="submit"
236 loading={loading}
237 disabled={!isFormValid || loading}
238 className="truncate"
239 >
240 {confirmLabel}
241 </Button>
242 </div>
243 </form>
244 </Form>
245 </DialogContent>
246 </Dialog>
247 )
248 }
249)
250
251TextConfirmModal.displayName = 'TextConfirmModal'
252
253export default TextConfirmModal