tee3apps-cms-sdk-react 0.0.9 → 0.0.11
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 +84 -20
- package/src/PageFormComponents/InputField.tsx +27 -6
- package/src/PageFormComponents/NumberField.tsx +16 -3
- package/src/PageFormComponents/PageForm.tsx +120 -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' ) {
|
|
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,61 @@ const Button: React.FC<ButtonProps> = ({ props, onFormSubmit }) => {
|
|
|
54
84
|
}
|
|
55
85
|
};
|
|
56
86
|
|
|
57
|
-
// Handle button click
|
|
58
|
-
const handleClick = () => {
|
|
59
|
-
//
|
|
60
|
-
|
|
87
|
+
// Handle button click based on button type
|
|
88
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
89
|
+
// Prevent default form submission behavior
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
e.stopPropagation();
|
|
61
92
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
const buttonType = props.buttonType || 'submit';
|
|
94
|
+
|
|
95
|
+
// Handle different button types
|
|
96
|
+
switch (buttonType) {
|
|
97
|
+
case 'submit':
|
|
98
|
+
// Trigger form submission if handler is provided
|
|
99
|
+
if (onFormSubmit) {
|
|
100
|
+
onFormSubmit();
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
case 'reset':
|
|
104
|
+
// Trigger form reset if handler is provided
|
|
105
|
+
if (onFormReset) {
|
|
106
|
+
onFormReset();
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case 'cancel':
|
|
110
|
+
// Trigger form cancel if handler is provided
|
|
111
|
+
if (onFormCancel) {
|
|
112
|
+
onFormCancel();
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
// Default to submit behavior
|
|
117
|
+
if (onFormSubmit) {
|
|
118
|
+
onFormSubmit();
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
65
121
|
}
|
|
66
122
|
|
|
67
|
-
// If there's a link configured, handle it
|
|
68
|
-
if (props.linktype === 'EXTERNAL' && props.link?.url) {
|
|
123
|
+
// If there's a link configured, handle it (only for non-submit buttons or when not submitting)
|
|
124
|
+
if (props.linktype === 'EXTERNAL' && props.link?.url && buttonType !== 'submit') {
|
|
69
125
|
window.open(props.link.url, props.link.target || '_blank');
|
|
70
126
|
}
|
|
71
127
|
};
|
|
72
128
|
|
|
73
|
-
// Get button text - handle both string and {all: string} formats
|
|
129
|
+
// Get button text - handle both string and {all: string, locale: string} formats
|
|
74
130
|
const getButtonText = () => {
|
|
75
131
|
if (typeof props.text === 'string') {
|
|
76
132
|
return props.text;
|
|
77
|
-
} else if (props.text
|
|
78
|
-
|
|
133
|
+
} else if (props.text && typeof props.text === 'object') {
|
|
134
|
+
// Try to get locale-specific text first, then fall back to 'all'
|
|
135
|
+
// You can extend this to detect current locale
|
|
136
|
+
const locale = 'en-IN'; // Default locale, can be made dynamic
|
|
137
|
+
if (props.text[locale]) {
|
|
138
|
+
return props.text[locale];
|
|
139
|
+
} else if (props.text.all) {
|
|
140
|
+
return props.text.all;
|
|
141
|
+
}
|
|
79
142
|
} else if (props.name?.all) {
|
|
80
143
|
return props.name.all;
|
|
81
144
|
}
|
|
@@ -86,7 +149,8 @@ const Button: React.FC<ButtonProps> = ({ props, onFormSubmit }) => {
|
|
|
86
149
|
<div className="mb-6">
|
|
87
150
|
<button
|
|
88
151
|
onClick={handleClick}
|
|
89
|
-
|
|
152
|
+
type="button"
|
|
153
|
+
disabled={props.disabled || (isSubmitting && props.buttonType === 'submit')}
|
|
90
154
|
style={{
|
|
91
155
|
backgroundColor: currentMode.bgColor || '#3498db',
|
|
92
156
|
color: textStyle.fontColor,
|
|
@@ -98,25 +162,25 @@ const Button: React.FC<ButtonProps> = ({ props, onFormSubmit }) => {
|
|
|
98
162
|
borderRadius: currentMode.radius || '4px',
|
|
99
163
|
padding: '10px 20px',
|
|
100
164
|
border: 'none',
|
|
101
|
-
cursor: props.disabled ? 'not-allowed' : 'pointer',
|
|
102
|
-
opacity: props.disabled ? 0.6 : 1,
|
|
165
|
+
cursor: (props.disabled || (isSubmitting && props.buttonType === 'submit')) ? 'not-allowed' : 'pointer',
|
|
166
|
+
opacity: (props.disabled || (isSubmitting && props.buttonType === 'submit')) ? 0.6 : 1,
|
|
103
167
|
transition: 'all 0.2s ease-in-out',
|
|
104
168
|
minWidth: '120px'
|
|
105
169
|
}}
|
|
106
170
|
onMouseEnter={(e) => {
|
|
107
|
-
if (!props.disabled) {
|
|
171
|
+
if (!props.disabled && !(isSubmitting && props.buttonType === 'submit')) {
|
|
108
172
|
e.currentTarget.style.opacity = '0.8';
|
|
109
173
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
110
174
|
}
|
|
111
175
|
}}
|
|
112
176
|
onMouseLeave={(e) => {
|
|
113
|
-
if (!props.disabled) {
|
|
177
|
+
if (!props.disabled && !(isSubmitting && props.buttonType === 'submit')) {
|
|
114
178
|
e.currentTarget.style.opacity = '1';
|
|
115
179
|
e.currentTarget.style.transform = 'translateY(0)';
|
|
116
180
|
}
|
|
117
181
|
}}
|
|
118
182
|
>
|
|
119
|
-
{getButtonText()}
|
|
183
|
+
{(isSubmitting && props.buttonType === 'submit') ? 'Submitting...' : getButtonText()}
|
|
120
184
|
</button>
|
|
121
185
|
|
|
122
186
|
|
|
@@ -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,123 @@ 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 URL is provided, automatically use it for API submission
|
|
213
|
+
if (url && url.trim() !== '') {
|
|
214
|
+
// Send data to API URL
|
|
215
|
+
setIsSubmitting(true);
|
|
216
|
+
try {
|
|
217
|
+
const httpMethod = method.toUpperCase();
|
|
218
|
+
const requestOptions: RequestInit = {
|
|
219
|
+
method: httpMethod,
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// For GET requests, append data as query parameters
|
|
226
|
+
// For POST requests, send data in the body
|
|
227
|
+
let requestUrl = url.trim();
|
|
228
|
+
if (httpMethod === 'GET') {
|
|
229
|
+
const queryParams = new URLSearchParams();
|
|
230
|
+
Object.keys(transformedData).forEach((key) => {
|
|
231
|
+
const value = transformedData[key];
|
|
232
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
233
|
+
queryParams.append(key, String(value));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
const queryString = queryParams.toString();
|
|
237
|
+
requestUrl = queryString ? `${requestUrl}?${queryString}` : requestUrl;
|
|
238
|
+
} else {
|
|
239
|
+
// POST, PUT, PATCH, etc.
|
|
240
|
+
requestOptions.body = JSON.stringify(transformedData);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const response = await fetch(requestUrl, requestOptions);
|
|
244
|
+
|
|
245
|
+
// Read response as text first (can only read body once)
|
|
246
|
+
const responseText = await response.text().catch(() => '');
|
|
247
|
+
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
const errorText = responseText || response.statusText || `HTTP ${response.status}`;
|
|
250
|
+
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Try to parse as JSON, fallback to text if not JSON
|
|
254
|
+
let responseData;
|
|
255
|
+
const contentType = response.headers.get('content-type');
|
|
256
|
+
if (contentType && contentType.includes('application/json') && responseText) {
|
|
257
|
+
try {
|
|
258
|
+
responseData = JSON.parse(responseText);
|
|
259
|
+
} catch {
|
|
260
|
+
// If JSON parsing fails, use text as message
|
|
261
|
+
responseData = { message: responseText };
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
// Not JSON or empty response, use text as message
|
|
265
|
+
responseData = responseText ? { message: responseText } : { message: 'Success' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
setIsSubmitting(false);
|
|
269
|
+
showToast('Form submitted successfully!', 'success');
|
|
270
|
+
|
|
271
|
+
// Also call onSubmit if provided, passing the response data
|
|
272
|
+
if (onSubmit) {
|
|
273
|
+
onSubmit(responseData || transformedData);
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
setIsSubmitting(false);
|
|
277
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to submit form';
|
|
278
|
+
console.error('Form submission error:', error);
|
|
279
|
+
showToast(`Error submitting form: ${errorMessage}`, 'error');
|
|
280
|
+
|
|
281
|
+
// Still call onSubmit with error data if provided
|
|
282
|
+
if (onSubmit) {
|
|
283
|
+
onSubmit({ error: errorMessage, data: transformedData });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
// Send transformed data back to parent component via onSubmit callback
|
|
288
|
+
if (onSubmit) {
|
|
289
|
+
onSubmit(transformedData);
|
|
290
|
+
} else {
|
|
291
|
+
// If neither URL nor onSubmit is provided, show a warning
|
|
292
|
+
showToast('No submission handler configured. Please provide either a URL or onSubmit callback.', 'warning');
|
|
293
|
+
}
|
|
194
294
|
}
|
|
195
|
-
}, [formValues, validateForm, getRequiredFields, transformFormValuesToNames, onSubmit, showToast]);
|
|
295
|
+
}, [formValues, validateForm, getRequiredFields, transformFormValuesToNames, onSubmit, showToast, isUrl, method, url]);
|
|
196
296
|
|
|
197
297
|
return (
|
|
198
298
|
<div className="page-form">
|
|
@@ -207,6 +307,9 @@ const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
|
207
307
|
validationErrors={validationErrors}
|
|
208
308
|
onFieldChange={handleFieldChange}
|
|
209
309
|
onFormSubmit={handleFormSubmit}
|
|
310
|
+
onFormReset={handleFormReset}
|
|
311
|
+
onFormCancel={handleFormCancel}
|
|
312
|
+
isSubmitting={isSubmitting}
|
|
210
313
|
/>
|
|
211
314
|
))}
|
|
212
315
|
|
|
@@ -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;
|