@rovula/ui 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/bundle.js +2 -2
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Form/Form.d.ts +1 -1
- package/dist/cjs/types/patterns/confirm-dialog/ConfirmDialog.d.ts +24 -0
- package/dist/cjs/types/patterns/confirm-dialog/ConfirmDialog.stories.d.ts +53 -0
- package/dist/cjs/types/patterns/form-dialog/FormDialog.d.ts +39 -0
- package/dist/cjs/types/patterns/form-dialog/FormDialog.stories.d.ts +62 -0
- package/dist/components/AlertDialog/AlertDialog.js +1 -1
- package/dist/components/Dialog/Dialog.js +1 -1
- package/dist/components/Form/Form.js +15 -4
- package/dist/esm/bundle.js +1 -1
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Form/Form.d.ts +1 -1
- package/dist/esm/types/patterns/confirm-dialog/ConfirmDialog.d.ts +24 -0
- package/dist/esm/types/patterns/confirm-dialog/ConfirmDialog.stories.d.ts +53 -0
- package/dist/esm/types/patterns/form-dialog/FormDialog.d.ts +39 -0
- package/dist/esm/types/patterns/form-dialog/FormDialog.stories.d.ts +62 -0
- package/dist/index.d.ts +1 -1
- package/dist/patterns/confirm-dialog/ConfirmDialog.js +44 -0
- package/dist/patterns/confirm-dialog/ConfirmDialog.stories.js +103 -0
- package/dist/patterns/form-dialog/FormDialog.js +10 -0
- package/dist/patterns/form-dialog/FormDialog.stories.js +223 -0
- package/package.json +1 -1
- package/src/components/AlertDialog/AlertDialog.tsx +11 -13
- package/src/components/Dialog/Dialog.tsx +3 -9
- package/src/components/Form/Form.tsx +19 -4
- package/src/patterns/confirm-dialog/ConfirmDialog.stories.tsx +193 -0
- package/src/patterns/confirm-dialog/ConfirmDialog.tsx +153 -0
- package/src/patterns/form-dialog/FormDialog.stories.tsx +437 -0
- package/src/patterns/form-dialog/FormDialog.tsx +137 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { useArgs } from "@storybook/preview-api";
|
|
4
|
+
import { FormDialog } from "./FormDialog";
|
|
5
|
+
import Button from "@/components/Button/Button";
|
|
6
|
+
import { TextInput } from "@/components/TextInput/TextInput";
|
|
7
|
+
import { useControlledForm, Field } from "@/components/Form";
|
|
8
|
+
import * as yup from "yup";
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Patterns/FormDialog",
|
|
12
|
+
component: FormDialog,
|
|
13
|
+
tags: ["autodocs"],
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: "fullscreen",
|
|
16
|
+
},
|
|
17
|
+
decorators: [
|
|
18
|
+
(Story) => (
|
|
19
|
+
<div className="p-5 flex w-full">
|
|
20
|
+
<Story />
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
argTypes: {
|
|
25
|
+
open: { control: "boolean" },
|
|
26
|
+
title: { control: "text" },
|
|
27
|
+
description: { control: "text" },
|
|
28
|
+
scrollable: { control: "boolean" },
|
|
29
|
+
},
|
|
30
|
+
} satisfies Meta<typeof FormDialog>;
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof FormDialog>;
|
|
34
|
+
|
|
35
|
+
const ContentArea = () => (
|
|
36
|
+
<div className="flex items-center justify-center bg-ramps-secondary-150 h-[200px] w-full rounded-sm">
|
|
37
|
+
<p className="typography-body3 text-text-contrast-max">
|
|
38
|
+
Content - Form Area
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const Default: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
open: false,
|
|
46
|
+
title: "Title",
|
|
47
|
+
description: "Subtitle description",
|
|
48
|
+
},
|
|
49
|
+
render: (args) => {
|
|
50
|
+
const [{ open }, updateArgs] = useArgs();
|
|
51
|
+
return (
|
|
52
|
+
<FormDialog
|
|
53
|
+
{...args}
|
|
54
|
+
open={open}
|
|
55
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
56
|
+
cancelAction={{
|
|
57
|
+
label: "Cancel",
|
|
58
|
+
onClick: () => updateArgs({ open: false }),
|
|
59
|
+
}}
|
|
60
|
+
confirmAction={{ label: "Confirm", disabled: true }}
|
|
61
|
+
trigger={
|
|
62
|
+
<Button fullwidth={false} onClick={() => updateArgs({ open: true })}>
|
|
63
|
+
Open
|
|
64
|
+
</Button>
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
<ContentArea />
|
|
68
|
+
</FormDialog>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const WithExtraAction: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
open: false,
|
|
76
|
+
title: "Title",
|
|
77
|
+
description: "Subtitle description",
|
|
78
|
+
},
|
|
79
|
+
render: (args) => {
|
|
80
|
+
const [{ open }, updateArgs] = useArgs();
|
|
81
|
+
return (
|
|
82
|
+
<FormDialog
|
|
83
|
+
{...args}
|
|
84
|
+
open={open}
|
|
85
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
86
|
+
extraAction={{
|
|
87
|
+
label: "Medium",
|
|
88
|
+
onClick: () => console.log("extra action"),
|
|
89
|
+
}}
|
|
90
|
+
cancelAction={{
|
|
91
|
+
label: "Cancel",
|
|
92
|
+
onClick: () => updateArgs({ open: false }),
|
|
93
|
+
}}
|
|
94
|
+
confirmAction={{ label: "Confirm", disabled: true }}
|
|
95
|
+
trigger={
|
|
96
|
+
<Button fullwidth={false} onClick={() => updateArgs({ open: true })}>
|
|
97
|
+
Open (with extra action)
|
|
98
|
+
</Button>
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
<ContentArea />
|
|
102
|
+
</FormDialog>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const WithForm: Story = {
|
|
108
|
+
args: {
|
|
109
|
+
open: false,
|
|
110
|
+
title: "Edit profile",
|
|
111
|
+
description: "Make changes to your profile here.",
|
|
112
|
+
},
|
|
113
|
+
render: (args) => {
|
|
114
|
+
const [{ open }, updateArgs] = useArgs();
|
|
115
|
+
const [name, setName] = useState("");
|
|
116
|
+
const isValid = name.trim().length > 0;
|
|
117
|
+
return (
|
|
118
|
+
<FormDialog
|
|
119
|
+
{...args}
|
|
120
|
+
open={open}
|
|
121
|
+
onOpenChange={(next) => {
|
|
122
|
+
if (!next) setName("");
|
|
123
|
+
updateArgs({ open: next });
|
|
124
|
+
}}
|
|
125
|
+
cancelAction={{
|
|
126
|
+
label: "Cancel",
|
|
127
|
+
onClick: () => {
|
|
128
|
+
setName("");
|
|
129
|
+
updateArgs({ open: false });
|
|
130
|
+
},
|
|
131
|
+
}}
|
|
132
|
+
confirmAction={{
|
|
133
|
+
label: "Save changes",
|
|
134
|
+
disabled: !isValid,
|
|
135
|
+
onClick: () => {
|
|
136
|
+
console.log("save:", name);
|
|
137
|
+
updateArgs({ open: false });
|
|
138
|
+
},
|
|
139
|
+
}}
|
|
140
|
+
trigger={
|
|
141
|
+
<Button
|
|
142
|
+
variant="outline"
|
|
143
|
+
fullwidth={false}
|
|
144
|
+
onClick={() => updateArgs({ open: true })}
|
|
145
|
+
>
|
|
146
|
+
Edit profile
|
|
147
|
+
</Button>
|
|
148
|
+
}
|
|
149
|
+
>
|
|
150
|
+
<div className="flex flex-col gap-4">
|
|
151
|
+
<TextInput
|
|
152
|
+
label="Display name"
|
|
153
|
+
required
|
|
154
|
+
value={name}
|
|
155
|
+
onChange={(e) => setName(e.target.value)}
|
|
156
|
+
fullwidth
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</FormDialog>
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const FigmaFormDefaultOpen: Story = {
|
|
165
|
+
args: {
|
|
166
|
+
open: true,
|
|
167
|
+
title: "Title",
|
|
168
|
+
description: "Subtitle description",
|
|
169
|
+
},
|
|
170
|
+
render: (args) => {
|
|
171
|
+
const [{ open }, updateArgs] = useArgs();
|
|
172
|
+
return (
|
|
173
|
+
<FormDialog
|
|
174
|
+
{...args}
|
|
175
|
+
open={open}
|
|
176
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
177
|
+
cancelAction={{
|
|
178
|
+
label: "Cancel",
|
|
179
|
+
onClick: () => updateArgs({ open: false }),
|
|
180
|
+
}}
|
|
181
|
+
confirmAction={{ label: "Confirm", disabled: true }}
|
|
182
|
+
trigger={
|
|
183
|
+
<Button fullwidth={false} onClick={() => updateArgs({ open: true })}>
|
|
184
|
+
Open
|
|
185
|
+
</Button>
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
<ContentArea />
|
|
189
|
+
</FormDialog>
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const FigmaFormWithActionDefaultOpen: Story = {
|
|
195
|
+
args: {
|
|
196
|
+
open: true,
|
|
197
|
+
title: "Title",
|
|
198
|
+
description: "Subtitle description",
|
|
199
|
+
},
|
|
200
|
+
render: (args) => {
|
|
201
|
+
const [{ open }, updateArgs] = useArgs();
|
|
202
|
+
return (
|
|
203
|
+
<FormDialog
|
|
204
|
+
{...args}
|
|
205
|
+
open={open}
|
|
206
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
207
|
+
extraAction={{ label: "Medium" }}
|
|
208
|
+
cancelAction={{
|
|
209
|
+
label: "Cancel",
|
|
210
|
+
onClick: () => updateArgs({ open: false }),
|
|
211
|
+
}}
|
|
212
|
+
confirmAction={{ label: "Confirm", disabled: true }}
|
|
213
|
+
trigger={
|
|
214
|
+
<Button fullwidth={false} onClick={() => updateArgs({ open: true })}>
|
|
215
|
+
Open
|
|
216
|
+
</Button>
|
|
217
|
+
}
|
|
218
|
+
>
|
|
219
|
+
<ContentArea />
|
|
220
|
+
</FormDialog>
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// ─── Real Form integration ────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
type EditProfileValues = {
|
|
228
|
+
displayName: string;
|
|
229
|
+
email: string;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const editProfileSchema = yup.object({
|
|
233
|
+
displayName: yup.string().required("Display name is required."),
|
|
234
|
+
email: yup
|
|
235
|
+
.string()
|
|
236
|
+
.required("Email is required.")
|
|
237
|
+
.email("Must be a valid email."),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
type EditProfileDialogProps = {
|
|
241
|
+
open: boolean;
|
|
242
|
+
onOpenChange: (open: boolean) => void;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const EditProfileDialog = ({ open, onOpenChange }: EditProfileDialogProps) => {
|
|
246
|
+
const formId = React.useId();
|
|
247
|
+
|
|
248
|
+
const { methods, FormRoot } = useControlledForm<EditProfileValues>({
|
|
249
|
+
defaultValues: { displayName: "", email: "" },
|
|
250
|
+
validationSchema: editProfileSchema,
|
|
251
|
+
mode: "onTouched",
|
|
252
|
+
reValidateMode: "onChange",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const isValid = methods.formState.isValid;
|
|
256
|
+
|
|
257
|
+
const handleClose = () => {
|
|
258
|
+
methods.reset();
|
|
259
|
+
onOpenChange(false);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const handleSubmit = (values: EditProfileValues) => {
|
|
263
|
+
console.log("submitted:", values);
|
|
264
|
+
handleClose();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<FormDialog
|
|
269
|
+
open={open}
|
|
270
|
+
onOpenChange={(next) => {
|
|
271
|
+
if (!next) handleClose();
|
|
272
|
+
else onOpenChange(next);
|
|
273
|
+
}}
|
|
274
|
+
title="Edit profile"
|
|
275
|
+
description="Make changes to your profile here."
|
|
276
|
+
formId={formId}
|
|
277
|
+
cancelAction={{ label: "Cancel", onClick: handleClose }}
|
|
278
|
+
confirmAction={{ label: "Save changes", disabled: !isValid }}
|
|
279
|
+
>
|
|
280
|
+
<FormRoot
|
|
281
|
+
id={formId}
|
|
282
|
+
onSubmit={handleSubmit}
|
|
283
|
+
className="flex flex-col gap-4"
|
|
284
|
+
>
|
|
285
|
+
<Field<EditProfileValues, "displayName">
|
|
286
|
+
name="displayName"
|
|
287
|
+
component={TextInput}
|
|
288
|
+
componentProps={{
|
|
289
|
+
label: "Display name",
|
|
290
|
+
required: true,
|
|
291
|
+
fullwidth: true,
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
<Field<EditProfileValues, "email">
|
|
295
|
+
name="email"
|
|
296
|
+
component={TextInput}
|
|
297
|
+
componentProps={{ label: "Email", required: true, fullwidth: true }}
|
|
298
|
+
/>
|
|
299
|
+
</FormRoot>
|
|
300
|
+
</FormDialog>
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const EditProfileDialogWithLoading = ({
|
|
305
|
+
open,
|
|
306
|
+
onOpenChange,
|
|
307
|
+
}: EditProfileDialogProps) => {
|
|
308
|
+
const formId = React.useId();
|
|
309
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
310
|
+
|
|
311
|
+
const { methods, FormRoot } = useControlledForm<EditProfileValues>({
|
|
312
|
+
defaultValues: { displayName: "", email: "" },
|
|
313
|
+
validationSchema: editProfileSchema,
|
|
314
|
+
mode: "onTouched",
|
|
315
|
+
reValidateMode: "onChange",
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const isValid = methods.formState.isValid;
|
|
319
|
+
|
|
320
|
+
const handleClose = () => {
|
|
321
|
+
methods.reset();
|
|
322
|
+
onOpenChange(false);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const handleSubmit = async (values: EditProfileValues) => {
|
|
326
|
+
setIsLoading(true);
|
|
327
|
+
try {
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
329
|
+
console.log("saved:", values);
|
|
330
|
+
handleClose();
|
|
331
|
+
} finally {
|
|
332
|
+
setIsLoading(false);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<FormDialog
|
|
338
|
+
open={open}
|
|
339
|
+
onOpenChange={(next) => {
|
|
340
|
+
if (!next && !isLoading) handleClose();
|
|
341
|
+
else if (next) onOpenChange(next);
|
|
342
|
+
}}
|
|
343
|
+
title="Edit profile"
|
|
344
|
+
description="Make changes to your profile here."
|
|
345
|
+
formId={formId}
|
|
346
|
+
cancelAction={{
|
|
347
|
+
label: "Cancel",
|
|
348
|
+
onClick: handleClose,
|
|
349
|
+
disabled: isLoading,
|
|
350
|
+
}}
|
|
351
|
+
confirmAction={{
|
|
352
|
+
label: "Save changes",
|
|
353
|
+
disabled: !isValid || isLoading,
|
|
354
|
+
isLoading,
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
<FormRoot
|
|
358
|
+
id={formId}
|
|
359
|
+
onSubmit={handleSubmit}
|
|
360
|
+
className="flex flex-col gap-4"
|
|
361
|
+
>
|
|
362
|
+
<Field<EditProfileValues, "displayName">
|
|
363
|
+
name="displayName"
|
|
364
|
+
component={TextInput}
|
|
365
|
+
componentProps={{
|
|
366
|
+
label: "Display name",
|
|
367
|
+
required: true,
|
|
368
|
+
fullwidth: true,
|
|
369
|
+
}}
|
|
370
|
+
/>
|
|
371
|
+
<Field<EditProfileValues, "email">
|
|
372
|
+
name="email"
|
|
373
|
+
component={TextInput}
|
|
374
|
+
componentProps={{ label: "Email", required: true, fullwidth: true }}
|
|
375
|
+
/>
|
|
376
|
+
</FormRoot>
|
|
377
|
+
</FormDialog>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Pattern A: useControlledForm
|
|
383
|
+
*
|
|
384
|
+
* Use when you need to:
|
|
385
|
+
* - access formState (isValid, isDirty) to control the confirm button
|
|
386
|
+
* - reset the form on close
|
|
387
|
+
* - trigger validation outside the form element
|
|
388
|
+
*/
|
|
389
|
+
export const WithRovulaForm: Story = {
|
|
390
|
+
args: { open: false },
|
|
391
|
+
render: () => {
|
|
392
|
+
const [{ open }, updateArgs] = useArgs();
|
|
393
|
+
return (
|
|
394
|
+
<>
|
|
395
|
+
<Button
|
|
396
|
+
variant="outline"
|
|
397
|
+
fullwidth={false}
|
|
398
|
+
onClick={() => updateArgs({ open: true })}
|
|
399
|
+
>
|
|
400
|
+
Edit profile
|
|
401
|
+
</Button>
|
|
402
|
+
<EditProfileDialog
|
|
403
|
+
open={open}
|
|
404
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
405
|
+
/>
|
|
406
|
+
</>
|
|
407
|
+
);
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Pattern B: useControlledForm + async API call + loading state
|
|
413
|
+
*
|
|
414
|
+
* Use when the confirm action calls an API.
|
|
415
|
+
* isLoading disables + shows spinner on the confirm button while the request is in flight.
|
|
416
|
+
*/
|
|
417
|
+
export const WithRovulaFormAndApiLoading: Story = {
|
|
418
|
+
args: { open: false },
|
|
419
|
+
render: () => {
|
|
420
|
+
const [{ open }, updateArgs] = useArgs();
|
|
421
|
+
return (
|
|
422
|
+
<>
|
|
423
|
+
<Button
|
|
424
|
+
variant="outline"
|
|
425
|
+
fullwidth={false}
|
|
426
|
+
onClick={() => updateArgs({ open: true })}
|
|
427
|
+
>
|
|
428
|
+
Edit profile (with loading)
|
|
429
|
+
</Button>
|
|
430
|
+
<EditProfileDialogWithLoading
|
|
431
|
+
open={open}
|
|
432
|
+
onOpenChange={(next) => updateArgs({ open: next })}
|
|
433
|
+
/>
|
|
434
|
+
</>
|
|
435
|
+
);
|
|
436
|
+
},
|
|
437
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogBody,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from "@/components/Dialog/Dialog";
|
|
14
|
+
import Button from "@/components/Button/Button";
|
|
15
|
+
import Loading from "@/components/Loading/Loading";
|
|
16
|
+
|
|
17
|
+
export type FormDialogAction = {
|
|
18
|
+
label: string;
|
|
19
|
+
onClick?: () => void;
|
|
20
|
+
variant?: React.ComponentProps<typeof Button>["variant"];
|
|
21
|
+
color?: React.ComponentProps<typeof Button>["color"];
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
isLoading?: boolean;
|
|
24
|
+
type?: "button" | "submit" | "reset";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type FormDialogProps = {
|
|
28
|
+
open?: boolean;
|
|
29
|
+
onOpenChange?: (open: boolean) => void;
|
|
30
|
+
title: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
children?: React.ReactNode;
|
|
33
|
+
trigger?: React.ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* Primary action (right side of footer).
|
|
36
|
+
*/
|
|
37
|
+
confirmAction?: FormDialogAction;
|
|
38
|
+
/**
|
|
39
|
+
* Secondary cancel action (right side of footer, outline style).
|
|
40
|
+
*/
|
|
41
|
+
cancelAction?: FormDialogAction;
|
|
42
|
+
/**
|
|
43
|
+
* Optional extra action placed on the left side of the footer.
|
|
44
|
+
*/
|
|
45
|
+
extraAction?: FormDialogAction;
|
|
46
|
+
scrollable?: boolean;
|
|
47
|
+
className?: string;
|
|
48
|
+
/**
|
|
49
|
+
* When provided, the confirm button becomes type="submit" and is linked to this form id.
|
|
50
|
+
* Use together with a <Form id={formId} .../> inside children.
|
|
51
|
+
*/
|
|
52
|
+
formId?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const FormDialog: React.FC<FormDialogProps> = ({
|
|
56
|
+
open,
|
|
57
|
+
onOpenChange,
|
|
58
|
+
title,
|
|
59
|
+
description,
|
|
60
|
+
children,
|
|
61
|
+
trigger,
|
|
62
|
+
confirmAction,
|
|
63
|
+
cancelAction,
|
|
64
|
+
extraAction,
|
|
65
|
+
scrollable = false,
|
|
66
|
+
className,
|
|
67
|
+
formId,
|
|
68
|
+
}) => {
|
|
69
|
+
const hasFooter = confirmAction || cancelAction || extraAction;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
73
|
+
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
|
74
|
+
<DialogContent className={className}>
|
|
75
|
+
<DialogHeader>
|
|
76
|
+
<DialogTitle>{title}</DialogTitle>
|
|
77
|
+
{description && (
|
|
78
|
+
<DialogDescription>{description}</DialogDescription>
|
|
79
|
+
)}
|
|
80
|
+
</DialogHeader>
|
|
81
|
+
|
|
82
|
+
{children && (
|
|
83
|
+
<DialogBody scrollable={scrollable}>{children}</DialogBody>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{hasFooter && (
|
|
87
|
+
<DialogFooter className={extraAction ? "justify-between" : undefined}>
|
|
88
|
+
{extraAction && (
|
|
89
|
+
<Button
|
|
90
|
+
type={extraAction.type ?? "button"}
|
|
91
|
+
variant={extraAction.variant ?? "outline"}
|
|
92
|
+
color={extraAction.color ?? "secondary"}
|
|
93
|
+
fullwidth={false}
|
|
94
|
+
disabled={extraAction.disabled}
|
|
95
|
+
isLoading={extraAction.isLoading}
|
|
96
|
+
onClick={extraAction.onClick}
|
|
97
|
+
>
|
|
98
|
+
{extraAction.label}
|
|
99
|
+
</Button>
|
|
100
|
+
)}
|
|
101
|
+
<div className="flex items-center gap-4">
|
|
102
|
+
{cancelAction && (
|
|
103
|
+
<Button
|
|
104
|
+
type={cancelAction.type ?? "button"}
|
|
105
|
+
variant={cancelAction.variant ?? "outline"}
|
|
106
|
+
color={cancelAction.color ?? "primary"}
|
|
107
|
+
fullwidth={false}
|
|
108
|
+
disabled={cancelAction.disabled}
|
|
109
|
+
isLoading={cancelAction.isLoading}
|
|
110
|
+
onClick={cancelAction.onClick}
|
|
111
|
+
>
|
|
112
|
+
{cancelAction.label}
|
|
113
|
+
</Button>
|
|
114
|
+
)}
|
|
115
|
+
{confirmAction && (
|
|
116
|
+
<Button
|
|
117
|
+
type={formId ? "submit" : (confirmAction.type ?? "button")}
|
|
118
|
+
form={formId}
|
|
119
|
+
variant={confirmAction.variant ?? "solid"}
|
|
120
|
+
color={confirmAction.color ?? "primary"}
|
|
121
|
+
fullwidth={false}
|
|
122
|
+
disabled={confirmAction.disabled}
|
|
123
|
+
isLoading={confirmAction.isLoading}
|
|
124
|
+
onClick={confirmAction.onClick}
|
|
125
|
+
>
|
|
126
|
+
{confirmAction.label}
|
|
127
|
+
</Button>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</DialogFooter>
|
|
131
|
+
)}
|
|
132
|
+
</DialogContent>
|
|
133
|
+
</Dialog>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
FormDialog.displayName = "FormDialog";
|