@webbycrown/advanced-fields 1.0.0 → 1.0.1

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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es6",
4
+ "jsx": "react",
5
+ "module": "esnext",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true
8
+ },
9
+ "include": ["./src/**/*.js", "./src/**/*.jsx"]
10
+ }
@@ -0,0 +1,397 @@
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 CheckboxInput = ({
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
+ } = attribute.options || attribute;
30
+
31
+ // Debug logging for checkboxType detection
32
+ // console.log('AdvancedCheckbox options detection:', {
33
+ // attribute,
34
+ // attributeOptions: attribute.options,
35
+ // checkboxType,
36
+ // checkboxOptions,
37
+ // allOptions: attribute.options || attribute
38
+ // });
39
+
40
+ // Initialize with default values if available
41
+ const getInitialValues = () => {
42
+ if (checkboxType === "single") {
43
+ // For single checkbox mode, return array with one value (like radio button)
44
+ if (value && Array.isArray(value) && value.length > 0) {
45
+ return value;
46
+ } else if (value && typeof value === "string" && value.trim()) {
47
+ return [value.trim()];
48
+ } else if (defaultSelected && typeof defaultSelected === "string" && defaultSelected.trim()) {
49
+ return [defaultSelected.trim()];
50
+ } else if (defaultSelected && Array.isArray(defaultSelected) && defaultSelected.length > 0) {
51
+ return defaultSelected;
52
+ }
53
+ return [];
54
+ } else {
55
+ // For multiple checkboxes, return array of selected values
56
+ if (value && Array.isArray(value)) {
57
+ return value;
58
+ } else if (value && typeof value === "string") {
59
+ return value.split(",").map(v => v.trim()).filter(v => v);
60
+ } else if (defaultSelected) {
61
+ return defaultSelected.split(",").map(v => v.trim()).filter(v => v);
62
+ }
63
+ return [];
64
+ }
65
+ };
66
+
67
+ const [fieldValue, setFieldValue] = useState(getInitialValues);
68
+ const [validationError, setValidationError] = useState(null);
69
+ const [hasInteracted, setHasInteracted] = useState(false);
70
+ const [isInitialized, setIsInitialized] = useState(false);
71
+
72
+ // Parse checkbox options
73
+ const options = checkboxOptions
74
+ .split("\n")
75
+ .filter((opt) => opt.trim())
76
+ .map((opt) => {
77
+ const [value, label] = opt.split("|");
78
+ return { value: value?.trim() || "", label: label?.trim() || value?.trim() || "" };
79
+ });
80
+
81
+ // Initialize selected values - only run once on mount
82
+ useEffect(() => {
83
+ const initialValues = getInitialValues();
84
+
85
+ console.log('AdvancedCheckbox initialization:', {
86
+ value,
87
+ defaultSelected,
88
+ initialValues,
89
+ required,
90
+ error
91
+ });
92
+
93
+ setFieldValue(initialValues);
94
+
95
+ // Validate the initial values
96
+ const validationResult = validateSelection(initialValues);
97
+ setValidationError(validationResult);
98
+
99
+ // Only trigger onChange if we have initial values and it's different from current value
100
+ // AND only if the value prop is not already set (to avoid overriding during publish)
101
+ // AND only if this is the first initialization
102
+ if (onChange && initialValues.length > 0 && (!value || (Array.isArray(value) && value.length === 0)) && !isInitialized) {
103
+ // Use setTimeout to ensure this happens after the component is fully mounted
104
+ setTimeout(() => {
105
+ onChange({
106
+ target: {
107
+ value: initialValues,
108
+ name: name,
109
+ id: name
110
+ }
111
+ });
112
+ setIsInitialized(true);
113
+ }, 0);
114
+ } else if (!isInitialized) {
115
+ setIsInitialized(true);
116
+ }
117
+ }, []); // Remove dependencies to only run once on mount
118
+
119
+ // Handle external value changes (like when loading existing data)
120
+ useEffect(() => {
121
+ // Only update if the value prop has changed and it's not empty
122
+ // This handles cases like loading existing content
123
+ if (value && Array.isArray(value) && value.length > 0) {
124
+ setFieldValue(value);
125
+ const validationResult = validateSelection(value);
126
+ setValidationError(validationResult);
127
+ }
128
+ }, [value]);
129
+
130
+ // Validation function - this should match server-side validation
131
+ const validateSelection = (val) => {
132
+ const values = Array.isArray(val) ? val : [];
133
+
134
+ // Check required validation first
135
+ if (required && values.length === 0) {
136
+ return customErrorMessage || 'This field is required';
137
+ }
138
+
139
+ // If field is empty and not required, no validation error
140
+ if (values.length === 0) {
141
+ return null;
142
+ }
143
+
144
+ // Check min/max choices validation (only for multiple mode)
145
+ if (checkboxType === "multiple") {
146
+ if (minChoices > 0 && values.length < minChoices) {
147
+ return customErrorMessage || `Please select at least ${minChoices} option${minChoices > 1 ? 's' : ''}`;
148
+ }
149
+ if (maxChoices > 0 && values.length > maxChoices) {
150
+ return customErrorMessage || `Please select at most ${maxChoices} option${maxChoices > 1 ? 's' : ''}`;
151
+ }
152
+ }
153
+
154
+ return null;
155
+ };
156
+
157
+ const handleCheckboxChange = (optionValue, isChecked) => {
158
+ let newValue;
159
+
160
+ if (checkboxType === "single") {
161
+ // For single checkbox mode, work like radio button - only one option can be selected
162
+ if (isChecked) {
163
+ // If checking an option, set it as the only selected value
164
+ newValue = [optionValue];
165
+ } else {
166
+ // If unchecking, clear the selection
167
+ newValue = [];
168
+ }
169
+ } else {
170
+ // For multiple checkboxes, handle array of values
171
+ if (isChecked) {
172
+ newValue = [...fieldValue, optionValue];
173
+ } else {
174
+ newValue = fieldValue.filter(val => val !== optionValue);
175
+ }
176
+ }
177
+
178
+ setFieldValue(newValue);
179
+ setHasInteracted(true);
180
+
181
+ // Validate selection only after user interaction
182
+ const error = validateSelection(newValue);
183
+ setValidationError(error);
184
+
185
+ console.log('AdvancedCheckbox handleCheckboxChange:', {
186
+ optionValue,
187
+ isChecked,
188
+ newValue,
189
+ error,
190
+ hasInteracted: true,
191
+ checkboxType,
192
+ isInitialized
193
+ });
194
+
195
+ // Always trigger onChange for user interactions
196
+ if (onChange) {
197
+ console.log(onChange)
198
+ onChange({
199
+ target: {
200
+ value: newValue,
201
+ name: name,
202
+ id: name
203
+ }
204
+ });
205
+ }
206
+ };
207
+
208
+ // Show validation error - prioritize Strapi's error, then our validation only after user interaction
209
+ const displayError = error || (hasInteracted && validationError);
210
+ console.log('displayError', displayError);
211
+ const renderCheckboxes = () => {
212
+ // Debug logging
213
+ // console.log('AdvancedCheckbox renderCheckboxes:', {
214
+ // checkboxType,
215
+ // checkboxOptions,
216
+ // options,
217
+ // attribute,
218
+ // attributeOptions: attribute.options,
219
+ // fieldValue
220
+ // });
221
+
222
+ const checkboxStyle = {
223
+ display: "flex",
224
+ alignItems: "center",
225
+ gap: "8px",
226
+ marginBottom: "8px",
227
+ };
228
+
229
+ const checkboxInputStyle = {
230
+ width: "16px",
231
+ height: "16px",
232
+ accentColor: "#4945ff",
233
+ margin: "0",
234
+ padding: "0",
235
+ opacity: "1",
236
+ visibility: "visible",
237
+ display: "block",
238
+ position: "relative",
239
+ zIndex: "1",
240
+ };
241
+
242
+ const checkboxLabelStyle = {
243
+ fontSize: "14px",
244
+ fontFamily: "inherit",
245
+ cursor: "pointer",
246
+ userSelect: "none",
247
+ };
248
+
249
+ // Single checkbox mode - show multiple options but only one selectable (like radio buttons)
250
+ if (checkboxType === "single") {
251
+ // If no options are defined for single mode, show a message
252
+ if (!options || options.length === 0) {
253
+ return (
254
+ <div style={{ padding: "8px", color: "#666", fontStyle: "italic" }}>
255
+ {formatMessage({
256
+ id: 'advanced-fields.checkbox.options.checkboxOptions.description',
257
+ defaultMessage: 'Define available options for multiple checkboxes (one per line: value|label)'
258
+ })}
259
+ </div>
260
+ );
261
+ }
262
+
263
+ if (layout === "horizontal") {
264
+ return (
265
+ <Flex wrap="wrap" gap={2}>
266
+ {options.map((option) => (
267
+ <div key={option.value} style={checkboxStyle}>
268
+ <input
269
+ type="checkbox"
270
+ id={`${name}-${option.value}`}
271
+ checked={fieldValue.includes(option.value)}
272
+ onChange={(e) => handleCheckboxChange(option.value, e.target.checked)}
273
+ disabled={disabled}
274
+ style={checkboxInputStyle}
275
+ />
276
+ <label
277
+ htmlFor={`${name}-${option.value}`}
278
+ style={checkboxLabelStyle}
279
+ >
280
+ {option.label}
281
+ </label>
282
+ </div>
283
+ ))}
284
+ </Flex>
285
+ );
286
+ }
287
+
288
+ return (
289
+ <div>
290
+ {options.map((option) => (
291
+ <div key={option.value} style={checkboxStyle}>
292
+ <input
293
+ type="checkbox"
294
+ id={`${name}-${option.value}`}
295
+ checked={fieldValue.includes(option.value)}
296
+ onChange={(e) => handleCheckboxChange(option.value, e.target.checked)}
297
+ disabled={disabled}
298
+ style={checkboxInputStyle}
299
+ />
300
+ <label
301
+ htmlFor={`${name}-${option.value}`}
302
+ style={checkboxLabelStyle}
303
+ >
304
+ {option.label}
305
+ </label>
306
+ </div>
307
+ ))}
308
+ </div>
309
+ );
310
+ }
311
+
312
+ // Multiple checkbox mode
313
+ // If no options are defined, show a message
314
+ if (!options || options.length === 0) {
315
+ return (
316
+ <div style={{ padding: "8px", color: "#666", fontStyle: "italic" }}>
317
+ {formatMessage({
318
+ id: 'advanced-fields.checkbox.options.checkboxOptions.description',
319
+ defaultMessage: 'Define available options for multiple checkboxes (one per line: value|label)'
320
+ })}
321
+ </div>
322
+ );
323
+ }
324
+
325
+ if (layout === "horizontal") {
326
+ return (
327
+ <Flex wrap="wrap" gap={2}>
328
+ {options.map((option) => (
329
+ <div key={option.value} style={checkboxStyle}>
330
+ <input
331
+ type="checkbox"
332
+ id={`${name}-${option.value}`}
333
+ checked={fieldValue.includes(option.value)}
334
+ onChange={(e) => handleCheckboxChange(option.value, e.target.checked)}
335
+ disabled={disabled}
336
+ style={checkboxInputStyle}
337
+ />
338
+ <label
339
+ htmlFor={`${name}-${option.value}`}
340
+ style={checkboxLabelStyle}
341
+ >
342
+ {option.label}
343
+ </label>
344
+ </div>
345
+ ))}
346
+ </Flex>
347
+ );
348
+ }
349
+
350
+ return (
351
+ <div>
352
+ {options.map((option) => (
353
+ <div key={option.value} style={checkboxStyle}>
354
+ <input
355
+ type="checkbox"
356
+ id={`${name}-${option.value}`}
357
+ checked={fieldValue.includes(option.value)}
358
+ onChange={(e) => handleCheckboxChange(option.value, e.target.checked)}
359
+ disabled={disabled}
360
+ style={checkboxInputStyle}
361
+ />
362
+ <label
363
+ htmlFor={`${name}-${option.value}`}
364
+ style={checkboxLabelStyle}
365
+ >
366
+ {option.label}
367
+ </label>
368
+ </div>
369
+ ))}
370
+ </div>
371
+ );
372
+ };
373
+
374
+ return (
375
+ <Box col={6}>
376
+ <Field.Root name={name} error={displayError}>
377
+ <Field.Label>
378
+ {intlLabel.id ? formatMessage(intlLabel) : intlLabel.defaultMessage || name}
379
+ {required && <span style={{ color: "#d02b20", marginLeft: "4px" }}>*</span>}
380
+ </Field.Label>
381
+ {renderCheckboxes()}
382
+ {displayError && (
383
+ <Field.Error>
384
+ {displayError}
385
+ </Field.Error>
386
+ )}
387
+ {description && (description.id || description.defaultMessage) && (
388
+ <Field.Hint>
389
+ {description.id ? formatMessage(description) : description.defaultMessage}
390
+ </Field.Hint>
391
+ )}
392
+ </Field.Root>
393
+ </Box>
394
+ );
395
+ };
396
+
397
+ export default CheckboxInput;
@@ -0,0 +1,163 @@
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 TextInput = ({
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
+ } = options;
42
+
43
+ // Initialize input value
44
+ useEffect(() => {
45
+ const initialValue = value === undefined ? defaultValue : value;
46
+ setInputValue(initialValue);
47
+
48
+ // Don't validate on initial load unless there's an error from Strapi
49
+ if (error) {
50
+ setValidationError(error);
51
+ }
52
+
53
+ if (onChange) {
54
+ onChange({ target: { value: initialValue } });
55
+ }
56
+ }, [value, defaultValue, onChange, error]);
57
+
58
+ // Validation function - this should match server-side validation
59
+ const validateInput = (val) => {
60
+ // Check required validation first
61
+ if (required && (!val || val.toString().trim().length === 0)) {
62
+ return customErrorMessage || "This field is required";
63
+ }
64
+
65
+ // If no value, no additional validation needed
66
+ if (!val || val.toString().trim().length === 0) {
67
+ return null;
68
+ }
69
+
70
+ const stringValue = val.toString().trim();
71
+
72
+ // Check min/max length validation
73
+ if (minLength > 0 && stringValue.length < minLength) {
74
+ return customErrorMessage || `Minimum length is ${minLength} characters`;
75
+ }
76
+ if (maxLength > 0 && stringValue.length > maxLength) {
77
+ return customErrorMessage || `Maximum length is ${maxLength} characters`;
78
+ }
79
+
80
+ // Check regex validation if provided
81
+ if (regex && stringValue) {
82
+ try {
83
+ const regexPattern = new RegExp(regex);
84
+ if (!regexPattern.test(stringValue)) {
85
+ return customErrorMessage || "Invalid format";
86
+ }
87
+ } catch (e) {
88
+ // Invalid regex pattern, skip validation
89
+ }
90
+ }
91
+
92
+ return null;
93
+ };
94
+
95
+ const handleChange = (e) => {
96
+ const newValue = e.target.value;
97
+ setInputValue(newValue);
98
+ setHasInteracted(true);
99
+
100
+ // Validate input only after user interaction
101
+ const error = validateInput(newValue);
102
+ setValidationError(error);
103
+
104
+ if (onChange) {
105
+ onChange(e);
106
+ }
107
+ };
108
+
109
+ // Show validation error - prioritize Strapi's error, then our validation only after user interaction
110
+ const displayError = error || (hasInteracted && validationError);
111
+
112
+ const renderInput = () => {
113
+ const commonProps = {
114
+ name,
115
+ value: inputValue,
116
+ onChange: handleChange,
117
+ disabled,
118
+ placeholder: placeholder || (intlLabel.id ? formatMessage(intlLabel) : intlLabel.defaultMessage || ""),
119
+ };
120
+
121
+ const inputStyle = {
122
+ width: "100%",
123
+ padding: "8px 12px",
124
+ border: `1px solid ${displayError ? "#d02b20" : "#dcdce4"}`,
125
+ borderRadius: "4px",
126
+ fontSize: "14px",
127
+ fontFamily: "inherit",
128
+ backgroundColor: disabled ? "#f6f6f9" : "#ffffff",
129
+ };
130
+
131
+ return (
132
+ <input
133
+ {...commonProps}
134
+ type="text"
135
+ style={inputStyle}
136
+ />
137
+ );
138
+ };
139
+
140
+ return (
141
+ <Box col={6}>
142
+ <Field.Root name={name} error={displayError}>
143
+ <Field.Label>
144
+ {intlLabel.id ? formatMessage(intlLabel) : intlLabel.defaultMessage || name}
145
+ {required && <span style={{ color: "#d02b20", marginLeft: "4px" }}>*</span>}
146
+ </Field.Label>
147
+ {renderInput()}
148
+ {displayError && (
149
+ <Field.Error>
150
+ {displayError}
151
+ </Field.Error>
152
+ )}
153
+ {description && (description.id || description.defaultMessage) && (
154
+ <Field.Hint>
155
+ {description.id ? formatMessage(description) : description.defaultMessage}
156
+ </Field.Hint>
157
+ )}
158
+ </Field.Root>
159
+ </Box>
160
+ );
161
+ };
162
+
163
+ export default TextInput;