tee3apps-cms-sdk-react 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +189 -1
- 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/App.css +37 -0
- package/src/PageComponents/Visual-Components/TabComponent.tsx +4 -4
- 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 +67 -0
- package/src/PageFormComponents/PageForm.tsx +195 -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,163 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '../App.css';
|
|
3
|
+
// Import your CSS file here
|
|
4
|
+
|
|
5
|
+
interface ImageData {
|
|
6
|
+
isDynamic: boolean;
|
|
7
|
+
shape: string;
|
|
8
|
+
url: string;
|
|
9
|
+
alt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ImageModeProps {
|
|
13
|
+
borderRadius: number;
|
|
14
|
+
width: string;
|
|
15
|
+
height: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ImageComponentProps {
|
|
19
|
+
images: any;
|
|
20
|
+
mode: {
|
|
21
|
+
web: ImageModeProps;
|
|
22
|
+
mobileweb: ImageModeProps;
|
|
23
|
+
mobileapp: ImageModeProps;
|
|
24
|
+
tablet: ImageModeProps;
|
|
25
|
+
};
|
|
26
|
+
linktype: string;
|
|
27
|
+
link: {
|
|
28
|
+
url: string;
|
|
29
|
+
target: string;
|
|
30
|
+
};
|
|
31
|
+
product: {
|
|
32
|
+
_id: string;
|
|
33
|
+
code: string;
|
|
34
|
+
name: object;
|
|
35
|
+
pd_type: string;
|
|
36
|
+
};
|
|
37
|
+
page: {
|
|
38
|
+
_id: string;
|
|
39
|
+
code: string;
|
|
40
|
+
name: object;
|
|
41
|
+
pg_type: string;
|
|
42
|
+
facet_params: any[];
|
|
43
|
+
track_params: any[];
|
|
44
|
+
};
|
|
45
|
+
tag: {
|
|
46
|
+
_id: string;
|
|
47
|
+
code: string;
|
|
48
|
+
name: object;
|
|
49
|
+
tagtype: string;
|
|
50
|
+
};
|
|
51
|
+
type: any[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ImageComponentMainProps {
|
|
55
|
+
props: ImageComponentProps;
|
|
56
|
+
deviceMode?: string;
|
|
57
|
+
boxHeight?: string; // Add this prop to receive box height
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ImageComponent: React.FC<ImageComponentMainProps> = ({
|
|
61
|
+
props,
|
|
62
|
+
deviceMode = 'web',
|
|
63
|
+
boxHeight = '280px',
|
|
64
|
+
}) => {
|
|
65
|
+
|
|
66
|
+
const bannerLink = (element: any): string | null => {
|
|
67
|
+
if (!element || !element.linktype) return null;
|
|
68
|
+
|
|
69
|
+
switch (element.linktype) {
|
|
70
|
+
case 'NONE':
|
|
71
|
+
return null;
|
|
72
|
+
|
|
73
|
+
case 'EXTERNAL':
|
|
74
|
+
case 'EXTERNAL_LINK':
|
|
75
|
+
return element.link?.url || null;
|
|
76
|
+
|
|
77
|
+
case 'PRODUCT': {
|
|
78
|
+
const pdType = element.product?.pd_type;
|
|
79
|
+
const code =
|
|
80
|
+
element.product?.pd_id?.code ||
|
|
81
|
+
element.product?.code ||
|
|
82
|
+
element.product?.product_data?.code;
|
|
83
|
+
|
|
84
|
+
if (!code) return null;
|
|
85
|
+
|
|
86
|
+
return pdType === 'VARIANT'
|
|
87
|
+
? `/variant/${code}`
|
|
88
|
+
: `model/${code}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'TAG': {
|
|
92
|
+
const tagCode = element.tag?.code;
|
|
93
|
+
if (!tagCode) return null;
|
|
94
|
+
// return `/${tagCode}/s?type=tag`;
|
|
95
|
+
return `/tag/${tagCode}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'PAGE': {
|
|
99
|
+
const pageId = element.page?._id || element.page?.code;
|
|
100
|
+
const pgType = element.page?.pg_type;
|
|
101
|
+
if (!pageId || !pgType) return null;
|
|
102
|
+
return `/page/${pageId}?type=${pgType}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
default:
|
|
106
|
+
return element.slug || null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getCurrentMode = () => {
|
|
111
|
+
switch (deviceMode) {
|
|
112
|
+
case 'mobileweb':
|
|
113
|
+
return props.mode.mobileweb;
|
|
114
|
+
case 'mobileapp':
|
|
115
|
+
return props.mode.mobileapp;
|
|
116
|
+
case 'tablet':
|
|
117
|
+
return props.mode.tablet;
|
|
118
|
+
case 'web':
|
|
119
|
+
default:
|
|
120
|
+
return props.mode.web;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const currentMode = getCurrentMode();
|
|
125
|
+
const containerHeight = boxHeight === 'auto' ? 'auto' : boxHeight;
|
|
126
|
+
const link = bannerLink(props);
|
|
127
|
+
const imageUrl = `https://pim.in-maa-1.linodeobjects.com/${props.images?.url || props.images?.all?.url}`;
|
|
128
|
+
const altText = props.images.alt || '';
|
|
129
|
+
|
|
130
|
+
const imageElement = (
|
|
131
|
+
<img
|
|
132
|
+
src={imageUrl}
|
|
133
|
+
alt={altText}
|
|
134
|
+
className="fitted-image"
|
|
135
|
+
style={{ cursor: link ? 'pointer' : 'default' }}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
className="image-box"
|
|
142
|
+
style={{
|
|
143
|
+
borderRadius: `${currentMode.borderRadius}px`,
|
|
144
|
+
height: containerHeight,
|
|
145
|
+
overflow: 'hidden',
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{link ? (
|
|
149
|
+
<a
|
|
150
|
+
href={link}
|
|
151
|
+
target={props?.linktype=='EXTERNAL' ? props?.link?.target :'_self'}
|
|
152
|
+
style={{ display: 'block', width: '100%', height: '100%' }}
|
|
153
|
+
>
|
|
154
|
+
{imageElement}
|
|
155
|
+
</a>
|
|
156
|
+
) : (
|
|
157
|
+
imageElement
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export default ImageComponent;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ComponentProps } from '../types';
|
|
3
|
+
import './Styles/InputField.css'; // Import external CSS
|
|
4
|
+
|
|
5
|
+
interface InputFieldProps {
|
|
6
|
+
props: ComponentProps;
|
|
7
|
+
value?: any;
|
|
8
|
+
onChange?: (code: string, value: any) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const InputField: React.FC<InputFieldProps> = ({ props, value = '', onChange }) => {
|
|
12
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
13
|
+
const newValue = e.target.value;
|
|
14
|
+
if (onChange) {
|
|
15
|
+
const code = props.code || `inputfield_${props.name?.all || 'field'}`;
|
|
16
|
+
onChange(code, newValue);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="input-field">
|
|
22
|
+
<label className="input-field__label">
|
|
23
|
+
{props.name?.all || 'Input Field'}
|
|
24
|
+
{props.required && <span className="input-field__required">*</span>}
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
{props.textArea ? (
|
|
28
|
+
<textarea
|
|
29
|
+
value={value || ''}
|
|
30
|
+
onChange={handleChange}
|
|
31
|
+
minLength={props.minLength}
|
|
32
|
+
maxLength={props.maxLength}
|
|
33
|
+
rows={4}
|
|
34
|
+
required={props.required}
|
|
35
|
+
className="input-field__textarea"
|
|
36
|
+
/>
|
|
37
|
+
) : (
|
|
38
|
+
<input
|
|
39
|
+
type="text"
|
|
40
|
+
value={value || ''}
|
|
41
|
+
onChange={handleChange}
|
|
42
|
+
minLength={props.minLength}
|
|
43
|
+
maxLength={props.maxLength}
|
|
44
|
+
required={props.required}
|
|
45
|
+
className="input-field__input"
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
{props.helperText && (
|
|
50
|
+
<p className="input-field__helper-text">{props.helperText}</p>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
<div className="input-field__status">
|
|
54
|
+
{props.format && `Format: ${props.format}`}
|
|
55
|
+
{props.postaltype && ` • Type: ${props.postaltype}`}
|
|
56
|
+
{props.minLength && props.maxLength && ` • Length: ${props.minLength}-${props.maxLength}`}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default InputField;
|
|
@@ -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,67 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
const PageForm: React.FC<PageFormProps> = ({ jsonData, onSubmit }) => {
|
|
12
|
+
// Form state to store all field values
|
|
13
|
+
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
|
14
|
+
const [deviceMode, setDeviceMode] = useState<string>('web');
|
|
15
|
+
const [validationErrors, setValidationErrors] = useState<Record<string, boolean>>({});
|
|
16
|
+
|
|
17
|
+
// Function to determine device mode based on screen width
|
|
18
|
+
const getDeviceMode = (width: number) => {
|
|
19
|
+
if (width <= 480) return 'mobileapp';
|
|
20
|
+
if (width <= 768) return 'mobileweb';
|
|
21
|
+
if (width <= 1024) return 'tablet';
|
|
22
|
+
return 'web';
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Function to get field code from component
|
|
26
|
+
const getFieldCode = (component: any) => {
|
|
27
|
+
return component.props.code || `${component.name.toLowerCase()}_${component.props.name?.all || 'field'}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Function to get field name from component
|
|
31
|
+
const getFieldName = (component: any) => {
|
|
32
|
+
return component.props.name?.all || component.name;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Function to create a mapping of field codes to field names
|
|
36
|
+
const getFieldCodeToNameMap = useCallback(() => {
|
|
37
|
+
const codeToNameMap: Record<string, string> = {};
|
|
38
|
+
|
|
39
|
+
jsonData.forEach((row) => {
|
|
40
|
+
row.columns?.forEach((box) => {
|
|
41
|
+
box.components?.forEach((component) => {
|
|
42
|
+
// Check if component is a form field
|
|
43
|
+
const fieldTypes = ['DateField', 'NumberField', 'SelectField', 'RadioField', 'InputField', 'BooleanField'];
|
|
44
|
+
if (fieldTypes.includes(component.name)) {
|
|
45
|
+
const code = getFieldCode(component);
|
|
46
|
+
const name = getFieldName(component);
|
|
47
|
+
codeToNameMap[code] = name;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return codeToNameMap;
|
|
54
|
+
}, [jsonData]);
|
|
55
|
+
|
|
56
|
+
// Function to collect all required fields from jsonData
|
|
57
|
+
const getRequiredFields = useCallback(() => {
|
|
58
|
+
const requiredFields: Record<string, { code: string; name: string }> = {};
|
|
59
|
+
|
|
60
|
+
jsonData.forEach((row) => {
|
|
61
|
+
row.columns?.forEach((box) => {
|
|
62
|
+
box.components?.forEach((component) => {
|
|
63
|
+
// Check if component is a form field and is required
|
|
64
|
+
const fieldTypes = ['DateField', 'NumberField', 'SelectField', 'RadioField', 'InputField', 'BooleanField'];
|
|
65
|
+
if (fieldTypes.includes(component.name) && component.props.required) {
|
|
66
|
+
const code = getFieldCode(component);
|
|
67
|
+
const name = getFieldName(component);
|
|
68
|
+
requiredFields[code] = { code, name };
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return requiredFields;
|
|
75
|
+
}, [jsonData]);
|
|
76
|
+
|
|
77
|
+
// Function to transform form values from code-based to name-based
|
|
78
|
+
const transformFormValuesToNames = useCallback(() => {
|
|
79
|
+
const codeToNameMap = getFieldCodeToNameMap();
|
|
80
|
+
const transformedData: Record<string, any> = {};
|
|
81
|
+
|
|
82
|
+
Object.keys(formValues).forEach((code) => {
|
|
83
|
+
const fieldName = codeToNameMap[code];
|
|
84
|
+
if (fieldName) {
|
|
85
|
+
transformedData[fieldName] = formValues[code];
|
|
86
|
+
} else {
|
|
87
|
+
// If no mapping found, keep the code as key
|
|
88
|
+
transformedData[code] = formValues[code];
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return transformedData;
|
|
93
|
+
}, [formValues, getFieldCodeToNameMap]);
|
|
94
|
+
|
|
95
|
+
// Effect to handle window resize
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const handleResize = () => {
|
|
98
|
+
const width = window.innerWidth;
|
|
99
|
+
setDeviceMode(getDeviceMode(width));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Call once on mount
|
|
103
|
+
handleResize();
|
|
104
|
+
|
|
105
|
+
window.addEventListener('resize', handleResize);
|
|
106
|
+
return () => {
|
|
107
|
+
window.removeEventListener('resize', handleResize);
|
|
108
|
+
};
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
// Handler to update form values
|
|
112
|
+
const handleFieldChange = useCallback((code: string, value: any) => {
|
|
113
|
+
setFormValues(prev => ({
|
|
114
|
+
...prev,
|
|
115
|
+
[code]: value
|
|
116
|
+
}));
|
|
117
|
+
// Clear validation error for this field when user types
|
|
118
|
+
if (validationErrors[code]) {
|
|
119
|
+
setValidationErrors(prev => {
|
|
120
|
+
const newErrors = { ...prev };
|
|
121
|
+
delete newErrors[code];
|
|
122
|
+
return newErrors;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}, [validationErrors]);
|
|
126
|
+
|
|
127
|
+
// Function to validate required fields
|
|
128
|
+
const validateForm = useCallback(() => {
|
|
129
|
+
const requiredFields = getRequiredFields();
|
|
130
|
+
const errors: Record<string, boolean> = {};
|
|
131
|
+
|
|
132
|
+
Object.keys(requiredFields).forEach((code) => {
|
|
133
|
+
const value = formValues[code];
|
|
134
|
+
// Check if value is empty, null, undefined, empty array, or empty string
|
|
135
|
+
if (
|
|
136
|
+
value === undefined ||
|
|
137
|
+
value === null ||
|
|
138
|
+
value === '' ||
|
|
139
|
+
(Array.isArray(value) && value.length === 0)
|
|
140
|
+
) {
|
|
141
|
+
errors[code] = true;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
setValidationErrors(errors);
|
|
146
|
+
return { isValid: Object.keys(errors).length === 0, errors };
|
|
147
|
+
}, [formValues, getRequiredFields]);
|
|
148
|
+
|
|
149
|
+
// Single method to handle form submission - validates, transforms, and sends data to parent
|
|
150
|
+
const handleFormSubmit = useCallback(() => {
|
|
151
|
+
// Validate form and get validation result
|
|
152
|
+
const { isValid, errors } = validateForm();
|
|
153
|
+
|
|
154
|
+
if (isValid) {
|
|
155
|
+
// Transform form values to use field names instead of codes
|
|
156
|
+
const transformedData = transformFormValuesToNames();
|
|
157
|
+
|
|
158
|
+
// Send transformed data back to parent component
|
|
159
|
+
if (onSubmit) {
|
|
160
|
+
onSubmit(transformedData);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// If validation fails, show error message with field names
|
|
164
|
+
const requiredFields = getRequiredFields();
|
|
165
|
+
const errorFields = Object.keys(errors)
|
|
166
|
+
.map(code => requiredFields[code]?.name || code)
|
|
167
|
+
.join(', ');
|
|
168
|
+
alert(`Please fill in all required fields: ${errorFields}`);
|
|
169
|
+
}
|
|
170
|
+
}, [formValues, validateForm, getRequiredFields, transformFormValuesToNames, onSubmit]);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="page-form">
|
|
174
|
+
<div className="page-form__container">
|
|
175
|
+
{/* Render each row */}
|
|
176
|
+
{jsonData.map((row, rowIndex) => (
|
|
177
|
+
<RowComponent
|
|
178
|
+
key={rowIndex}
|
|
179
|
+
row={row}
|
|
180
|
+
deviceMode={deviceMode}
|
|
181
|
+
formValues={formValues}
|
|
182
|
+
validationErrors={validationErrors}
|
|
183
|
+
onFieldChange={handleFieldChange}
|
|
184
|
+
onFormSubmit={handleFormSubmit}
|
|
185
|
+
/>
|
|
186
|
+
))}
|
|
187
|
+
|
|
188
|
+
{/* Footer */}
|
|
189
|
+
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
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;
|