@devcustrom/strapi-plugin-api-select 1.1.2 → 1.3.0
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 +1 -1
- package/admin/src/plugin/ApiSelectInput.jsx +275 -171
- package/admin/src/plugin/test.jsx +817 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import * as PropTypes from "prop-types";
|
|
1
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
3
2
|
import {
|
|
4
3
|
Field,
|
|
5
4
|
SingleSelect,
|
|
@@ -9,58 +8,101 @@ import {
|
|
|
9
8
|
Loader,
|
|
10
9
|
Box,
|
|
11
10
|
Typography,
|
|
12
|
-
Flex
|
|
11
|
+
Flex,
|
|
13
12
|
} from "@strapi/design-system";
|
|
14
13
|
|
|
15
|
-
//
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Safely resolves a dot-separated path inside an object.
|
|
20
|
+
* Handles null/undefined at any level without throwing.
|
|
21
|
+
*/
|
|
16
22
|
const getNestedValue = (obj, path) => {
|
|
17
23
|
if (!path) return obj;
|
|
18
|
-
return path.split(
|
|
19
|
-
|
|
24
|
+
return path.split(".").reduce((current, key) => {
|
|
25
|
+
if (current === null || current === undefined) return null;
|
|
26
|
+
return current[key] !== undefined ? current[key] : null;
|
|
20
27
|
}, obj);
|
|
21
28
|
};
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Guarantees that a value is a plain string primitive.
|
|
32
|
+
* Objects are JSON-stringified; everything else is cast with String().
|
|
33
|
+
*/
|
|
34
|
+
const toStringValue = (val) => {
|
|
35
|
+
if (val === null || val === undefined) return "";
|
|
36
|
+
if (typeof val === "object") return JSON.stringify(val);
|
|
37
|
+
return String(val);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Maps a raw API item to { label, value, item, ...extraKeys }.
|
|
42
|
+
* All returned values are guaranteed strings.
|
|
43
|
+
*/
|
|
23
44
|
const mapResponseData = (item, mapping, fallbackLabelKey, fallbackValueKey) => {
|
|
24
|
-
let mapped = { label:
|
|
45
|
+
let mapped = { label: "", value: "", item };
|
|
25
46
|
|
|
26
|
-
if (mapping && typeof mapping ===
|
|
47
|
+
if (mapping && typeof mapping === "object") {
|
|
27
48
|
mapped.label =
|
|
28
49
|
getNestedValue(item, mapping.label || mapping.text || mapping.name) ||
|
|
29
50
|
getNestedValue(item, fallbackLabelKey) ||
|
|
30
|
-
item.label ||
|
|
51
|
+
item.label ||
|
|
52
|
+
item.name ||
|
|
53
|
+
item.text ||
|
|
54
|
+
"Unknown";
|
|
31
55
|
|
|
32
56
|
mapped.value =
|
|
33
57
|
getNestedValue(item, mapping.value || mapping.id) ||
|
|
34
58
|
getNestedValue(item, fallbackValueKey) ||
|
|
35
|
-
item.value ||
|
|
59
|
+
item.value ||
|
|
60
|
+
item.id ||
|
|
61
|
+
mapped.label;
|
|
36
62
|
|
|
37
|
-
Object.keys(mapping).forEach(key => {
|
|
38
|
-
if (key !==
|
|
63
|
+
Object.keys(mapping).forEach((key) => {
|
|
64
|
+
if (key !== "label" && key !== "value") {
|
|
39
65
|
mapped[key] = getNestedValue(item, mapping[key]);
|
|
40
66
|
}
|
|
41
67
|
});
|
|
42
68
|
} else {
|
|
43
69
|
mapped.label =
|
|
44
70
|
getNestedValue(item, fallbackLabelKey) ||
|
|
45
|
-
item.label ||
|
|
46
|
-
item.
|
|
71
|
+
item.label ||
|
|
72
|
+
item.name ||
|
|
73
|
+
item.text ||
|
|
74
|
+
item.title ||
|
|
75
|
+
item.displayName ||
|
|
76
|
+
item.description ||
|
|
77
|
+
"Unknown Option";
|
|
47
78
|
|
|
48
79
|
mapped.value =
|
|
49
80
|
getNestedValue(item, fallbackValueKey) ||
|
|
50
|
-
item.value ||
|
|
81
|
+
item.value ||
|
|
82
|
+
item.id ||
|
|
83
|
+
item.identifier ||
|
|
84
|
+
item.uuid ||
|
|
85
|
+
mapped.label;
|
|
51
86
|
}
|
|
52
87
|
|
|
53
|
-
if (!mapped.label || typeof mapped.label !==
|
|
54
|
-
mapped.label = `Option ${mapped.value ||
|
|
88
|
+
if (!mapped.label || typeof mapped.label !== "string") {
|
|
89
|
+
mapped.label = `Option ${mapped.value || "Unknown"}`;
|
|
55
90
|
}
|
|
56
91
|
|
|
57
92
|
if (!mapped.value) {
|
|
58
93
|
mapped.value = mapped.label || `option_${Date.now()}`;
|
|
59
94
|
}
|
|
60
95
|
|
|
96
|
+
// FIX #4: guarantee value is always a string primitive, never an object
|
|
97
|
+
mapped.value = toStringValue(mapped.value);
|
|
98
|
+
|
|
61
99
|
return mapped;
|
|
62
100
|
};
|
|
63
101
|
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Component
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
64
106
|
const ApiSelectInput = ({
|
|
65
107
|
name,
|
|
66
108
|
value,
|
|
@@ -80,38 +122,46 @@ const ApiSelectInput = ({
|
|
|
80
122
|
const [loading, setLoading] = useState(false);
|
|
81
123
|
const [fetchError, setFetchError] = useState(null);
|
|
82
124
|
|
|
83
|
-
|
|
84
|
-
|
|
125
|
+
// FIX #1: memoize configValues so it doesn't recreate on every render
|
|
126
|
+
// and trigger an infinite useEffect loop.
|
|
127
|
+
const configValues = useMemo(() => {
|
|
128
|
+
const config = attribute?.options || {};
|
|
129
|
+
return config.options || config;
|
|
130
|
+
}, [attribute]);
|
|
85
131
|
|
|
86
132
|
const {
|
|
87
133
|
optionsApi,
|
|
88
|
-
optionLabelKey =
|
|
89
|
-
optionValueKey =
|
|
90
|
-
selectMode =
|
|
91
|
-
authMode =
|
|
134
|
+
optionLabelKey = "name",
|
|
135
|
+
optionValueKey = "id",
|
|
136
|
+
selectMode = "single",
|
|
137
|
+
authMode = "public",
|
|
92
138
|
placeholder: configPlaceholder,
|
|
93
|
-
httpMethod =
|
|
94
|
-
requestPayload =
|
|
95
|
-
customHeaders =
|
|
96
|
-
responseDataPath =
|
|
97
|
-
responseMappingConfig =
|
|
139
|
+
httpMethod = "GET",
|
|
140
|
+
requestPayload = "",
|
|
141
|
+
customHeaders = "",
|
|
142
|
+
responseDataPath = "",
|
|
143
|
+
responseMappingConfig = "",
|
|
98
144
|
} = configValues;
|
|
99
145
|
|
|
100
146
|
const getAuthToken = () => {
|
|
101
147
|
try {
|
|
102
148
|
return (
|
|
103
|
-
localStorage.getItem(
|
|
104
|
-
localStorage.getItem(
|
|
105
|
-
localStorage.getItem(
|
|
149
|
+
localStorage.getItem("jwtToken")?.replaceAll('"', "") ||
|
|
150
|
+
localStorage.getItem("strapi_jwt")?.replaceAll('"', "") ||
|
|
151
|
+
localStorage.getItem("token")?.replaceAll('"', "")
|
|
106
152
|
);
|
|
107
|
-
} catch
|
|
153
|
+
} catch {
|
|
108
154
|
return null;
|
|
109
155
|
}
|
|
110
156
|
};
|
|
111
157
|
|
|
158
|
+
// FIX #2: AbortController to prevent race conditions when params change
|
|
159
|
+
// while a previous fetch is still in flight.
|
|
112
160
|
useEffect(() => {
|
|
113
161
|
if (!optionsApi) return;
|
|
114
162
|
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
|
|
115
165
|
const fetchOptions = async () => {
|
|
116
166
|
setLoading(true);
|
|
117
167
|
setFetchError(null);
|
|
@@ -122,35 +172,41 @@ const ApiSelectInput = ({
|
|
|
122
172
|
|
|
123
173
|
const fetchOpts = {
|
|
124
174
|
method: httpMethod,
|
|
125
|
-
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
credentials: authMode === "proxy" ? "include" : "omit",
|
|
126
177
|
headers: {
|
|
127
|
-
|
|
178
|
+
"Content-Type": "application/json",
|
|
128
179
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
129
180
|
},
|
|
130
181
|
};
|
|
131
182
|
|
|
183
|
+
// FIX #6: log invalid customHeaders JSON instead of silently swallowing
|
|
132
184
|
if (customHeaders?.trim()) {
|
|
133
185
|
try {
|
|
134
186
|
Object.assign(fetchOpts.headers, JSON.parse(customHeaders));
|
|
135
|
-
} catch (e) {
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.warn("[ApiSelect] Invalid customHeaders JSON:", e.message);
|
|
189
|
+
}
|
|
136
190
|
}
|
|
137
191
|
|
|
138
|
-
if (httpMethod ===
|
|
192
|
+
if (httpMethod === "POST" && requestPayload?.trim()) {
|
|
139
193
|
fetchOpts.body = requestPayload;
|
|
140
194
|
}
|
|
141
195
|
|
|
142
|
-
if (authMode ===
|
|
196
|
+
if (authMode === "proxy") {
|
|
143
197
|
const params = new URLSearchParams({
|
|
144
198
|
api: optionsApi,
|
|
145
199
|
labelKey: optionLabelKey,
|
|
146
200
|
valueKey: optionValueKey,
|
|
147
201
|
method: httpMethod,
|
|
148
202
|
});
|
|
149
|
-
if (requestPayload) params.append(
|
|
150
|
-
if (customHeaders) params.append(
|
|
203
|
+
if (requestPayload) params.append("payload", requestPayload);
|
|
204
|
+
if (customHeaders) params.append("headers", customHeaders);
|
|
205
|
+
// FIX #8: proxy mode was not forwarding responseDataPath
|
|
206
|
+
if (responseDataPath) params.append("dataPath", responseDataPath);
|
|
151
207
|
|
|
152
208
|
url = `${window.strapi.backendURL}/api-select/fetch?${params}`;
|
|
153
|
-
fetchOpts.method =
|
|
209
|
+
fetchOpts.method = "GET";
|
|
154
210
|
delete fetchOpts.body;
|
|
155
211
|
}
|
|
156
212
|
|
|
@@ -163,7 +219,8 @@ const ApiSelectInput = ({
|
|
|
163
219
|
if (responseDataPath) {
|
|
164
220
|
itemsArray = getNestedValue(data, responseDataPath);
|
|
165
221
|
} else {
|
|
166
|
-
itemsArray =
|
|
222
|
+
itemsArray =
|
|
223
|
+
data.data || data.results || data.items || data.response || data;
|
|
167
224
|
}
|
|
168
225
|
|
|
169
226
|
if (!Array.isArray(itemsArray)) {
|
|
@@ -171,28 +228,42 @@ const ApiSelectInput = ({
|
|
|
171
228
|
return;
|
|
172
229
|
}
|
|
173
230
|
|
|
231
|
+
// FIX #7: log invalid responseMappingConfig JSON
|
|
174
232
|
let customMapping = null;
|
|
175
233
|
if (responseMappingConfig?.trim()) {
|
|
176
234
|
try {
|
|
177
235
|
customMapping = JSON.parse(responseMappingConfig);
|
|
178
|
-
} catch (e) {
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.warn(
|
|
238
|
+
"[ApiSelect] Invalid responseMappingConfig JSON:",
|
|
239
|
+
e.message
|
|
240
|
+
);
|
|
241
|
+
}
|
|
179
242
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const mappedOptions = itemsArray.map(item =>
|
|
243
|
+
|
|
244
|
+
// FIX #10: removed console.log(itemsArray) leftover
|
|
245
|
+
const mappedOptions = itemsArray.map((item) =>
|
|
183
246
|
mapResponseData(item, customMapping, optionLabelKey, optionValueKey)
|
|
184
247
|
);
|
|
185
248
|
|
|
186
249
|
setOptions(mappedOptions);
|
|
187
250
|
} catch (err) {
|
|
251
|
+
// FIX #2: ignore abort errors — they are intentional
|
|
252
|
+
if (err.name === "AbortError") return;
|
|
188
253
|
setFetchError(err.message);
|
|
189
254
|
setOptions([]);
|
|
190
255
|
} finally {
|
|
191
|
-
|
|
256
|
+
// Guard: don't update state if the effect was cleaned up
|
|
257
|
+
if (!controller.signal.aborted) {
|
|
258
|
+
setLoading(false);
|
|
259
|
+
}
|
|
192
260
|
}
|
|
193
261
|
};
|
|
194
262
|
|
|
195
263
|
fetchOptions();
|
|
264
|
+
|
|
265
|
+
// FIX #2: cleanup — abort in-flight request when deps change or unmount
|
|
266
|
+
return () => controller.abort();
|
|
196
267
|
}, [
|
|
197
268
|
optionsApi,
|
|
198
269
|
optionLabelKey,
|
|
@@ -205,139 +276,190 @@ const ApiSelectInput = ({
|
|
|
205
276
|
responseMappingConfig,
|
|
206
277
|
]);
|
|
207
278
|
|
|
208
|
-
|
|
209
|
-
|
|
279
|
+
// -------------------------------------------------------------------------
|
|
280
|
+
// Change handlers
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
// The custom field is registered with type: "json", so Strapi requires a
|
|
284
|
+
// valid JSON value — an object or array, never a bare string.
|
|
285
|
+
//
|
|
286
|
+
// Single mode → store { value: "selected-string" }
|
|
287
|
+
// Multi mode → store ["val1", "val2"] (array is valid JSON)
|
|
288
|
+
// Empty/clear → store null
|
|
289
|
+
|
|
290
|
+
const handleSingleChange = (val) => {
|
|
291
|
+
onChange({
|
|
292
|
+
target: {
|
|
293
|
+
name,
|
|
294
|
+
value: val ? { value: val } : null,
|
|
295
|
+
type: "json",
|
|
296
|
+
},
|
|
297
|
+
});
|
|
210
298
|
};
|
|
211
299
|
|
|
212
|
-
const
|
|
213
|
-
onChange({ target: { name, value:
|
|
300
|
+
const handleSingleClear = () => {
|
|
301
|
+
onChange({ target: { name, value: null, type: "json" } });
|
|
214
302
|
};
|
|
215
303
|
|
|
216
|
-
const
|
|
304
|
+
const handleMultiChange = (values) => {
|
|
305
|
+
onChange({
|
|
306
|
+
target: {
|
|
307
|
+
name,
|
|
308
|
+
value: values?.length ? values : null,
|
|
309
|
+
type: "json",
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
};
|
|
217
313
|
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (fetchError) return 'Error loading options';
|
|
221
|
-
return placeholder || configPlaceholder || (selectMode === 'single' ? 'Select option...' : 'Select options...');
|
|
314
|
+
const handleMultiClear = () => {
|
|
315
|
+
onChange({ target: { name, value: null, type: "json" } });
|
|
222
316
|
};
|
|
223
317
|
|
|
224
|
-
|
|
225
|
-
|
|
318
|
+
// -------------------------------------------------------------------------
|
|
319
|
+
// Derived display values
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
const displayLabel =
|
|
323
|
+
label || intlLabel?.defaultMessage || name || "Select Option";
|
|
324
|
+
|
|
325
|
+
const getPlaceholder = () => {
|
|
326
|
+
if (loading) return "Loading...";
|
|
327
|
+
if (fetchError) return "Error loading options";
|
|
226
328
|
return (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
<svg
|
|
231
|
-
width={20}
|
|
232
|
-
height={20}
|
|
233
|
-
viewBox="0 0 24 24"
|
|
234
|
-
focusable="false"
|
|
235
|
-
aria-hidden="true"
|
|
236
|
-
>
|
|
237
|
-
<use href={iconUrl} />
|
|
238
|
-
</svg>
|
|
239
|
-
)}
|
|
240
|
-
<span>{option.label}</span>
|
|
241
|
-
</Flex>
|
|
329
|
+
placeholder ||
|
|
330
|
+
configPlaceholder ||
|
|
331
|
+
(selectMode === "single" ? "Select option..." : "Select options...")
|
|
242
332
|
);
|
|
243
333
|
};
|
|
244
334
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
335
|
+
// Unwrap the stored JSON structure back to a plain string for the Select.
|
|
336
|
+
// DB value for single: { value: "icon-name" } → "icon-name"
|
|
337
|
+
// DB value for multi: ["icon1", "icon2"] → ["icon1", "icon2"]
|
|
338
|
+
// Fallback: handle legacy bare strings gracefully.
|
|
339
|
+
const normalizedSingleValue = useMemo(() => {
|
|
340
|
+
if (!value) return "";
|
|
341
|
+
if (typeof value === "object" && !Array.isArray(value) && value.value != null)
|
|
342
|
+
return String(value.value);
|
|
343
|
+
// Legacy: bare string stored before this fix
|
|
344
|
+
if (typeof value === "string") return value;
|
|
345
|
+
return "";
|
|
346
|
+
}, [value]);
|
|
347
|
+
|
|
348
|
+
const normalizedMultiValue = useMemo(() => {
|
|
349
|
+
if (!value) return [];
|
|
350
|
+
return (Array.isArray(value) ? value : [value]).map(String);
|
|
351
|
+
}, [value]);
|
|
352
|
+
|
|
353
|
+
// -------------------------------------------------------------------------
|
|
354
|
+
// Option rendering
|
|
355
|
+
// -------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
const renderOptionContent = (option) => (
|
|
358
|
+
<Flex gap={2} alignItems="center">
|
|
359
|
+
<span>{option.label}</span>
|
|
360
|
+
</Flex>
|
|
361
|
+
);
|
|
263
362
|
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<Field.Label>{displayLabel}</Field.Label>
|
|
270
|
-
<Box
|
|
271
|
-
padding={2}
|
|
272
|
-
background="danger100"
|
|
273
|
-
borderColor="danger600"
|
|
274
|
-
borderStyle="solid"
|
|
275
|
-
borderWidth="1px"
|
|
276
|
-
hasRadius
|
|
277
|
-
>
|
|
278
|
-
<Typography variant="pi" textColor="danger600">
|
|
279
|
-
Failed to load options: {fetchError}
|
|
280
|
-
</Typography>
|
|
281
|
-
</Box>
|
|
282
|
-
{description && <Field.Hint>{description}</Field.Hint>}
|
|
283
|
-
<Field.Error />
|
|
284
|
-
</Field.Root>
|
|
285
|
-
</Box>
|
|
286
|
-
);
|
|
287
|
-
}
|
|
363
|
+
// -------------------------------------------------------------------------
|
|
364
|
+
// FIX #11: unified render — don't swap the entire JSX tree for loading/error
|
|
365
|
+
// states; keep the Select in place so Strapi doesn't lose the field's ref
|
|
366
|
+
// and layout position. Show inline feedback instead.
|
|
367
|
+
// -------------------------------------------------------------------------
|
|
288
368
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
</
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
369
|
+
const renderFeedback = () => {
|
|
370
|
+
if (loading) {
|
|
371
|
+
return (
|
|
372
|
+
<Flex padding={2} gap={2} alignItems="center">
|
|
373
|
+
<Loader small />
|
|
374
|
+
<Typography variant="pi" textColor="neutral600">
|
|
375
|
+
Loading options...
|
|
376
|
+
</Typography>
|
|
377
|
+
</Flex>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (fetchError) {
|
|
381
|
+
return (
|
|
382
|
+
<Box
|
|
383
|
+
padding={2}
|
|
384
|
+
background="danger100"
|
|
385
|
+
borderColor="danger600"
|
|
386
|
+
borderStyle="solid"
|
|
387
|
+
borderWidth="1px"
|
|
388
|
+
hasRadius
|
|
389
|
+
>
|
|
390
|
+
<Typography variant="pi" textColor="danger600">
|
|
391
|
+
Failed to load options: {fetchError}
|
|
392
|
+
</Typography>
|
|
393
|
+
</Box>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
};
|
|
317
398
|
|
|
318
|
-
|
|
319
|
-
|
|
399
|
+
// -------------------------------------------------------------------------
|
|
400
|
+
// Render
|
|
401
|
+
// -------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
if (selectMode === "multiple") {
|
|
404
|
+
return (
|
|
320
405
|
<Field.Root name={name} error={error} required={required}>
|
|
321
406
|
<Field.Label>{displayLabel}</Field.Label>
|
|
322
|
-
|
|
407
|
+
{renderFeedback()}
|
|
408
|
+
{/* FIX #9: spread {...props} BEFORE controlled props so nothing
|
|
409
|
+
accidentally overrides onChange / onClear / value */}
|
|
410
|
+
<MultiSelect
|
|
411
|
+
{...props}
|
|
323
412
|
ref={forwardedRef}
|
|
324
413
|
name={name}
|
|
325
|
-
value={
|
|
326
|
-
onChange={
|
|
327
|
-
|
|
414
|
+
value={normalizedMultiValue}
|
|
415
|
+
onChange={handleMultiChange}
|
|
416
|
+
onClear={handleMultiClear}
|
|
417
|
+
disabled={disabled || loading}
|
|
328
418
|
placeholder={getPlaceholder()}
|
|
329
|
-
|
|
419
|
+
withTags
|
|
330
420
|
>
|
|
331
421
|
{options.map((option, index) => (
|
|
332
|
-
<
|
|
422
|
+
<MultiSelectOption
|
|
423
|
+
key={option.value || index}
|
|
424
|
+
value={option.value || ""}
|
|
425
|
+
>
|
|
333
426
|
{renderOptionContent(option)}
|
|
334
|
-
</
|
|
427
|
+
</MultiSelectOption>
|
|
335
428
|
))}
|
|
336
|
-
</
|
|
429
|
+
</MultiSelect>
|
|
337
430
|
{description && <Field.Hint>{description}</Field.Hint>}
|
|
338
431
|
<Field.Error />
|
|
339
432
|
</Field.Root>
|
|
340
|
-
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<Field.Root name={name} error={error} required={required}>
|
|
438
|
+
<Field.Label>{displayLabel}</Field.Label>
|
|
439
|
+
{renderFeedback()}
|
|
440
|
+
{/* FIX #9: spread before controlled props */}
|
|
441
|
+
<SingleSelect
|
|
442
|
+
{...props}
|
|
443
|
+
ref={forwardedRef}
|
|
444
|
+
name={name}
|
|
445
|
+
value={normalizedSingleValue}
|
|
446
|
+
onChange={handleSingleChange}
|
|
447
|
+
onClear={handleSingleClear}
|
|
448
|
+
disabled={disabled || loading}
|
|
449
|
+
placeholder={getPlaceholder()}
|
|
450
|
+
>
|
|
451
|
+
{options.map((option, index) => (
|
|
452
|
+
<SingleSelectOption
|
|
453
|
+
key={option.value || index}
|
|
454
|
+
value={option.value || ""}
|
|
455
|
+
>
|
|
456
|
+
{renderOptionContent(option)}
|
|
457
|
+
</SingleSelectOption>
|
|
458
|
+
))}
|
|
459
|
+
</SingleSelect>
|
|
460
|
+
{description && <Field.Hint>{description}</Field.Hint>}
|
|
461
|
+
<Field.Error />
|
|
462
|
+
</Field.Root>
|
|
341
463
|
);
|
|
342
464
|
};
|
|
343
465
|
|
|
@@ -354,22 +476,4 @@ ApiSelectInput.defaultProps = {
|
|
|
354
476
|
attribute: {},
|
|
355
477
|
};
|
|
356
478
|
|
|
357
|
-
ApiSelectInput.propTypes = {
|
|
358
|
-
name: PropTypes.string.isRequired,
|
|
359
|
-
value: PropTypes.any,
|
|
360
|
-
onChange: PropTypes.func.isRequired,
|
|
361
|
-
attribute: PropTypes.object,
|
|
362
|
-
placeholder: PropTypes.string,
|
|
363
|
-
error: PropTypes.string,
|
|
364
|
-
required: PropTypes.bool,
|
|
365
|
-
disabled: PropTypes.bool,
|
|
366
|
-
label: PropTypes.string,
|
|
367
|
-
description: PropTypes.string,
|
|
368
|
-
intlLabel: PropTypes.shape({
|
|
369
|
-
id: PropTypes.string,
|
|
370
|
-
defaultMessage: PropTypes.string,
|
|
371
|
-
}),
|
|
372
|
-
forwardedRef: PropTypes.any,
|
|
373
|
-
};
|
|
374
|
-
|
|
375
479
|
export default ApiSelectInput;
|