@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 +17 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +148 -17
- package/dist/index.mjs +148 -17
- package/dist/types/components/DataTable/context/useDataTableServerContext.d.ts +1 -1
- package/dist/types/components/DataTable/controls/ReloadButton.d.ts +1 -1
- package/package.json +1 -1
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
|
|
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 =
|
|
2856
|
-
const
|
|
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
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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
|
-
}
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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 =
|
|
2836
|
-
const
|
|
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
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
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
|
-
}
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
1
|
export interface ReloadButtonProps {
|
|
2
2
|
variant?: string;
|
|
3
3
|
}
|
|
4
|
-
export declare const ReloadButton: ({ variant
|
|
4
|
+
export declare const ReloadButton: ({ variant }: ReloadButtonProps) => import("react/jsx-runtime").JSX.Element;
|