@vendure/dashboard 3.3.6-master-202507040234 → 3.3.6-master-202507041203

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.3.6-master-202507040234",
4
+ "version": "3.3.6-master-202507041203",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -86,8 +86,8 @@
86
86
  "@types/react-dom": "^19.0.4",
87
87
  "@types/react-grid-layout": "^1.3.5",
88
88
  "@uidotdev/usehooks": "^2.4.1",
89
- "@vendure/common": "^3.3.6-master-202507040234",
90
- "@vendure/core": "^3.3.6-master-202507040234",
89
+ "@vendure/common": "^3.3.6-master-202507041203",
90
+ "@vendure/core": "^3.3.6-master-202507041203",
91
91
  "@vitejs/plugin-react": "^4.3.4",
92
92
  "awesome-graphql-client": "^2.1.0",
93
93
  "class-variance-authority": "^0.7.1",
@@ -130,5 +130,5 @@
130
130
  "lightningcss-linux-arm64-musl": "^1.29.3",
131
131
  "lightningcss-linux-x64-musl": "^1.29.1"
132
132
  },
133
- "gitHead": "6634367c8f5aeb5c25502c51c15686d1d4fa807c"
133
+ "gitHead": "bf87fca72814e2d3eb0a12b0d6575f234f1be67c"
134
134
  }
@@ -3,8 +3,9 @@ import { TrashIcon } from 'lucide-react';
3
3
  import { toast } from 'sonner';
4
4
 
5
5
  import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
6
+ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
7
+ import { getMutationName } from '@/vdb/framework/document-introspection/get-document-structure.js';
6
8
  import { api } from '@/vdb/graphql/api.js';
7
- import { getMutationName, usePaginatedList } from '@/vdb/index.js';
8
9
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
9
10
 
