@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,344 @@
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 RadioInput = ({
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
+ const [selectedValues, setSelectedValues] = useState([]);
21
+ const [validationError, setValidationError] = useState(null);
22
+ const [hasInteracted, setHasInteracted] = useState(false);
23
+ const [isInitialized, setIsInitialized] = useState(false);
24
+
25
+ const {
26
+ selectionType = "single",
27
+ layout = "vertical",
28
+ minChoices = 0,
29
+ maxChoices = 0,
30
+ defaultSelected = "",
31
+ radioOptions = "",
32
+ customErrorMessage = "",
33
+ } = attribute.options || attribute;
34
+
35
+ // Parse radio options
36
+ const options = radioOptions
37
+ .split("\n")
38
+ .filter((opt) => opt.trim())
39
+ .map((opt) => {
40
+ const [value, label] = opt.split("|");
41
+ return { value: value?.trim() || "", label: label?.trim() || value?.trim() || "" };
42
+ });
43
+
44
+ // Initialize selected values
45
+ useEffect(() => {
46
+ let initialValues = [];
47
+
48
+ if (value && Array.isArray(value)) {
49
+ initialValues = value;
50
+ } else if (value && typeof value === "string") {
51
+ initialValues = value.split(",").map(v => v.trim()).filter(v => v);
52
+ } else if (defaultSelected) {
53
+ initialValues = defaultSelected.split(",").map(v => v.trim()).filter(v => v);
54
+ }
55
+
56
+ setSelectedValues(initialValues);
57
+
58
+ // Validate the initial values
59
+ const validationResult = validateSelection(initialValues);
60
+ setValidationError(validationResult);
61
+
62
+ // Only trigger onChange if we have initial values and it's different from current value
63
+ // AND only if the value prop is not already set (to avoid overriding during publish)
64
+ // AND only if this is the first initialization
65
+ if (onChange && initialValues.length > 0 && (!value || (Array.isArray(value) && value.length === 0)) && !isInitialized) {
66
+ // Use setTimeout to ensure this happens after the component is fully mounted
67
+ setTimeout(() => {
68
+ onChange({
69
+ target: {
70
+ value: initialValues,
71
+ name: name,
72
+ id: name
73
+ }
74
+ });
75
+ setIsInitialized(true);
76
+ }, 0);
77
+ } else if (!isInitialized) {
78
+ setIsInitialized(true);
79
+ }
80
+ }, [value, defaultSelected, onChange, error]);
81
+
82
+ // Validation function - this should match server-side validation
83
+ const validateSelection = (values) => {
84
+ const valArray = Array.isArray(values) ? values : [];
85
+
86
+ // Check required validation first
87
+ if (required && valArray.length === 0) {
88
+ return customErrorMessage || 'This field is required';
89
+ }
90
+
91
+ // If field is empty and not required, no validation error
92
+ if (valArray.length === 0) {
93
+ return null;
94
+ }
95
+
96
+ // Check min/max choices validation (only for multiple mode)
97
+ if (selectionType === "multiple") {
98
+ if (minChoices > 0 && valArray.length < minChoices) {
99
+ return customErrorMessage || `Please select at least ${minChoices} option${minChoices > 1 ? 's' : ''}`;
100
+ }
101
+ if (maxChoices > 0 && valArray.length > maxChoices) {
102
+ return customErrorMessage || `Please select at most ${maxChoices} option${maxChoices > 1 ? 's' : ''}`;
103
+ }
104
+ }
105
+
106
+ return null;
107
+ };
108
+
109
+ const handleRadioChange = (optionValue, isChecked) => {
110
+ let newValues;
111
+
112
+ if (selectionType === "single") {
113
+ // Single selection: replace current selection
114
+ newValues = isChecked ? [optionValue] : [];
115
+ } else {
116
+ // Multiple selection: add/remove from array
117
+ if (isChecked) {
118
+ newValues = [...selectedValues, optionValue];
119
+ } else {
120
+ newValues = selectedValues.filter(val => val !== optionValue);
121
+ }
122
+ }
123
+
124
+ setSelectedValues(newValues);
125
+ setHasInteracted(true);
126
+
127
+ // Validate selection only after user interaction
128
+ const error = validateSelection(newValues);
129
+ setValidationError(error);
130
+
131
+ // console.log('AdvancedRadio handleRadioChange:', {
132
+ // optionValue,
133
+ // isChecked,
134
+ // newValues,
135
+ // error,
136
+ // hasInteracted: true
137
+ // });
138
+
139
+ if (onChange) {
140
+ // Create a proper event object with name and id attributes
141
+ const event = {
142
+ target: {
143
+ name: name,
144
+ id: name,
145
+ value: newValues
146
+ }
147
+ };
148
+ onChange(event);
149
+ }
150
+ };
151
+
152
+ // Show validation error - prioritize Strapi's error, then our validation only after user interaction
153
+ const displayError = error || (hasInteracted && validationError);
154
+
155
+ const renderRadios = () => {
156
+ // Debug logging
157
+ // console.log('AdvancedRadio renderRadios:', {
158
+ // radioOptions,
159
+ // options,
160
+ // attribute,
161
+ // attributeOptions: attribute.options,
162
+ // selectedValues
163
+ // });
164
+
165
+ const radioStyle = {
166
+ display: "flex",
167
+ alignItems: "center",
168
+ gap: "8px",
169
+ padding: "4px 0",
170
+ };
171
+
172
+ const radioInputStyle = {
173
+ width: "16px",
174
+ height: "16px",
175
+ accentColor: "#4945ff",
176
+ margin: "0",
177
+ padding: "0",
178
+ opacity: "1",
179
+ visibility: "visible",
180
+ display: "block",
181
+ position: "relative",
182
+ zIndex: "1",
183
+ cursor: "pointer",
184
+ border: "2px solid #dcdce4",
185
+ borderRadius: "50%",
186
+ backgroundColor: "white",
187
+ transition: "all 0.2s ease",
188
+ };
189
+
190
+ // Custom radio button style for multiple selection
191
+ const customRadioStyle = {
192
+ width: "16px",
193
+ height: "16px",
194
+ borderRadius: "50%",
195
+ borderWidth: "1px",
196
+ borderStyle: "solid",
197
+ backgroundColor: "#ffffff",
198
+ cursor: "pointer",
199
+ transition: "all 0.2s ease",
200
+ position: "relative",
201
+ display: "flex",
202
+ alignItems: "center",
203
+ justifyContent: "center",
204
+ };
205
+
206
+ const customRadioCheckedStyle = {
207
+ ...customRadioStyle,
208
+ backgroundColor: "#ffffff",
209
+ borderColor: "#4945ff",
210
+ };
211
+
212
+ const customRadioDotStyle = {
213
+ width: "10px",
214
+ height: "10px",
215
+ borderRadius: "50%",
216
+ backgroundColor: "#4945ff",
217
+ };
218
+
219
+ const radioLabelStyle = {
220
+ fontSize: "14px",
221
+ fontFamily: "inherit",
222
+ cursor: "pointer",
223
+ userSelect: "none",
224
+ color: "#32324d",
225
+ fontWeight: "400",
226
+ lineHeight: "1.5",
227
+ marginLeft: "4px",
228
+ };
229
+
230
+ // If no options are defined, show a message
231
+ if (!options || options.length === 0) {
232
+ return (
233
+ <div style={{ padding: "8px", color: "#666", fontStyle: "italic" }}>
234
+ {formatMessage({
235
+ id: 'advanced-fields.radio.no-options',
236
+ defaultMessage: 'No options defined. Please configure this field in the content type settings.'
237
+ })}
238
+ </div>
239
+ );
240
+ }
241
+
242
+ if (layout === "horizontal") {
243
+ return (
244
+ <Flex wrap="wrap" gap={2}>
245
+ {options.map((option) => (
246
+ <div key={option.value} style={radioStyle}>
247
+ {selectionType === "multiple" ? (
248
+ // Custom radio button appearance for multiple selection
249
+ <div
250
+ style={selectedValues.includes(option.value) ? customRadioCheckedStyle : customRadioStyle}
251
+ onClick={() => handleRadioChange(option.value, !selectedValues.includes(option.value))}
252
+ >
253
+ {selectedValues.includes(option.value) && (
254
+ <div style={customRadioDotStyle} />
255
+ )}
256
+ </div>
257
+ ) : (
258
+ // Regular radio button for single selection
259
+ <input
260
+ type="radio"
261
+ id={`${name}-${option.value}`}
262
+ name={name}
263
+ checked={selectedValues.includes(option.value)}
264
+ onChange={(e) => handleRadioChange(option.value, e.target.checked)}
265
+ disabled={disabled}
266
+ style={radioInputStyle}
267
+ />
268
+ )}
269
+ <label
270
+ htmlFor={selectionType === "single" ? `${name}-${option.value}` : undefined}
271
+ style={radioLabelStyle}
272
+ onClick={selectionType === "multiple" ? () => handleRadioChange(option.value, !selectedValues.includes(option.value)) : undefined}
273
+ >
274
+ {option.label}
275
+ </label>
276
+ </div>
277
+ ))}
278
+ </Flex>
279
+ );
280
+ }
281
+
282
+ return (
283
+ <div>
284
+ {options.map((option) => (
285
+ <div key={option.value} style={radioStyle}>
286
+ {selectionType === "multiple" ? (
287
+ // Custom radio button appearance for multiple selection
288
+ <div
289
+ style={selectedValues.includes(option.value) ? customRadioCheckedStyle : customRadioStyle}
290
+ onClick={() => handleRadioChange(option.value, !selectedValues.includes(option.value))}
291
+ >
292
+ {selectedValues.includes(option.value) && (
293
+ <div style={customRadioDotStyle} />
294
+ )}
295
+ </div>
296
+ ) : (
297
+ // Regular radio button for single selection
298
+ <input
299
+ type="radio"
300
+ id={`${name}-${option.value}`}
301
+ name={name}
302
+ checked={selectedValues.includes(option.value)}
303
+ onChange={(e) => handleRadioChange(option.value, e.target.checked)}
304
+ disabled={disabled}
305
+ style={radioInputStyle}
306
+ />
307
+ )}
308
+ <label
309
+ htmlFor={selectionType === "single" ? `${name}-${option.value}` : undefined}
310
+ style={radioLabelStyle}
311
+ onClick={selectionType === "multiple" ? () => handleRadioChange(option.value, !selectedValues.includes(option.value)) : undefined}
312
+ >
313
+ {option.label}
314
+ </label>
315
+ </div>
316
+ ))}
317
+ </div>
318
+ );
319
+ };
320
+
321
+ return (
322
+ <Box col={6}>
323
+ <Field.Root name={name} error={displayError}>
324
+ <Field.Label>
325
+ {intlLabel.id ? formatMessage(intlLabel) : intlLabel.defaultMessage || name}
326
+ {required && <span style={{ color: "#d02b20", marginLeft: "4px" }}>*</span>}
327
+ </Field.Label>
328
+ {renderRadios()}
329
+ {displayError && (
330
+ <Field.Error>
331
+ {displayError}
332
+ </Field.Error>
333
+ )}
334
+ {description && (description.id || description.defaultMessage) && (
335
+ <Field.Hint>
336
+ {description.id ? formatMessage(description) : description.defaultMessage}
337
+ </Field.Hint>
338
+ )}
339
+ </Field.Root>
340
+ </Box>
341
+ );
342
+ };
343
+
344
+ export default RadioInput;
@@ -0,0 +1,18 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import { PLUGIN_ID } from '../pluginId';
4
+
5
+ /**
6
+ * @type {import('react').FC<{ setPlugin: (id: string) => void }>}
7
+ */
8
+ const Initializer = ({ setPlugin }) => {
9
+ const ref = useRef(setPlugin);
10
+
11
+ useEffect(() => {
12
+ ref.current(PLUGIN_ID);
13
+ }, []);
14
+
15
+ return null;
16
+ };
17
+
18
+ export { Initializer };
@@ -0,0 +1,115 @@
1
+ import { PuzzlePiece } from '@strapi/icons';
2
+ import { Box } from '@strapi/design-system';
3
+
4
+ const PluginIcon = (props) => {
5
+ return <PuzzlePiece {...props} />;
6
+ };
7
+
8
+ // Advanced Input Icon - Clean text input with validation indicator
9
+ const TextFieldIcon = () => (
10
+ <Box
11
+ style={{
12
+ width: '28px',
13
+ height: '28px',
14
+ display: 'flex',
15
+ alignItems: 'center',
16
+ justifyContent: 'center',
17
+ backgroundColor: '#f8f9fa',
18
+ borderRadius: '6px',
19
+ border: '1px solid #e9ecef',
20
+ }}
21
+ >
22
+ <svg
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ viewBox="0 0 24 24"
25
+ width="18"
26
+ height="18"
27
+ fill="none"
28
+ stroke="#495057"
29
+ strokeWidth="2"
30
+ strokeLinecap="round"
31
+ strokeLinejoin="round"
32
+ >
33
+ <rect x="3" y="4" width="18" height="16" rx="2" ry="2"/>
34
+ <line x1="7" y1="8" x2="17" y2="8"/>
35
+ <line x1="7" y1="12" x2="13" y2="12"/>
36
+ <circle cx="18" cy="6" r="2" fill="#28a745"/>
37
+ <path d="m16 5 2 2-2 2" stroke="#fff" strokeWidth="1.5"/>
38
+ </svg>
39
+ </Box>
40
+ );
41
+
42
+ // Advanced Checkbox Icon - Modern checkbox with multiple states
43
+ const CheckIcon = () => (
44
+ <Box
45
+ style={{
46
+ width: '28px',
47
+ height: '28px',
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ backgroundColor: '#f8f9fa',
52
+ borderRadius: '6px',
53
+ border: '1px solid #e9ecef',
54
+ }}
55
+ >
56
+ <svg
57
+ xmlns="http://www.w3.org/2000/svg"
58
+ viewBox="0 0 24 24"
59
+ width="18"
60
+ height="18"
61
+ fill="none"
62
+ stroke="#495057"
63
+ strokeWidth="2"
64
+ strokeLinecap="round"
65
+ strokeLinejoin="round"
66
+ >
67
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
68
+ <polyline points="9,12 12,15 22,5" stroke="#28a745" strokeWidth="2.5"/>
69
+ <rect x="3" y="8" width="12" height="2" rx="1" fill="#6c757d" opacity="0.3"/>
70
+ <rect x="3" y="12" width="8" height="2" rx="1" fill="#6c757d" opacity="0.3"/>
71
+ </svg>
72
+ </Box>
73
+ );
74
+
75
+ // Advanced Radio Icon - Clean radio button with selection states
76
+ const MultipleChoiceIcon1 = () => (
77
+ <Box
78
+ style={{
79
+ width: '28px',
80
+ height: '28px',
81
+ display: 'flex',
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ backgroundColor: '#f8f9fa',
85
+ borderRadius: '6px',
86
+ border: '1px solid #e9ecef',
87
+ }}
88
+ >
89
+ <svg
90
+ xmlns="http://www.w3.org/2000/svg"
91
+ viewBox="0 0 24 24"
92
+ width="18"
93
+ height="18"
94
+ fill="none"
95
+ stroke="#495057"
96
+ strokeWidth="2"
97
+ strokeLinecap="round"
98
+ strokeLinejoin="round"
99
+ >
100
+ <circle cx="12" cy="12" r="10"/>
101
+ <circle cx="12" cy="12" r="3" fill="#007bff"/>
102
+ <circle cx="12" cy="5" r="2" fill="#6c757d" opacity="0.3"/>
103
+ <circle cx="12" cy="19" r="2" fill="#6c757d" opacity="0.3"/>
104
+ <circle cx="5" cy="12" r="2" fill="#6c757d" opacity="0.3"/>
105
+ <circle cx="19" cy="12" r="2" fill="#6c757d" opacity="0.3"/>
106
+ </svg>
107
+ </Box>
108
+ );
109
+
110
+ export {
111
+ PluginIcon,
112
+ CheckIcon,
113
+ TextFieldIcon,
114
+ MultipleChoiceIcon1
115
+ };