@bsol-oss/react-datatable5 12.0.0-beta.92 → 12.0.0-beta.94

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 CHANGED
@@ -297,6 +297,23 @@ All validation types are fully supported across all languages with culturally ap
297
297
 
298
298
  For more details of props and examples, please review the stories in storybook platform.
299
299
 
300
+ ## Documentation
301
+
302
+ Additional documentation is available in the `docs/` directory:
303
+
304
+ ### General Guides
305
+
306
+ - **[DataTable Server Usage](./docs/DATATABLE_SERVER_USAGE.md)** - Server-side DataTable integration guide
307
+ - **[DefaultForm Usage](./docs/DEFAULTFORM_USAGE.md)** - Comprehensive form component guide
308
+ - **[Validation & i18n](./docs/VALIDATION_I18N.md)** - Validation and internationalization setup
309
+ - **[Deployment Guide](./docs/deployment.md)** - Deployment instructions
310
+
311
+ ### IdPicker Customization
312
+
313
+ - **[IdPicker Quick Reference](./docs/IDPICKER_QUICK_REFERENCE.md)** - ⚡ Quick guide to IdPicker customization
314
+ - **[IdPicker Custom Display](./docs/IDPICKER_CUSTOM_DISPLAY.md)** - Customize how items appear in the combobox
315
+ - **[IdPicker Labels](./docs/IDPICKER_LABELS.md)** - Customize UI labels (buttons, placeholders, messages)
316
+
300
317
  ## Development
301
318
 