10
11
  interface DeleteBulkActionProps {
@@ -4,9 +4,9 @@ import { useState } from 'react';
4
4
  import { toast } from 'sonner';
5
5
 
6
6
  import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
7
+ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
7
8
  import { api } from '@/vdb/graphql/api.js';
8
9
  import { duplicateEntityDocument } from '@/vdb/graphql/common-operations.js';
9
- import { usePaginatedList } from '@/vdb/index.js';
10
10
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
11
11
 
12
12
  interface DuplicateBulkActionProps {
@@ -6,7 +6,10 @@ import { Trans } from '@/vdb/lib/trans.js';
6
6
  import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
7
7
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
8
8
  import { api } from '@/vdb/graphql/api.js';
9
- import { BulkActionComponent, useChannel, DataTableBulkActionItem, usePaginatedList } from '@/vdb/index.js';
9
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
10
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
11
+ import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
12
+ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
10
13
  import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
11
14
  import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
12
15
  import {
@@ -4,7 +4,8 @@ import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-cha
4
4
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
5
5
  import { api } from '@/vdb/graphql/api.js';
6
6
  import { ResultOf } from '@/vdb/graphql/graphql.js';
7
- import { BulkActionComponent, useChannel } from '@/vdb/index.js';
7
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
8
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
8
9
  import { useLingui } from '@/vdb/lib/trans.js';
9
10
  import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
10
11
  import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
@@ -1,7 +1,8 @@
1
1
  import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
2
2
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
3
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
3
4
  import { api } from '@/vdb/graphql/api.js';
4
- import { BulkActionComponent, useChannel } from '@/vdb/index.js';
5
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
5
6
  import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
6
7
 
7
8
  import {
@@ -6,7 +6,9 @@ import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-cha
6
6
  import { usePriceFactor } from '@/vdb/components/shared/assign-to-channel-dialog.js';
7
7
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
8
8
  import { api } from '@/vdb/graphql/api.js';
9
- import { BulkActionComponent, useChannel, usePaginatedList } from '@/vdb/index.js';
9
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
10
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
11
+ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
10
12
  import { Trans } from '@/vdb/lib/trans.js';
11
13
  import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
12
14
 
@@ -4,9 +4,11 @@ import { useState } from 'react';
4
4
  import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
5
5
  import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
6
6
  import { usePriceFactor } from '@/vdb/components/shared/assign-to-channel-dialog.js';
7
+ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
7
8
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
9
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
8
10
  import { api } from '@/vdb/graphql/api.js';
9
- import { BulkActionComponent, useChannel, usePaginatedList } from '@/vdb/index.js';
11
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
10
12
  import { Trans } from '@/vdb/lib/trans.js';
11
13
  import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
12
14
  import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
@@ -1,4 +1,5 @@
1
1
  import { Money } from '@/vdb/components/data-display/money.js';
2
+ import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
2
3
  import {
3
4
  PaginatedListDataTable,
4
5
  PaginatedListRefresherRegisterFn,
@@ -6,7 +7,6 @@ import {
6
7
  import { StockLevelLabel } from '@/vdb/components/shared/stock-level-label.js';
7
8
  import { graphql } from '@/vdb/graphql/graphql.js';
8
9
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
9
- import { DetailPageButton } from '@/vdb/index.js';
10
10
  import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
11
11
  import { useState } from 'react';
12
12
  import { productVariantListDocument } from '../products.graphql.js';
@@ -1,7 +1,8 @@
1
1
  import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
2
2
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
3
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
3
4
  import { api } from '@/vdb/graphql/api.js';
4
- import { BulkActionComponent, useChannel } from '@/vdb/index.js';
5
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
5
6
  import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
6
7
  import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
7
8
 
@@ -1,7 +1,8 @@
1
1
  import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
2
2
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
3
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
3
4
  import { api } from '@/vdb/graphql/api.js';
4
- import { BulkActionComponent, useChannel } from '@/vdb/index.js';
5
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
5
6
  import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
6
7
 
7
8
  import {
@@ -1,7 +1,8 @@
1
1
  import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
2
2
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
3
3
  import { api } from '@/vdb/graphql/api.js';
4
- import { BulkActionComponent, useChannel } from '@/vdb/index.js';
4
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
5
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
5
6
  import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
6
7
 
7
8
  import {
@@ -32,6 +32,8 @@ export interface RelationSelectorConfig<T = any> {
32
32
  multiple?: boolean;
33
33
  /** Custom filter function for search */
34
34
  buildSearchFilter?: (searchTerm: string) => any;
35
+ /** Custom filter function for fetching by IDs */
36
+ buildIdsFilter?: (ids: string[]) => any;
35
37
  /** Custom label renderer function for rich display */
36
38
  label?: (item: T) => React.ReactNode;
37
39
  }
@@ -96,6 +98,19 @@ export function useRelationSelector<T>(config: RelationSelectorConfig<T>) {
96
98
  [config.labelKey]: { contains: term },
97
99
  }));
98
100
 
101
+ // Build the default IDs filter if none provided
102
+ const buildIdsFilter = React.useCallback(
103
+ (ids: string[]) => {
104
+ if (config.buildIdsFilter) {
105
+ return config.buildIdsFilter(ids);
106
+ }
107
+ return {
108
+ [config.idKey]: { in: ids },
109
+ };
110
+ },
111
+ [config.idKey, config.buildIdsFilter],
112
+ );
113
+
99
114
  const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, error } = useInfiniteQuery({
100
115
  queryKey: ['relationSelector', getQueryName(config.listQuery), debouncedSearch],
101
116
  queryFn: async ({ pageParam = 0 }) => {
@@ -125,6 +140,30 @@ export function useRelationSelector<T>(config: RelationSelectorConfig<T>) {
125
140
 
126
141
  const items = data?.pages.flatMap(page => page?.items ?? []) ?? [];
127
142
 
143
+ // Function to fetch items by IDs
144
+ const fetchItemsByIds = React.useCallback(
145
+ async (ids: string[]): Promise<T[]> => {
146
+ if (ids.length === 0) return [];
147
+
148
+ const variables: any = {
149
+ options: {
150
+ take: ids.length,
151
+ filter: buildIdsFilter(ids),
152
+ },
153
+ };
154
+
155
+ try {
156
+ const response = (await api.query(config.listQuery, variables)) as any;
157
+ const result = response[getQueryName(config.listQuery)];
158
+ return result?.items ?? [];
159
+ } catch (error) {
160
+ console.error('Error fetching items by IDs:', error);
161
+ return [];
162
+ }
163
+ },
164
+ [buildIdsFilter, config.listQuery],
165
+ );
166
+
128
167
  return {
129
168
  items,
130
169
  isLoading,
@@ -134,6 +173,7 @@ export function useRelationSelector<T>(config: RelationSelectorConfig<T>) {
134
173
  error,
135
174
  searchTerm,
136
175
  setSearchTerm,
176
+ fetchItemsByIds,
137
177
  };
138
178
  }
139
179
 
@@ -149,19 +189,104 @@ export function RelationSelector<T>({
149
189
  }: Readonly<RelationSelectorProps<T>>) {
150
190
  const [open, setOpen] = useState(false);
151
191
  const [selectedItemsCache, setSelectedItemsCache] = useState<T[]>([]);
192
+ const fetchedIdsRef = React.useRef<Set<string>>(new Set());
193
+ const fetchingIdsRef = React.useRef<Set<string>>(new Set());
152
194
  const isMultiple = config.multiple ?? false;
153
195
 
154
- const { items, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, searchTerm, setSearchTerm } =
155
- useRelationSelector(config);
196
+ const {
197
+ items,
198
+ isLoading,
199
+ fetchNextPage,
200
+ hasNextPage,
201
+ isFetchingNextPage,
202
+ searchTerm,
203
+ setSearchTerm,
204
+ fetchItemsByIds,
205
+ } = useRelationSelector(config);
206
+
207
+ // Store a stable reference to fetchItemsByIds
208
+ const fetchItemsByIdsRef = React.useRef(fetchItemsByIds);
209
+ fetchItemsByIdsRef.current = fetchItemsByIds;
156
210
 
157
211
  // Normalize value to always be an array for easier handling
158
212
  const selectedIds = React.useMemo(() => {
159
213
  if (isMultiple) {
160
214
  return Array.isArray(value) ? value : value ? [value] : [];
161
215
  }
162
- return value ? [String(value)] : [];
216
+ // For single select, ensure we only have at most one ID
217
+ const singleValue = Array.isArray(value) ? value[0] : value;
218
+ return singleValue ? [String(singleValue)] : [];
163
219
  }, [value, isMultiple]);
164
220
 
221
+ // Fetch selected items by IDs on mount and when selectedIds change
222
+ React.useEffect(() => {
223
+ const fetchSelectedItems = async () => {
224
+ if (selectedIds.length === 0) {
225
+ setSelectedItemsCache([]);
226
+ fetchedIdsRef.current.clear();
227
+ fetchingIdsRef.current.clear();
228
+ return;
229
+ }
230
+
231
+ // Find which selected IDs we haven't fetched yet and aren't currently fetching
232
+ const missingIds = selectedIds.filter(
233
+ id => !fetchedIdsRef.current.has(id) && !fetchingIdsRef.current.has(id),
234
+ );
235
+
236
+ if (missingIds.length > 0) {
237
+ // Mark these IDs as being fetched
238
+ missingIds.forEach(id => fetchingIdsRef.current.add(id));
239
+
240
+ try {
241
+ const fetchedItems = await fetchItemsByIdsRef.current(missingIds);
242
+
243
+ // Mark these IDs as fetched and remove from fetching
244
+ missingIds.forEach(id => {
245
+ fetchedIdsRef.current.add(id);
246
+ fetchingIdsRef.current.delete(id);
247
+ });
248
+
249
+ setSelectedItemsCache(prev => {
250
+ if (!isMultiple) {
251
+ // For single select, replace the entire cache
252
+ return fetchedItems;
253
+ }
254
+ // For multi-select, filter and append
255
+ const stillSelected = prev.filter(item =>
256
+ selectedIds.includes(String(item[config.idKey])),
257
+ );
258
+ return [...stillSelected, ...fetchedItems];
259
+ });
260
+ } catch (error) {
261
+ // Remove from fetching set on error
262
+ missingIds.forEach(id => fetchingIdsRef.current.delete(id));
263
+ console.error('Error fetching items by IDs:', error);
264
+ }
265
+ } else {
266
+ // Just filter out items that are no longer selected
267
+ setSelectedItemsCache(prev => {
268
+ if (!isMultiple) {
269
+ // For single select, if no items need fetching but we have a selection,
270
+ // keep only items that are in the current selection
271
+ return prev.filter(item => selectedIds.includes(String(item[config.idKey])));
272
+ }
273
+ // For multi-select, normal filtering
274
+ return prev.filter(item => selectedIds.includes(String(item[config.idKey])));
275
+ });
276
+ }
277
+
278
+ // Clean up fetched IDs that are no longer selected
279
+ const selectedIdsSet = new Set(selectedIds);
280
+ for (const fetchedId of fetchedIdsRef.current) {
281
+ if (!selectedIdsSet.has(fetchedId)) {
282
+ fetchedIdsRef.current.delete(fetchedId);
283
+ }
284
+ }
285
+ };
286
+
287
+ fetchSelectedItems();
288
+ }, [selectedIds, config.idKey, isMultiple]);
289
+
165
290
  const handleSelect = (item: T) => {
166
291
  const itemId = String(item[config.idKey]);
167
292
 
@@ -182,10 +307,20 @@ export function RelationSelector<T>({
182
307
  }
183
308
  });
184
309
 
310
+ // Mark the item as fetched to prevent duplicate fetching
311
+ if (!isCurrentlySelected) {
312
+ fetchedIdsRef.current.add(itemId);
313
+ } else {
314
+ fetchedIdsRef.current.delete(itemId);
315
+ }
316
+
185
317
  onChange(newSelectedIds);
186
318
  } else {
187
319
  // For single select, update cache with the new item
188
320
  setSelectedItemsCache([item]);
321
+ // Mark as fetched for single select too
322
+ fetchedIdsRef.current.clear();
323
+ fetchedIdsRef.current.add(itemId);
189
324
  onChange(itemId);
190
325
  setOpen(false);
191
326
  setSearchTerm('');
@@ -214,31 +349,14 @@ export function RelationSelector<T>({
214
349
  }
215
350
  };
216
351
 
217
- // Clean up cache when selectedIds change externally (e.g., form reset)
218
- React.useEffect(() => {
219
- setSelectedItemsCache(prev => prev.filter(item => selectedIds.includes(String(item[config.idKey]))));
220
- }, [selectedIds, config.idKey]);
221
-
222
- // Populate cache with items from search results that are selected but not yet cached
223
- React.useEffect(() => {
224
- const itemsToAdd = items.filter(item => {
225
- const itemId = String(item[config.idKey]);
226
- const isSelected = selectedIds.includes(itemId);
227
- const isAlreadyCached = selectedItemsCache.some(
228
- cachedItem => String(cachedItem[config.idKey]) === itemId,
229
- );
230
- return isSelected && !isAlreadyCached;
231
- });
232
-
233
- if (itemsToAdd.length > 0) {
234
- setSelectedItemsCache(prev => [...prev, ...itemsToAdd]);
235
- }
236
- }, [items, selectedIds, selectedItemsCache, config.idKey]);
237
-
238
352
  // Get selected items for display from cache, filtered by current selection
239
353
  const selectedItems = React.useMemo(() => {
240
- return selectedItemsCache.filter(item => selectedIds.includes(String(item[config.idKey])));
241
- }, [selectedItemsCache, selectedIds, config.idKey]);
354
+ const filteredItems = selectedItemsCache.filter(item =>
355
+ selectedIds.includes(String(item[config.idKey])),
356
+ );
357
+ // For single select, ensure we only display one item
358
+ return isMultiple ? filteredItems : filteredItems.slice(0, 1);
359
+ }, [selectedItemsCache, selectedIds, config.idKey, isMultiple]);
242
360
 
243
361
  return (
244
362
  <div className={className}>
@@ -3,7 +3,8 @@ import { useState } from 'react';
3
3
 
4
4
  import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
5
5
  import { AssignToChannelDialog } from '@/vdb/components/shared/assign-to-channel-dialog.js';
6
- import { useChannel, usePaginatedList } from '@/vdb/index.js';
6
+ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
7
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
7
8
  import { Trans } from '@/vdb/lib/trans.js';
8
9
 
9
10
  interface AssignToChannelBulkActionProps {
@@ -3,8 +3,9 @@ import { LayersIcon } from 'lucide-react';
3
3
  import { toast } from 'sonner';
4
4
 
5
5
  import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
6
+ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
6
7
  import { ResultOf } from '@/vdb/graphql/graphql.js';
7
- import { useChannel, usePaginatedList } from '@/vdb/index.js';
8
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
8
9
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
9
10
 
10
11
  interface RemoveFromChannelBulkActionProps {
@@ -1,5 +1,6 @@
1
+ import { PaginatedListDataTable } from '@/vdb/components/shared/paginated-list-data-table.js';
1
2
  import { Button } from '@/vdb/components/ui/button.js';
2
- import { PaginatedListDataTable, useLocalFormat } from '@/vdb/index.js';
3
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3
4
  import { Link } from '@tanstack/react-router';
4
5
  import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
5
6
  import { formatRelative } from 'date-fns';
@@ -1,7 +1,8 @@
1
1
  import { AnimatedCurrency, AnimatedNumber } from '@/vdb/components/shared/animated-number.js';
2
2
  import { Tabs, TabsList, TabsTrigger } from '@/vdb/components/ui/tabs.js';
3
3
  import { api } from '@/vdb/graphql/api.js';
4
- import { useChannel, useLocalFormat } from '@/vdb/index.js';
4
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
5
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
5
6
  import { useQuery } from '@tanstack/react-query';
6
7
  import { endOfDay, endOfMonth, startOfDay, startOfMonth, subDays, subMonths } from 'date-fns';
7
8
  import { useMemo, useState } from 'react';