tee3apps-cms-sdk-react 0.0.5 → 0.0.7
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 +244 -2
- package/dist/index.d.ts +189 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/App.css +37 -0
- package/src/Page.tsx +5 -2
- package/src/PageComponents/Visual-Components/GroupProductComponent.tsx +182 -318
- package/src/PageComponents/Visual-Components/RichTextComponent.tsx +28 -0
- package/src/PageFormComponents/BooleanField.tsx +56 -0
- package/src/PageFormComponents/BoxRenderer.tsx +229 -0
- package/src/PageFormComponents/Button.tsx +127 -0
- package/src/PageFormComponents/DateField.tsx +46 -0
- package/src/PageFormComponents/ImageComponent.tsx +163 -0
- package/src/PageFormComponents/InputField.tsx +62 -0
- package/src/PageFormComponents/NumberField.tsx +56 -0
- package/src/PageFormComponents/PageForm.css +213 -0
- package/src/PageFormComponents/PageForm.tsx +267 -0
- package/src/PageFormComponents/RadioField.tsx +59 -0
- package/src/PageFormComponents/RowComponent.tsx +82 -0
- package/src/PageFormComponents/SelectField.tsx +183 -0
- package/src/PageFormComponents/Styles/BooleanField.css +56 -0
- package/src/PageFormComponents/Styles/DateField.css +44 -0
- package/src/PageFormComponents/Styles/InputField.css +50 -0
- package/src/PageFormComponents/Styles/NumberField.css +57 -0
- package/src/PageFormComponents/Styles/RadioField.css +66 -0
- package/src/PageFormComponents/Styles/SelectField.css +264 -0
- package/src/PageFormComponents/TextComponent.tsx +45 -0
- package/src/index.ts +2 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ComponentProps } from '../types';
|
|
3
|
+
import './Styles/NumberField.css'; // Import the external CSS
|
|
4
|
+
|
|
5
|
+
interface NumberFieldProps {
|
|
6
|
+
props: ComponentProps;
|
|
7
|
+
value?: any;
|
|
8
|
+
onChange?: (code: string, value: any) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const NumberField: React.FC<NumberFieldProps> = ({ props, value = '', onChange }) => {
|
|
12
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
13
|
+
if (onChange) {
|
|
14
|
+
const code = props.code || `numberfield_${props.name?.all || 'field'}`;
|
|
15
|
+
onChange(code, e.target.value);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="number-field">
|
|
21
|
+
<label className="number-field__label">
|
|
22
|
+
{props.name?.all || 'Number Field'}
|
|
23
|
+
{props.required && <span className="number-field__required">*</span>}
|
|
24
|
+
</label>
|
|
25
|
+
|
|
26
|
+
<div className="number-field__input-wrapper">
|
|
27
|
+
<input
|
|
28
|
+
type="number"
|
|
29
|
+
value={value || ''}
|
|
30
|
+
onChange={handleChange}
|
|
31
|
+
min={props.min}
|
|
32
|
+
max={props.max}
|
|
33
|
+
step={props.step}
|
|
34
|
+
required={props.required}
|
|
35
|
+
className="number-field__input"
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
{props.suffix && (
|
|
39
|
+
<span className="number-field__suffix">
|
|
40
|
+
{props.suffix}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{props.helperText && (
|
|
46
|
+
<p className="number-field__helper-text">{props.helperText}</p>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div className="number-field__status">
|
|
50
|
+
Range: {props.min}-{props.max} • Step: {props.step}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default NumberField;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
.page-form {
|
|
2
|
+
min-height: 100vh;
|
|
3
|
+
background: linear-gradient(to bottom right, #f9fafb, #f3f4f6); /* from-gray-50 to-gray-100 */
|
|
4
|
+
padding-top: 2rem;
|
|
5
|
+
padding-bottom: 2rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.page-form__container {
|
|
9
|
+
max-width: 112rem; /* max-w-7xl (1120px) */
|
|
10
|
+
margin-left: auto;
|
|
11
|
+
margin-right: auto;
|
|
12
|
+
padding-left: 1rem;
|
|
13
|
+
padding-right: 1rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@media (min-width: 640px) {
|
|
17
|
+
.page-form__container {
|
|
18
|
+
padding-left: 1.5rem; /* sm:px-6 */
|
|
19
|
+
padding-right: 1.5rem;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@media (min-width: 1024px) {
|
|
24
|
+
.page-form__container {
|
|
25
|
+
padding-left: 2rem; /* lg:px-8 */
|
|
26
|
+
padding-right: 2rem;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.page-form__footer {
|
|
31
|
+
margin-top: 3rem; /* mt-12 */
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.page-form__status {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 0.5rem; /* gap-2 */
|
|
39
|
+
padding: 0.5rem 1rem; /* px-4 py-2 */
|
|
40
|
+
background-color: #fff; /* bg-white */
|
|
41
|
+
border-radius: 9999px; /* rounded-full */
|
|
42
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); /* shadow-sm */
|
|
43
|
+
border: 1px solid #e5e7eb; /* border-gray-200 */
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.page-form__status-indicator {
|
|
47
|
+
width: 0.5rem; /* w-2 */
|
|
48
|
+
height: 0.5rem; /* h-2 */
|
|
49
|
+
background-color: #10b981; /* green-500 */
|
|
50
|
+
border-radius: 9999px; /* rounded-full */
|
|
51
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.page-form__status-text {
|
|
55
|
+
font-size: 0.875rem; /* text-sm */
|
|
56
|
+
color: #4b5563; /* gray-600 */
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* pulse animation */
|
|
60
|
+
@keyframes pulse {
|
|
61
|
+
0%, 100% {
|
|
62
|
+
opacity: 1;
|
|
63
|
+
}
|
|
64
|
+
50% {
|
|
65
|
+
opacity: 0.4;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Toast Notification Styles */
|
|
70
|
+
.toast-container {
|
|
71
|
+
position: fixed;
|
|
72
|
+
top: 20px;
|
|
73
|
+
right: 20px;
|
|
74
|
+
z-index: 10000;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 12px;
|
|
78
|
+
max-width: 400px;
|
|
79
|
+
width: calc(100% - 40px);
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@media (max-width: 480px) {
|
|
84
|
+
.toast-container {
|
|
85
|
+
top: 10px;
|
|
86
|
+
right: 10px;
|
|
87
|
+
left: 10px;
|
|
88
|
+
width: calc(100% - 20px);
|
|
89
|
+
max-width: none;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.toast {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: flex-start;
|
|
96
|
+
gap: 12px;
|
|
97
|
+
padding: 16px;
|
|
98
|
+
background-color: #ffffff;
|
|
99
|
+
border-radius: 8px;
|
|
100
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
101
|
+
border-left: 4px solid;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
pointer-events: all;
|
|
104
|
+
animation: slideInRight 0.3s ease-out, fadeOut 0.3s ease-out 4.7s forwards;
|
|
105
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.toast:hover {
|
|
109
|
+
transform: translateX(-4px);
|
|
110
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.toast--error {
|
|
114
|
+
border-left-color: #ef4444;
|
|
115
|
+
background-color: #fef2f2;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.toast--error .toast__icon {
|
|
119
|
+
color: #ef4444;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.toast--success {
|
|
123
|
+
border-left-color: #10b981;
|
|
124
|
+
background-color: #f0fdf4;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.toast--success .toast__icon {
|
|
128
|
+
color: #10b981;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.toast--info {
|
|
132
|
+
border-left-color: #3b82f6;
|
|
133
|
+
background-color: #eff6ff;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.toast--info .toast__icon {
|
|
137
|
+
color: #3b82f6;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.toast--warning {
|
|
141
|
+
border-left-color: #f59e0b;
|
|
142
|
+
background-color: #fffbeb;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.toast--warning .toast__icon {
|
|
146
|
+
color: #f59e0b;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.toast__icon {
|
|
150
|
+
flex-shrink: 0;
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
width: 20px;
|
|
155
|
+
height: 20px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.toast__message {
|
|
159
|
+
flex: 1;
|
|
160
|
+
font-size: 14px;
|
|
161
|
+
line-height: 1.5;
|
|
162
|
+
color: #1f2937;
|
|
163
|
+
font-weight: 500;
|
|
164
|
+
word-wrap: break-word;
|
|
165
|
+
overflow-wrap: break-word;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.toast__close {
|
|
169
|
+
flex-shrink: 0;
|
|
170
|
+
background: none;
|
|
171
|
+
border: none;
|
|
172
|
+
padding: 4px;
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
color: #6b7280;
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: center;
|
|
178
|
+
border-radius: 4px;
|
|
179
|
+
transition: background-color 0.2s ease, color 0.2s ease;
|
|
180
|
+
margin-left: 4px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.toast__close:hover {
|
|
184
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
185
|
+
color: #1f2937;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.toast__close:active {
|
|
189
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Animations */
|
|
193
|
+
@keyframes slideInRight {
|
|
194
|
+
from {
|
|
195
|
+
transform: translateX(100%);
|
|
196
|
+
opacity: 0;
|
|
197
|
+
}
|
|
198
|
+
to {
|
|
199
|
+
transform: translateX(0);
|
|
200
|
+
opacity: 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes fadeOut {
|
|
205
|
+
from {
|
|
206
|
+
opacity: 1;
|
|
207
|
+
transform: translateX(0);
|
|
208
|
+
}
|
|
209
|
+
to {
|
|
210
|
+
opacity: 0;
|
|
211
|
+
transform: translateX(100%);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import type { Row } from '../types';
|
|
3
|
+
import RowComponent from './RowComponent';
|
|
4
|
+
import './PageForm.css'; // Import the new CSS file
|
|
5
|
+
|
|
6
|
+
interface PageFormProps {
|
|
7
|
+
jsonData: Row[];
|
|
8
|
+
onSubmit?: (data: Record<string, any>) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Toast {
|
|
12
|
+
id: string;
|
|
13
|
+
message: string;
|
|
14
|
+
type: 'error' | 'success' | 'info' | 'warning';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
18
|
+
// Form state to store all field values
|
|
19
|
+
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
|
20
|
+
const [deviceMode, setDeviceMode] = useState<string>('web');
|
|
21
|
+
const [validationErrors, setValidationErrors] = useState<Record<string, boolean>>({});
|
|
22
|
+
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
23
|
+
|
|
24
|
+
// Function to determine device mode based on screen width
|
|
25
|
+
const getDeviceMode = (width: number) => {
|
|
26
|
+
if (width <= 480) return 'mobileapp';
|
|
27
|
+
if (width <= 768) return 'mobileweb';
|
|
28
|
+
if (width <= 1024) return 'tablet';
|
|
29
|
+
return 'web';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Function to get field code from component
|
|
33
|
+
const getFieldCode = (component: any) => {
|
|
34
|
+
return component.props.code || `${component.name.toLowerCase()}_${component.props.name?.all || 'field'}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Function to get field name from component
|
|
38
|
+
const getFieldName = (component: any) => {
|
|
39
|
+
return component.props.name?.all || component.name;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Function to create a mapping of field codes to field names
|
|
43
|
+
const getFieldCodeToNameMap = useCallback(() => {
|
|
44
|
+
const codeToNameMap: Record<string, string> = {};
|
|
45
|
+
|
|
46
|
+
jsonData.forEach((row) => {
|
|
47
|
+
row.columns?.forEach((box) => {
|
|
48
|
+
box.components?.forEach((component) => {
|
|
49
|
+
// Check if component is a form field
|
|
50
|
+
const fieldTypes = ['DateField', 'NumberField', 'SelectField', 'RadioField', 'InputField', 'BooleanField'];
|
|
51
|
+
if (fieldTypes.includes(component.name)) {
|
|
52
|
+
const code = getFieldCode(component);
|
|
53
|
+
const name = getFieldName(component);
|
|
54
|
+
codeToNameMap[code] = name;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return codeToNameMap;
|
|
61
|
+
}, [jsonData]);
|
|
62
|
+
|
|
63
|
+
// Function to collect all required fields from jsonData
|
|
64
|
+
const getRequiredFields = useCallback(() => {
|
|
65
|
+
const requiredFields: Record<string, { code: string; name: string }> = {};
|
|
66
|
+
|
|
67
|
+
jsonData.forEach((row) => {
|
|
68
|
+
row.columns?.forEach((box) => {
|
|
69
|
+
box.components?.forEach((component) => {
|
|
70
|
+
// 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) {
|
|
73
|
+
const code = getFieldCode(component);
|
|
74
|
+
const name = getFieldName(component);
|
|
75
|
+
requiredFields[code] = { code, name };
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return requiredFields;
|
|
82
|
+
}, [jsonData]);
|
|
83
|
+
|
|
84
|
+
// Function to transform form values from code-based to name-based
|
|
85
|
+
const transformFormValuesToNames = useCallback(() => {
|
|
86
|
+
const codeToNameMap = getFieldCodeToNameMap();
|
|
87
|
+
const transformedData: Record<string, any> = {};
|
|
88
|
+
|
|
89
|
+
Object.keys(formValues).forEach((code) => {
|
|
90
|
+
const fieldName = codeToNameMap[code];
|
|
91
|
+
if (fieldName) {
|
|
92
|
+
transformedData[fieldName] = formValues[code];
|
|
93
|
+
} else {
|
|
94
|
+
// If no mapping found, keep the code as key
|
|
95
|
+
transformedData[code] = formValues[code];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return transformedData;
|
|
100
|
+
}, [formValues, getFieldCodeToNameMap]);
|
|
101
|
+
|
|
102
|
+
// Effect to handle window resize
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const handleResize = () => {
|
|
105
|
+
const width = window.innerWidth;
|
|
106
|
+
setDeviceMode(getDeviceMode(width));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Call once on mount
|
|
110
|
+
handleResize();
|
|
111
|
+
|
|
112
|
+
window.addEventListener('resize', handleResize);
|
|
113
|
+
return () => {
|
|
114
|
+
window.removeEventListener('resize', handleResize);
|
|
115
|
+
};
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
// Handler to update form values
|
|
119
|
+
const handleFieldChange = useCallback((code: string, value: any) => {
|
|
120
|
+
setFormValues(prev => ({
|
|
121
|
+
...prev,
|
|
122
|
+
[code]: value
|
|
123
|
+
}));
|
|
124
|
+
// Clear validation error for this field when user types
|
|
125
|
+
if (validationErrors[code]) {
|
|
126
|
+
setValidationErrors(prev => {
|
|
127
|
+
const newErrors = { ...prev };
|
|
128
|
+
delete newErrors[code];
|
|
129
|
+
return newErrors;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}, [validationErrors]);
|
|
133
|
+
|
|
134
|
+
// Function to validate required fields
|
|
135
|
+
const validateForm = useCallback(() => {
|
|
136
|
+
const requiredFields = getRequiredFields();
|
|
137
|
+
const errors: Record<string, boolean> = {};
|
|
138
|
+
|
|
139
|
+
Object.keys(requiredFields).forEach((code) => {
|
|
140
|
+
const value = formValues[code];
|
|
141
|
+
// Check if value is empty, null, undefined, empty array, or empty string
|
|
142
|
+
if (
|
|
143
|
+
value === undefined ||
|
|
144
|
+
value === null ||
|
|
145
|
+
value === '' ||
|
|
146
|
+
(Array.isArray(value) && value.length === 0)
|
|
147
|
+
) {
|
|
148
|
+
errors[code] = true;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
setValidationErrors(errors);
|
|
153
|
+
return { isValid: Object.keys(errors).length === 0, errors };
|
|
154
|
+
}, [formValues, getRequiredFields]);
|
|
155
|
+
|
|
156
|
+
// Function to show toast notification
|
|
157
|
+
const showToast = useCallback((message: string, type: Toast['type'] = 'error') => {
|
|
158
|
+
const id = Date.now().toString();
|
|
159
|
+
const newToast: Toast = { id, message, type };
|
|
160
|
+
|
|
161
|
+
setToasts(prev => [...prev, newToast]);
|
|
162
|
+
|
|
163
|
+
// Auto remove toast after 5 seconds
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
setToasts(prev => prev.filter(toast => toast.id !== id));
|
|
166
|
+
}, 5000);
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
// Function to remove toast
|
|
170
|
+
const removeToast = useCallback((id: string) => {
|
|
171
|
+
setToasts(prev => prev.filter(toast => toast.id !== id));
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
// Single method to handle form submission - validates, transforms, and sends data to parent
|
|
175
|
+
const handleFormSubmit = useCallback(() => {
|
|
176
|
+
// Validate form and get validation result
|
|
177
|
+
const { isValid, errors } = validateForm();
|
|
178
|
+
|
|
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 {
|
|
188
|
+
// If validation fails, show error message with field names
|
|
189
|
+
const requiredFields = getRequiredFields();
|
|
190
|
+
const errorFields = Object.keys(errors)
|
|
191
|
+
.map(code => requiredFields[code]?.name || code)
|
|
192
|
+
.join(', ');
|
|
193
|
+
showToast(`Please fill in all required fields: ${errorFields}`, 'error');
|
|
194
|
+
}
|
|
195
|
+
}, [formValues, validateForm, getRequiredFields, transformFormValuesToNames, onSubmit, showToast]);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="page-form">
|
|
199
|
+
<div className="page-form__container">
|
|
200
|
+
{/* Render each row */}
|
|
201
|
+
{jsonData.map((row, rowIndex) => (
|
|
202
|
+
<RowComponent
|
|
203
|
+
key={rowIndex}
|
|
204
|
+
row={row}
|
|
205
|
+
deviceMode={deviceMode}
|
|
206
|
+
formValues={formValues}
|
|
207
|
+
validationErrors={validationErrors}
|
|
208
|
+
onFieldChange={handleFieldChange}
|
|
209
|
+
onFormSubmit={handleFormSubmit}
|
|
210
|
+
/>
|
|
211
|
+
))}
|
|
212
|
+
|
|
213
|
+
{/* Footer */}
|
|
214
|
+
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Toast Container */}
|
|
218
|
+
<div className="toast-container">
|
|
219
|
+
{toasts.map((toast) => (
|
|
220
|
+
<div
|
|
221
|
+
key={toast.id}
|
|
222
|
+
className={`toast toast--${toast.type}`}
|
|
223
|
+
onClick={() => removeToast(toast.id)}
|
|
224
|
+
>
|
|
225
|
+
<div className="toast__icon">
|
|
226
|
+
{toast.type === 'error' && (
|
|
227
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
|
228
|
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
229
|
+
</svg>
|
|
230
|
+
)}
|
|
231
|
+
{toast.type === 'success' && (
|
|
232
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
|
233
|
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
234
|
+
</svg>
|
|
235
|
+
)}
|
|
236
|
+
{toast.type === 'info' && (
|
|
237
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
|
238
|
+
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
|
239
|
+
</svg>
|
|
240
|
+
)}
|
|
241
|
+
{toast.type === 'warning' && (
|
|
242
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
|
243
|
+
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
244
|
+
</svg>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
<div className="toast__message">{toast.message}</div>
|
|
248
|
+
<button
|
|
249
|
+
className="toast__close"
|
|
250
|
+
onClick={(e) => {
|
|
251
|
+
e.stopPropagation();
|
|
252
|
+
removeToast(toast.id);
|
|
253
|
+
}}
|
|
254
|
+
aria-label="Close"
|
|
255
|
+
>
|
|
256
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
257
|
+
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
|
258
|
+
</svg>
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export default PageForm;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ComponentProps } from '../types';
|
|
3
|
+
import './Styles/RadioField.css'; // Import external stylesheet
|
|
4
|
+
|
|
5
|
+
interface RadioFieldProps {
|
|
6
|
+
props: ComponentProps;
|
|
7
|
+
value?: any;
|
|
8
|
+
onChange?: (code: string, value: any) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RadioField: React.FC<RadioFieldProps> = ({ props, value = '', onChange }) => {
|
|
12
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
13
|
+
if (onChange) {
|
|
14
|
+
const code = props.code || `radiofield_${props.name?.all || 'field'}`;
|
|
15
|
+
onChange(code, e.target.value);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="radio-field">
|
|
21
|
+
<label className="radio-field__label">
|
|
22
|
+
{props.name?.all || 'Radio Field'}
|
|
23
|
+
{props.required && <span className="radio-field__required">*</span>}
|
|
24
|
+
</label>
|
|
25
|
+
|
|
26
|
+
<div className="radio-field__options">
|
|
27
|
+
{props.lov?.length === 0 ? (
|
|
28
|
+
<div className="radio-field__empty">No options available</div>
|
|
29
|
+
) : (
|
|
30
|
+
props.lov?.map((item) => (
|
|
31
|
+
<label
|
|
32
|
+
key={item.id}
|
|
33
|
+
className="radio-field__option"
|
|
34
|
+
>
|
|
35
|
+
<input
|
|
36
|
+
type="radio"
|
|
37
|
+
name={props.code}
|
|
38
|
+
value={item.code}
|
|
39
|
+
checked={value === item.code}
|
|
40
|
+
onChange={handleChange}
|
|
41
|
+
className="radio-field__input"
|
|
42
|
+
required={props.required}
|
|
43
|
+
/>
|
|
44
|
+
<span className="radio-field__option-label">{item.name}</span>
|
|
45
|
+
</label>
|
|
46
|
+
))
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{props.helperText && (
|
|
51
|
+
<p className="radio-field__helper-text">{props.helperText}</p>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default RadioField;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import BoxRenderer from './BoxRenderer';
|
|
3
|
+
import type { Row } from '../types';
|
|
4
|
+
|
|
5
|
+
interface RowComponentProps {
|
|
6
|
+
row: Row;
|
|
7
|
+
deviceMode?: string;
|
|
8
|
+
formValues?: Record<string, any>;
|
|
9
|
+
validationErrors?: Record<string, boolean>;
|
|
10
|
+
onFieldChange?: (code: string, value: any) => void;
|
|
11
|
+
onFormSubmit?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const RowComponent: React.FC<RowComponentProps> = ({
|
|
15
|
+
row,
|
|
16
|
+
deviceMode = 'web',
|
|
17
|
+
formValues = {},
|
|
18
|
+
validationErrors = {},
|
|
19
|
+
onFieldChange,
|
|
20
|
+
onFormSubmit
|
|
21
|
+
}) => {
|
|
22
|
+
const getCurrentMode = () => {
|
|
23
|
+
switch(deviceMode) {
|
|
24
|
+
case 'mobileweb':
|
|
25
|
+
return row.props.mode.mobileweb || {};
|
|
26
|
+
case 'mobileapp':
|
|
27
|
+
return row.props.mode.mobileapp || {};
|
|
28
|
+
case 'tablet':
|
|
29
|
+
return row.props.mode.tablet || {};
|
|
30
|
+
case 'web':
|
|
31
|
+
default:
|
|
32
|
+
return row.props.mode.web;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const currentMode = getCurrentMode();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
style={{
|
|
41
|
+
minHeight: row.props.minHeight,
|
|
42
|
+
backgroundColor: row.props.bgColor,
|
|
43
|
+
backgroundImage: row.props.bgImage ? `url(${row.props.bgImage})` : 'none',
|
|
44
|
+
backgroundSize: row.props.bgsize,
|
|
45
|
+
backgroundPosition: 'center',
|
|
46
|
+
width: '100%',
|
|
47
|
+
marginBottom: '2px',
|
|
48
|
+
border: '1px solid #e0e0e0',
|
|
49
|
+
borderRadius: '4px',
|
|
50
|
+
boxSizing: 'border-box',
|
|
51
|
+
overflow: 'hidden',
|
|
52
|
+
position: 'relative',
|
|
53
|
+
display: currentMode.isVisible !== false ? 'block' : 'none',
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{/* Row Content */}
|
|
57
|
+
<div style={{
|
|
58
|
+
width: '100%',
|
|
59
|
+
display: 'block',
|
|
60
|
+
position: 'relative',
|
|
61
|
+
padding: '5px',
|
|
62
|
+
boxSizing: 'border-box'
|
|
63
|
+
}}>
|
|
64
|
+
{row.columns.map((box, boxIndex) => (
|
|
65
|
+
<BoxRenderer
|
|
66
|
+
key={boxIndex}
|
|
67
|
+
box={box}
|
|
68
|
+
deviceMode={deviceMode}
|
|
69
|
+
formValues={formValues}
|
|
70
|
+
validationErrors={validationErrors}
|
|
71
|
+
onFieldChange={onFieldChange}
|
|
72
|
+
onFormSubmit={onFormSubmit}
|
|
73
|
+
/>
|
|
74
|
+
))}
|
|
75
|
+
{/* Clear float after all children */}
|
|
76
|
+
<div style={{ clear: 'both' }} />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default RowComponent;
|