302
319
  ```
package/dist/index.d.ts CHANGED
@@ -108,7 +108,7 @@ declare const ViewDialog: ({ icon }: EditViewButtonProps) => react_jsx_runtime.J
108
108
  interface ReloadButtonProps {
109
109
  variant?: string;
110
110
  }
111
- declare const ReloadButton: ({ variant, }: ReloadButtonProps) => react_jsx_runtime.JSX.Element;
111
+ declare const ReloadButton: ({ variant }: ReloadButtonProps) => react_jsx_runtime.JSX.Element;
112
112
 
113
113
  declare const GlobalFilter: () => react_jsx_runtime.JSX.Element;
114
114
 
package/dist/index.js CHANGED
@@ -2848,23 +2848,28 @@ const DataTableServerContext = React.createContext({
2848
2848
  const useDataTableServerContext = () => {
2849
2849
  const context = React.useContext(DataTableServerContext);
2850
2850
  const { query } = context;
2851
- const isEmpty = (query.data?.count ?? 0) <= 0;
2851
+ const isEmpty = query ? (query.data?.count ?? 0) <= 0 : false;
2852
2852
  return { ...context, isEmpty };
2853
2853
  };
2854
2854
 
2855
- const ReloadButton = ({ variant = "icon", }) => {
2856
- const { url } = useDataTableServerContext();
2855
+ const ReloadButton = ({ variant = 'icon' }) => {
2856
+ const serverContext = useDataTableServerContext();
2857
+ const { url, query } = serverContext;
2857
2858
  const queryClient = reactQuery.useQueryClient();
2858
2859
  const { tableLabel } = useDataTableContext();
2859
2860
  const { reloadTooltip, reloadButtonText } = tableLabel;
2860
- if (variant === "icon") {
2861
- return (jsxRuntime.jsx(Tooltip, { showArrow: true, content: reloadTooltip, children: jsxRuntime.jsx(Button, { variant: "ghost", onClick: () => {
2862
- queryClient.invalidateQueries({ queryKey: [url] });
2863
- }, "aria-label": "refresh", children: jsxRuntime.jsx(io5.IoReload, {}) }) }));
2864
- }
2865
- return (jsxRuntime.jsxs(Button, { variant: "ghost", onClick: () => {
2861
+ const handleReload = () => {
2862
+ // Only invalidate queries for server-side tables (when query exists)
2863
+ if (query && url) {
2866
2864
  queryClient.invalidateQueries({ queryKey: [url] });
2867
- }, children: [jsxRuntime.jsx(io5.IoReload, {}), " ", reloadButtonText] }));
2865
+ }
2866
+ // For client-side tables, reload button doesn't need to do anything
2867
+ // as the data is already in memory
2868
+ };
2869
+ if (variant === 'icon') {
2870
+ return (jsxRuntime.jsx(Tooltip, { showArrow: true, content: reloadTooltip, children: jsxRuntime.jsx(Button, { variant: 'ghost', onClick: handleReload, "aria-label": 'refresh', children: jsxRuntime.jsx(io5.IoReload, {}) }) }));
2871
+ }
2872
+ return (jsxRuntime.jsxs(Button, { variant: 'ghost', onClick: handleReload, children: [jsxRuntime.jsx(io5.IoReload, {}), " ", reloadButtonText] }));
2868
2873
  };
2869
2874
 
2870
2875
  const InputGroup = React__namespace.forwardRef(function InputGroup(props, ref) {
@@ -3115,7 +3120,7 @@ const Table = ({ children, emptyComponent = EmptyResult, canResize = true, showL
3115
3120
  if (!showLoading && table.getRowModel().rows.length <= 0) {
3116
3121
  return emptyComponent;
3117
3122
  }
3118
- return (jsxRuntime.jsx(react.Box, { ref: containerRef, width: "100%", overflow: "auto", children: jsxRuntime.jsx(react.Table.Root, { stickyHeader: true, variant: 'outline', width: canResize ? table.getCenterTotalSize() : undefined, display: 'grid', alignContent: 'start', overflowY: 'auto', bg: { base: 'colorPalette.50', _dark: 'colorPalette.950' }, ...props, children: children }) }));
3123
+ return (jsxRuntime.jsx(react.Box, { ref: containerRef, width: "100%", overflow: "auto", children: jsxRuntime.jsx(react.Table.Root, { stickyHeader: true, variant: 'outline', width: canResize ? table.getCenterTotalSize() : undefined, display: 'grid', alignContent: 'start', bg: { base: 'colorPalette.50', _dark: 'colorPalette.950' }, ...props, children: children }) }));
3119
3124
  };
3120
3125
 
3121
3126
  const Checkbox = React__namespace.forwardRef(function Checkbox(props, ref) {
@@ -5502,6 +5507,69 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
5502
5507
  }, 300);
5503
5508
  return () => clearTimeout(timer);
5504
5509
  }, [searchText]);
5510
+ // Find IDs that are in currentValue but missing from idMap
5511
+ const missingIds = React.useMemo(() => {
5512
+ return currentValue.filter((id) => !idMap[id]);
5513
+ }, [currentValue, idMap]);
5514
+ // Stable key for query based on sorted missing IDs
5515
+ const missingIdsKey = React.useMemo(() => {
5516
+ return JSON.stringify([...missingIds].sort());
5517
+ }, [missingIds]);
5518
+ // Query to fetch initial values that are missing from idMap
5519
+ // This query runs automatically when missingIds.length > 0 and updates idMap
5520
+ const initialValuesQuery = reactQuery.useQuery({
5521
+ queryKey: [`idpicker-initial`, column, missingIdsKey],
5522
+ queryFn: async () => {
5523
+ if (missingIds.length === 0) {
5524
+ return { data: [], count: 0 };
5525
+ }
5526
+ if (customQueryFn) {
5527
+ const { data, idMap } = await customQueryFn({
5528
+ searching: '',
5529
+ limit: missingIds.length,
5530
+ offset: 0,
5531
+ where: [
5532
+ {
5533
+ id: column_ref,
5534
+ value: missingIds.length === 1 ? missingIds[0] : missingIds,
5535
+ },
5536
+ ],
5537
+ });
5538
+ setIdMap((state) => {
5539
+ return { ...state, ...idMap };
5540
+ });
5541
+ return data;
5542
+ }
5543
+ const data = await getTableData({
5544
+ serverUrl,
5545
+ searching: '',
5546
+ in_table: table,
5547
+ limit: missingIds.length,
5548
+ offset: 0,
5549
+ where: [
5550
+ {
5551
+ id: column_ref,
5552
+ value: missingIds.length === 1 ? missingIds[0] : missingIds,
5553
+ },
5554
+ ],
5555
+ });
5556
+ const newMap = Object.fromEntries((data ?? { data: [] }).data.map((item) => {
5557
+ return [
5558
+ item[column_ref],
5559
+ {
5560
+ ...item,
5561
+ },
5562
+ ];
5563
+ }));
5564
+ setIdMap((state) => {
5565
+ return { ...state, ...newMap };
5566
+ });
5567
+ return data;
5568
+ },
5569
+ enabled: missingIds.length > 0, // Only fetch if there are missing IDs
5570
+ staleTime: 300000,
5571
+ });
5572
+ const { isLoading: isLoadingInitialValues, isFetching: isFetchingInitialValues, } = initialValuesQuery;
5505
5573
  // Query for search results (async loading)
5506
5574
  const query = reactQuery.useQuery({
5507
5575
  queryKey: [`idpicker`, { column, searchText: debouncedSearchText, limit }],
@@ -5544,12 +5612,35 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
5544
5612
  const dataList = data?.data ?? [];
5545
5613
  // Check if we're currently searching (user typed but debounce hasn't fired yet)
5546
5614
  const isSearching = searchText !== debouncedSearchText;
5615
+ // Extract items from idMap for currentValue IDs
5616
+ // Use useMemo with a stable dependency to minimize recalculations
5617
+ const currentValueKey = React.useMemo(() => JSON.stringify([...currentValue].sort()), [currentValue]);
5618
+ // Serialize the relevant part of idMap to detect when items we care about change
5619
+ const idMapKey = React.useMemo(() => {
5620
+ const relevantItems = currentValue
5621
+ .map((id) => {
5622
+ const item = idMap[id];
5623
+ return item ? JSON.stringify({ id, hasItem: true }) : null;
5624
+ })
5625
+ .filter(Boolean)
5626
+ .sort()
5627
+ .join('|');
5628
+ return relevantItems;
5629
+ }, [currentValue, idMap]);
5630
+ const idMapItems = React.useMemo(() => {
5631
+ return currentValue
5632
+ .map((id) => idMap[id])
5633
+ .filter((item) => item !== undefined);
5634
+ // Depend on idMapKey which only changes when items we care about change
5635
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5636
+ }, [currentValueKey, idMapKey]);
5547
5637
  // Transform data for combobox collection
5548
5638
  // label is used for filtering/searching (must be a string)
5549
5639
  // raw item is stored for custom rendering
5640
+ // Also include items from idMap that match currentValue (for initial values display)
5550
5641
  const comboboxItems = React.useMemo(() => {
5551
5642
  const renderFn = renderDisplay || defaultRenderDisplay;
5552
- return dataList.map((item) => {
5643
+ const itemsFromDataList = dataList.map((item) => {
5553
5644
  const rendered = renderFn(item);
5554
5645
  return {
5555
5646
  label: typeof rendered === 'string' ? rendered : JSON.stringify(item), // Use string for filtering
@@ -5557,7 +5648,24 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
5557
5648
  raw: item,
5558
5649
  };
5559
5650
  });
5560
- }, [dataList, column_ref, renderDisplay]);
5651
+ // Add items from idMap that match currentValue but aren't in dataList
5652
+ // This ensures initial values are displayed correctly in the combobox
5653
+ const itemsFromIdMap = idMapItems
5654
+ .map((item) => {
5655
+ // Check if this item is already in itemsFromDataList
5656
+ const alreadyIncluded = itemsFromDataList.some((i) => i.value === String(item[column_ref]));
5657
+ if (alreadyIncluded)
5658
+ return null;
5659
+ const rendered = renderFn(item);
5660
+ return {
5661
+ label: typeof rendered === 'string' ? rendered : JSON.stringify(item),
5662
+ value: String(item[column_ref]),
5663
+ raw: item,
5664
+ };
5665
+ })
5666
+ .filter((item) => item !== null);
5667
+ return [...itemsFromIdMap, ...itemsFromDataList];
5668
+ }, [dataList, column_ref, renderDisplay, idMapItems]);
5561
5669
  // Use filter hook for combobox
5562
5670
  const { contains } = react.useFilter({ sensitivity: 'base' });
5563
5671
  // Create collection for combobox
@@ -5581,22 +5689,45 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
5581
5689
  setValue(colLabel, details.value[0] || '');
5582
5690
  }
5583
5691
  };
5692
+ // Track previous comboboxItems to avoid unnecessary updates
5693
+ const prevComboboxItemsRef = React.useRef('');
5694
+ const prevSearchTextRef = React.useRef('');
5584
5695
  // Update collection and filter when data changes
5696
+ // This includes both search results and initial values from idMap
5585
5697
  React.useEffect(() => {
5586
- if (dataList.length > 0 && comboboxItems.length > 0) {
5698
+ // Create a stable string representation to compare (only value and label, not raw)
5699
+ const currentItemsKey = JSON.stringify(comboboxItems.map((item) => ({ value: item.value, label: item.label })));
5700
+ const itemsChanged = prevComboboxItemsRef.current !== currentItemsKey;
5701
+ const searchChanged = prevSearchTextRef.current !== searchText;
5702
+ // Only update if items or search actually changed
5703
+ if (!itemsChanged && !searchChanged) {
5704
+ return;
5705
+ }
5706
+ if (comboboxItems.length > 0 && itemsChanged) {
5587
5707
  set(comboboxItems);
5588
- // Apply filter to the collection using the immediate searchText for UI responsiveness
5708
+ prevComboboxItemsRef.current = currentItemsKey;
5709
+ }
5710
+ // Apply filter to the collection using the immediate searchText for UI responsiveness
5711
+ if (searchChanged) {
5589
5712
  if (searchText) {
5590
5713
  filter(searchText);
5591
5714
  }
5715
+ prevSearchTextRef.current = searchText;
5592
5716
  }
5593
- // Only depend on dataList and searchText, not comboboxItems (which is derived from dataList)
5594
5717
  // set and filter are stable functions from useListCollection
5718
+ // comboboxItems and searchText are the only dependencies we care about
5595
5719
  // eslint-disable-next-line react-hooks/exhaustive-deps
5596
- }, [dataList, searchText]);
5720
+ }, [comboboxItems, searchText]);
5597
5721
  return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
5598
5722
  gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [isMultiple && currentValue.length > 0 && (jsxRuntime.jsx(react.Flex, { flexFlow: 'wrap', gap: 1, mb: 2, children: currentValue.map((id) => {
5599
5723
  const item = idMap[id];
5724
+ // Show loading skeleton while fetching initial values
5725
+ if (item === undefined &&
5726
+ (isLoadingInitialValues || isFetchingInitialValues) &&
5727
+ missingIds.includes(id)) {
5728
+ return (jsxRuntime.jsx(react.Skeleton, { height: "24px", width: "100px", borderRadius: "md" }, id));
5729
+ }
5730
+ // Only show "not found" if we're not loading and item is still missing
5600
5731
  if (item === undefined) {
5601
5732
  return (jsxRuntime.jsx(react.Text, { fontSize: "sm", children: idPickerLabels?.undefined ?? formI18n.t('undefined') }, id));
5602
5733
  }
package/dist/index.mjs CHANGED
@@ -2828,23 +2828,28 @@ const DataTableServerContext = createContext({
2828
2828
  const useDataTableServerContext = () => {
2829
2829
  const context = useContext(DataTableServerContext);
2830
2830
  const { query } = context;
2831
- const isEmpty = (query.data?.count ?? 0) <= 0;
2831
+ const isEmpty = query ? (query.data?.count ?? 0) <= 0 : false;
2832
2832
  return { ...context, isEmpty };
2833
2833
  };
2834
2834
 
2835
- const ReloadButton = ({ variant = "icon", }) => {
2836
- const { url } = useDataTableServerContext();
2835
+ const ReloadButton = ({ variant = 'icon' }) => {
2836
+ const serverContext = useDataTableServerContext();
2837
+ const { url, query } = serverContext;
2837
2838
  const queryClient = useQueryClient();
2838
2839
  const { tableLabel } = useDataTableContext();
2839
2840
  const { reloadTooltip, reloadButtonText } = tableLabel;
2840
- if (variant === "icon") {
2841
- return (jsx(Tooltip, { showArrow: true, content: reloadTooltip, children: jsx(Button, { variant: "ghost", onClick: () => {
2842
- queryClient.invalidateQueries({ queryKey: [url] });
2843
- }, "aria-label": "refresh", children: jsx(IoReload, {}) }) }));
2844
- }
2845
- return (jsxs(Button, { variant: "ghost", onClick: () => {
2841
+ const handleReload = () => {
2842
+ // Only invalidate queries for server-side tables (when query exists)
2843
+ if (query && url) {
2846
2844
  queryClient.invalidateQueries({ queryKey: [url] });
2847
- }, children: [jsx(IoReload, {}), " ", reloadButtonText] }));
2845
+ }
2846
+ // For client-side tables, reload button doesn't need to do anything
2847
+ // as the data is already in memory
2848
+ };
2849
+ if (variant === 'icon') {
2850
+ return (jsx(Tooltip, { showArrow: true, content: reloadTooltip, children: jsx(Button, { variant: 'ghost', onClick: handleReload, "aria-label": 'refresh', children: jsx(IoReload, {}) }) }));
2851
+ }
2852
+ return (jsxs(Button, { variant: 'ghost', onClick: handleReload, children: [jsx(IoReload, {}), " ", reloadButtonText] }));
2848
2853
  };
2849
2854
 
2850
2855
  const InputGroup = React.forwardRef(function InputGroup(props, ref) {
@@ -3095,7 +3100,7 @@ const Table = ({ children, emptyComponent = EmptyResult, canResize = true, showL
3095
3100
  if (!showLoading && table.getRowModel().rows.length <= 0) {
3096
3101
  return emptyComponent;
3097
3102
  }
3098
- return (jsx(Box, { ref: containerRef, width: "100%", overflow: "auto", children: jsx(Table$1.Root, { stickyHeader: true, variant: 'outline', width: canResize ? table.getCenterTotalSize() : undefined, display: 'grid', alignContent: 'start', overflowY: 'auto', bg: { base: 'colorPalette.50', _dark: 'colorPalette.950' }, ...props, children: children }) }));
3103
+ return (jsx(Box, { ref: containerRef, width: "100%", overflow: "auto", children: jsx(Table$1.Root, { stickyHeader: true, variant: 'outline', width: canResize ? table.getCenterTotalSize() : undefined, display: 'grid', alignContent: 'start', bg: { base: 'colorPalette.50', _dark: 'colorPalette.950' }, ...props, children: children }) }));
3099
3104
  };
3100
3105
 
3101
3106
  const Checkbox = React.forwardRef(function Checkbox(props, ref) {
@@ -5482,6 +5487,69 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
5482
5487
  }, 300);
5483
5488
  return () => clearTimeout(timer);
5484
5489
  }, [searchText]);
5490
+ // Find IDs that are in currentValue but missing from idMap
5491
+ const missingIds = useMemo(() => {
5492
+ return currentValue.filter((id) => !idMap[id]);
5493
+ }, [currentValue, idMap]);
5494
+ // Stable key for query based on sorted missing IDs
5495
+ const missingIdsKey = useMemo(() => {
5496
+ return JSON.stringify([...missingIds].sort());
5497
+ }, [missingIds]);
5498
+ // Query to fetch initial values that are missing from idMap
5499
+ // This query runs automatically when missingIds.length > 0 and updates idMap
5500
+ const initialValuesQuery = useQuery({
5501
+ queryKey: [`idpicker-initial`, column, missingIdsKey],
5502
+ queryFn: async () => {
5503
+ if (missingIds.length === 0) {
5504
+ return { data: [], count: 0 };
5505
+ }
5506
+ if (customQueryFn) {
5507
+ const { data, idMap } = await customQueryFn({
5508
+ searching: '',
5509
+ limit: missingIds.length,
5510
+ offset: 0,
5511
+ where: [
5512
+ {
5513
+ id: column_ref,
5514
+ value: missingIds.length === 1 ? missingIds[0] : missingIds,
5515
+ },
5516
+ ],
5517
+ });
5518
+ setIdMap((state) => {
5519
+ return { ...state, ...idMap };
5520
+ });
5521
+ return data;
5522
+ }
5523
+ const data = await getTableData({
5524
+ serverUrl,
5525
+ searching: '',
5526
+ in_table: table,
5527
+ limit: missingIds.length,
5528
+ offset: 0,
5529
+ where: [
5530
+ {
5531
+ id: column_ref,
5532
+ value: missingIds.length === 1 ? missingIds[0] : missingIds,
5533
+ },
5534
+ ],
5535
+ });
5536
+ const newMap = Object.fromEntries((data ?? { data: [] }).data.map((item) => {
5537
+ return [
5538
+ item[column_ref],
5539
+ {
5540
+ ...item,
5541
+ },
5542
+ ];
5543
+ }));
5544
+ setIdMap((state) => {
5545
+ return { ...state, ...newMap };
5546
+ });
5547
+ return data;
5548
+ },
5549
+ enabled: missingIds.length > 0, // Only fetch if there are missing IDs
5550
+ staleTime: 300000,
5551
+ });
5552
+ const { isLoading: isLoadingInitialValues, isFetching: isFetchingInitialValues, } = initialValuesQuery;
5485
5553
  // Query for search results (async loading)
5486
5554
  const query = useQuery({
5487
5555
  queryKey: [`idpicker`, { column, searchText: debouncedSearchText, limit }],
@@ -5524,12 +5592,35 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
5524
5592
  const dataList = data?.data ?? [];
5525
5593
  // Check if we're currently searching (user typed but debounce hasn't fired yet)
5526
5594
  const isSearching = searchText !== debouncedSearchText;
5595
+ // Extract items from idMap for currentValue IDs
5596
+ // Use useMemo with a stable dependency to minimize recalculations
5597
+ const currentValueKey = useMemo(() => JSON.stringify([...currentValue].sort()), [currentValue]);
5598
+ // Serialize the relevant part of idMap to detect when items we care about change
5599
+ const idMapKey = useMemo(() => {
5600
+ const relevantItems = currentValue
5601
+ .map((id) => {
5602
+ const item = idMap[id];
5603
+ return item ? JSON.stringify({ id, hasItem: true }) : null;
5604
+ })
5605
+ .filter(Boolean)
5606
+ .sort()
5607
+ .join('|');
5608
+ return relevantItems;
5609
+ }, [currentValue, idMap]);
5610
+ const idMapItems = useMemo(() => {
5611
+ return currentValue
5612
+ .map((id) => idMap[id])
5613
+ .filter((item) => item !== undefined);
5614
+ // Depend on idMapKey which only changes when items we care about change
5615
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5616
+ }, [currentValueKey, idMapKey]);
5527
5617
  // Transform data for combobox collection
5528
5618
  // label is used for filtering/searching (must be a string)
5529
5619
  // raw item is stored for custom rendering
5620
+ // Also include items from idMap that match currentValue (for initial values display)
5530
5621
  const comboboxItems = useMemo(() => {
5531
5622
  const renderFn = renderDisplay || defaultRenderDisplay;
5532
- return dataList.map((item) => {
5623
+ const itemsFromDataList = dataList.map((item) => {
5533
5624
  const rendered = renderFn(item);
5534
5625
  return {
5535
5626
  label: typeof rendered === 'string' ? rendered : JSON.stringify(item), // Use string for filtering
@@ -5537,7 +5628,24 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
5537
5628
  raw: item,
5538
5629
  };
5539
5630
  });
5540
- }, [dataList, column_ref, renderDisplay]);
5631
+ // Add items from idMap that match currentValue but aren't in dataList
5632
+ // This ensures initial values are displayed correctly in the combobox
5633
+ const itemsFromIdMap = idMapItems
5634
+ .map((item) => {
5635
+ // Check if this item is already in itemsFromDataList
5636
+ const alreadyIncluded = itemsFromDataList.some((i) => i.value === String(item[column_ref]));
5637
+ if (alreadyIncluded)
5638
+ return null;
5639
+ const rendered = renderFn(item);
5640
+ return {
5641
+ label: typeof rendered === 'string' ? rendered : JSON.stringify(item),
5642
+ value: String(item[column_ref]),
5643
+ raw: item,
5644
+ };
5645
+ })
5646
+ .filter((item) => item !== null);
5647
+ return [...itemsFromIdMap, ...itemsFromDataList];
5648
+ }, [dataList, column_ref, renderDisplay, idMapItems]);
5541
5649
  // Use filter hook for combobox
5542
5650
  const { contains } = useFilter({ sensitivity: 'base' });
5543
5651
  // Create collection for combobox
@@ -5561,22 +5669,45 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
5561
5669
  setValue(colLabel, details.value[0] || '');
5562
5670
  }
5563
5671
  };
5672
+ // Track previous comboboxItems to avoid unnecessary updates
5673
+ const prevComboboxItemsRef = useRef('');
5674
+ const prevSearchTextRef = useRef('');
5564
5675
  // Update collection and filter when data changes
5676
+ // This includes both search results and initial values from idMap
5565
5677
  useEffect(() => {
5566
- if (dataList.length > 0 && comboboxItems.length > 0) {
5678
+ // Create a stable string representation to compare (only value and label, not raw)
5679
+ const currentItemsKey = JSON.stringify(comboboxItems.map((item) => ({ value: item.value, label: item.label })));
5680
+ const itemsChanged = prevComboboxItemsRef.current !== currentItemsKey;
5681
+ const searchChanged = prevSearchTextRef.current !== searchText;
5682
+ // Only update if items or search actually changed
5683
+ if (!itemsChanged && !searchChanged) {
5684
+ return;
5685
+ }
5686
+ if (comboboxItems.length > 0 && itemsChanged) {
5567
5687
  set(comboboxItems);
5568
- // Apply filter to the collection using the immediate searchText for UI responsiveness
5688
+ prevComboboxItemsRef.current = currentItemsKey;
5689
+ }
5690
+ // Apply filter to the collection using the immediate searchText for UI responsiveness
5691
+ if (searchChanged) {
5569
5692
  if (searchText) {
5570
5693
  filter(searchText);
5571
5694
  }
5695
+ prevSearchTextRef.current = searchText;
5572
5696
  }
5573
- // Only depend on dataList and searchText, not comboboxItems (which is derived from dataList)
5574
5697
  // set and filter are stable functions from useListCollection
5698
+ // comboboxItems and searchText are the only dependencies we care about
5575
5699
  // eslint-disable-next-line react-hooks/exhaustive-deps
5576
- }, [dataList, searchText]);
5700
+ }, [comboboxItems, searchText]);
5577
5701
  return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
5578
5702
  gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [isMultiple && currentValue.length > 0 && (jsx(Flex, { flexFlow: 'wrap', gap: 1, mb: 2, children: currentValue.map((id) => {
5579
5703
  const item = idMap[id];
5704
+ // Show loading skeleton while fetching initial values
5705
+ if (item === undefined &&
5706
+ (isLoadingInitialValues || isFetchingInitialValues) &&
5707
+ missingIds.includes(id)) {
5708
+ return (jsx(Skeleton, { height: "24px", width: "100px", borderRadius: "md" }, id));
5709
+ }
5710
+ // Only show "not found" if we're not loading and item is still missing
5580
5711
  if (item === undefined) {
5581
5712
  return (jsx(Text, { fontSize: "sm", children: idPickerLabels?.undefined ?? formI18n.t('undefined') }, id));
5582
5713
  }
@@ -1,4 +1,4 @@
1
- import { DataTableServerContext } from "./DataTableServerContext";
1
+ import { DataTableServerContext } from './DataTableServerContext';
2
2
  export interface UseDataTableServerContext extends DataTableServerContext {
3
3
  isEmpty: boolean;
4
4
  }
@@ -1,4 +1,4 @@
1
1
  export interface ReloadButtonProps {
2
2
  variant?: string;
3
3
  }
4
- export declare const ReloadButton: ({ variant, }: ReloadButtonProps) => import("react/jsx-runtime").JSX.Element;
4
+ export declare const ReloadButton: ({ variant }: ReloadButtonProps) => import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsol-oss/react-datatable5",
3
- "version": "12.0.0-beta.92",
3
+ "version": "12.0.0-beta.94",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",