@webbycrown/advanced-fields 1.0.0 → 1.0.2
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 +19 -6
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/AdvancedCheckbox/index.jsx +377 -0
- package/admin/src/components/AdvancedInput/index.jsx +179 -0
- package/admin/src/components/AdvancedRadio/index.jsx +344 -0
- package/admin/src/components/Initializer.jsx +18 -0
- package/admin/src/components/PluginIcon.jsx +108 -0
- package/admin/src/index.js +787 -0
- package/admin/src/pages/App.jsx +13 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/en.json +86 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/index.js +158 -0
- package/package.json +6 -3
- package/strapi-server.js +186 -0
- package/strapi-server.mjs +74 -0
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Professional custom fields for Strapi CMS that provide advanced functionality with comprehensive validation, dynamic options, and user-friendly interfaces.
|
|
8
8
|
|
|
9
|
+
📦 **NPM Package**: [@webbycrown/advanced-fields](https://www.npmjs.com/package/@webbycrown/advanced-fields)
|
|
10
|
+
|
|
9
11
|
## ✨ Features
|
|
10
12
|
|
|
11
13
|
### 🔤 Advanced Input
|
|
@@ -14,6 +16,7 @@ Professional custom fields for Strapi CMS that provide advanced functionality wi
|
|
|
14
16
|
- **Custom Error Messages**: User-friendly validation feedback
|
|
15
17
|
- **Default Values**: Pre-filled content for new entries
|
|
16
18
|
- **Placeholder Support**: Helpful hints for content creators
|
|
19
|
+
- **Field Notes**: Display helpful notes below the field
|
|
17
20
|
- **Private Fields**: Hide sensitive data from API responses
|
|
18
21
|
|
|
19
22
|
### ☑️ Advanced Checkbox
|
|
@@ -22,6 +25,7 @@ Professional custom fields for Strapi CMS that provide advanced functionality wi
|
|
|
22
25
|
- **Min/Max Validation**: Control minimum and maximum selections
|
|
23
26
|
- **Layout Options**: Vertical, horizontal, or grid layouts
|
|
24
27
|
- **Default Selections**: Pre-select options for new entries
|
|
28
|
+
- **Field Notes**: Display helpful notes below the field
|
|
25
29
|
|
|
26
30
|
### 🔘 Advanced Radio
|
|
27
31
|
- **Single & Multiple Selection**: Choose between single or multiple radio selections
|
|
@@ -29,6 +33,7 @@ Professional custom fields for Strapi CMS that provide advanced functionality wi
|
|
|
29
33
|
- **Selection Limits**: Control minimum and maximum choices
|
|
30
34
|
- **Layout Flexibility**: Multiple visual layouts
|
|
31
35
|
- **Custom Validation**: Tailored error messages
|
|
36
|
+
- **Field Notes**: Display helpful notes below the field
|
|
32
37
|
|
|
33
38
|
## 🛠️ Installation
|
|
34
39
|
|
|
@@ -57,7 +62,7 @@ yarn add @webbycrown/advanced-fields
|
|
|
57
62
|
### Advanced Input Configuration
|
|
58
63
|
|
|
59
64
|
```javascript
|
|
60
|
-
// Example: Text validation with custom error message
|
|
65
|
+
// Example: Text validation with custom error message and field note
|
|
61
66
|
{
|
|
62
67
|
"required": true,
|
|
63
68
|
"maxLength": 255,
|
|
@@ -66,7 +71,8 @@ yarn add @webbycrown/advanced-fields
|
|
|
66
71
|
"options": {
|
|
67
72
|
"customErrorMessage": "Please enter valid text",
|
|
68
73
|
"placeholder": "Enter your text here",
|
|
69
|
-
"defaultValue": "Default text"
|
|
74
|
+
"defaultValue": "Default text",
|
|
75
|
+
"fieldNote": "This field accepts alphanumeric characters and spaces only"
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
```
|
|
@@ -74,7 +80,7 @@ yarn add @webbycrown/advanced-fields
|
|
|
74
80
|
### Advanced Checkbox Configuration
|
|
75
81
|
|
|
76
82
|
```javascript
|
|
77
|
-
// Example: Multiple checkbox with validation
|
|
83
|
+
// Example: Multiple checkbox with validation and field note
|
|
78
84
|
{
|
|
79
85
|
"required": true,
|
|
80
86
|
"options": {
|
|
@@ -83,7 +89,8 @@ yarn add @webbycrown/advanced-fields
|
|
|
83
89
|
"minChoices": 1,
|
|
84
90
|
"maxChoices": 2,
|
|
85
91
|
"layout": "vertical",
|
|
86
|
-
"defaultSelected": "1\n2"
|
|
92
|
+
"defaultSelected": "1\n2",
|
|
93
|
+
"fieldNote": "Please select at least 1 and at most 2 options"
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
```
|
|
@@ -91,14 +98,15 @@ yarn add @webbycrown/advanced-fields
|
|
|
91
98
|
### Advanced Radio Configuration
|
|
92
99
|
|
|
93
100
|
```javascript
|
|
94
|
-
// Example: Single radio with dynamic options
|
|
101
|
+
// Example: Single radio with dynamic options and field note
|
|
95
102
|
{
|
|
96
103
|
"required": true,
|
|
97
104
|
"options": {
|
|
98
105
|
"selectionType": "single",
|
|
99
106
|
"radioOptions": "small|Small\nmedium|Medium\nlarge|Large",
|
|
100
107
|
"layout": "horizontal",
|
|
101
|
-
"defaultSelected": "medium"
|
|
108
|
+
"defaultSelected": "medium",
|
|
109
|
+
"fieldNote": "Choose the size that best fits your needs"
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
```
|
|
@@ -117,6 +125,7 @@ yarn add @webbycrown/advanced-fields
|
|
|
117
125
|
| `options.defaultValue` | string | Pre-filled value | `""` |
|
|
118
126
|
| `options.placeholder` | string | Placeholder text | `""` |
|
|
119
127
|
| `options.customErrorMessage` | string | Custom error message | `""` |
|
|
128
|
+
| `options.fieldNote` | string | Helpful note displayed below field | `""` |
|
|
120
129
|
| `private` | boolean | Hide from API | `false` |
|
|
121
130
|
|
|
122
131
|
### Advanced Checkbox Options
|
|
@@ -131,6 +140,7 @@ yarn add @webbycrown/advanced-fields
|
|
|
131
140
|
| `options.maxChoices` | number | Maximum selections | `0` |
|
|
132
141
|
| `options.layout` | string | `vertical`, `horizontal`, or `grid` | `vertical` |
|
|
133
142
|
| `options.customErrorMessage` | string | Custom error message | `""` |
|
|
143
|
+
| `options.fieldNote` | string | Helpful note displayed below field | `""` |
|
|
134
144
|
| `private` | boolean | Hide from API | `false` |
|
|
135
145
|
|
|
136
146
|
### Advanced Radio Options
|
|
@@ -145,6 +155,7 @@ yarn add @webbycrown/advanced-fields
|
|
|
145
155
|
| `options.maxChoices` | number | Maximum selections | `0` |
|
|
146
156
|
| `options.layout` | string | `vertical`, `horizontal`, or `grid` | `vertical` |
|
|
147
157
|
| `options.customErrorMessage` | string | Custom error message | `""` |
|
|
158
|
+
| `options.fieldNote` | string | Helpful note displayed below field | `""` |
|
|
148
159
|
| `private` | boolean | Hide from API | `false` |
|
|
149
160
|
|
|
150
161
|
## 🔧 API Usage
|
|
@@ -259,8 +270,10 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
259
270
|
- 🔘 Advanced Radio (single/multiple)
|
|
260
271
|
- 🎨 Multiple layout options
|
|
261
272
|
- ✅ Comprehensive validation system
|
|
273
|
+
- 📝 Field notes support for all field types
|
|
262
274
|
- 📱 Responsive design
|
|
263
275
|
- 🌐 Internationalization support
|
|
276
|
+
- 🚀 Published to NPM: [@webbycrown/advanced-fields](https://www.npmjs.com/package/@webbycrown/advanced-fields)
|
|
264
277
|
|
|
265
278
|
---
|
|
266
279
|
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import { Field, Box, Flex, Typography } from "@strapi/design-system";
|
|
5
|
+
import { useState, useEffect } from "react";
|
|
6
|
+
|
|
7
|
+
const AdvancedCheckbox = ({
|
|
8
|
+
attribute = {},
|
|
9
|
+
description = { id: "", defaultMessage: "" },
|
|
10
|
+
disabled,
|
|
11
|
+
error,
|
|
12
|
+
intlLabel = { id: "", defaultMessage: "" },
|
|
13
|
+
labelAction,
|
|
14
|
+
name,
|
|
15
|
+
onChange,
|
|
16
|
+
required,
|
|
17
|
+
value,
|
|
18
|
+
}) => {
|
|
19
|
+
const { formatMessage } = useIntl();
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
checkboxType = "multiple",
|
|
23
|
+
layout = "vertical",
|
|
24
|
+
minChoices = 0,
|
|
25
|
+
maxChoices = 0,
|
|
26
|
+
defaultSelected = "",
|
|
27
|
+
checkboxOptions = "",
|
|
28
|
+
customErrorMessage = "",
|
|
29
|
+
fieldNote = "",
|
|
30
|
+
} = attribute.options || attribute;
|
|
31
|
+
|
|
32
|
+
// Also check attribute.options for fieldNote
|
|
33
|
+
const fieldNoteFromAttribute = attribute.options?.fieldNote || '';
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// Initialize with default values if available
|
|
37
|
+
const getInitialValues = () => {
|
|
38
|
+
if (checkboxType === "single") {
|
|
39
|
+
// For single checkbox mode, return array with one value (like radio button)
|
|
40
|
+
if (value && Array.isArray(value) && value.length > 0) {
|
|
41
|
+
return value;
|
|
42
|
+
} else if (value && typeof value === "string" && value.trim()) {
|
|
43
|
+
return [value.trim()];
|
|
44
|
+
} else if (defaultSelected && typeof defaultSelected === "string" && defaultSelected.trim()) {
|
|
45
|
+
return [defaultSelected.trim()];
|
|
46
|
+
} else if (defaultSelected && Array.isArray(defaultSelected) && defaultSelected.length > 0) {
|
|
47
|
+
return defaultSelected;
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
} else {
|
|
51
|
+
// For multiple checkboxes, return array of selected values
|
|
52
|
+
if (value && Array.isArray(value)) {
|
|
53
|
+
return value;
|
|
54
|
+
} else if (value && typeof value === "string") {
|
|
55
|
+
return value.split(",").map(v => v.trim()).filter(v => v);
|
|
56
|
+
} else if (defaultSelected) {
|
|
57
|
+
return defaultSelected.split(",").map(v => v.trim()).filter(v => v);
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const [fieldValue, setFieldValue] = useState(getInitialValues);
|
|
64
|
+
const [validationError, setValidationError] = useState(null);
|
|
65
|
+
const [hasInteracted, setHasInteracted] = useState(false);
|
|
66
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
67
|
+
|
|
68
|
+
// Parse checkbox options
|
|
69
|
+
const options = checkboxOptions
|
|
70
|
+
.split("\n")
|
|
71
|
+
.filter((opt) => opt.trim())
|
|
72
|
+
.map((opt) => {
|
|
73
|
+
const [value, label] = opt.split("|");
|
|
74
|
+
return { value: value?.trim() || "", label: label?.trim() || value?.trim() || "" };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Initialize selected values - only run once on mount
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const initialValues = getInitialValues();
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
setFieldValue(initialValues);
|
|
83
|
+
|
|
84
|
+
// Validate the initial values
|
|
85
|
+
const validationResult = validateSelection(initialValues);
|
|
86
|
+
setValidationError(validationResult);
|
|
87
|
+
|
|
88
|
+
// Only trigger onChange if we have initial values and it's different from current value
|
|
89
|
+
// AND only if the value prop is not already set (to avoid overriding during publish)
|
|
90
|
+
// AND only if this is the first initialization
|
|
91
|
+
if (onChange && initialValues.length > 0 && (!value || (Array.isArray(value) && value.length === 0)) && !isInitialized) {
|
|
92
|
+
// Use setTimeout to ensure this happens after the component is fully mounted
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
onChange({
|
|
95
|
+
target: {
|
|
96
|
+
value: initialValues,
|
|
97
|
+
name: name,
|
|
98
|
+
id: name
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
setIsInitialized(true);
|
|
102
|
+
}, 0);
|
|
103
|
+
} else if (!isInitialized) {
|
|
104
|
+
setIsInitialized(true);
|
|
105
|
+
}
|
|
106
|
+
}, []); // Remove dependencies to only run once on mount
|
|
107
|
+
|
|
108
|
+
// Handle external value changes (like when loading existing data)
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
// Only update if the value prop has changed and it's not empty
|
|
111
|
+
// This handles cases like loading existing content
|
|
112
|
+
if (value && Array.isArray(value) && value.length > 0) {
|
|
113
|
+
setFieldValue(value);
|
|
114
|
+
const validationResult = validateSelection(value);
|
|
115
|
+
setValidationError(validationResult);
|
|
116
|
+
}
|
|
117
|
+
}, [value]);
|
|
118
|
+
|
|
119
|
+
// Validation function - this should match server-side validation
|
|
120
|
+
const validateSelection = (val) => {
|
|
121
|
+
const values = Array.isArray(val) ? val : [];
|
|
122
|
+
|
|
123
|
+
// Check required validation first
|
|
124
|
+
if (required && values.length === 0) {
|
|
125
|
+
return customErrorMessage || 'This field is required';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// If field is empty and not required, no validation error
|
|
129
|
+
if (values.length === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check min/max choices validation (only for multiple mode)
|
|
134
|
+
if (checkboxType === "multiple") {
|
|
135
|
+
if (minChoices > 0 && values.length < minChoices) {
|
|
136
|
+
return customErrorMessage || `Please select at least ${minChoices} option${minChoices > 1 ? 's' : ''}`;
|
|
137
|
+
}
|
|
138
|
+
if (maxChoices > 0 && values.length > maxChoices) {
|
|
139
|
+
return customErrorMessage || `Please select at most ${maxChoices} option${maxChoices > 1 ? 's' : ''}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleCheckboxChange = (optionValue, isChecked) => {
|
|
147
|
+
let newValue;
|
|
148
|
+
|
|
149
|
+
if (checkboxType === "single") {
|
|
150
|
+
// For single checkbox mode, work like radio button - only one option can be selected
|
|
151
|
+
if (isChecked) {
|
|
152
|
+
// If checking an option, set it as the only selected value
|
|
153
|
+
newValue = [optionValue];
|
|
154
|
+
} else {
|
|
155
|
+
// If unchecking, clear the selection
|
|
156
|
+
newValue = [];
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
// For multiple checkboxes, handle array of values
|
|
160
|
+
if (isChecked) {
|
|
161
|
+
newValue = [...fieldValue, optionValue];
|
|
162
|
+
} else {
|
|
163
|
+
newValue = fieldValue.filter(val => val !== optionValue);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setFieldValue(newValue);
|
|
168
|
+
setHasInteracted(true);
|
|
169
|
+
|
|
170
|
+
// Validate selection only after user interaction
|
|
171
|
+
const error = validateSelection(newValue);
|
|
172
|
+
setValidationError(error);
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// Always trigger onChange for user interactions
|
|
176
|
+
if (onChange) {
|
|
177
|
+
onChange({
|
|
178
|
+
target: {
|
|
179
|
+
value: newValue,
|
|
180
|
+
name: name,
|
|
181
|
+
id: name
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Show validation error - prioritize Strapi's error, then our validation only after user interaction
|
|
188
|
+
const displayError = error || (hasInteracted && validationError);
|
|
189
|
+
const renderCheckboxes = () => {
|
|
190
|
+
|
|
191
|
+
const checkboxStyle = {
|
|
192
|
+
display: "flex",
|
|
193
|
+
alignItems: "center",
|
|
194
|
+
gap: "8px",
|
|
195
|
+
marginBottom: "8px",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const checkboxInputStyle = {
|
|
199
|
+
width: "16px",
|
|
200
|
+
height: "16px",
|
|
201
|
+
accentColor: "#4945ff",
|
|
202
|
+
margin: "0",
|
|
203
|
+
padding: "0",
|
|
204
|
+
opacity: "1",
|
|
205
|
+
visibility: "visible",
|
|
206
|
+
display: "block",
|
|
207
|
+
position: "relative",
|
|
208
|
+
zIndex: "1",
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const checkboxLabelStyle = {
|
|
212
|
+
fontSize: "14px",
|
|
213
|
+
fontFamily: "inherit",
|
|
214
|
+
cursor: "pointer",
|
|
215
|
+
userSelect: "none",
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Single checkbox mode - show multiple options but only one selectable (like radio buttons)
|
|
219
|
+
if (checkboxType === "single") {
|
|
220
|
+
// If no options are defined for single mode, show a message
|
|
221
|
+
if (!options || options.length === 0) {
|
|
222
|
+
return (
|
|
223
|
+
<div style={{ padding: "8px", color: "#666", fontStyle: "italic" }}>
|
|
224
|
+
{formatMessage({
|
|
225
|
+
id: 'advanced-fields.checkbox.options.checkboxOptions.description',
|
|
226
|
+
defaultMessage: 'Define available options for multiple checkboxes (one per line: value|label)'
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (layout === "horizontal") {
|
|
233
|
+
return (
|
|
234
|
+
<Flex wrap="wrap" gap={2}>
|
|
235
|
+
{options.map((option) => (
|
|
236
|
+
<div key={option.value} style={checkboxStyle}>
|
|
237
|
+
<input
|
|
238
|
+
type="checkbox"
|
|
239
|
+
id={`${name}-${option.value}`}
|
|
240
|
+
checked={fieldValue.includes(option.value)}
|
|
241
|
+
onChange={(e) => handleCheckboxChange(option.value, e.target.checked)}
|
|
242
|
+
disabled={disabled}
|
|
243
|
+
style={checkboxInputStyle}
|
|
244
|
+
/>
|
|
245
|
+
<label
|
|
246
|
+
htmlFor={`${name}-${option.value}`}
|
|
247
|
+
style={checkboxLabelStyle}
|
|
248
|
+
>
|
|
249
|
+
{option.label}
|
|
250
|
+
</label>
|
|
251
|
+
</div>
|
|
252
|
+
))}
|
|
253
|
+
</Flex>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div>
|
|
259
|
+
{options.map((option) => (
|
|
260
|
+
<div key={option.value} style={checkboxStyle}>
|
|
261
|
+
<input
|
|
262
|
+
type="checkbox"
|
|
263
|
+
id={`${name}-${option.value}`}
|
|
264
|
+
checked={fieldValue.includes(option.value)}
|
|
265
|
+
onChange={(e) => handleCheckboxChange(option.value, e.target.checked)}
|
|
266
|
+
disabled={disabled}
|
|
267
|
+
style={checkboxInputStyle}
|
|
268
|
+
/>
|
|
269
|
+
<label
|
|
270
|
+
htmlFor={`${name}-${option.value}`}
|
|
271
|
+
style={checkboxLabelStyle}
|
|
272
|
+
>
|
|
273
|
+
{option.label}
|
|
274
|
+
</label>
|
|
275
|
+
</div>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Multiple checkbox mode
|
|
282
|
+
// If no options are defined, show a message
|
|
283
|
+
if (!options || options.length === 0) {
|
|
284
|
+
return (
|
|
285
|
+
<div style={{ padding: "8px", color: "#666", fontStyle: "italic" }}>
|
|
286
|
+
{formatMessage({
|
|
287
|
+
id: 'advanced-fields.checkbox.options.checkboxOptions.description',
|
|
288
|
+
defaultMessage: 'Define available options for multiple checkboxes (one per line: value|label)'
|
|
289
|
+
})}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (layout === "horizontal") {
|
|
295
|
+
return (
|
|
296
|
+
<Flex wrap="wrap" gap={2}>
|
|
297
|
+
{options.map((option) => (
|
|
298
|
+
<div key={option.value} style={checkboxStyle}>
|
|
299
|
+
<input
|
|
300
|
+
type="checkbox"
|
|
301
|
+
id={`${name}-${option.value}`}
|
|
302
|
+
checked={fieldValue.includes(option.value)}
|
|
303
|
+
onChange={(e) => handleCheckboxChange(option.value, e.target.checked)}
|
|
304
|
+
disabled={disabled}
|
|
305
|
+
style={checkboxInputStyle}
|
|
306
|
+
/>
|
|
307
|
+
<label
|
|
308
|
+
htmlFor={`${name}-${option.value}`}
|
|
309
|
+
style={checkboxLabelStyle}
|
|
310
|
+
>
|
|
311
|
+
{option.label}
|
|
312
|
+
</label>
|
|
313
|
+
</div>
|
|
314
|
+
))}
|
|
315
|
+
</Flex>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div>
|
|
321
|
+
{options.map((option) => (
|
|
322
|
+
<div key={option.value} style={checkboxStyle}>
|
|
323
|
+
<input
|
|
324
|
+
type="checkbox"
|
|
325
|
+
id={`${name}-${option.value}`}
|
|
326
|
+
checked={fieldValue.includes(option.value)}
|
|
327
|
+
onChange={(e) => handleCheckboxChange(option.value, e.target.checked)}
|
|
328
|
+
disabled={disabled}
|
|
329
|
+
style={checkboxInputStyle}
|
|
330
|
+
/>
|
|
331
|
+
<label
|
|
332
|
+
htmlFor={`${name}-${option.value}`}
|
|
333
|
+
style={checkboxLabelStyle}
|
|
334
|
+
>
|
|
335
|
+
{option.label}
|
|
336
|
+
</label>
|
|
337
|
+
</div>
|
|
338
|
+
))}
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<Box col={6}>
|
|
345
|
+
<Field.Root name={name} error={displayError}>
|
|
346
|
+
<Field.Label>
|
|
347
|
+
{intlLabel.id ? formatMessage(intlLabel) : intlLabel.defaultMessage || name}
|
|
348
|
+
{required && <span style={{ color: "#d02b20", marginLeft: "4px" }}>*</span>}
|
|
349
|
+
</Field.Label>
|
|
350
|
+
{renderCheckboxes()}
|
|
351
|
+
{displayError && (
|
|
352
|
+
<Field.Error>
|
|
353
|
+
{displayError}
|
|
354
|
+
</Field.Error>
|
|
355
|
+
)}
|
|
356
|
+
{description && (description.id || description.defaultMessage) && (
|
|
357
|
+
<Field.Hint>
|
|
358
|
+
{description.id ? formatMessage(description) : description.defaultMessage}
|
|
359
|
+
</Field.Hint>
|
|
360
|
+
)}
|
|
361
|
+
{(fieldNote || fieldNoteFromAttribute) && (
|
|
362
|
+
<span style={{
|
|
363
|
+
fontStyle: 'italic',
|
|
364
|
+
color: '#666',
|
|
365
|
+
fontSize: '12px',
|
|
366
|
+
display: 'block',
|
|
367
|
+
marginTop: '4px'
|
|
368
|
+
}}>
|
|
369
|
+
{fieldNote || fieldNoteFromAttribute}
|
|
370
|
+
</span>
|
|
371
|
+
)}
|
|
372
|
+
</Field.Root>
|
|
373
|
+
</Box>
|
|
374
|
+
);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export default AdvancedCheckbox;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import { Field, Box } from "@strapi/design-system";
|
|
5
|
+
import { Cross } from "@strapi/icons";
|
|
6
|
+
import { useState, useEffect } from "react";
|
|
7
|
+
|
|
8
|
+
const AdvancedInput = ({
|
|
9
|
+
attribute = {},
|
|
10
|
+
description = { id: "", defaultMessage: "" },
|
|
11
|
+
disabled,
|
|
12
|
+
error,
|
|
13
|
+
intlLabel = { id: "", defaultMessage: "" },
|
|
14
|
+
labelAction,
|
|
15
|
+
name,
|
|
16
|
+
onChange,
|
|
17
|
+
required,
|
|
18
|
+
value,
|
|
19
|
+
}) => {
|
|
20
|
+
const { formatMessage } = useIntl();
|
|
21
|
+
const [inputValue, setInputValue] = useState(value || "");
|
|
22
|
+
const [validationError, setValidationError] = useState(null);
|
|
23
|
+
const [hasInteracted, setHasInteracted] = useState(false);
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
minLength = 0,
|
|
27
|
+
maxLength = 0,
|
|
28
|
+
min = 0,
|
|
29
|
+
max = 0,
|
|
30
|
+
step = 1,
|
|
31
|
+
rows = 4,
|
|
32
|
+
options = {},
|
|
33
|
+
} = attribute;
|
|
34
|
+
|
|
35
|
+
// Extract options with defaults
|
|
36
|
+
const {
|
|
37
|
+
placeholder = '',
|
|
38
|
+
defaultValue = '',
|
|
39
|
+
customErrorMessage = '',
|
|
40
|
+
regex = '',
|
|
41
|
+
fieldNote = '',
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
// Also check attribute.options for fieldNote
|
|
45
|
+
const fieldNoteFromAttribute = attribute.options?.fieldNote || '';
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// Initialize input value
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const initialValue = value === undefined ? defaultValue : value;
|
|
51
|
+
setInputValue(initialValue);
|
|
52
|
+
|
|
53
|
+
// Don't validate on initial load unless there's an error from Strapi
|
|
54
|
+
if (error) {
|
|
55
|
+
setValidationError(error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (onChange) {
|
|
59
|
+
onChange({ target: { value: initialValue } });
|
|
60
|
+
}
|
|
61
|
+
}, [value, defaultValue, onChange, error]);
|
|
62
|
+
|
|
63
|
+
// Validation function - this should match server-side validation
|
|
64
|
+
const validateInput = (val) => {
|
|
65
|
+
// Check required validation first
|
|
66
|
+
if (required && (!val || val.toString().trim().length === 0)) {
|
|
67
|
+
return customErrorMessage || "This field is required";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If no value, no additional validation needed
|
|
71
|
+
if (!val || val.toString().trim().length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const stringValue = val.toString().trim();
|
|
76
|
+
|
|
77
|
+
// Check min/max length validation
|
|
78
|
+
if (minLength > 0 && stringValue.length < minLength) {
|
|
79
|
+
return customErrorMessage || `Minimum length is ${minLength} characters`;
|
|
80
|
+
}
|
|
81
|
+
if (maxLength > 0 && stringValue.length > maxLength) {
|
|
82
|
+
return customErrorMessage || `Maximum length is ${maxLength} characters`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check regex validation if provided
|
|
86
|
+
if (regex && stringValue) {
|
|
87
|
+
try {
|
|
88
|
+
const regexPattern = new RegExp(regex);
|
|
89
|
+
if (!regexPattern.test(stringValue)) {
|
|
90
|
+
return customErrorMessage || "Invalid format";
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Invalid regex pattern, skip validation
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleChange = (e) => {
|
|
101
|
+
const newValue = e.target.value;
|
|
102
|
+
setInputValue(newValue);
|
|
103
|
+
setHasInteracted(true);
|
|
104
|
+
|
|
105
|
+
// Validate input only after user interaction
|
|
106
|
+
const error = validateInput(newValue);
|
|
107
|
+
setValidationError(error);
|
|
108
|
+
|
|
109
|
+
if (onChange) {
|
|
110
|
+
onChange(e);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Show validation error - prioritize Strapi's error, then our validation only after user interaction
|
|
115
|
+
const displayError = error || (hasInteracted && validationError);
|
|
116
|
+
|
|
117
|
+
const renderInput = () => {
|
|
118
|
+
const commonProps = {
|
|
119
|
+
name,
|
|
120
|
+
value: inputValue,
|
|
121
|
+
onChange: handleChange,
|
|
122
|
+
disabled,
|
|
123
|
+
placeholder: placeholder || (intlLabel.id ? formatMessage(intlLabel) : intlLabel.defaultMessage || ""),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const inputStyle = {
|
|
127
|
+
width: "100%",
|
|
128
|
+
padding: "8px 12px",
|
|
129
|
+
border: `1px solid ${displayError ? "#d02b20" : "#dcdce4"}`,
|
|
130
|
+
borderRadius: "4px",
|
|
131
|
+
fontSize: "14px",
|
|
132
|
+
fontFamily: "inherit",
|
|
133
|
+
backgroundColor: disabled ? "#f6f6f9" : "#ffffff",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<input
|
|
138
|
+
{...commonProps}
|
|
139
|
+
type="text"
|
|
140
|
+
style={inputStyle}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Box col={6}>
|
|
147
|
+
<Field.Root name={name} error={displayError}>
|
|
148
|
+
<Field.Label>
|
|
149
|
+
{intlLabel.id ? formatMessage(intlLabel) : intlLabel.defaultMessage || name}
|
|
150
|
+
{required && <span style={{ color: "#d02b20", marginLeft: "4px" }}>*</span>}
|
|
151
|
+
</Field.Label>
|
|
152
|
+
{renderInput()}
|
|
153
|
+
{displayError && (
|
|
154
|
+
<Field.Error>
|
|
155
|
+
{displayError}
|
|
156
|
+
</Field.Error>
|
|
157
|
+
)}
|
|
158
|
+
{description && (description.id || description.defaultMessage) && (
|
|
159
|
+
<Field.Hint>
|
|
160
|
+
{description.id ? formatMessage(description) : description.defaultMessage}
|
|
161
|
+
</Field.Hint>
|
|
162
|
+
)}
|
|
163
|
+
{(fieldNote || fieldNoteFromAttribute) && (
|
|
164
|
+
<span style={{
|
|
165
|
+
fontStyle: 'italic',
|
|
166
|
+
color: '#666',
|
|
167
|
+
fontSize: '12px',
|
|
168
|
+
display: 'block',
|
|
169
|
+
marginTop: '4px'
|
|
170
|
+
}}>
|
|
171
|
+
{fieldNote || fieldNoteFromAttribute}
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
</Field.Root>
|
|
175
|
+
</Box>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export default AdvancedInput;
|