@axzydev/axzy_ui_system 1.4.8 → 1.5.1
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/README.md +2 -11
- package/dist/index.cjs +1826 -1351
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +115 -14
- package/dist/index.d.ts +115 -14
- package/dist/index.js +1481 -1010
- package/dist/index.js.map +1 -1
- package/dist/layered.css +1 -1
- package/package.json +3 -16
- package/src/App.tsx +90 -73
- package/src/components/atoms/badget/badget.tsx +23 -5
- package/src/components/atoms/chip/chip.tsx +3 -1
- package/src/components/atoms/image/image.tsx +3 -1
- package/src/components/atoms/input/input.tsx +8 -5
- package/src/components/atoms/loader/loader.tsx +3 -1
- package/src/components/atoms/slide/slide.props.ts +12 -0
- package/src/components/atoms/slide/slide.tsx +3 -0
- package/src/components/molecules/alert/alert.tsx +3 -1
- package/src/components/molecules/breadcrumbs/breadcrumbs.tsx +3 -1
- package/src/components/molecules/calendar/calendar.tsx +9 -6
- package/src/components/molecules/card/card.tsx +5 -2
- package/src/components/molecules/chip-input/chip-input.tsx +9 -7
- package/src/components/molecules/date-picker/datePicker.tsx +9 -9
- package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +1 -1
- package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +6 -3
- package/src/components/molecules/field/field.tsx +3 -1
- package/src/components/molecules/form-header/form-header.tsx +3 -1
- package/src/components/molecules/line-bar-chart/line-bar-chart.props.ts +2 -2
- package/src/components/molecules/line-bar-chart/line-bar-chart.tsx +20 -15
- package/src/components/molecules/masked-input/masked-input.tsx +3 -1
- package/src/components/molecules/multi-select/multi-select.tsx +12 -9
- package/src/components/molecules/pagination/pagination.tsx +11 -5
- package/src/components/molecules/search-select/search-select.tsx +11 -9
- package/src/components/molecules/select/select.tsx +6 -4
- package/src/components/molecules/stepper/stepper.tsx +6 -4
- package/src/components/molecules/table/table.tsx +30 -28
- package/src/components/molecules/time-picker/timePicker.tsx +8 -6
- package/src/components/molecules/wysiwyg/wysiwyg.tsx +23 -12
- package/src/components/organisms/confirm-dialog/confirm-dialog.props.ts +4 -4
- package/src/components/organisms/confirm-dialog/confirm-dialog.tsx +16 -8
- package/src/components/organisms/data-table/dataTable.tsx +31 -25
- package/src/components/organisms/dialog/dialog.tsx +3 -0
- package/src/components/organisms/drawer/drawer.tsx +3 -0
- package/src/components/organisms/dropfile/dropfile.tsx +19 -17
- package/src/components/organisms/form-builder/fieldRenderer.tsx +4 -2
- package/src/components/organisms/navbar/navbar.tsx +4 -2
- package/src/components/organisms/page-header/page-header.tsx +3 -1
- package/src/components/organisms/searchTable/components/PaginationControls.tsx +8 -6
- package/src/components/organisms/searchTable/components/PaginationInfo.tsx +3 -1
- package/src/components/organisms/searchTable/components/SearchAndSortBar.tsx +7 -5
- package/src/components/organisms/searchTable/components/SearchInput.tsx +4 -2
- package/src/components/organisms/searchTable/components/SortButton.tsx +13 -11
- package/src/components/organisms/searchTable/components/TableEmptyState.tsx +4 -2
- package/src/components/organisms/searchTable/components/TableRow.tsx +6 -4
- package/src/components/organisms/searchTable/searchTable.tsx +4 -2
- package/src/components/organisms/sidebar/sidebar.props.ts +1 -1
- package/src/components/organisms/sidebar/sidebar.tsx +9 -4
- package/src/components/organisms/toast/toast.tsx +3 -1
- package/src/components/organisms/topbar/topbar.tsx +4 -2
- package/src/components/templates/page/page.props.ts +4 -4
- package/src/components/templates/page/page.tsx +6 -4
- package/src/components/theme-provider/themeProvider.tsx +28 -17
- package/src/dev.css +61 -2
- package/src/global.d.ts +10 -0
- package/src/i18n/context.ts +9 -0
- package/src/i18n/index.ts +22 -0
- package/src/i18n/locales/en.ts +163 -0
- package/src/i18n/locales/es.ts +165 -0
- package/src/i18n/provider.tsx +44 -0
- package/src/i18n/translator.ts +82 -0
- package/src/i18n/types.ts +54 -0
- package/src/i18n/useITTranslation.ts +20 -0
- package/src/index.css +0 -1
- package/src/index.ts +7 -1
- package/src/sandbox/LandingShowcase.tsx +224 -580
- package/src/sandbox/NotFoundShowcase.tsx +51 -36
- package/src/sandbox/TopbarSearch.tsx +6 -4
- package/src/sandbox/controls.tsx +180 -0
- package/src/sandbox/i18n.ts +1939 -0
- package/src/sandbox/icons.tsx +25 -0
- package/src/sandbox/locale.tsx +72 -0
- package/src/sandbox/ui.tsx +474 -0
- package/src/sandbox/useHashRoute.ts +46 -10
- package/src/showcases/DataShowcases.tsx +103 -87
- package/src/showcases/FeedbackShowcases.tsx +37 -30
- package/src/showcases/FormShowcases.tsx +303 -215
- package/src/showcases/GettingStartedShowcase.tsx +151 -145
- package/src/showcases/HelpShowcase.tsx +126 -128
- package/src/showcases/HomeShowcase.tsx +678 -482
- package/src/showcases/LayoutPrimitivesShowcases.tsx +129 -114
- package/src/showcases/NavigationShowcases.tsx +45 -37
- package/src/showcases/PageShowcases.tsx +171 -152
- package/src/showcases/ProductoDetalle.tsx +272 -129
- package/src/showcases/ShowcaseLayout.tsx +256 -219
- package/src/showcases/SizesShowcase.tsx +123 -64
- package/src/showcases/StructureShowcases.tsx +203 -138
- package/src/showcases/portfolioUi.tsx +153 -0
- package/src/showcases/techIcons.ts +3 -0
- package/src/utils/table.utils.ts +10 -2
- package/src/components/atoms/avatar/avatar.doc.mdx +0 -48
- package/src/components/atoms/avatar/avatar.stories.tsx +0 -64
- package/src/components/atoms/badget/badget.doc.mdx +0 -46
- package/src/components/atoms/badget/badget.stories.tsx +0 -75
- package/src/components/atoms/button/button.doc.mdx +0 -59
- package/src/components/atoms/button/button.stories.tsx +0 -173
- package/src/components/atoms/checkbox/checkbox.doc.mdx +0 -54
- package/src/components/atoms/checkbox/checkbox.stories.tsx +0 -53
- package/src/components/atoms/chip/chip.doc.mdx +0 -47
- package/src/components/atoms/chip/chip.stories.tsx +0 -64
- package/src/components/atoms/divider/divider.doc.mdx +0 -42
- package/src/components/atoms/divider/divider.stories.tsx +0 -33
- package/src/components/atoms/flex/flex.doc.mdx +0 -59
- package/src/components/atoms/flex/flex.stories.tsx +0 -70
- package/src/components/atoms/grid/grid.doc.mdx +0 -54
- package/src/components/atoms/grid/grid.stories.tsx +0 -71
- package/src/components/atoms/image/image.doc.mdx +0 -44
- package/src/components/atoms/image/image.stories.tsx +0 -48
- package/src/components/atoms/input/input.doc.mdx +0 -70
- package/src/components/atoms/input/input.stories.tsx +0 -113
- package/src/components/atoms/input-number/input-number.doc.mdx +0 -74
- package/src/components/atoms/input-number/input-number.stories.tsx +0 -111
- package/src/components/atoms/loader/loader.doc.mdx +0 -43
- package/src/components/atoms/loader/loader.stories.tsx +0 -104
- package/src/components/atoms/popover/popover.doc.mdx +0 -53
- package/src/components/atoms/popover/popover.stories.tsx +0 -24
- package/src/components/atoms/progress/progress.doc.mdx +0 -48
- package/src/components/atoms/progress/progress.stories.tsx +0 -39
- package/src/components/atoms/radio/radio.doc.mdx +0 -62
- package/src/components/atoms/radio/radio.stories.tsx +0 -48
- package/src/components/atoms/segmented-control/segmented-control.doc.mdx +0 -60
- package/src/components/atoms/segmented-control/segmented-control.stories.tsx +0 -62
- package/src/components/atoms/skeleton/skeleton.doc.mdx +0 -43
- package/src/components/atoms/skeleton/skeleton.stories.tsx +0 -29
- package/src/components/atoms/slide/slide.doc.mdx +0 -46
- package/src/components/atoms/slide/slide.stories.tsx +0 -120
- package/src/components/atoms/slider/slider.doc.mdx +0 -49
- package/src/components/atoms/slider/slider.stories.tsx +0 -29
- package/src/components/atoms/stack/stack.doc.mdx +0 -126
- package/src/components/atoms/stack/stack.stories.tsx +0 -283
- package/src/components/atoms/text/text.doc.mdx +0 -41
- package/src/components/atoms/text/text.stories.tsx +0 -66
- package/src/components/atoms/textarea/textarea.doc.mdx +0 -64
- package/src/components/atoms/textarea/textarea.stories.tsx +0 -26
- package/src/components/atoms/tooltip/tooltip.doc.mdx +0 -47
- package/src/components/atoms/tooltip/tooltip.stories.tsx +0 -19
- package/src/components/isolation/isolation.stories.tsx +0 -202
- package/src/components/molecules/accordion/accordion.doc.mdx +0 -47
- package/src/components/molecules/accordion/accordion.stories.tsx +0 -68
- package/src/components/molecules/alert/alert.doc.mdx +0 -48
- package/src/components/molecules/alert/alert.stories.tsx +0 -40
- package/src/components/molecules/breadcrumbs/breadcrumbs.doc.mdx +0 -57
- package/src/components/molecules/breadcrumbs/breadcrumbs.stories.tsx +0 -20
- package/src/components/molecules/calendar/calendar.doc.mdx +0 -76
- package/src/components/molecules/calendar/calendar.stories.tsx +0 -90
- package/src/components/molecules/card/card.doc.mdx +0 -54
- package/src/components/molecules/card/card.stories.tsx +0 -57
- package/src/components/molecules/chip-input/chip-input.doc.mdx +0 -44
- package/src/components/molecules/chip-input/chip-input.stories.tsx +0 -52
- package/src/components/molecules/date-picker/date-picker.doc.mdx +0 -65
- package/src/components/molecules/date-picker/date-picker.stories.tsx +0 -88
- package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +0 -46
- package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +0 -41
- package/src/components/molecules/empty-state/empty-state.doc.mdx +0 -50
- package/src/components/molecules/empty-state/empty-state.stories.tsx +0 -19
- package/src/components/molecules/field/field.doc.mdx +0 -43
- package/src/components/molecules/field/field.stories.tsx +0 -42
- package/src/components/molecules/form-header/form-header.doc.mdx +0 -38
- package/src/components/molecules/form-header/form-header.stories.tsx +0 -52
- package/src/components/molecules/masked-input/masked-input.doc.mdx +0 -78
- package/src/components/molecules/masked-input/masked-input.stories.tsx +0 -126
- package/src/components/molecules/multi-select/multi-select.doc.mdx +0 -55
- package/src/components/molecules/multi-select/multi-select.stories.tsx +0 -59
- package/src/components/molecules/pagination/pagination.doc.mdx +0 -62
- package/src/components/molecules/pagination/pagination.stories.tsx +0 -110
- package/src/components/molecules/search-select/search-select.doc.mdx +0 -77
- package/src/components/molecules/search-select/search-select.stories.tsx +0 -146
- package/src/components/molecules/select/select.doc.mdx +0 -75
- package/src/components/molecules/select/select.stories.tsx +0 -85
- package/src/components/molecules/stat-card/stat-card.doc.mdx +0 -48
- package/src/components/molecules/stat-card/stat-card.stories.tsx +0 -40
- package/src/components/molecules/stepper/stepper.doc.mdx +0 -54
- package/src/components/molecules/stepper/stepper.stories.tsx +0 -154
- package/src/components/molecules/table/table.doc.mdx +0 -140
- package/src/components/molecules/table/table.stories.tsx +0 -402
- package/src/components/molecules/tabs/tabs.doc.mdx +0 -52
- package/src/components/molecules/tabs/tabs.stories.tsx +0 -31
- package/src/components/molecules/time-picker/timePicker.doc.mdx +0 -68
- package/src/components/molecules/time-picker/timePicker.stories.tsx +0 -130
- package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +0 -95
- package/src/components/organisms/confirm-dialog/confirm-dialog.doc.mdx +0 -60
- package/src/components/organisms/confirm-dialog/confirm-dialog.stories.tsx +0 -32
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +0 -132
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +0 -353
- package/src/components/organisms/dialog/dialog.doc.mdx +0 -54
- package/src/components/organisms/dialog/dialog.stories.tsx +0 -126
- package/src/components/organisms/drawer/drawer.doc.mdx +0 -59
- package/src/components/organisms/drawer/drawer.stories.tsx +0 -40
- package/src/components/organisms/dropfile/dropfile.doc.mdx +0 -74
- package/src/components/organisms/dropfile/dropfile.stories.tsx +0 -93
- package/src/components/organisms/form-builder/formBuilder.doc.mdx +0 -251
- package/src/components/organisms/form-builder/formBuilder.stories.tsx +0 -914
- package/src/components/organisms/navbar/navbar.doc.mdx +0 -77
- package/src/components/organisms/navbar/navbar.stories.tsx +0 -83
- package/src/components/organisms/page-header/page-header.doc.mdx +0 -54
- package/src/components/organisms/page-header/page-header.stories.tsx +0 -74
- package/src/components/organisms/searchTable/searchTable.doc.mdx +0 -79
- package/src/components/organisms/searchTable/searchTable.stories.tsx +0 -73
- package/src/components/organisms/sidebar/sidebar.doc.mdx +0 -176
- package/src/components/organisms/sidebar/sidebar.stories.tsx +0 -234
- package/src/components/organisms/toast/toast.doc.mdx +0 -50
- package/src/components/organisms/toast/toast.stories.tsx +0 -137
- package/src/components/organisms/topbar/topbar.doc.mdx +0 -72
- package/src/components/organisms/topbar/topbar.stories.tsx +0 -79
- package/src/components/organisms/triple-filter/tripleFilter.doc.mdx +0 -62
- package/src/components/organisms/triple-filter/tripleFilter.stories.tsx +0 -31
- package/src/components/templates/layout/layout.doc.mdx +0 -53
- package/src/components/templates/layout/layout.stories.tsx +0 -113
- package/src/components/templates/page/page.doc.mdx +0 -86
- package/src/components/templates/page/page.stories.tsx +0 -97
- package/src/components/theme-provider/themeProvider.doc.mdx +0 -85
- package/src/components/theme-provider/themeProvider.stories.tsx +0 -58
|
@@ -1,914 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import ITFormBuilder from "./formBuilder";
|
|
3
|
-
import { Formik } from "formik";
|
|
4
|
-
import * as Yup from "yup";
|
|
5
|
-
import { useState } from "react";
|
|
6
|
-
import { FieldConfig, FieldConfigV2 } from "@/types/field.types";
|
|
7
|
-
import {
|
|
8
|
-
FaUser,
|
|
9
|
-
FaEnvelope,
|
|
10
|
-
FaSearch,
|
|
11
|
-
FaLock,
|
|
12
|
-
FaCalculator,
|
|
13
|
-
FaCommentDots,
|
|
14
|
-
FaCheckCircle,
|
|
15
|
-
} from "react-icons/fa";
|
|
16
|
-
import { expect, fn, userEvent } from "storybook/test";
|
|
17
|
-
import { useFormBuilder } from "./useFormBuilder";
|
|
18
|
-
|
|
19
|
-
const meta: Meta<typeof ITFormBuilder> = {
|
|
20
|
-
title: "Components/Form Elements/ITFormBuilder",
|
|
21
|
-
component: ITFormBuilder,
|
|
22
|
-
parameters: {
|
|
23
|
-
layout: "centered",
|
|
24
|
-
docs: {
|
|
25
|
-
description: {
|
|
26
|
-
component:
|
|
27
|
-
"Dynamic form builder using Formik and Yup configuration. Supports Legacy V1 and the powerful Enterprise V2 Grid mapping, validating, calculating, and conditional logic.",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
export default meta;
|
|
33
|
-
type Story = StoryObj<typeof ITFormBuilder>;
|
|
34
|
-
|
|
35
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
-
// Helpers shared by the test stories
|
|
37
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
/** Controlled wrapper that drives the builder with local React state. */
|
|
40
|
-
const ControlledForm = ({
|
|
41
|
-
children,
|
|
42
|
-
initialValues,
|
|
43
|
-
}: {
|
|
44
|
-
children: (props: {
|
|
45
|
-
values: Record<string, any>;
|
|
46
|
-
setValues: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
|
47
|
-
setFieldValue: (field: string, value: any) => Promise<void>;
|
|
48
|
-
handleChange: (e: any) => void;
|
|
49
|
-
handleBlur: (e: any) => void;
|
|
50
|
-
touched: Record<string, boolean>;
|
|
51
|
-
errors: Record<string, string>;
|
|
52
|
-
}) => React.ReactNode;
|
|
53
|
-
initialValues: Record<string, any>;
|
|
54
|
-
}) => {
|
|
55
|
-
const [values, setValues] = useState<Record<string, any>>(initialValues);
|
|
56
|
-
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
|
57
|
-
const [errors] = useState<Record<string, string>>({});
|
|
58
|
-
|
|
59
|
-
const handleChange = (e: any) => {
|
|
60
|
-
const { name, value, type, checked } = e.target ?? {};
|
|
61
|
-
setValues((prev) => ({
|
|
62
|
-
...prev,
|
|
63
|
-
[name]: type === "checkbox" ? checked : value,
|
|
64
|
-
}));
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const handleBlur = (e: any) => {
|
|
68
|
-
const { name } = e.target ?? {};
|
|
69
|
-
if (name) setTouched((prev) => ({ ...prev, [name]: true }));
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const setFieldValue = async (field: string, value: any) => {
|
|
73
|
-
setValues((prev) => ({ ...prev, [field]: value }));
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<>
|
|
78
|
-
{children({
|
|
79
|
-
values,
|
|
80
|
-
setValues,
|
|
81
|
-
setFieldValue,
|
|
82
|
-
handleChange,
|
|
83
|
-
handleBlur,
|
|
84
|
-
touched,
|
|
85
|
-
errors,
|
|
86
|
-
})}
|
|
87
|
-
{/* Surface state so test play functions and humans can read it. */}
|
|
88
|
-
<pre
|
|
89
|
-
data-testid="form-state"
|
|
90
|
-
style={{
|
|
91
|
-
marginTop: 16,
|
|
92
|
-
padding: 8,
|
|
93
|
-
background: "#0f172a",
|
|
94
|
-
color: "#a7f3d0",
|
|
95
|
-
fontSize: 11,
|
|
96
|
-
borderRadius: 8,
|
|
97
|
-
maxWidth: 480,
|
|
98
|
-
overflow: "auto",
|
|
99
|
-
}}
|
|
100
|
-
>
|
|
101
|
-
{JSON.stringify({ values, touched, errors }, null, 2)}
|
|
102
|
-
</pre>
|
|
103
|
-
</>
|
|
104
|
-
);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
-
// Showcase stories (kept as-is for the Storybook gallery)
|
|
109
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
const FormWrapper = ({
|
|
112
|
-
children,
|
|
113
|
-
initialValues = {},
|
|
114
|
-
validationSchema,
|
|
115
|
-
widthClass = "w-[800px]",
|
|
116
|
-
}: {
|
|
117
|
-
children: (props: any) => React.ReactNode;
|
|
118
|
-
initialValues?: any;
|
|
119
|
-
validationSchema?: any;
|
|
120
|
-
widthClass?: string;
|
|
121
|
-
}) => {
|
|
122
|
-
const [submittedValues, setSubmittedValues] = useState<any>(null);
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<div
|
|
126
|
-
className={`p-6 bg-white rounded-xl shadow-sm border border-gray-100 max-w-full ${widthClass}`}
|
|
127
|
-
>
|
|
128
|
-
<Formik
|
|
129
|
-
initialValues={initialValues}
|
|
130
|
-
validationSchema={validationSchema}
|
|
131
|
-
onSubmit={(values) => {
|
|
132
|
-
setSubmittedValues(values);
|
|
133
|
-
console.log("Form submitted:", values);
|
|
134
|
-
}}
|
|
135
|
-
>
|
|
136
|
-
{(formikProps) => (
|
|
137
|
-
<form onSubmit={formikProps.handleSubmit}>
|
|
138
|
-
{children(formikProps)}
|
|
139
|
-
|
|
140
|
-
<div className="mt-6 flex items-center justify-end gap-3 pt-4 border-t border-gray-100">
|
|
141
|
-
<button
|
|
142
|
-
type="button"
|
|
143
|
-
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
|
144
|
-
onClick={() => formikProps.resetForm()}
|
|
145
|
-
>
|
|
146
|
-
Limpiar Formulario
|
|
147
|
-
</button>
|
|
148
|
-
<button
|
|
149
|
-
type="submit"
|
|
150
|
-
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 shadow-sm transition-colors"
|
|
151
|
-
>
|
|
152
|
-
Enviar Datos
|
|
153
|
-
</button>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
{submittedValues && (
|
|
157
|
-
<div className="mt-4 p-3 bg-gray-50 rounded-lg text-xs font-mono overflow-auto max-h-40">
|
|
158
|
-
<p className="font-semibold text-gray-500 mb-1">
|
|
159
|
-
Payload Generado:
|
|
160
|
-
</p>
|
|
161
|
-
<pre>{JSON.stringify(submittedValues, null, 2)}</pre>
|
|
162
|
-
</div>
|
|
163
|
-
)}
|
|
164
|
-
</form>
|
|
165
|
-
)}
|
|
166
|
-
</Formik>
|
|
167
|
-
</div>
|
|
168
|
-
);
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const showcaseConfig: FieldConfigV2[] = [
|
|
172
|
-
{
|
|
173
|
-
name: "username",
|
|
174
|
-
label: "Usuario (Icono Izquierdo Fijo)",
|
|
175
|
-
type: "text",
|
|
176
|
-
placeholder: "Ej. dev123",
|
|
177
|
-
required: true,
|
|
178
|
-
leftIcon: <FaUser className="text-gray-400" />,
|
|
179
|
-
column: { sm: 12, md: 6 },
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: "promoCode",
|
|
183
|
-
label: "Código Promo (Icono Derecho Click)",
|
|
184
|
-
type: "text",
|
|
185
|
-
placeholder: "Ingresa código...",
|
|
186
|
-
rightIcon: (
|
|
187
|
-
<button
|
|
188
|
-
type="button"
|
|
189
|
-
onClick={() => alert("¡Disparando búsqueda de código promocional!")}
|
|
190
|
-
className="text-primary-600 hover:text-primary-800 transition px-2"
|
|
191
|
-
>
|
|
192
|
-
<FaSearch />
|
|
193
|
-
</button>
|
|
194
|
-
),
|
|
195
|
-
column: { sm: 12, md: 6 },
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
name: "email",
|
|
199
|
-
label: "Correo (Validación estricta YUP)",
|
|
200
|
-
type: "email",
|
|
201
|
-
placeholder: "ejemplo@correo.com",
|
|
202
|
-
required: true,
|
|
203
|
-
leftIcon: <FaEnvelope className="text-gray-400" />,
|
|
204
|
-
column: { sm: 12, md: 6 },
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
name: "password",
|
|
208
|
-
label: "Contraseña",
|
|
209
|
-
type: "password",
|
|
210
|
-
required: true,
|
|
211
|
-
placeholder: "••••••••",
|
|
212
|
-
leftIcon: <FaLock className="text-gray-400" />,
|
|
213
|
-
column: { sm: 12, md: 6 },
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
name: "subtotal",
|
|
217
|
-
label: "Subtotal (Dispara OnChange)",
|
|
218
|
-
type: "number",
|
|
219
|
-
required: true,
|
|
220
|
-
currencyFormat: true,
|
|
221
|
-
leftIcon: <FaCalculator className="text-gray-400" />,
|
|
222
|
-
column: { sm: 12, md: 4 },
|
|
223
|
-
onChangeAction: (val, { setFieldValue }) => {
|
|
224
|
-
const num = parseFloat(val) || 0;
|
|
225
|
-
setFieldValue("iva", (num * 0.16).toFixed(2));
|
|
226
|
-
setFieldValue("total", (num * 1.16).toFixed(2));
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
name: "iva",
|
|
231
|
-
label: "IVA (Read-Only)",
|
|
232
|
-
type: "number",
|
|
233
|
-
currencyFormat: true,
|
|
234
|
-
readOnly: true,
|
|
235
|
-
column: { sm: 12, md: 4 },
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
name: "total",
|
|
239
|
-
label: "Total (Disabled)",
|
|
240
|
-
type: "number",
|
|
241
|
-
currencyFormat: true,
|
|
242
|
-
disabled: true,
|
|
243
|
-
column: { sm: 12, md: 4 },
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
name: "country",
|
|
247
|
-
label: "País (Muestra RFC si es MX)",
|
|
248
|
-
type: "select",
|
|
249
|
-
required: true,
|
|
250
|
-
options: [
|
|
251
|
-
{ value: "", label: "Selecciona país..." },
|
|
252
|
-
{ value: "MX", label: "México" },
|
|
253
|
-
{ value: "US", label: "Estados Unidos" },
|
|
254
|
-
{ value: "OTHER", label: "Otro" },
|
|
255
|
-
],
|
|
256
|
-
column: { sm: 12, md: 6 },
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
name: "rfc",
|
|
260
|
-
label: "RFC de México (Renderización Condicional)",
|
|
261
|
-
type: "text",
|
|
262
|
-
dependsOn: ["country"],
|
|
263
|
-
renderWhen: (vals) => vals.country === "MX",
|
|
264
|
-
required: true,
|
|
265
|
-
showHintLength: true,
|
|
266
|
-
maxLength: 13,
|
|
267
|
-
column: { sm: 12, md: 6 },
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
name: "date",
|
|
271
|
-
label: "Selector de Fecha",
|
|
272
|
-
type: "date",
|
|
273
|
-
required: true,
|
|
274
|
-
column: { sm: 12, md: 4 },
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
name: "time",
|
|
278
|
-
label: "Selector de Hora",
|
|
279
|
-
type: "time" as any,
|
|
280
|
-
required: true,
|
|
281
|
-
column: { sm: 12, md: 4 },
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
name: "comments",
|
|
285
|
-
label: "Comentarios (Textarea + Límite)",
|
|
286
|
-
type: "textarea" as any,
|
|
287
|
-
rows: 3,
|
|
288
|
-
showHintLength: true,
|
|
289
|
-
maxLength: 200,
|
|
290
|
-
leftIcon: <FaCommentDots className="text-gray-400" />,
|
|
291
|
-
column: { sm: 12, md: 4 },
|
|
292
|
-
},
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
const showcaseSchema = Yup.object().shape({
|
|
296
|
-
username: Yup.string().required("El nombre de usuario es requerido"),
|
|
297
|
-
promoCode: Yup.string(),
|
|
298
|
-
email: Yup.string()
|
|
299
|
-
.email("Formato de correo inválido")
|
|
300
|
-
.required("El correo es requerido"),
|
|
301
|
-
password: Yup.string()
|
|
302
|
-
.min(8, "Mínimo 8 caracteres")
|
|
303
|
-
.required("La contraseña es requerida"),
|
|
304
|
-
subtotal: Yup.number()
|
|
305
|
-
.typeError("Debe ser un número")
|
|
306
|
-
.required("Requerido")
|
|
307
|
-
.min(1, "Mayor a 0"),
|
|
308
|
-
country: Yup.string().required("Selecciona un país"),
|
|
309
|
-
rfc: Yup.string().when("country", {
|
|
310
|
-
is: "MX",
|
|
311
|
-
then: (schema) =>
|
|
312
|
-
schema
|
|
313
|
-
.length(13, "Debe tener 13 caracteres exactos")
|
|
314
|
-
.required("El RFC es obligatorio para MX"),
|
|
315
|
-
otherwise: (schema) => schema.notRequired(),
|
|
316
|
-
}),
|
|
317
|
-
date: Yup.date().required("Fecha requerida"),
|
|
318
|
-
time: Yup.string().required("Hora requerida"),
|
|
319
|
-
comments: Yup.string().max(200, "Máximo 200 caracteres"),
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
export const FullFeaturesShowcaseV2: Story = {
|
|
323
|
-
render: () => (
|
|
324
|
-
<FormWrapper
|
|
325
|
-
initialValues={{
|
|
326
|
-
username: "",
|
|
327
|
-
promoCode: "",
|
|
328
|
-
email: "",
|
|
329
|
-
password: "",
|
|
330
|
-
subtotal: "",
|
|
331
|
-
iva: "",
|
|
332
|
-
total: "",
|
|
333
|
-
country: "",
|
|
334
|
-
rfc: "",
|
|
335
|
-
date: null,
|
|
336
|
-
time: "",
|
|
337
|
-
comments: "",
|
|
338
|
-
}}
|
|
339
|
-
validationSchema={showcaseSchema}
|
|
340
|
-
widthClass="w-[1024px]"
|
|
341
|
-
>
|
|
342
|
-
{(formikProps) => (
|
|
343
|
-
<div className="space-y-4">
|
|
344
|
-
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
|
345
|
-
ITFormBuilder V2 Completo
|
|
346
|
-
</h3>
|
|
347
|
-
<p className="text-sm text-gray-500 mb-6">
|
|
348
|
-
Prueba los cálculos en tiempo real en "Subtotal", mira el
|
|
349
|
-
comportamiento `disabled` vs `readonly`, interactúa con íconos e
|
|
350
|
-
ingresa el país "México" para renderizar dinámicamente un campo.
|
|
351
|
-
</p>
|
|
352
|
-
<ITFormBuilder
|
|
353
|
-
config={showcaseConfig as any}
|
|
354
|
-
columns={12}
|
|
355
|
-
values={formikProps.values}
|
|
356
|
-
handleChange={formikProps.handleChange}
|
|
357
|
-
handleBlur={formikProps.handleBlur}
|
|
358
|
-
touched={formikProps.touched}
|
|
359
|
-
errors={formikProps.errors}
|
|
360
|
-
setFieldValue={formikProps.setFieldValue}
|
|
361
|
-
/>
|
|
362
|
-
</div>
|
|
363
|
-
)}
|
|
364
|
-
</FormWrapper>
|
|
365
|
-
),
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const loginFields: FieldConfig[] = [
|
|
369
|
-
{
|
|
370
|
-
name: "email",
|
|
371
|
-
label: "Email Address",
|
|
372
|
-
type: "text",
|
|
373
|
-
placeholder: "john@example.com",
|
|
374
|
-
required: true,
|
|
375
|
-
column: 12,
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
name: "password",
|
|
379
|
-
label: "Password",
|
|
380
|
-
type: "password",
|
|
381
|
-
placeholder: "••••••••",
|
|
382
|
-
required: true,
|
|
383
|
-
column: 12,
|
|
384
|
-
},
|
|
385
|
-
];
|
|
386
|
-
|
|
387
|
-
const loginSchema = Yup.object().shape({
|
|
388
|
-
email: Yup.string().email("Invalid email").required("Required"),
|
|
389
|
-
password: Yup.string().required("Required"),
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
export const LegacyV1LoginExample: Story = {
|
|
393
|
-
render: () => (
|
|
394
|
-
<FormWrapper
|
|
395
|
-
initialValues={{ email: "", password: "" }}
|
|
396
|
-
validationSchema={loginSchema}
|
|
397
|
-
widthClass="w-[500px]"
|
|
398
|
-
>
|
|
399
|
-
{(formikProps) => (
|
|
400
|
-
<div className="space-y-4">
|
|
401
|
-
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
402
|
-
Legacy Login (V1 Config Backward Compatibility)
|
|
403
|
-
</h3>
|
|
404
|
-
<ITFormBuilder
|
|
405
|
-
fields={loginFields}
|
|
406
|
-
columns={12}
|
|
407
|
-
values={formikProps.values}
|
|
408
|
-
handleChange={formikProps.handleChange}
|
|
409
|
-
handleBlur={formikProps.handleBlur}
|
|
410
|
-
touched={formikProps.touched}
|
|
411
|
-
errors={formikProps.errors}
|
|
412
|
-
/>
|
|
413
|
-
</div>
|
|
414
|
-
)}
|
|
415
|
-
</FormWrapper>
|
|
416
|
-
),
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
-
// Test stories (interaction tests, run via `pnpm exec vitest run`)
|
|
421
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
422
|
-
|
|
423
|
-
/** Renders a V2 config of three fields and asserts they are present in the DOM. */
|
|
424
|
-
export const TestRendersV2Basic: Story = {
|
|
425
|
-
name: "Test: renders V2 basic fields",
|
|
426
|
-
render: () => (
|
|
427
|
-
<ControlledForm initialValues={{ name: "", role: "", dob: null }}>
|
|
428
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
429
|
-
<ITFormBuilder
|
|
430
|
-
config={[
|
|
431
|
-
{ name: "name", label: "Name", type: "text", required: true, column: 6 },
|
|
432
|
-
{
|
|
433
|
-
name: "role",
|
|
434
|
-
label: "Role",
|
|
435
|
-
type: "select",
|
|
436
|
-
required: true,
|
|
437
|
-
column: 6,
|
|
438
|
-
options: [
|
|
439
|
-
{ value: "admin", label: "Admin" },
|
|
440
|
-
{ value: "user", label: "User" },
|
|
441
|
-
],
|
|
442
|
-
},
|
|
443
|
-
{ name: "dob", label: "Date of Birth", type: "date", column: 12 },
|
|
444
|
-
]}
|
|
445
|
-
columns={12}
|
|
446
|
-
values={values}
|
|
447
|
-
handleChange={handleChange}
|
|
448
|
-
handleBlur={handleBlur}
|
|
449
|
-
touched={touched}
|
|
450
|
-
errors={errors}
|
|
451
|
-
setFieldValue={setFieldValue}
|
|
452
|
-
/>
|
|
453
|
-
)}
|
|
454
|
-
</ControlledForm>
|
|
455
|
-
),
|
|
456
|
-
play: async ({ canvas }) => {
|
|
457
|
-
await expect(canvas.getByLabelText(/Name/)).toBeInTheDocument();
|
|
458
|
-
await expect(canvas.getByLabelText(/Role/)).toBeInTheDocument();
|
|
459
|
-
await expect(canvas.getByLabelText(/Date of Birth/)).toBeInTheDocument();
|
|
460
|
-
},
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
/** Typing in the Name input bubbles through `handleChange` into the state. */
|
|
464
|
-
export const TestTypingUpdatesState: Story = {
|
|
465
|
-
name: "Test: typing updates state via handleChange",
|
|
466
|
-
render: () => (
|
|
467
|
-
<ControlledForm initialValues={{ email: "" }}>
|
|
468
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
469
|
-
<ITFormBuilder
|
|
470
|
-
config={[
|
|
471
|
-
{ name: "email", label: "Email", type: "email", required: true },
|
|
472
|
-
]}
|
|
473
|
-
values={values}
|
|
474
|
-
handleChange={handleChange}
|
|
475
|
-
handleBlur={handleBlur}
|
|
476
|
-
touched={touched}
|
|
477
|
-
errors={errors}
|
|
478
|
-
setFieldValue={setFieldValue}
|
|
479
|
-
/>
|
|
480
|
-
)}
|
|
481
|
-
</ControlledForm>
|
|
482
|
-
),
|
|
483
|
-
play: async ({ canvas }) => {
|
|
484
|
-
const input = canvas.getByLabelText(/Email/);
|
|
485
|
-
await userEvent.type(input, "user@example.com");
|
|
486
|
-
await expect(input).toHaveValue("user@example.com");
|
|
487
|
-
|
|
488
|
-
const stateDump = canvas.getByTestId("form-state");
|
|
489
|
-
await expect(stateDump.textContent).toContain('"email": "user@example.com"');
|
|
490
|
-
},
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
/** RFC field appears only when country === "MX"; switching away hides it. */
|
|
494
|
-
export const TestConditionalRenderWhen: Story = {
|
|
495
|
-
name: "Test: renderWhen shows / hides dependent field",
|
|
496
|
-
render: () => (
|
|
497
|
-
<ControlledForm initialValues={{ country: "", rfc: "" }}>
|
|
498
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
499
|
-
<ITFormBuilder
|
|
500
|
-
config={[
|
|
501
|
-
{
|
|
502
|
-
name: "country",
|
|
503
|
-
label: "Country",
|
|
504
|
-
type: "select",
|
|
505
|
-
required: true,
|
|
506
|
-
options: [
|
|
507
|
-
{ value: "", label: "Select..." },
|
|
508
|
-
{ value: "MX", label: "México" },
|
|
509
|
-
{ value: "US", label: "USA" },
|
|
510
|
-
],
|
|
511
|
-
},
|
|
512
|
-
{
|
|
513
|
-
name: "rfc",
|
|
514
|
-
label: "RFC",
|
|
515
|
-
type: "text",
|
|
516
|
-
dependsOn: ["country"],
|
|
517
|
-
renderWhen: (v) => v.country === "MX",
|
|
518
|
-
required: true,
|
|
519
|
-
},
|
|
520
|
-
]}
|
|
521
|
-
values={values}
|
|
522
|
-
handleChange={handleChange}
|
|
523
|
-
handleBlur={handleBlur}
|
|
524
|
-
touched={touched}
|
|
525
|
-
errors={errors}
|
|
526
|
-
setFieldValue={setFieldValue}
|
|
527
|
-
/>
|
|
528
|
-
)}
|
|
529
|
-
</ControlledForm>
|
|
530
|
-
),
|
|
531
|
-
play: async ({ canvas }) => {
|
|
532
|
-
// Initially hidden.
|
|
533
|
-
await expect(canvas.queryByLabelText(/RFC/)).toBeNull();
|
|
534
|
-
|
|
535
|
-
// Pick Mexico → appears.
|
|
536
|
-
const country = canvas.getByLabelText(/Country/);
|
|
537
|
-
await userEvent.selectOptions(country, "MX");
|
|
538
|
-
await expect(canvas.getByLabelText(/RFC/)).toBeInTheDocument();
|
|
539
|
-
|
|
540
|
-
// Switch to USA → hidden again.
|
|
541
|
-
await userEvent.selectOptions(country, "US");
|
|
542
|
-
await expect(canvas.queryByLabelText(/RFC/)).toBeNull();
|
|
543
|
-
},
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
/** Typing in subtotal triggers `onChangeAction` to recompute total. */
|
|
547
|
-
export const TestOnChangeActionComputesTotal: Story = {
|
|
548
|
-
name: "Test: onChangeAction updates derived total",
|
|
549
|
-
render: () => (
|
|
550
|
-
<ControlledForm initialValues={{ subtotal: "", total: "" }}>
|
|
551
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
552
|
-
<ITFormBuilder
|
|
553
|
-
config={[
|
|
554
|
-
{
|
|
555
|
-
name: "subtotal",
|
|
556
|
-
label: "Subtotal",
|
|
557
|
-
type: "number",
|
|
558
|
-
currencyFormat: true,
|
|
559
|
-
onChangeAction: (val, ctx) => {
|
|
560
|
-
const n = parseFloat(String(val).replace(/,/g, "")) || 0;
|
|
561
|
-
ctx.setFieldValue("total", (n * 1.16).toFixed(2));
|
|
562
|
-
},
|
|
563
|
-
},
|
|
564
|
-
{ name: "total", label: "Total", type: "number", disabled: true },
|
|
565
|
-
]}
|
|
566
|
-
values={values}
|
|
567
|
-
handleChange={handleChange}
|
|
568
|
-
handleBlur={handleBlur}
|
|
569
|
-
touched={touched}
|
|
570
|
-
errors={errors}
|
|
571
|
-
setFieldValue={setFieldValue}
|
|
572
|
-
/>
|
|
573
|
-
)}
|
|
574
|
-
</ControlledForm>
|
|
575
|
-
),
|
|
576
|
-
play: async ({ canvas }) => {
|
|
577
|
-
const subtotal = canvas.getByLabelText(/Subtotal/);
|
|
578
|
-
await userEvent.type(subtotal, "100");
|
|
579
|
-
|
|
580
|
-
// Total should be derived to "116.50".
|
|
581
|
-
const total = canvas.getByLabelText(/Total/) as HTMLInputElement;
|
|
582
|
-
await expect(total.value).toBe("116.50");
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
/** Disabled field cannot receive input; programmatic value still renders. */
|
|
587
|
-
export const TestDisabledFieldBlocksInput: Story = {
|
|
588
|
-
name: "Test: disabled field blocks typing",
|
|
589
|
-
render: () => (
|
|
590
|
-
<ControlledForm initialValues={{ a: "", b: "fixed" }}>
|
|
591
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
592
|
-
<ITFormBuilder
|
|
593
|
-
config={[
|
|
594
|
-
{ name: "a", label: "Active", type: "text" },
|
|
595
|
-
{ name: "b", label: "Locked", type: "text", disabled: true },
|
|
596
|
-
]}
|
|
597
|
-
values={values}
|
|
598
|
-
handleChange={handleChange}
|
|
599
|
-
handleBlur={handleBlur}
|
|
600
|
-
touched={touched}
|
|
601
|
-
errors={errors}
|
|
602
|
-
setFieldValue={setFieldValue}
|
|
603
|
-
/>
|
|
604
|
-
)}
|
|
605
|
-
</ControlledForm>
|
|
606
|
-
),
|
|
607
|
-
play: async ({ canvas }) => {
|
|
608
|
-
const active = canvas.getByLabelText(/Active/);
|
|
609
|
-
const locked = canvas.getByLabelText(/Locked/) as HTMLInputElement;
|
|
610
|
-
|
|
611
|
-
await expect(locked).toBeDisabled();
|
|
612
|
-
await expect(locked.value).toBe("fixed");
|
|
613
|
-
|
|
614
|
-
await userEvent.type(active, "hello");
|
|
615
|
-
await userEvent.type(locked, "X"); // should be a no-op
|
|
616
|
-
await expect(locked.value).toBe("fixed");
|
|
617
|
-
await expect(active).toHaveValue("hello");
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
/** Required flag becomes true once another field exceeds a threshold. */
|
|
622
|
-
export const TestDynamicRequired: Story = {
|
|
623
|
-
name: "Test: dynamicProps toggles required at runtime",
|
|
624
|
-
render: () => (
|
|
625
|
-
<ControlledForm initialValues={{ amount: 0, justification: "" }}>
|
|
626
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
627
|
-
<ITFormBuilder
|
|
628
|
-
config={[
|
|
629
|
-
{
|
|
630
|
-
name: "amount",
|
|
631
|
-
label: "Amount",
|
|
632
|
-
type: "number",
|
|
633
|
-
defaultValue: 0,
|
|
634
|
-
dependsOn: ["amount"],
|
|
635
|
-
dynamicProps: (v) =>
|
|
636
|
-
Number(v.amount) > 50 ? { required: true } : { required: false },
|
|
637
|
-
},
|
|
638
|
-
{
|
|
639
|
-
name: "justification",
|
|
640
|
-
label: "Justification",
|
|
641
|
-
type: "text",
|
|
642
|
-
dependsOn: ["amount"],
|
|
643
|
-
dynamicProps: (v) =>
|
|
644
|
-
Number(v.amount) > 50 ? { required: true } : { required: false },
|
|
645
|
-
},
|
|
646
|
-
]}
|
|
647
|
-
values={values}
|
|
648
|
-
handleChange={handleChange}
|
|
649
|
-
handleBlur={handleBlur}
|
|
650
|
-
touched={touched}
|
|
651
|
-
errors={errors}
|
|
652
|
-
setFieldValue={setFieldValue}
|
|
653
|
-
/>
|
|
654
|
-
)}
|
|
655
|
-
</ControlledForm>
|
|
656
|
-
),
|
|
657
|
-
play: async ({ canvas }) => {
|
|
658
|
-
const amount = canvas.getByLabelText(/Amount/);
|
|
659
|
-
await expect(amount).not.toBeRequired();
|
|
660
|
-
|
|
661
|
-
await userEvent.clear(amount);
|
|
662
|
-
await userEvent.type(amount, "75");
|
|
663
|
-
|
|
664
|
-
const justification = canvas.getByLabelText(/Justification/);
|
|
665
|
-
await expect(justification).toBeRequired();
|
|
666
|
-
},
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
/** Async option loader renders a loading placeholder then the resolved list. */
|
|
670
|
-
export const TestAsyncOptionsLoad: Story = {
|
|
671
|
-
name: "Test: async options loader resolves and renders options",
|
|
672
|
-
render: () => {
|
|
673
|
-
const config: FieldConfigV2[] = [
|
|
674
|
-
{
|
|
675
|
-
name: "city",
|
|
676
|
-
label: "City",
|
|
677
|
-
type: "select",
|
|
678
|
-
options: async () => {
|
|
679
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
680
|
-
return [
|
|
681
|
-
{ value: "cdmx", label: "Ciudad de México" },
|
|
682
|
-
{ value: "gdl", label: "Guadalajara" },
|
|
683
|
-
];
|
|
684
|
-
},
|
|
685
|
-
},
|
|
686
|
-
];
|
|
687
|
-
return (
|
|
688
|
-
<ControlledForm initialValues={{ city: "" }}>
|
|
689
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
690
|
-
<ITFormBuilder
|
|
691
|
-
config={config}
|
|
692
|
-
values={values}
|
|
693
|
-
handleChange={handleChange}
|
|
694
|
-
handleBlur={handleBlur}
|
|
695
|
-
touched={touched}
|
|
696
|
-
errors={errors}
|
|
697
|
-
setFieldValue={setFieldValue}
|
|
698
|
-
/>
|
|
699
|
-
)}
|
|
700
|
-
</ControlledForm>
|
|
701
|
-
);
|
|
702
|
-
},
|
|
703
|
-
play: async ({ canvas }) => {
|
|
704
|
-
// Loading state shown until the loader settles.
|
|
705
|
-
await expect(await canvas.findByTestId("field-loading-city")).toBeInTheDocument();
|
|
706
|
-
|
|
707
|
-
// After settling, the search-select is rendered and accepts a value.
|
|
708
|
-
const select = await canvas.findByLabelText(/City/);
|
|
709
|
-
await userEvent.selectOptions(select, "gdl");
|
|
710
|
-
await expect(select).toHaveValue("gdl");
|
|
711
|
-
},
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
/** Section type renders the heading and nested fields. */
|
|
715
|
-
export const TestSectionRendersNestedFields: Story = {
|
|
716
|
-
name: "Test: section renders nested fields",
|
|
717
|
-
render: () => (
|
|
718
|
-
<ControlledForm initialValues={{ firstName: "", lastName: "" }}>
|
|
719
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
720
|
-
<ITFormBuilder
|
|
721
|
-
config={[
|
|
722
|
-
{
|
|
723
|
-
name: "identity",
|
|
724
|
-
label: "Personal Information",
|
|
725
|
-
type: "section",
|
|
726
|
-
fields: [
|
|
727
|
-
{ name: "firstName", label: "First Name", type: "text" },
|
|
728
|
-
{ name: "lastName", label: "Last Name", type: "text" },
|
|
729
|
-
],
|
|
730
|
-
},
|
|
731
|
-
]}
|
|
732
|
-
columns={12}
|
|
733
|
-
values={values}
|
|
734
|
-
handleChange={handleChange}
|
|
735
|
-
handleBlur={handleBlur}
|
|
736
|
-
touched={touched}
|
|
737
|
-
errors={errors}
|
|
738
|
-
setFieldValue={setFieldValue}
|
|
739
|
-
/>
|
|
740
|
-
)}
|
|
741
|
-
</ControlledForm>
|
|
742
|
-
),
|
|
743
|
-
play: async ({ canvas }) => {
|
|
744
|
-
await expect(
|
|
745
|
-
canvas.getByRole("heading", { name: /personal information/i }),
|
|
746
|
-
).toBeInTheDocument();
|
|
747
|
-
await expect(canvas.getByLabelText(/First Name/)).toBeInTheDocument();
|
|
748
|
-
await expect(canvas.getByLabelText(/Last Name/)).toBeInTheDocument();
|
|
749
|
-
},
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
/** Custom `type: "custom"` mounts the user-defined component with the value. */
|
|
753
|
-
export const TestCustomComponentReceivesValue: Story = {
|
|
754
|
-
name: "Test: custom component renders and updates value",
|
|
755
|
-
render: () => {
|
|
756
|
-
const ConfirmToggle = (props: any) => {
|
|
757
|
-
const checked = !!props.value;
|
|
758
|
-
return (
|
|
759
|
-
<label data-testid="confirm-toggle">
|
|
760
|
-
<input
|
|
761
|
-
type="checkbox"
|
|
762
|
-
checked={checked}
|
|
763
|
-
onChange={(e) => props.onChange?.(e.target.checked)}
|
|
764
|
-
/>
|
|
765
|
-
{props.label}
|
|
766
|
-
</label>
|
|
767
|
-
);
|
|
768
|
-
};
|
|
769
|
-
return (
|
|
770
|
-
<ControlledForm initialValues={{ confirm: false }}>
|
|
771
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
772
|
-
<ITFormBuilder
|
|
773
|
-
config={[
|
|
774
|
-
{
|
|
775
|
-
name: "confirm",
|
|
776
|
-
label: "Confirm subscription",
|
|
777
|
-
type: "custom",
|
|
778
|
-
component: ConfirmToggle,
|
|
779
|
-
defaultValue: false,
|
|
780
|
-
},
|
|
781
|
-
]}
|
|
782
|
-
values={values}
|
|
783
|
-
handleChange={handleChange}
|
|
784
|
-
handleBlur={handleBlur}
|
|
785
|
-
touched={touched}
|
|
786
|
-
errors={errors}
|
|
787
|
-
setFieldValue={setFieldValue}
|
|
788
|
-
/>
|
|
789
|
-
)}
|
|
790
|
-
</ControlledForm>
|
|
791
|
-
);
|
|
792
|
-
},
|
|
793
|
-
play: async ({ canvas }) => {
|
|
794
|
-
const checkbox = canvas.getByLabelText(/Confirm subscription/) as HTMLInputElement;
|
|
795
|
-
await expect(checkbox.checked).toBe(false);
|
|
796
|
-
|
|
797
|
-
await userEvent.click(checkbox);
|
|
798
|
-
await expect(checkbox.checked).toBe(true);
|
|
799
|
-
|
|
800
|
-
await expect(canvas.getByTestId("form-state").textContent).toContain(
|
|
801
|
-
'"confirm": true',
|
|
802
|
-
);
|
|
803
|
-
},
|
|
804
|
-
};
|
|
805
|
-
|
|
806
|
-
/** V1 legacy path still renders the supported input types. */
|
|
807
|
-
export const TestLegacyV1RendersBasic: Story = {
|
|
808
|
-
name: "Test: V1 legacy renders basic inputs",
|
|
809
|
-
render: () => (
|
|
810
|
-
<ControlledForm initialValues={{ email: "", password: "" }}>
|
|
811
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
812
|
-
<ITFormBuilder
|
|
813
|
-
fields={[
|
|
814
|
-
{ name: "email", label: "Email", type: "text", required: true, column: 6 },
|
|
815
|
-
{ name: "password", label: "Password", type: "password", required: true, column: 6 },
|
|
816
|
-
]}
|
|
817
|
-
columns={12}
|
|
818
|
-
values={values}
|
|
819
|
-
handleChange={handleChange}
|
|
820
|
-
handleBlur={handleBlur}
|
|
821
|
-
touched={touched}
|
|
822
|
-
errors={errors}
|
|
823
|
-
setFieldValue={setFieldValue}
|
|
824
|
-
/>
|
|
825
|
-
)}
|
|
826
|
-
</ControlledForm>
|
|
827
|
-
),
|
|
828
|
-
play: async ({ canvas }) => {
|
|
829
|
-
const email = canvas.getByLabelText(/Email/);
|
|
830
|
-
const password = canvas.getByLabelText(/Password/);
|
|
831
|
-
|
|
832
|
-
await userEvent.type(email, "a@b.co");
|
|
833
|
-
await userEvent.type(password, "secret123");
|
|
834
|
-
|
|
835
|
-
await expect(email).toHaveValue("a@b.co");
|
|
836
|
-
await expect(password).toHaveValue("secret123");
|
|
837
|
-
},
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
/** Custom button can read live progress via `useFormBuilder`. */
|
|
841
|
-
export const TestUseFormBuilderProgress: Story = {
|
|
842
|
-
name: "Test: useFormBuilder returns live progress",
|
|
843
|
-
render: () => {
|
|
844
|
-
const ProgressBadge = () => {
|
|
845
|
-
const { progress } = useFormBuilder();
|
|
846
|
-
return (
|
|
847
|
-
<span data-testid="progress-badge">
|
|
848
|
-
{progress}% <FaCheckCircle />
|
|
849
|
-
</span>
|
|
850
|
-
);
|
|
851
|
-
};
|
|
852
|
-
return (
|
|
853
|
-
<ControlledForm initialValues={{ a: "", b: "" }}>
|
|
854
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => (
|
|
855
|
-
<ITFormBuilder
|
|
856
|
-
config={[
|
|
857
|
-
{ name: "a", label: "Field A", type: "text", required: true },
|
|
858
|
-
{ name: "b", label: "Field B", type: "text", required: true },
|
|
859
|
-
]}
|
|
860
|
-
values={values}
|
|
861
|
-
handleChange={handleChange}
|
|
862
|
-
handleBlur={handleBlur}
|
|
863
|
-
touched={touched}
|
|
864
|
-
errors={errors}
|
|
865
|
-
setFieldValue={setFieldValue}
|
|
866
|
-
>
|
|
867
|
-
<ProgressBadge />
|
|
868
|
-
</ITFormBuilder>
|
|
869
|
-
)}
|
|
870
|
-
</ControlledForm>
|
|
871
|
-
);
|
|
872
|
-
},
|
|
873
|
-
play: async ({ canvas }) => {
|
|
874
|
-
const badge = canvas.getByTestId("progress-badge");
|
|
875
|
-
await expect(badge.textContent).toContain("0%");
|
|
876
|
-
|
|
877
|
-
await userEvent.type(canvas.getByLabelText(/Field A/), "hello");
|
|
878
|
-
|
|
879
|
-
await expect(badge.textContent).toContain("50%");
|
|
880
|
-
|
|
881
|
-
await userEvent.type(canvas.getByLabelText(/Field B/), "world");
|
|
882
|
-
|
|
883
|
-
await expect(badge.textContent).toContain("100%");
|
|
884
|
-
},
|
|
885
|
-
};
|
|
886
|
-
|
|
887
|
-
/** `fn()` is exposed for spy assertions; smoke-validates the props API. */
|
|
888
|
-
export const TestPropsApiAcceptsSpies: Story = {
|
|
889
|
-
name: "Test: handleChange / setFieldValue spies are called",
|
|
890
|
-
render: () => (
|
|
891
|
-
<ControlledForm initialValues={{ x: "" }}>
|
|
892
|
-
{({ values, handleChange, handleBlur, touched, errors, setFieldValue }) => {
|
|
893
|
-
const handleChangeSpy = fn(handleChange);
|
|
894
|
-
const setFieldValueSpy = fn(setFieldValue);
|
|
895
|
-
return (
|
|
896
|
-
<ITFormBuilder
|
|
897
|
-
config={[{ name: "x", label: "X", type: "text" }]}
|
|
898
|
-
values={values}
|
|
899
|
-
handleChange={handleChangeSpy as any}
|
|
900
|
-
handleBlur={handleBlur}
|
|
901
|
-
touched={touched}
|
|
902
|
-
errors={errors}
|
|
903
|
-
setFieldValue={setFieldValueSpy}
|
|
904
|
-
/>
|
|
905
|
-
);
|
|
906
|
-
}}
|
|
907
|
-
</ControlledForm>
|
|
908
|
-
),
|
|
909
|
-
play: async ({ canvas }) => {
|
|
910
|
-
const input = canvas.getByLabelText(/^X/);
|
|
911
|
-
await userEvent.type(input, "z");
|
|
912
|
-
await expect(input).toHaveValue("z");
|
|
913
|
-
},
|
|
914
|
-
};
|