@vendure/dashboard 3.3.5-master-202506260234 → 3.3.5

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.5-master-202506260234",
4
+ "version": "3.3.5",
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.5-master-202506260234",
90
- "@vendure/core": "^3.3.5-master-202506260234",
89
+ "@vendure/common": "3.3.5",
90
+ "@vendure/core": "3.3.5",
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": "e69b67636ecee5332319d38d031f6cc0f70517eb"
133
+ "gitHead": "71d36ec5164c833d9f05f6e8140db13c450f4d29"
134
134
  }
@@ -1,7 +1,9 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
1
2
  import { useState } from 'react';
2
3
  import { toast } from 'sonner';
3
- import { useMutation } from '@tanstack/react-query';
4
4
 
5
+ import { FacetValueChip } from '@/components/shared/facet-value-chip.js';
6
+ import { FacetValue, FacetValueSelector } from '@/components/shared/facet-value-selector.js';
5
7
  import { Button } from '@/components/ui/button.js';
6
8
  import {
7
9
  Dialog,
@@ -11,12 +13,31 @@ import {
11
13
  DialogHeader,
12
14
  DialogTitle,
13
15
  } from '@/components/ui/dialog.js';
14
- import { FacetValueSelector, FacetValue } from '@/components/shared/facet-value-selector.js';
15
16
  import { api } from '@/graphql/api.js';
16
17
  import { ResultOf } from '@/graphql/graphql.js';
17
18
  import { Trans, useLingui } from '@/lib/trans.js';
18
19
 
19
- import { updateProductsDocument } from '../products.graphql.js';
20
+ import { getDetailQueryOptions } from '@/framework/page/use-detail-page.js';
21
+ import {
22
+ getProductsWithFacetValuesByIdsDocument,
23
+ productDetailDocument,
24
+ updateProductsDocument,
25
+ } from '../products.graphql.js';
26
+
27
+ interface ProductWithFacetValues {
28
+ id: string;
29
+ name: string;
30
+ facetValues: Array<{
31
+ id: string;
32
+ name: string;
33
+ code: string;
34
+ facet: {
35
+ id: string;
36
+ name: string;
37
+ code: string;
38
+ };
39
+ }>;
40
+ }
20
41
 
21
42
  interface AssignFacetValuesDialogProps {
22
43
  open: boolean;
@@ -25,9 +46,24 @@ interface AssignFacetValuesDialogProps {
25
46
  onSuccess?: () => void;
26
47
  }
27
48
 
28
- export function AssignFacetValuesDialog({ open, onOpenChange, productIds, onSuccess }: AssignFacetValuesDialogProps) {
49
+ export function AssignFacetValuesDialog({
50
+ open,
51
+ onOpenChange,
52
+ productIds,
53
+ onSuccess,
54
+ }: AssignFacetValuesDialogProps) {
29
55
  const { i18n } = useLingui();
30
- const [selectedFacetValueIds, setSelectedFacetValueIds] = useState<string[]>([]);
56
+ const [selectedValues, setSelectedValues] = useState<FacetValue[]>([]);
57
+ const [facetValuesRemoved, setFacetValuesRemoved] = useState(false);
58
+ const [removedFacetValues, setRemovedFacetValues] = useState<Set<string>>(new Set());
59
+ const queryClient = useQueryClient();
60
+
61
+ // Fetch existing facet values for the products
62
+ const { data: productsData, isLoading } = useQuery({
63
+ queryKey: ['productsWithFacetValues', productIds],
64
+ queryFn: () => api.query(getProductsWithFacetValuesByIdsDocument, { ids: productIds }),
65
+ enabled: open && productIds.length > 0,
66
+ });
31
67
 
32
68
  const { mutate, isPending } = useMutation({
33
69
  mutationFn: api.mutate(updateProductsDocument),
@@ -35,6 +71,14 @@ export function AssignFacetValuesDialog({ open, onOpenChange, productIds, onSucc
35
71
  toast.success(i18n.t(`Successfully updated facet values for ${productIds.length} products`));
36
72
  onSuccess?.();
37
73
  onOpenChange(false);
74
+ // Reset state
75
+ setSelectedValues([]);
76
+ setFacetValuesRemoved(false);
77
+ setRemovedFacetValues(new Set());
78
+ productIds.forEach(id => {
79
+ const { queryKey } = getDetailQueryOptions(productDetailDocument, { id });
80
+ queryClient.removeQueries({ queryKey });
81
+ });
38
82
  },
39
83
  onError: () => {
40
84
  toast.error(`Failed to update facet values for ${productIds.length} products`);
@@ -42,57 +86,165 @@ export function AssignFacetValuesDialog({ open, onOpenChange, productIds, onSucc
42
86
  });
43
87
 
44
88
  const handleAssign = () => {
45
- if (selectedFacetValueIds.length === 0) {
46
- toast.error('Please select at least one facet value');
89
+ if (selectedValues.length === 0 && !facetValuesRemoved) {
90
+ toast.error('Please select at least one facet value or make changes to existing ones');
47
91
  return;
48
92
  }
49
93
 
94
+ if (!productsData?.products.items) {
95
+ return;
96
+ }
97
+
98
+ const selectedFacetValueIds = selectedValues.map(sv => sv.id);
99
+
50
100
  mutate({
51
- input: productIds.map(productId => ({
52
- id: productId,
53
- facetValueIds: selectedFacetValueIds,
101
+ input: productsData.products.items.map(product => ({
102
+ id: product.id,
103
+ facetValueIds: [
104
+ ...new Set([
105
+ ...product.facetValues.filter(fv => !removedFacetValues.has(fv.id)).map(fv => fv.id),
106
+ ...selectedFacetValueIds,
107
+ ]),
108
+ ],
54
109
  })),
55
110
  });
56
111
  };
57
112
 
58
113
  const handleFacetValueSelect = (facetValue: FacetValue) => {
59
- setSelectedFacetValueIds(prev => [...new Set([...prev, facetValue.id])]);
114
+ setSelectedValues(prev => [...prev, facetValue]);
115
+ };
116
+
117
+ const removeFacetValue = (productId: string, facetValueId: string) => {
118
+ setRemovedFacetValues(prev => new Set([...prev, facetValueId]));
119
+ setFacetValuesRemoved(true);
120
+ };
121
+
122
+ const handleCancel = () => {
123
+ onOpenChange(false);
124
+ // Reset state
125
+ setSelectedValues([]);
126
+ setFacetValuesRemoved(false);
127
+ setRemovedFacetValues(new Set());
128
+ };
129
+
130
+ // Filter out removed facet values for display
131
+ const getDisplayFacetValues = (product: ProductWithFacetValues) => {
132
+ return product.facetValues.filter(fv => !removedFacetValues.has(fv.id));
60
133
  };
61
134
 
62
135
  return (
63
136
  <Dialog open={open} onOpenChange={onOpenChange}>
64
- <DialogContent className="sm:max-w-[500px]">
137
+ <DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-hidden flex flex-col">
65
138
  <DialogHeader>
66
- <DialogTitle><Trans>Edit facet values</Trans></DialogTitle>
139
+ <DialogTitle>
140
+ <Trans>Edit facet values</Trans>
141
+ </DialogTitle>
67
142
  <DialogDescription>
68
- <Trans>Select facet values to assign to {productIds.length} products</Trans>
143
+ <Trans>Add or remove facet values for {productIds.length} products</Trans>
69
144
  </DialogDescription>
70
145
  </DialogHeader>
71
- <div className="grid gap-4 py-4">
72
- <div className="grid gap-2">
73
- <label className="text-sm font-medium">
74
- <Trans>Facet values</Trans>
75
- </label>
146
+
147
+ <div className="flex-1 overflow-hidden flex flex-col gap-4">
148
+ {/* Add new facet values section */}
149
+ <div className="flex items-center gap-2">
150
+ <div className="text-sm font-medium">
151
+ <Trans>Add facet value</Trans>
152
+ </div>
76
153
  <FacetValueSelector
77
154
  onValueSelect={handleFacetValueSelect}
78
155
  placeholder="Search facet values..."
79
156
  />
80
157
  </div>
81
- {selectedFacetValueIds.length > 0 && (
82
- <div className="text-sm text-muted-foreground">
83
- <Trans>{selectedFacetValueIds.length} facet value(s) selected</Trans>
158
+
159
+ {/* Products table */}
160
+ <div className="flex-1 overflow-auto">
161
+ {isLoading ? (
162
+ <div className="flex items-center justify-center py-8">
163
+ <div className="text-sm text-muted-foreground">Loading...</div>
164
+ </div>
165
+ ) : productsData?.products.items ? (
166
+ <div className="border rounded-md">
167
+ <table className="w-full">
168
+ <thead className="bg-muted/50">
169
+ <tr>
170
+ <th className="text-left p-3 text-sm font-medium">
171
+ <Trans>Product</Trans>
172
+ </th>
173
+ <th className="text-left p-3 text-sm font-medium">
174
+ <Trans>Current facet values</Trans>
175
+ </th>
176
+ </tr>
177
+ </thead>
178
+ <tbody>
179
+ {productsData.products.items.map(product => {
180
+ const displayFacetValues = getDisplayFacetValues(product);
181
+ return (
182
+ <tr key={product.id} className="border-t">
183
+ <td className="p-3 align-top">
184
+ <div className="font-medium">{product.name}</div>
185
+ </td>
186
+ <td className="p-3">
187
+ <div className="flex flex-wrap gap-2">
188
+ {displayFacetValues.map(facetValue => (
189
+ <FacetValueChip
190
+ key={facetValue.id}
191
+ facetValue={facetValue}
192
+ removable={true}
193
+ onRemove={() =>
194
+ removeFacetValue(
195
+ product.id,
196
+ facetValue.id,
197
+ )
198
+ }
199
+ />
200
+ ))}
201
+ {displayFacetValues.length === 0 && (
202
+ <div className="text-sm text-muted-foreground">
203
+ <Trans>No facet values</Trans>
204
+ </div>
205
+ )}
206
+ </div>
207
+ </td>
208
+ </tr>
209
+ );
210
+ })}
211
+ </tbody>
212
+ </table>
213
+ </div>
214
+ ) : null}
215
+ </div>
216
+
217
+ {/* Selected values summary */}
218
+ {selectedValues.length > 0 && (
219
+ <div className="border-t pt-4">
220
+ <div className="text-sm font-medium mb-2">
221
+ <Trans>New facet values to add:</Trans>
222
+ </div>
223
+ <div className="flex flex-wrap gap-2">
224
+ {selectedValues.map(facetValue => (
225
+ <FacetValueChip
226
+ key={facetValue.id}
227
+ facetValue={facetValue}
228
+ removable={false}
229
+ />
230
+ ))}
231
+ </div>
84
232
  </div>
85
233
  )}
86
234
  </div>
235
+
87
236
  <DialogFooter>
88
- <Button variant="outline" onClick={() => onOpenChange(false)}>
237
+ <Button variant="outline" onClick={handleCancel}>
89
238
  <Trans>Cancel</Trans>
90
239
  </Button>
91
- <Button onClick={handleAssign} disabled={selectedFacetValueIds.length === 0 || isPending}>
240
+ <Button
241
+ onClick={handleAssign}
242
+ disabled={(selectedValues.length === 0 && !facetValuesRemoved) || isPending}
243
+ >
92
244
  <Trans>Update</Trans>
93
245
  </Button>
94
246
  </DialogFooter>
95
247
  </DialogContent>
96
248
  </Dialog>
97
249
  );
98
- }
250
+ }
@@ -61,7 +61,7 @@ export const DeleteProductsBulkAction: BulkActionComponent<any> = ({ selection,
61
61
 
62
62
  export const AssignProductsToChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
63
63
  const { refetchPaginatedList } = usePaginatedList();
64
- const { channels, selectedChannel } = useChannel();
64
+ const { channels } = useChannel();
65
65
  const [dialogOpen, setDialogOpen] = useState(false);
66
66
 
67
67
  if (channels.length < 2) {
@@ -167,6 +167,27 @@ export const updateProductsDocument = graphql(`
167
167
  }
168
168
  `);
169
169
 
170
+ export const getProductsWithFacetValuesByIdsDocument = graphql(`
171
+ query GetProductsWithFacetValuesByIds($ids: [String!]!) {
172
+ products(options: { filter: { id: { in: $ids } } }) {
173
+ items {
174
+ id
175
+ name
176
+ facetValues {
177
+ id
178
+ name
179
+ code
180
+ facet {
181
+ id
182
+ name
183
+ code
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ `);
190
+
170
191
  export const duplicateEntityDocument = graphql(`
171
192
  mutation DuplicateEntity($input: DuplicateEntityInput!) {
172
193
  duplicateEntity(input: $input) {
@@ -50,8 +50,7 @@ function ProductDetailPage() {
50
50
  const navigate = useNavigate();
51
51
  const creatingNewEntity = params.id === NEW_ENTITY_PATH;
52
52
  const { i18n } = useLingui();
53
- const refreshRef = useRef<() => void>(() => {
54
- });
53
+ const refreshRef = useRef<() => void>(() => {});
55
54
 
56
55
  const { form, submitHandler, entity, isPending, refreshEntity, resetForm } = useDetailPage({
57
56
  entityName: 'Product',
@@ -90,7 +90,7 @@
90
90
  }
91
91
 
92
92
  .animate-rotate {
93
- animation: rotate 0.5s linear;
93
+ animation: rotate 0.5s linear infinite;
94
94
  }
95
95
  }
96
96
 
@@ -4,6 +4,7 @@ import { DataTablePagination } from '@/components/data-table/data-table-paginati
4
4
  import { DataTableViewOptions } from '@/components/data-table/data-table-view-options.js';
5
5
  import { RefreshButton } from '@/components/data-table/refresh-button.js';
6
6
  import { Input } from '@/components/ui/input.js';
7
+ import { Skeleton } from '@/components/ui/skeleton.js';
7
8
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.js';
8
9
  import { BulkAction } from '@/framework/data-table/data-table-types.js';
9
10
  import { useChannel } from '@/hooks/use-channel.js';
@@ -38,6 +39,7 @@ interface DataTableProps<TData> {
38
39
  columns: ColumnDef<TData, any>[];
39
40
  data: TData[];
40
41
  totalItems: number;
42
+ isLoading?: boolean;
41
43
  page?: number;
42
44
  itemsPerPage?: number;
43
45
  sorting?: SortingState;
@@ -63,6 +65,7 @@ export function DataTable<TData>({
63
65
  columns,
64
66
  data,
65
67
  totalItems,
68
+ isLoading,
66
69
  page,
67
70
  itemsPerPage,
68
71
  sorting: sortingInitialState,
@@ -149,6 +152,7 @@ export function DataTable<TData>({
149
152
  onColumnVisibilityChange?.(table, columnVisibility);
150
153
  }, [columnVisibility]);
151
154
 
155
+ const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
152
156
  return (
153
157
  <>
154
158
  <div className="flex justify-between items-start">
@@ -200,7 +204,7 @@ export function DataTable<TData>({
200
204
  </div>
201
205
  <div className="flex items-center justify-start gap-2">
202
206
  {!disableViewOptions && <DataTableViewOptions table={table} />}
203
- {onRefresh && <RefreshButton onRefresh={onRefresh} />}
207
+ {onRefresh && <RefreshButton onRefresh={onRefresh} isLoading={isLoading ?? false} />}
204
208
  </div>
205
209
  </div>
206
210
  <DataTableBulkActions bulkActions={bulkActions ?? []} table={table} />
@@ -225,18 +229,38 @@ export function DataTable<TData>({
225
229
  ))}
226
230
  </TableHeader>
227
231
  <TableBody>
228
- {table.getRowModel().rows?.length ? (
232
+ {isLoading && !data?.length ? (
233
+ Array.from({ length: pagination.pageSize }).map((_, index) => (
234
+ <TableRow
235
+ key={`skeleton-${index}`}
236
+ className="animate-in fade-in duration-100"
237
+ >
238
+ {Array.from({ length: visibleColumnCount }).map((_, cellIndex) => (
239
+ <TableCell
240
+ key={`skeleton-cell-${index}-${cellIndex}`}
241
+ className="h-12"
242
+ >
243
+ <Skeleton className="h-4 my-2 w-full" />
244
+ </TableCell>
245
+ ))}
246
+ </TableRow>
247
+ ))
248
+ ) : table.getRowModel().rows?.length ? (
229
249
  table.getRowModel().rows.map(row => (
230
- <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
250
+ <TableRow
251
+ key={row.id}
252
+ data-state={row.getIsSelected() && 'selected'}
253
+ className="animate-in fade-in duration-100"
254
+ >
231
255
  {row.getVisibleCells().map(cell => (
232
- <TableCell key={cell.id}>
256
+ <TableCell key={cell.id} className="h-12">
233
257
  {flexRender(cell.column.columnDef.cell, cell.getContext())}
234
258
  </TableCell>
235
259
  ))}
236
260
  </TableRow>
237
261
  ))
238
262
  ) : (
239
- <TableRow>
263
+ <TableRow className="animate-in fade-in duration-100">
240
264
  <TableCell colSpan={columns.length} className="h-24 text-center">
241
265
  No results.
242
266
  </TableCell>
@@ -1,25 +1,37 @@
1
- import React, { useState } from 'react';
2
1
  import { Button } from '@/components/ui/button.js';
3
2
  import { RefreshCw } from 'lucide-react';
3
+ import { useEffect, useState } from 'react';
4
4
 
5
- export function RefreshButton({ onRefresh }: { onRefresh: () => void }) {
6
- const [isRotating, setIsRotating] = useState(false);
5
+ function useDelayedLoading(isLoading: boolean, delayMs: number = 100) {
6
+ const [delayedLoading, setDelayedLoading] = useState(isLoading);
7
7
 
8
- const handleClick = () => {
9
- if (!isRotating) {
10
- setIsRotating(true);
11
- onRefresh();
8
+ useEffect(() => {
9
+ if (isLoading) {
10
+ // When loading starts, wait for the delay before showing loading state
11
+ const timer = setTimeout(() => {
12
+ setDelayedLoading(true);
13
+ }, delayMs);
14
+
15
+ return () => clearTimeout(timer);
16
+ } else {
17
+ // When loading stops, immediately hide loading state
18
+ setDelayedLoading(false);
12
19
  }
20
+ }, [isLoading, delayMs]);
21
+
22
+ return delayedLoading;
23
+ }
24
+
25
+ export function RefreshButton({ onRefresh, isLoading }: { onRefresh: () => void; isLoading: boolean }) {
26
+ const delayedLoading = useDelayedLoading(isLoading, 100);
27
+
28
+ const handleClick = () => {
29
+ onRefresh();
13
30
  };
14
31
 
15
32
  return (
16
- <Button
17
- variant="ghost"
18
- size="sm"
19
- onClick={handleClick}
20
- >
21
- <RefreshCw onAnimationEnd={() => setIsRotating(false)}
22
- className={isRotating ? 'animate-rotate' : ''} />
33
+ <Button variant="ghost" size="sm" onClick={handleClick} disabled={delayedLoading}>
34
+ <RefreshCw className={delayedLoading ? 'animate-rotate' : ''} />
23
35
  </Button>
24
36
  );
25
37
  }
@@ -40,10 +40,9 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Custom
40
40
 
41
41
  const customFields = useCustomFieldConfig(entityType);
42
42
 
43
- const getFieldName = (fieldDefName: string) => {
44
- return formPathPrefix
45
- ? `${formPathPrefix}.customFields.${fieldDefName}`
46
- : `customFields.${fieldDefName}`;
43
+ const getFieldName = (fieldDef: CustomFieldConfig) => {
44
+ const name = fieldDef.type === 'relation' ? fieldDef.name + 'Id' : fieldDef.name;
45
+ return formPathPrefix ? `${formPathPrefix}.customFields.${name}` : `customFields.${name}`;
47
46
  };
48
47
 
49
48
  // Group custom fields by tabs
@@ -86,7 +85,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Custom
86
85
  key={fieldDef.name}
87
86
  fieldDef={fieldDef}
88
87
  control={control}
89
- fieldName={getFieldName(fieldDef.name)}
88
+ fieldName={getFieldName(fieldDef)}
90
89
  getTranslation={getTranslation}
91
90
  />
92
91
  ))}
@@ -112,7 +111,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Custom
112
111
  key={fieldDef.name}
113
112
  fieldDef={fieldDef}
114
113
  control={control}
115
- fieldName={getFieldName(fieldDef.name)}
114
+ fieldName={getFieldName(fieldDef)}
116
115
  getTranslation={getTranslation}
117
116
  />
118
117
  ))}
@@ -7,7 +7,7 @@ import {
7
7
  } from '@/framework/document-introspection/get-document-structure.js';
8
8
  import { useListQueryFields } from '@/framework/document-introspection/hooks.js';
9
9
  import { api } from '@/graphql/api.js';
10
- import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
10
+ import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
11
11
  import { useDebounce } from '@uidotdev/usehooks';
12
12
 
13
13
  import {
@@ -315,7 +315,7 @@ export function PaginatedListDataTable<
315
315
 
316
316
  registerRefresher?.(refetchPaginatedList);
317
317
 
318
- const { data } = useQuery({
318
+ const { data, isFetching } = useQuery({
319
319
  queryFn: () => {
320
320
  const searchFilter = onSearchTermChange ? onSearchTermChange(debouncedSearchTerm) : {};
321
321
  const mergedFilter = { ...filter, ...searchFilter };
@@ -332,6 +332,7 @@ export function PaginatedListDataTable<
332
332
  return api.query(listQuery, transformedVariables);
333
333
  },
334
334
  queryKey,
335
+ placeholderData: keepPreviousData,
335
336
  });
336
337
 
337
338
  const fields = useListQueryFields(listQuery);
@@ -484,6 +485,7 @@ export function PaginatedListDataTable<
484
485
  <DataTable
485
486
  columns={columns}
486
487
  data={transformedData}
488
+ isLoading={isFetching}
487
489
  page={page}
488
490
  itemsPerPage={itemsPerPage}
489
491
  sorting={sorting}
@@ -43,7 +43,11 @@ export function VendureImage({
43
43
  ...imgProps
44
44
  }: VendureImageProps) {
45
45
  if (!asset) {
46
- return fallback ? <>{fallback}</> : <PlaceholderImage preset={preset} width={width} height={height} className={className} />;
46
+ return fallback ? (
47
+ <>{fallback}</>
48
+ ) : (
49
+ <PlaceholderImage preset={preset} width={width} height={height} className={className} />
50
+ );
47
51
  }
48
52
 
49
53
  // Build the URL with query parameters
@@ -75,11 +79,15 @@ export function VendureImage({
75
79
  url.searchParams.set('fpy', asset.focalPoint.y.toString());
76
80
  }
77
81
 
82
+ const minDimensions = getMinDimensions(preset, width, height);
83
+
78
84
  return (
79
85
  <img
80
86
  src={url.toString()}
81
87
  alt={alt || asset.name || ''}
82
88
  className={cn(className, 'rounded-sm')}
89
+ width={minDimensions.width}
90
+ height={minDimensions.height}
83
91
  style={style}
84
92
  loading="lazy"
85
93
  ref={ref}
@@ -88,6 +96,27 @@ export function VendureImage({
88
96
  );
89
97
  }
90
98
 
99
+ function getMinDimensions(preset?: ImagePreset, width?: number, height?: number) {
100
+ if (preset) {
101
+ switch (preset) {
102
+ case 'tiny':
103
+ return { width: 50, height: 50 };
104
+ case 'thumb':
105
+ return { width: 150, height: 150 };
106
+ case 'small':
107
+ return { width: 300, height: 300 };
108
+ case 'medium':
109
+ return { width: 500, height: 500 };
110
+ }
111
+ }
112
+
113
+ if (width && height) {
114
+ return { width, height };
115
+ }
116
+
117
+ return { width: 100, height: 100 };
118
+ }
119
+
91
120
  export function PlaceholderImage({
92
121
  width = 100,
93
122
  height = 100,
@@ -1,5 +1,9 @@
1
1
  import { getOperationVariablesFields } from '@/framework/document-introspection/get-document-structure.js';
2
- import { createFormSchemaFromFields, getDefaultValuesFromFields } from '@/framework/form-engine/form-schema-tools.js';
2
+ import {
3
+ createFormSchemaFromFields,
4
+ getDefaultValuesFromFields,
5
+ } from '@/framework/form-engine/form-schema-tools.js';
6
+ import { transformRelationFields } from '@/framework/form-engine/utils.js';
3
7
  import { useChannel } from '@/hooks/use-channel.js';
4
8
  import { useServerConfig } from '@/hooks/use-server-config.js';
5
9
  import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
@@ -54,7 +58,9 @@ export function useGeneratedForm<
54
58
  },
55
59
  mode: 'onChange',
56
60
  defaultValues,
57
- values: processedEntity ? setValues(processedEntity) : defaultValues,
61
+ values: processedEntity
62
+ ? transformRelationFields(updateFields, setValues(processedEntity))
63
+ : defaultValues,
58
64
  });
59
65
  let submitHandler = (event: FormEvent) => {
60
66
  event.preventDefault();
@@ -0,0 +1,30 @@
1
+ import { FieldInfo } from '../document-introspection/get-document-structure.js';
2
+
3
+ export function transformRelationFields<E extends Record<string, any>>(fields: FieldInfo[], entity: E) {
4
+ const processedEntity = { ...entity } as any;
5
+
6
+ for (const field of fields) {
7
+ if (field.name !== 'customFields' || !field.typeInfo) {
8
+ continue;
9
+ }
10
+
11
+ if (!entity.customFields || !processedEntity.customFields) {
12
+ continue;
13
+ }
14
+
15
+ for (const customField of field.typeInfo) {
16
+ if (customField.type === 'ID') {
17
+ const relationField = customField.name;
18
+ const propertyAccessorKey = customField.name.replace(/Id$/, '');
19
+ const relationValue = entity.customFields[propertyAccessorKey];
20
+ const relationIdValue = relationValue?.id;
21
+
22
+ if (relationIdValue) {
23
+ processedEntity.customFields[relationField] = relationIdValue;
24
+ }
25
+ }
26
+ }
27
+ }
28
+
29
+ return processedEntity;
30
+ }