tee3apps-cms-sdk-react 0.0.9 → 0.0.10
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 +122 -3
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/PageFormComponents/BoxRenderer.tsx +36 -2
- package/src/PageFormComponents/Button.tsx +79 -19
- package/src/PageFormComponents/InputField.tsx +27 -6
- package/src/PageFormComponents/NumberField.tsx +16 -3
- package/src/PageFormComponents/PageForm.tsx +94 -17
- package/src/PageFormComponents/RadioField.tsx +3 -6
- package/src/PageFormComponents/RowComponent.tsx +10 -1
- package/src/PageFormComponents/Styles/InputField.css +6 -0
- package/src/PageFormComponents/Styles/NumberField.css +16 -0
- package/src/PageFormComponents/Styles/RadioField.css +6 -0
- package/src/PageFormComponents/Styles/TermsAndCondition.css +170 -0
- package/src/PageFormComponents/TermsAndCondition.tsx +130 -0
- package/src/types.ts +7 -0
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import InputField from './InputField';
|
|
|
9
9
|
import RadioField from './RadioField';
|
|
10
10
|
import SelectField from './SelectField';
|
|
11
11
|
import ImageComponent from './ImageComponent';
|
|
12
|
+
import TermsAndCondition from './TermsAndCondition';
|
|
12
13
|
|
|
13
14
|
interface BoxRendererProps {
|
|
14
15
|
box: Box;
|
|
@@ -19,6 +20,9 @@ interface BoxRendererProps {
|
|
|
19
20
|
validationErrors?: Record<string, boolean>;
|
|
20
21
|
onFieldChange?: (code: string, value: any) => void;
|
|
21
22
|
onFormSubmit?: () => void;
|
|
23
|
+
onFormReset?: () => void;
|
|
24
|
+
onFormCancel?: () => void;
|
|
25
|
+
isSubmitting?: boolean;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const BoxRenderer: React.FC<BoxRendererProps> = ({
|
|
@@ -29,7 +33,10 @@ const BoxRenderer: React.FC<BoxRendererProps> = ({
|
|
|
29
33
|
formValues = {},
|
|
30
34
|
validationErrors = {},
|
|
31
35
|
onFieldChange,
|
|
32
|
-
onFormSubmit
|
|
36
|
+
onFormSubmit,
|
|
37
|
+
onFormReset,
|
|
38
|
+
onFormCancel,
|
|
39
|
+
isSubmitting = false
|
|
33
40
|
}) => {
|
|
34
41
|
// Get colspan based on device mode
|
|
35
42
|
const getColspan = () => {
|
|
@@ -211,8 +218,35 @@ const BoxRenderer: React.FC<BoxRendererProps> = ({
|
|
|
211
218
|
);
|
|
212
219
|
} else if (component.name === 'TextComponent') {
|
|
213
220
|
return <TextComponent key={index} props={component.props} />;
|
|
221
|
+
} else if (component.name === 'TermsAndCondition' || component.name === 'TERMSANDCONDITION') {
|
|
222
|
+
const code = getFieldCode(component);
|
|
223
|
+
const hasError = validationErrors[code];
|
|
224
|
+
return (
|
|
225
|
+
<div key={index} style={{ width: '100%' }}>
|
|
226
|
+
<TermsAndCondition
|
|
227
|
+
props={component.props}
|
|
228
|
+
value={formValues[code] ?? component.props.checked ?? false}
|
|
229
|
+
onChange={onFieldChange}
|
|
230
|
+
/>
|
|
231
|
+
{hasError && (component.props.isRequired || component.props.required) && (
|
|
232
|
+
<div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>
|
|
233
|
+
This field is required
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
214
238
|
} else if (component.name === 'ButtonField') {
|
|
215
|
-
return
|
|
239
|
+
return (
|
|
240
|
+
<Button
|
|
241
|
+
key={index}
|
|
242
|
+
props={component.props}
|
|
243
|
+
onFormSubmit={onFormSubmit}
|
|
244
|
+
onFormReset={onFormReset}
|
|
245
|
+
onFormCancel={onFormCancel}
|
|
246
|
+
isSubmitting={isSubmitting}
|
|
247
|
+
deviceMode={deviceMode}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
216
250
|
} else {
|
|
217
251
|
return (
|
|
218
252
|
<div key={index} className="mb-4 p-4 bg-gray-100 rounded-md border-2 border-dashed border-gray-300">
|
|
@@ -20,13 +20,43 @@ interface ButtonModeProps {
|
|
|
20
20
|
interface ButtonProps {
|
|
21
21
|
props: ComponentProps;
|
|
22
22
|
onFormSubmit?: () => void;
|
|
23
|
+
onFormReset?: () => void;
|
|
24
|
+
onFormCancel?: () => void;
|
|
25
|
+
isSubmitting?: boolean;
|
|
26
|
+
deviceMode?: string;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
const Button: React.FC<ButtonProps> = ({
|
|
29
|
+
const Button: React.FC<ButtonProps> = ({
|
|
30
|
+
props,
|
|
31
|
+
onFormSubmit,
|
|
32
|
+
onFormReset,
|
|
33
|
+
onFormCancel,
|
|
34
|
+
isSubmitting = false,
|
|
35
|
+
deviceMode = 'web'
|
|
36
|
+
}) => {
|
|
26
37
|
// Get device-specific mode properties with proper typing
|
|
27
38
|
const getCurrentMode = (): ButtonModeProps => {
|
|
39
|
+
let modeProps: ButtonModeProps | undefined;
|
|
40
|
+
|
|
41
|
+
// Get mode based on deviceMode
|
|
42
|
+
switch (deviceMode) {
|
|
43
|
+
case 'mobileweb':
|
|
44
|
+
modeProps = props.mode?.mobileweb as ButtonModeProps;
|
|
45
|
+
break;
|
|
46
|
+
case 'mobileapp':
|
|
47
|
+
modeProps = props.mode?.mobileapp as ButtonModeProps;
|
|
48
|
+
break;
|
|
49
|
+
case 'tablet':
|
|
50
|
+
modeProps = props.mode?.tablet as ButtonModeProps;
|
|
51
|
+
break;
|
|
52
|
+
case 'web':
|
|
53
|
+
default:
|
|
54
|
+
modeProps = props.mode?.web as ButtonModeProps;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
// Default to web mode if not specified
|
|
29
|
-
return
|
|
59
|
+
return modeProps || {
|
|
30
60
|
radius: '4px',
|
|
31
61
|
bgColor: '#3498db',
|
|
32
62
|
textstyle: {
|
|
@@ -54,28 +84,57 @@ const Button: React.FC<ButtonProps> = ({ props, onFormSubmit }) => {
|
|
|
54
84
|
}
|
|
55
85
|
};
|
|
56
86
|
|
|
57
|
-
// Handle button click
|
|
87
|
+
// Handle button click based on button type
|
|
58
88
|
const handleClick = () => {
|
|
59
|
-
|
|
60
|
-
console.log('Button clicked:', props.name?.all || 'Button');
|
|
89
|
+
const buttonType = props.buttonType || 'submit';
|
|
61
90
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
// Handle different button types
|
|
92
|
+
switch (buttonType) {
|
|
93
|
+
case 'submit':
|
|
94
|
+
// Trigger form submission if handler is provided
|
|
95
|
+
if (onFormSubmit) {
|
|
96
|
+
onFormSubmit();
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case 'reset':
|
|
100
|
+
// Trigger form reset if handler is provided
|
|
101
|
+
if (onFormReset) {
|
|
102
|
+
onFormReset();
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case 'cancel':
|
|
106
|
+
// Trigger form cancel if handler is provided
|
|
107
|
+
if (onFormCancel) {
|
|
108
|
+
onFormCancel();
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
// Default to submit behavior
|
|
113
|
+
if (onFormSubmit) {
|
|
114
|
+
onFormSubmit();
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
65
117
|
}
|
|
66
118
|
|
|
67
|
-
// If there's a link configured, handle it
|
|
68
|
-
if (props.linktype === 'EXTERNAL' && props.link?.url) {
|
|
119
|
+
// If there's a link configured, handle it (only for non-submit buttons or when not submitting)
|
|
120
|
+
if (props.linktype === 'EXTERNAL' && props.link?.url && buttonType !== 'submit') {
|
|
69
121
|
window.open(props.link.url, props.link.target || '_blank');
|
|
70
122
|
}
|
|
71
123
|
};
|
|
72
124
|
|
|
73
|
-
// Get button text - handle both string and {all: string} formats
|
|
125
|
+
// Get button text - handle both string and {all: string, locale: string} formats
|
|
74
126
|
const getButtonText = () => {
|
|
75
127
|
if (typeof props.text === 'string') {
|
|
76
128
|
return props.text;
|
|
77
|
-
} else if (props.text
|
|
78
|
-
|
|
129
|
+
} else if (props.text && typeof props.text === 'object') {
|
|
130
|
+
// Try to get locale-specific text first, then fall back to 'all'
|
|
131
|
+
// You can extend this to detect current locale
|
|
132
|
+
const locale = 'en-IN'; // Default locale, can be made dynamic
|
|
133
|
+
if (props.text[locale]) {
|
|
134
|
+
return props.text[locale];
|
|
135
|
+
} else if (props.text.all) {
|
|
136
|
+
return props.text.all;
|
|
137
|
+
}
|
|
79
138
|
} else if (props.name?.all) {
|
|
80
139
|
return props.name.all;
|
|
81
140
|
}
|
|
@@ -86,7 +145,8 @@ const Button: React.FC<ButtonProps> = ({ props, onFormSubmit }) => {
|
|
|
86
145
|
<div className="mb-6">
|
|
87
146
|
<button
|
|
88
147
|
onClick={handleClick}
|
|
89
|
-
|
|
148
|
+
type={props.buttonType === 'submit' ? 'submit' : 'button'}
|
|
149
|
+
disabled={props.disabled || (isSubmitting && props.buttonType === 'submit')}
|
|
90
150
|
style={{
|
|
91
151
|
backgroundColor: currentMode.bgColor || '#3498db',
|
|
92
152
|
color: textStyle.fontColor,
|
|
@@ -98,25 +158,25 @@ const Button: React.FC<ButtonProps> = ({ props, onFormSubmit }) => {
|
|
|
98
158
|
borderRadius: currentMode.radius || '4px',
|
|
99
159
|
padding: '10px 20px',
|
|
100
160
|
border: 'none',
|
|
101
|
-
cursor: props.disabled ? 'not-allowed' : 'pointer',
|
|
102
|
-
opacity: props.disabled ? 0.6 : 1,
|
|
161
|
+
cursor: (props.disabled || (isSubmitting && props.buttonType === 'submit')) ? 'not-allowed' : 'pointer',
|
|
162
|
+
opacity: (props.disabled || (isSubmitting && props.buttonType === 'submit')) ? 0.6 : 1,
|
|
103
163
|
transition: 'all 0.2s ease-in-out',
|
|
104
164
|
minWidth: '120px'
|
|
105
165
|
}}
|
|
106
166
|
onMouseEnter={(e) => {
|
|
107
|
-
if (!props.disabled) {
|
|
167
|
+
if (!props.disabled && !(isSubmitting && props.buttonType === 'submit')) {
|
|
108
168
|
e.currentTarget.style.opacity = '0.8';
|
|
109
169
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
110
170
|
}
|
|
111
171
|
}}
|
|
112
172
|
onMouseLeave={(e) => {
|
|
113
|
-
if (!props.disabled) {
|
|
173
|
+
if (!props.disabled && !(isSubmitting && props.buttonType === 'submit')) {
|
|
114
174
|
e.currentTarget.style.opacity = '1';
|
|
115
175
|
e.currentTarget.style.transform = 'translateY(0)';
|
|
116
176
|
}
|
|
117
177
|
}}
|
|
118
178
|
>
|
|
119
|
-
{getButtonText()}
|
|
179
|
+
{(isSubmitting && props.buttonType === 'submit') ? 'Submitting...' : getButtonText()}
|
|
120
180
|
</button>
|
|
121
181
|
|
|
122
182
|
|
|
@@ -10,13 +10,36 @@ interface InputFieldProps {
|
|
|
10
10
|
|
|
11
11
|
const InputField: React.FC<InputFieldProps> = ({ props, value = '', onChange }) => {
|
|
12
12
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
13
|
-
|
|
13
|
+
let newValue = e.target.value;
|
|
14
|
+
|
|
15
|
+
// Handle postal code formatting
|
|
16
|
+
if (props.format === 'Postalcode' && !props.textArea) {
|
|
17
|
+
// Remove non-alphanumeric characters for postal code
|
|
18
|
+
newValue = newValue.replace(/[^a-zA-Z0-9\s-]/g, '');
|
|
19
|
+
|
|
20
|
+
// Apply formatting based on postaltype
|
|
21
|
+
if (props.postaltype === 'State') {
|
|
22
|
+
// US postal code format: 12345 or 12345-6789
|
|
23
|
+
newValue = newValue.replace(/(\d{5})(\d{0,4})/, (match, p1, p2) => {
|
|
24
|
+
return p2 ? `${p1}-${p2}` : p1;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
if (onChange) {
|
|
15
30
|
const code = props.code || `inputfield_${props.name?.all || 'field'}`;
|
|
16
31
|
onChange(code, newValue);
|
|
17
32
|
}
|
|
18
33
|
};
|
|
19
34
|
|
|
35
|
+
// Determine input type based on format
|
|
36
|
+
const getInputType = () => {
|
|
37
|
+
if (props.format === 'Postalcode') {
|
|
38
|
+
return 'text'; // Use text to allow formatting
|
|
39
|
+
}
|
|
40
|
+
return 'text';
|
|
41
|
+
};
|
|
42
|
+
|
|
20
43
|
return (
|
|
21
44
|
<div className="input-field">
|
|
22
45
|
<label className="input-field__label">
|
|
@@ -28,6 +51,7 @@ const InputField: React.FC<InputFieldProps> = ({ props, value = '', onChange })
|
|
|
28
51
|
<textarea
|
|
29
52
|
value={value || ''}
|
|
30
53
|
onChange={handleChange}
|
|
54
|
+
placeholder={props.helperText || ''}
|
|
31
55
|
minLength={props.minLength}
|
|
32
56
|
maxLength={props.maxLength}
|
|
33
57
|
rows={4}
|
|
@@ -36,9 +60,10 @@ const InputField: React.FC<InputFieldProps> = ({ props, value = '', onChange })
|
|
|
36
60
|
/>
|
|
37
61
|
) : (
|
|
38
62
|
<input
|
|
39
|
-
type=
|
|
63
|
+
type={getInputType()}
|
|
40
64
|
value={value || ''}
|
|
41
65
|
onChange={handleChange}
|
|
66
|
+
placeholder={props.helperText || ''}
|
|
42
67
|
minLength={props.minLength}
|
|
43
68
|
maxLength={props.maxLength}
|
|
44
69
|
required={props.required}
|
|
@@ -46,10 +71,6 @@ const InputField: React.FC<InputFieldProps> = ({ props, value = '', onChange })
|
|
|
46
71
|
/>
|
|
47
72
|
)}
|
|
48
73
|
|
|
49
|
-
{props.helperText && (
|
|
50
|
-
<p className="input-field__helper-text">{props.helperText}</p>
|
|
51
|
-
)}
|
|
52
|
-
|
|
53
74
|
<div className="input-field__status">
|
|
54
75
|
{props.format && `Format: ${props.format}`}
|
|
55
76
|
{props.postaltype && ` • Type: ${props.postaltype}`}
|
|
@@ -16,6 +16,11 @@ const NumberField: React.FC<NumberFieldProps> = ({ props, value = '', onChange }
|
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// Parse HTML content safely
|
|
20
|
+
const createMarkup = (html: string) => {
|
|
21
|
+
return { __html: html };
|
|
22
|
+
};
|
|
23
|
+
|
|
19
24
|
return (
|
|
20
25
|
<div className="number-field">
|
|
21
26
|
<label className="number-field__label">
|
|
@@ -32,6 +37,7 @@ const NumberField: React.FC<NumberFieldProps> = ({ props, value = '', onChange }
|
|
|
32
37
|
max={props.max}
|
|
33
38
|
step={props.step}
|
|
34
39
|
required={props.required}
|
|
40
|
+
placeholder={props.helperText || ''}
|
|
35
41
|
className="number-field__input"
|
|
36
42
|
/>
|
|
37
43
|
|
|
@@ -42,12 +48,19 @@ const NumberField: React.FC<NumberFieldProps> = ({ props, value = '', onChange }
|
|
|
42
48
|
)}
|
|
43
49
|
</div>
|
|
44
50
|
|
|
45
|
-
{props.
|
|
46
|
-
<
|
|
51
|
+
{props.termsandcondition?.all && (
|
|
52
|
+
<div
|
|
53
|
+
className="number-field__terms"
|
|
54
|
+
dangerouslySetInnerHTML={createMarkup(props.termsandcondition.all)}
|
|
55
|
+
/>
|
|
47
56
|
)}
|
|
48
57
|
|
|
49
58
|
<div className="number-field__status">
|
|
50
|
-
|
|
59
|
+
{props.onText && props.offText ? (
|
|
60
|
+
<span>{props.onText} / {props.offText}</span>
|
|
61
|
+
) : (
|
|
62
|
+
<span>Range: {props.min}-{props.max} • Step: {props.step}</span>
|
|
63
|
+
)}
|
|
51
64
|
</div>
|
|
52
65
|
</div>
|
|
53
66
|
);
|
|
@@ -6,6 +6,9 @@ import './PageForm.css'; // Import the new CSS file
|
|
|
6
6
|
interface PageFormProps {
|
|
7
7
|
jsonData: Row[];
|
|
8
8
|
onSubmit?: (data: Record<string, any>) => void;
|
|
9
|
+
isUrl?: boolean;
|
|
10
|
+
method?: 'POST' | 'GET' | 'post' | 'get';
|
|
11
|
+
url?: string;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
interface Toast {
|
|
@@ -14,12 +17,13 @@ interface Toast {
|
|
|
14
17
|
type: 'error' | 'success' | 'info' | 'warning';
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
20
|
+
const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit, isUrl = false, method = 'POST', url }) => {
|
|
18
21
|
// Form state to store all field values
|
|
19
22
|
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
|
20
23
|
const [deviceMode, setDeviceMode] = useState<string>('web');
|
|
21
24
|
const [validationErrors, setValidationErrors] = useState<Record<string, boolean>>({});
|
|
22
25
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
26
|
+
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
23
27
|
|
|
24
28
|
// Function to determine device mode based on screen width
|
|
25
29
|
const getDeviceMode = (width: number) => {
|
|
@@ -47,7 +51,7 @@ const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
|
47
51
|
row.columns?.forEach((box) => {
|
|
48
52
|
box.components?.forEach((component) => {
|
|
49
53
|
// Check if component is a form field
|
|
50
|
-
const fieldTypes = ['DateField', 'NumberField', 'SelectField', 'RadioField', 'InputField', 'BooleanField'];
|
|
54
|
+
const fieldTypes = ['DateField', 'NumberField', 'SelectField', 'RadioField', 'InputField', 'BooleanField', 'TermsAndCondition', 'TERMSANDCONDITION'];
|
|
51
55
|
if (fieldTypes.includes(component.name)) {
|
|
52
56
|
const code = getFieldCode(component);
|
|
53
57
|
const name = getFieldName(component);
|
|
@@ -68,8 +72,8 @@ const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
|
68
72
|
row.columns?.forEach((box) => {
|
|
69
73
|
box.components?.forEach((component) => {
|
|
70
74
|
// Check if component is a form field and is required
|
|
71
|
-
const fieldTypes = ['DateField', 'NumberField', 'SelectField', 'RadioField', 'InputField', 'BooleanField'];
|
|
72
|
-
if (fieldTypes.includes(component.name) && component.props.required) {
|
|
75
|
+
const fieldTypes = ['DateField', 'NumberField', 'SelectField', 'RadioField', 'InputField', 'BooleanField', 'TermsAndCondition', 'TERMSANDCONDITION'];
|
|
76
|
+
if (fieldTypes.includes(component.name) && (component.props.required || component.props.isRequired)) {
|
|
73
77
|
const code = getFieldCode(component);
|
|
74
78
|
const name = getFieldName(component);
|
|
75
79
|
requiredFields[code] = { code, name };
|
|
@@ -138,11 +142,12 @@ const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
|
138
142
|
|
|
139
143
|
Object.keys(requiredFields).forEach((code) => {
|
|
140
144
|
const value = formValues[code];
|
|
141
|
-
// Check if value is empty, null, undefined, empty array, or
|
|
145
|
+
// Check if value is empty, null, undefined, empty array, empty string, or false (for checkboxes)
|
|
142
146
|
if (
|
|
143
147
|
value === undefined ||
|
|
144
148
|
value === null ||
|
|
145
149
|
value === '' ||
|
|
150
|
+
value === false ||
|
|
146
151
|
(Array.isArray(value) && value.length === 0)
|
|
147
152
|
) {
|
|
148
153
|
errors[code] = true;
|
|
@@ -171,28 +176,97 @@ const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
|
171
176
|
setToasts(prev => prev.filter(toast => toast.id !== id));
|
|
172
177
|
}, []);
|
|
173
178
|
|
|
174
|
-
//
|
|
175
|
-
const
|
|
179
|
+
// Handler to reset form values
|
|
180
|
+
const handleFormReset = useCallback(() => {
|
|
181
|
+
setFormValues({});
|
|
182
|
+
setValidationErrors({});
|
|
183
|
+
showToast('Form has been reset', 'info');
|
|
184
|
+
}, [showToast]);
|
|
185
|
+
|
|
186
|
+
// Handler to cancel form (same as reset for now, but can be customized)
|
|
187
|
+
const handleFormCancel = useCallback(() => {
|
|
188
|
+
setFormValues({});
|
|
189
|
+
setValidationErrors({});
|
|
190
|
+
showToast('Form has been cancelled', 'info');
|
|
191
|
+
}, [showToast]);
|
|
192
|
+
|
|
193
|
+
// Single method to handle form submission - validates, transforms, and sends data to parent or API
|
|
194
|
+
const handleFormSubmit = useCallback(async () => {
|
|
176
195
|
// Validate form and get validation result
|
|
177
196
|
const { isValid, errors } = validateForm();
|
|
178
197
|
|
|
179
|
-
if (isValid) {
|
|
180
|
-
// Transform form values to use field names instead of codes
|
|
181
|
-
const transformedData = transformFormValuesToNames();
|
|
182
|
-
|
|
183
|
-
// Send transformed data back to parent component
|
|
184
|
-
if (onSubmit) {
|
|
185
|
-
onSubmit(transformedData);
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
198
|
+
if (!isValid) {
|
|
188
199
|
// If validation fails, show error message with field names
|
|
189
200
|
const requiredFields = getRequiredFields();
|
|
190
201
|
const errorFields = Object.keys(errors)
|
|
191
202
|
.map(code => requiredFields[code]?.name || code)
|
|
192
203
|
.join(', ');
|
|
193
204
|
showToast(`Please fill in all required fields: ${errorFields}`, 'error');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Transform form values to use field names instead of codes
|
|
209
|
+
const transformedData = transformFormValuesToNames();
|
|
210
|
+
|
|
211
|
+
// Check if we should send to URL or use onSubmit callback
|
|
212
|
+
if (isUrl && url) {
|
|
213
|
+
// Send data to API URL
|
|
214
|
+
setIsSubmitting(true);
|
|
215
|
+
try {
|
|
216
|
+
const httpMethod = method.toUpperCase();
|
|
217
|
+
const requestOptions: RequestInit = {
|
|
218
|
+
method: httpMethod,
|
|
219
|
+
headers: {
|
|
220
|
+
'Content-Type': 'application/json',
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// For GET requests, append data as query parameters
|
|
225
|
+
// For POST requests, send data in the body
|
|
226
|
+
let requestUrl = url;
|
|
227
|
+
if (httpMethod === 'GET') {
|
|
228
|
+
const queryParams = new URLSearchParams();
|
|
229
|
+
Object.keys(transformedData).forEach((key) => {
|
|
230
|
+
queryParams.append(key, String(transformedData[key]));
|
|
231
|
+
});
|
|
232
|
+
requestUrl = `${url}?${queryParams.toString()}`;
|
|
233
|
+
} else {
|
|
234
|
+
// POST, PUT, PATCH, etc.
|
|
235
|
+
requestOptions.body = JSON.stringify(transformedData);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const response = await fetch(requestUrl, requestOptions);
|
|
239
|
+
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const responseData = await response.json().catch(() => response.text());
|
|
245
|
+
|
|
246
|
+
setIsSubmitting(false);
|
|
247
|
+
showToast('Form submitted successfully!', 'success');
|
|
248
|
+
|
|
249
|
+
// Also call onSubmit if provided, passing the response data
|
|
250
|
+
if (onSubmit) {
|
|
251
|
+
onSubmit(responseData || transformedData);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
setIsSubmitting(false);
|
|
255
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to submit form';
|
|
256
|
+
showToast(`Error submitting form: ${errorMessage}`, 'error');
|
|
257
|
+
|
|
258
|
+
// Still call onSubmit with error data if provided
|
|
259
|
+
if (onSubmit) {
|
|
260
|
+
onSubmit({ error: errorMessage, data: transformedData });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
// Send transformed data back to parent component via onSubmit callback
|
|
265
|
+
if (onSubmit) {
|
|
266
|
+
onSubmit(transformedData);
|
|
267
|
+
}
|
|
194
268
|
}
|
|
195
|
-
}, [formValues, validateForm, getRequiredFields, transformFormValuesToNames, onSubmit, showToast]);
|
|
269
|
+
}, [formValues, validateForm, getRequiredFields, transformFormValuesToNames, onSubmit, showToast, isUrl, method, url]);
|
|
196
270
|
|
|
197
271
|
return (
|
|
198
272
|
<div className="page-form">
|
|
@@ -207,6 +281,9 @@ const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
|
207
281
|
validationErrors={validationErrors}
|
|
208
282
|
onFieldChange={handleFieldChange}
|
|
209
283
|
onFormSubmit={handleFormSubmit}
|
|
284
|
+
onFormReset={handleFormReset}
|
|
285
|
+
onFormCancel={handleFormCancel}
|
|
286
|
+
isSubmitting={isSubmitting}
|
|
210
287
|
/>
|
|
211
288
|
))}
|
|
212
289
|
|
|
@@ -20,6 +20,9 @@ const RadioField: React.FC<RadioFieldProps> = ({ props, value = '', onChange })
|
|
|
20
20
|
<div className="radio-field">
|
|
21
21
|
<label className="radio-field__label">
|
|
22
22
|
{props.name?.all || 'Radio Field'}
|
|
23
|
+
{props.helperText && (
|
|
24
|
+
<span className="radio-field__placeholder"> ({props.helperText})</span>
|
|
25
|
+
)}
|
|
23
26
|
{props.required && <span className="radio-field__required">*</span>}
|
|
24
27
|
</label>
|
|
25
28
|
|
|
@@ -46,12 +49,6 @@ const RadioField: React.FC<RadioFieldProps> = ({ props, value = '', onChange })
|
|
|
46
49
|
))
|
|
47
50
|
)}
|
|
48
51
|
</div>
|
|
49
|
-
|
|
50
|
-
{props.helperText && (
|
|
51
|
-
<p className="radio-field__helper-text">{props.helperText}</p>
|
|
52
|
-
)}
|
|
53
|
-
|
|
54
|
-
|
|
55
52
|
</div>
|
|
56
53
|
);
|
|
57
54
|
};
|
|
@@ -9,6 +9,9 @@ interface RowComponentProps {
|
|
|
9
9
|
validationErrors?: Record<string, boolean>;
|
|
10
10
|
onFieldChange?: (code: string, value: any) => void;
|
|
11
11
|
onFormSubmit?: () => void;
|
|
12
|
+
onFormReset?: () => void;
|
|
13
|
+
onFormCancel?: () => void;
|
|
14
|
+
isSubmitting?: boolean;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
const RowComponent: React.FC<RowComponentProps> = ({
|
|
@@ -17,7 +20,10 @@ const RowComponent: React.FC<RowComponentProps> = ({
|
|
|
17
20
|
formValues = {},
|
|
18
21
|
validationErrors = {},
|
|
19
22
|
onFieldChange,
|
|
20
|
-
onFormSubmit
|
|
23
|
+
onFormSubmit,
|
|
24
|
+
onFormReset,
|
|
25
|
+
onFormCancel,
|
|
26
|
+
isSubmitting = false
|
|
21
27
|
}) => {
|
|
22
28
|
const getCurrentMode = () => {
|
|
23
29
|
switch(deviceMode) {
|
|
@@ -70,6 +76,9 @@ const RowComponent: React.FC<RowComponentProps> = ({
|
|
|
70
76
|
validationErrors={validationErrors}
|
|
71
77
|
onFieldChange={onFieldChange}
|
|
72
78
|
onFormSubmit={onFormSubmit}
|
|
79
|
+
onFormReset={onFormReset}
|
|
80
|
+
onFormCancel={onFormCancel}
|
|
81
|
+
isSubmitting={isSubmitting}
|
|
73
82
|
/>
|
|
74
83
|
))}
|
|
75
84
|
{/* Clear float after all children */}
|
|
@@ -50,6 +50,22 @@
|
|
|
50
50
|
color: #6b7280; /* gray-500 */
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
.number-field__terms {
|
|
54
|
+
margin-top: 0.25rem;
|
|
55
|
+
font-size: 0.75rem;
|
|
56
|
+
color: #4b5563; /* gray-600 */
|
|
57
|
+
line-height: 1.5;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.number-field__terms p {
|
|
61
|
+
margin: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.number-field__input::placeholder {
|
|
65
|
+
color: #9ca3af; /* gray-400 */
|
|
66
|
+
opacity: 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
.number-field__status {
|
|
54
70
|
margin-top: 0.25rem;
|
|
55
71
|
font-size: 0.75rem;
|