@k3-tech/react-kit 0.0.56 → 0.0.58
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.js +89 -70
- package/dist/kit/builder/form/components/sectionNodes.d.ts.map +1 -1
- package/dist/kit/builder/form/types.d.ts +3 -7
- package/dist/kit/builder/form/types.d.ts.map +1 -1
- package/dist/kit/builder/section/SectionBuilder.d.ts.map +1 -1
- package/dist/kit/builder/section/types.d.ts +3 -0
- package/dist/kit/builder/section/types.d.ts.map +1 -1
- package/dist/kit/components/autocomplete/Autocomplete.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/kit/builder/form/components/sectionNodes.tsx +3 -0
- package/src/kit/builder/form/types.ts +11 -7
- package/src/kit/builder/section/SectionBuilder.tsx +18 -10
- package/src/kit/builder/section/types.ts +3 -0
- package/src/kit/components/autocomplete/Autocomplete.tsx +102 -64
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -249,65 +249,91 @@ export function Autocomplete<T = unknown>({
|
|
|
249
249
|
});
|
|
250
250
|
}, [currentValue, options, storeOption]);
|
|
251
251
|
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
// Tracks selected values currently being resolved via loadSelected. Doubles
|
|
253
|
+
// as the loading indicator and the recompute trigger: removing a value on
|
|
254
|
+
// settle re-renders, and by then its label is already stored in labelMapRef.
|
|
255
|
+
const [loadingSelectedValues, setLoadingSelectedValues] = useState<
|
|
256
|
+
Set<string | number>
|
|
257
|
+
>(() => new Set());
|
|
258
|
+
|
|
259
|
+
// Values with a loadSelected fetch currently in flight. Prevents duplicate
|
|
260
|
+
// concurrent requests when the parent re-renders with a fresh currentValue
|
|
261
|
+
// reference (common in multiple mode) before the first fetch resolves.
|
|
262
|
+
const inFlightSelectedRef = useRef<Set<string | number>>(new Set());
|
|
263
|
+
|
|
264
|
+
// Load selected labels via loadSelected whenever currentValue changes.
|
|
265
|
+
// Covers mount, programmatic value changes (even while the dropdown is
|
|
266
|
+
// closed), and dropdown opens — anything not already cached or resolvable
|
|
267
|
+
// from the local options array.
|
|
256
268
|
useEffect(() => {
|
|
257
|
-
|
|
258
|
-
if (!loadSelected || hasLoadedInitial.current) return;
|
|
259
|
-
if (!currentValue) return;
|
|
260
|
-
|
|
261
|
-
const values = Array.isArray(currentValue) ? currentValue : [currentValue];
|
|
262
|
-
if (values.length === 0) return;
|
|
263
|
-
|
|
264
|
-
// Check if any values are missing labels
|
|
265
|
-
const missing = values.filter((v) => !labelMapRef.current.has(v));
|
|
266
|
-
if (missing.length === 0) {
|
|
267
|
-
hasLoadedInitial.current = true;
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
269
|
+
if (!loadSelected) return;
|
|
270
270
|
|
|
271
|
-
hasLoadedInitial.current = true;
|
|
272
|
-
let cancelled = false;
|
|
273
|
-
loadSelected(missing)
|
|
274
|
-
.then((opts) => {
|
|
275
|
-
if (!cancelled && opts.length > 0) {
|
|
276
|
-
opts.forEach(storeOption);
|
|
277
|
-
// Only trigger re-render if we actually stored something
|
|
278
|
-
setLabelsLoadedCounter((c) => c + 1);
|
|
279
|
-
}
|
|
280
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
281
|
-
})
|
|
282
|
-
.catch(() => { });
|
|
283
|
-
return () => {
|
|
284
|
-
cancelled = true;
|
|
285
|
-
};
|
|
286
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
287
|
-
}, [loadSelected, storeOption, currentValue]); // Only run once on mount - uses closure values
|
|
288
|
-
|
|
289
|
-
// Load selected labels if missing when dropdown opens
|
|
290
|
-
useEffect(() => {
|
|
291
|
-
if (!loadSelected || !isOpen) return;
|
|
292
271
|
const values = Array.isArray(currentValue)
|
|
293
272
|
? currentValue
|
|
294
273
|
: currentValue
|
|
295
274
|
? [currentValue]
|
|
296
275
|
: [];
|
|
297
|
-
|
|
276
|
+
if (values.length === 0) return;
|
|
277
|
+
|
|
278
|
+
// Only fetch values whose labels we don't already have and aren't already
|
|
279
|
+
// being fetched.
|
|
280
|
+
const missing = values.filter(
|
|
281
|
+
(v) => !labelMapRef.current.has(v) && !inFlightSelectedRef.current.has(v),
|
|
282
|
+
);
|
|
298
283
|
if (missing.length === 0) return;
|
|
299
284
|
|
|
300
|
-
|
|
285
|
+
missing.forEach((v) => {
|
|
286
|
+
inFlightSelectedRef.current.add(v);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Mark missing values as loading so the UI shows a spinner instead of
|
|
290
|
+
// flashing the raw value while the fetch is in flight.
|
|
291
|
+
setLoadingSelectedValues((prev) => {
|
|
292
|
+
const next = new Set(prev);
|
|
293
|
+
missing.forEach((v) => {
|
|
294
|
+
next.add(v);
|
|
295
|
+
});
|
|
296
|
+
return next;
|
|
297
|
+
});
|
|
298
|
+
|
|
301
299
|
loadSelected(missing)
|
|
302
300
|
.then((opts) => {
|
|
303
|
-
if
|
|
301
|
+
// Store unconditionally — these are ref writes, safe even if this
|
|
302
|
+
// effect run was superseded. (Guarding on a "cancelled" flag here
|
|
303
|
+
// dropped labels in multiple mode, where currentValue is a fresh
|
|
304
|
+
// array reference each render and re-runs/cancels the effect.)
|
|
305
|
+
opts.forEach((opt) => {
|
|
306
|
+
storeOption(opt);
|
|
307
|
+
// Align the returned option back to the originally-requested value
|
|
308
|
+
// so getLabel finds it even when loadSelected returns a different
|
|
309
|
+
// value type (e.g. a GraphQL ID serialized as a string while
|
|
310
|
+
// currentValue holds a number). Match loosely by string, then store
|
|
311
|
+
// under the exact requested value.
|
|
312
|
+
const requested = missing.find(
|
|
313
|
+
(m) => String(m) === String(opt.value),
|
|
314
|
+
);
|
|
315
|
+
if (requested !== undefined && requested !== opt.value) {
|
|
316
|
+
storeOption({ ...opt, value: requested });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
304
319
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
305
320
|
})
|
|
306
|
-
.catch(() => { })
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
321
|
+
.catch(() => { })
|
|
322
|
+
.finally(() => {
|
|
323
|
+
missing.forEach((v) => {
|
|
324
|
+
inFlightSelectedRef.current.delete(v);
|
|
325
|
+
});
|
|
326
|
+
// Clear the loading flags regardless of outcome; the resulting state
|
|
327
|
+
// change re-renders and recomputes selectedItems from labelMapRef.
|
|
328
|
+
setLoadingSelectedValues((prev) => {
|
|
329
|
+
const next = new Set(prev);
|
|
330
|
+
missing.forEach((v) => {
|
|
331
|
+
next.delete(v);
|
|
332
|
+
});
|
|
333
|
+
return next;
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}, [currentValue, loadSelected, storeOption]);
|
|
311
337
|
|
|
312
338
|
// Get label helper
|
|
313
339
|
const getLabel = useCallback((v: string | number) => {
|
|
@@ -316,6 +342,12 @@ export function Autocomplete<T = unknown>({
|
|
|
316
342
|
|
|
317
343
|
// Selected items for display
|
|
318
344
|
const selectedItems = useMemo(() => {
|
|
345
|
+
const toItem = (v: string | number) => ({
|
|
346
|
+
value: v,
|
|
347
|
+
label: getLabel(v),
|
|
348
|
+
raw: rawMapRef.current.get(v),
|
|
349
|
+
loading: loadingSelectedValues.has(v),
|
|
350
|
+
});
|
|
319
351
|
if (!isMultiple) {
|
|
320
352
|
if (
|
|
321
353
|
currentValue === null ||
|
|
@@ -323,22 +355,14 @@ export function Autocomplete<T = unknown>({
|
|
|
323
355
|
Array.isArray(currentValue)
|
|
324
356
|
)
|
|
325
357
|
return [];
|
|
326
|
-
return [
|
|
327
|
-
{
|
|
328
|
-
value: currentValue,
|
|
329
|
-
label: getLabel(currentValue),
|
|
330
|
-
raw: rawMapRef.current.get(currentValue),
|
|
331
|
-
},
|
|
332
|
-
];
|
|
358
|
+
return [toItem(currentValue)];
|
|
333
359
|
}
|
|
334
360
|
const values = Array.isArray(currentValue) ? currentValue : [];
|
|
335
|
-
return values.map(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
341
|
-
}, [currentValue, isMultiple, getLabel]); // labelsLoadedCounter triggers re-compute when loadSelected completes
|
|
361
|
+
return values.map(toItem);
|
|
362
|
+
// loadingSelectedValues also drives the recompute when loadSelected
|
|
363
|
+
// settles: clearing a value's flag re-renders, and its label is in
|
|
364
|
+
// labelMapRef (a ref React can't track) by then.
|
|
365
|
+
}, [currentValue, isMultiple, getLabel, loadingSelectedValues]);
|
|
342
366
|
|
|
343
367
|
// Handle selection
|
|
344
368
|
const handleSelect = useCallback(
|
|
@@ -450,13 +474,18 @@ export function Autocomplete<T = unknown>({
|
|
|
450
474
|
if (isMultiple || isOpen) {
|
|
451
475
|
return searchInput;
|
|
452
476
|
}
|
|
453
|
-
// In single mode when closed, show selected item label
|
|
477
|
+
// In single mode when closed, show selected item label. While its label
|
|
478
|
+
// is still resolving, show nothing (a spinner renders in the trailing
|
|
479
|
+
// icons) so we don't flash the raw value.
|
|
454
480
|
if (selectedItems.length > 0) {
|
|
455
|
-
return selectedItems[0].label;
|
|
481
|
+
return selectedItems[0].loading ? '' : selectedItems[0].label;
|
|
456
482
|
}
|
|
457
483
|
return '';
|
|
458
484
|
}, [isMultiple, isOpen, searchInput, selectedItems]);
|
|
459
485
|
|
|
486
|
+
// Whether the single-mode selection is still resolving its label
|
|
487
|
+
const isSingleSelectionLoading = !isMultiple && !!selectedItems[0]?.loading;
|
|
488
|
+
|
|
460
489
|
// Downshift
|
|
461
490
|
const { getInputProps, getItemProps, getMenuProps, highlightedIndex } =
|
|
462
491
|
useCombobox({
|
|
@@ -542,7 +571,13 @@ export function Autocomplete<T = unknown>({
|
|
|
542
571
|
variant={chipVariant}
|
|
543
572
|
className={cn('gap-1', chipClassName)}
|
|
544
573
|
>
|
|
545
|
-
<span className="max-w-[100px] truncate">
|
|
574
|
+
<span className="max-w-[100px] truncate">
|
|
575
|
+
{item.loading ? (
|
|
576
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
577
|
+
) : (
|
|
578
|
+
item.label
|
|
579
|
+
)}
|
|
580
|
+
</span>
|
|
546
581
|
<button
|
|
547
582
|
type="button"
|
|
548
583
|
onClick={(e) => {
|
|
@@ -583,7 +618,10 @@ export function Autocomplete<T = unknown>({
|
|
|
583
618
|
)}
|
|
584
619
|
/>
|
|
585
620
|
<div className="flex items-center gap-2 shrink-0">
|
|
586
|
-
{
|
|
621
|
+
{isSingleSelectionLoading && (
|
|
622
|
+
<Loader2 className="h-4 w-4 animate-spin opacity-50" />
|
|
623
|
+
)}
|
|
624
|
+
{showClearButton && !isSingleSelectionLoading && (
|
|
587
625
|
<button
|
|
588
626
|
type="button"
|
|
589
627
|
onClick={(e) => {
|