@vendure/dashboard 3.2.3 → 3.3.0

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.
Files changed (123) hide show
  1. package/dist/plugin/utils/ast-utils.d.ts +10 -0
  2. package/dist/plugin/utils/ast-utils.js +96 -0
  3. package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
  4. package/dist/plugin/utils/ast-utils.spec.js +120 -0
  5. package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
  6. package/dist/plugin/utils/config-loader.js +325 -0
  7. package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
  8. package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +7 -1
  9. package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -3
  10. package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
  11. package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
  12. package/dist/plugin/vite-plugin-config-loader.js +18 -9
  13. package/dist/plugin/vite-plugin-config.js +4 -6
  14. package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
  15. package/dist/plugin/vite-plugin-gql-tada.js +2 -2
  16. package/dist/plugin/vite-plugin-ui-config.js +3 -2
  17. package/package.json +16 -11
  18. package/src/app/app-providers.tsx +9 -9
  19. package/src/app/main.tsx +1 -1
  20. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
  21. package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
  22. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
  23. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
  24. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
  25. package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
  26. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
  27. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
  28. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
  30. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
  31. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -2
  32. package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
  33. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
  34. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
  35. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
  36. package/src/app/routes/_authenticated/_products/products.tsx +1 -1
  37. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -0
  38. package/src/app/routes/_authenticated/_system/job-queue.tsx +7 -8
  39. package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +241 -0
  40. package/src/app/routes/_authenticated.tsx +12 -1
  41. package/src/app/styles.css +15 -0
  42. package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
  43. package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
  44. package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
  45. package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
  46. package/src/lib/components/data-table/data-table-types.ts +1 -0
  47. package/src/lib/components/data-table/data-table-view-options.tsx +73 -24
  48. package/src/lib/components/data-table/data-table.tsx +49 -44
  49. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
  50. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
  51. package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
  52. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
  53. package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
  54. package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
  55. package/src/lib/components/data-table/refresh-button.tsx +25 -0
  56. package/src/lib/components/layout/nav-user.tsx +20 -15
  57. package/src/lib/components/layout/prerelease-popup.tsx +1 -5
  58. package/src/lib/components/shared/alerts.tsx +19 -1
  59. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
  60. package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
  61. package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
  62. package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
  63. package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
  64. package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
  65. package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
  66. package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
  67. package/src/lib/components/shared/custom-fields-form.tsx +4 -3
  68. package/src/lib/components/shared/customer-selector.tsx +13 -14
  69. package/src/lib/components/shared/detail-page-button.tsx +2 -2
  70. package/src/lib/components/shared/entity-assets.tsx +3 -3
  71. package/src/lib/components/shared/error-page.tsx +2 -2
  72. package/src/lib/components/shared/navigation-confirmation.tsx +49 -0
  73. package/src/lib/components/shared/paginated-list-data-table.tsx +10 -1
  74. package/src/lib/components/shared/product-variant-selector.tsx +111 -0
  75. package/src/lib/components/shared/vendure-image.tsx +1 -1
  76. package/src/lib/components/ui/calendar.tsx +508 -63
  77. package/src/lib/framework/alert/alert-extensions.tsx +31 -0
  78. package/src/lib/framework/alert/alert-item.tsx +47 -0
  79. package/src/lib/framework/alert/alerts-indicator.tsx +23 -0
  80. package/src/lib/framework/alert/types.ts +13 -0
  81. package/src/lib/framework/dashboard-widget/base-widget.tsx +1 -0
  82. package/src/lib/framework/defaults.ts +34 -0
  83. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
  84. package/src/lib/framework/document-introspection/get-document-structure.ts +71 -13
  85. package/src/lib/framework/extension-api/define-dashboard-extension.ts +15 -5
  86. package/src/lib/framework/extension-api/extension-api-types.ts +81 -12
  87. package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
  88. package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
  89. package/src/lib/framework/layout-engine/page-layout.tsx +196 -35
  90. package/src/lib/framework/layout-engine/page-provider.tsx +10 -0
  91. package/src/lib/framework/page/detail-page.tsx +62 -9
  92. package/src/lib/framework/page/list-page.tsx +42 -4
  93. package/src/lib/framework/page/page-api.ts +1 -1
  94. package/src/lib/framework/page/use-detail-page.ts +82 -0
  95. package/src/lib/framework/registry/registry-types.ts +6 -2
  96. package/src/lib/graphql/fragments.tsx +8 -0
  97. package/src/lib/graphql/graphql-env.d.ts +25 -9
  98. package/src/lib/hooks/use-auth.tsx +13 -1
  99. package/src/lib/hooks/use-channel.ts +13 -0
  100. package/src/lib/hooks/use-local-format.ts +28 -1
  101. package/src/lib/hooks/use-page.tsx +2 -3
  102. package/src/lib/hooks/use-permissions.ts +13 -0
  103. package/src/lib/index.ts +7 -8
  104. package/src/lib/providers/auth.tsx +22 -9
  105. package/src/lib/providers/channel-provider.tsx +9 -1
  106. package/src/lib/providers/server-config.tsx +7 -1
  107. package/src/lib/providers/user-settings.tsx +24 -0
  108. package/vite/utils/ast-utils.spec.ts +128 -0
  109. package/vite/utils/ast-utils.ts +119 -0
  110. package/vite/utils/config-loader.ts +410 -0
  111. package/vite/{schema-generator.ts → utils/schema-generator.ts} +11 -6
  112. package/vite/{ui-config.ts → utils/ui-config.ts} +7 -3
  113. package/vite/vite-plugin-admin-api-schema.ts +2 -12
  114. package/vite/vite-plugin-config-loader.ts +25 -13
  115. package/vite/vite-plugin-config.ts +1 -0
  116. package/vite/vite-plugin-dashboard-metadata.ts +19 -15
  117. package/vite/vite-plugin-gql-tada.ts +2 -2
  118. package/vite/vite-plugin-ui-config.ts +3 -2
  119. package/dist/plugin/config-loader.js +0 -141
  120. package/src/lib/components/shared/asset-preview.tsx +0 -345
  121. package/src/lib/components/ui/avatar.tsx +0 -38
  122. package/vite/config-loader.ts +0 -181
  123. /package/dist/plugin/{ui-config.d.ts → utils/ui-config.d.ts} +0 -0
@@ -4,7 +4,7 @@ import { useAuth } from '@/hooks/use-auth.js';
4
4
  import { Link, useNavigate, useRouter } from '@tanstack/react-router';
5
5
  import { ChevronsUpDown, LogOut, Monitor, Moon, Sparkles, Sun } from 'lucide-react';
6
6
 
7
- import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.js';
7
+ // import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.js';
8
8
  import {
9
9
  DropdownMenu,
10
10
  DropdownMenuContent,
@@ -44,14 +44,14 @@ export function NavUser() {
44
44
  });
45
45
  };
46
46
 
47
+ const avatarFallback = useMemo(() => {
48
+ return user?.firstName?.charAt(0) ?? '' + user?.lastName?.charAt(0) ?? '';
49
+ }, [user]);
50
+
47
51
  if (!user) {
48
52
  return <></>;
49
53
  }
50
54
 
51
- const avatarFallback = useMemo(() => {
52
- return user.firstName.charAt(0) + user.lastName.charAt(0);
53
- }, [user]);
54
-
55
55
  const isDevMode = (import.meta as any).env?.MODE === 'development';
56
56
 
57
57
  return (
@@ -64,10 +64,15 @@ export function NavUser() {
64
64
  size="lg"
65
65
  className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
66
66
  >
67
- <Avatar className="h-8 w-8 rounded-lg">
68
- <AvatarImage src={user.id} alt={user.firstName} />
69
- <AvatarFallback className="rounded-lg">{avatarFallback}</AvatarFallback>
70
- </Avatar>
67
+ {/* Avatar component temporarily disabled due to https://github.com/radix-ui/primitives/issues/3489
68
+ error in published package version */}
69
+ {/*<Avatar className="h-8 w-8 rounded-lg">*/}
70
+ {/* <AvatarImage src={user.id} alt={user.firstName} />*/}
71
+ {/* <AvatarFallback className="rounded-lg">{avatarFallback}</AvatarFallback>*/}
72
+ {/*</Avatar>*/}
73
+ <div className='relative flex rounded-lg border justify-center items-center w-8 h-8'>
74
+ {avatarFallback}
75
+ </div>
71
76
  <div className="grid flex-1 text-left text-sm leading-tight">
72
77
  <span className="truncate font-semibold">
73
78
  {user.firstName} {user.lastName}
@@ -85,12 +90,12 @@ export function NavUser() {
85
90
  >
86
91
  <DropdownMenuLabel className="p-0 font-normal">
87
92
  <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
88
- <Avatar className="h-8 w-8 rounded-lg">
89
- <AvatarImage src={user.id} alt={user.firstName} />
90
- <AvatarFallback className="rounded-lg">
91
- {avatarFallback}
92
- </AvatarFallback>
93
- </Avatar>
93
+ {/*<Avatar className="h-8 w-8 rounded-lg">*/}
94
+ {/* <AvatarImage src={user.id} alt={user.firstName} />*/}
95
+ {/* <AvatarFallback className="rounded-lg">*/}
96
+ {/* {avatarFallback}*/}
97
+ {/* </AvatarFallback>*/}
98
+ {/*</Avatar>*/}
94
99
  <div className="grid flex-1 text-left text-sm leading-tight">
95
100
  <span className="truncate font-semibold">
96
101
  {user.firstName} {user.lastName}
@@ -11,17 +11,13 @@ export function PrereleasePopup() {
11
11
  description: (
12
12
  <div className="space-y-2">
13
13
  <p>
14
- This is an <span className="font-bold">alpha</span> version of our new Vendure
14
+ This is a <span className="font-bold">beta</span> version of our new Vendure
15
15
  Dashboard!
16
16
  </p>
17
17
  <p>
18
18
  This release allows you to explore the new interface and functionality, but it's not
19
19
  yet ready for production use.
20
20
  </p>
21
- <p>
22
- If you find missing or broken functionality, you don't need to report it on GitHub at
23
- this point - we're already working on it!
24
- </p>
25
21
  </div>
26
22
  ),
27
23
  duration: 1000 * 60,
@@ -1,19 +1,37 @@
1
1
  import { BellIcon } from 'lucide-react';
2
2
  import { Button } from '../ui/button.js';
3
3
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog.js';
4
+ import { useAlerts } from '../../framework/alert/alert-extensions.js';
5
+ import { AlertItem } from '../../framework/alert/alert-item.js';
6
+ import { ScrollArea } from '../ui/scroll-area.js';
7
+ import { AlertsIndicator } from '../../framework/alert/alerts-indicator.js';
4
8
 
5
9
  export function Alerts() {
10
+ const { alerts } = useAlerts();
11
+
12
+ if (alerts.length === 0) {
13
+ return null;
14
+ }
15
+
6
16
  return (
7
17
  <Dialog>
8
18
  <DialogTrigger asChild>
9
- <Button size="icon" variant="ghost">
19
+ <Button size="icon" variant="ghost" className="relative">
10
20
  <BellIcon />
21
+ <AlertsIndicator />
11
22
  </Button>
12
23
  </DialogTrigger>
13
24
  <DialogContent>
14
25
  <DialogHeader>
15
26
  <DialogTitle>Alerts</DialogTitle>
16
27
  </DialogHeader>
28
+ <ScrollArea className="max-h-[500px]">
29
+ <div className="flex flex-col divide-y divide-border">
30
+ {alerts.map(alert => (
31
+ <AlertItem className="py-2" key={alert.id} alert={alert} />
32
+ ))}
33
+ </div>
34
+ </ScrollArea>
17
35
  </DialogContent>
18
36
  </Dialog>
19
37
  );
@@ -0,0 +1,93 @@
1
+ import { Button } from "@/components/ui/button.js";
2
+ import { cn } from "@/lib/utils.js";
3
+ import { Crosshair, X } from "lucide-react";
4
+ import { useRef, useState } from "react";
5
+ import { DndContext, useDraggable } from "@dnd-kit/core";
6
+ import { restrictToParentElement } from "@dnd-kit/modifiers";
7
+ import { CSS } from "@dnd-kit/utilities";
8
+ import { Trans } from "@/lib/trans.js";
9
+
10
+ export interface AssetFocalPointEditorProps {
11
+ settingFocalPoint: boolean;
12
+ focalPoint: Point | undefined;
13
+ width: number;
14
+ height: number;
15
+ onFocalPointChange: (point: Point) => void;
16
+ onCancel: () => void;
17
+ children?: React.ReactNode;
18
+ }
19
+
20
+ interface Point {
21
+ x: number;
22
+ y: number;
23
+ }
24
+
25
+ function DraggableFocalPoint({ point }: { point: Point }) {
26
+ const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
27
+ id: "focal-point",
28
+ });
29
+ const style = {
30
+ left: `${point.x * 100}%`,
31
+ top: `${point.y * 100}%`,
32
+ transform: isDragging ? `translate(-50%, -50%) ${CSS.Translate.toString(transform)}` : `translate(-50%, -50%)`,
33
+ };
34
+
35
+ return (
36
+ <div
37
+ ref={setNodeRef}
38
+ style={style}
39
+ {...listeners}
40
+ {...attributes}
41
+ className="absolute w-8 h-8 rounded-full border-4 border-white bg-brand/20 shadow-lg cursor-move"
42
+ />
43
+ );
44
+ }
45
+
46
+ export function AssetFocalPointEditor({ settingFocalPoint, focalPoint, width, height, onFocalPointChange, onCancel, children }: AssetFocalPointEditorProps) {
47
+
48
+ const [focalPointCurrent, setFocalPointCurrent] = useState<Point>(focalPoint ?? { x: 0.5, y: 0.5 });
49
+
50
+ const handleDragEnd = (event: any) => {
51
+ const { delta } = event;
52
+ const newX = Math.max(0, Math.min(1, focalPointCurrent.x + delta.x / width));
53
+ const newY = Math.max(0, Math.min(1, focalPointCurrent.y + delta.y / height));
54
+ const newPoint = { x: newX, y: newY };
55
+ setFocalPointCurrent(newPoint);
56
+ };
57
+
58
+ return (
59
+ <div
60
+ className={cn(
61
+ 'relative',
62
+ 'flex items-center justify-center',
63
+ settingFocalPoint && 'cursor-crosshair',
64
+ )}
65
+ >
66
+ <div className="relative" style={{ width: `${width}px`, height: `${height}px` }}>
67
+ {children}
68
+ {settingFocalPoint && (
69
+ <DndContext onDragEnd={handleDragEnd} modifiers={[restrictToParentElement]}>
70
+ <DraggableFocalPoint
71
+ point={focalPointCurrent}
72
+ />
73
+ </DndContext>
74
+ )}
75
+ </div>
76
+
77
+ {settingFocalPoint && (
78
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
79
+ <Button type="button" variant="secondary" onClick={onCancel}>
80
+ <X className="mr-2 h-4 w-4" />
81
+ <Trans>Cancel</Trans>
82
+ </Button>
83
+ <Button type="button" onClick={() => {
84
+ onFocalPointChange(focalPointCurrent);
85
+ }}>
86
+ <Crosshair className="mr-2 h-4 w-4" />
87
+ <Trans>Set Focal Point</Trans>
88
+ </Button>
89
+ </div>
90
+ )}
91
+ </div>
92
+ );
93
+ }
@@ -23,6 +23,7 @@ import { Loader2, Search, Upload, X } from 'lucide-react';
23
23
  import { useCallback, useState } from 'react';
24
24
  import { useDropzone } from 'react-dropzone';
25
25
  import { useDebounce } from '@uidotdev/usehooks';
26
+ import { DetailPageButton } from '../detail-page-button.js';
26
27
 
27
28
  const getAssetListDocument = graphql(
28
29
  `
@@ -72,7 +73,14 @@ export type Asset = AssetFragment;
72
73
  export interface AssetGalleryProps {
73
74
  onSelect?: (assets: Asset[]) => void;
74
75
  selectable?: boolean;
75
- multiSelect?: boolean;
76
+ /**
77
+ * @description
78
+ * Defines whether multiple assets can be selected.
79
+ *
80
+ * If set to 'auto', the asset selection will be toggled when the user clicks on an asset.
81
+ * If set to 'manual', multiple selection will occur only if the user holds down the control/cmd key.
82
+ */
83
+ multiSelect?: 'auto' | 'manual';
76
84
  initialSelectedAssets?: Asset[];
77
85
  pageSize?: number;
78
86
  fixedHeight?: boolean;
@@ -84,7 +92,7 @@ export interface AssetGalleryProps {
84
92
  export function AssetGallery({
85
93
  onSelect,
86
94
  selectable = true,
87
- multiSelect = false,
95
+ multiSelect = undefined,
88
96
  initialSelectedAssets = [],
89
97
  pageSize = 24,
90
98
  fixedHeight = false,
@@ -149,24 +157,44 @@ export function AssetGallery({
149
157
  const totalPages = Math.ceil(totalItems / pageSize);
150
158
 
151
159
  // Handle selection
152
- const handleSelect = (asset: Asset) => {
153
- if (!multiSelect) {
154
- setSelected([asset]);
155
- onSelect?.([asset]);
160
+ const handleSelect = (asset: Asset, event: React.MouseEvent) => {
161
+ if (multiSelect === 'auto') {
162
+ const isSelected = selected.some(a => a.id === asset.id);
163
+ let newSelected: Asset[];
164
+
165
+ if (isSelected) {
166
+ newSelected = selected.filter(a => a.id !== asset.id);
167
+ } else {
168
+ newSelected = [...selected, asset];
169
+ }
170
+
171
+ setSelected(newSelected);
172
+ onSelect?.(newSelected);
156
173
  return;
157
174
  }
158
175
 
159
- const isSelected = selected.some(a => a.id === asset.id);
160
- let newSelected: Asset[];
161
176
 
162
- if (isSelected) {
163
- newSelected = selected.filter(a => a.id !== asset.id);
177
+ // Manual mode - check for modifier key
178
+ const isModifierKeyPressed = event.metaKey || event.ctrlKey;
179
+
180
+ if (multiSelect === 'manual' && isModifierKeyPressed) {
181
+ // Toggle selection
182
+ const isSelected = selected.some(a => a.id === asset.id);
183
+ let newSelected: Asset[];
184
+
185
+ if (isSelected) {
186
+ newSelected = selected.filter(a => a.id !== asset.id);
187
+ } else {
188
+ newSelected = [...selected, asset];
189
+ }
190
+
191
+ setSelected(newSelected);
192
+ onSelect?.(newSelected);
164
193
  } else {
165
- newSelected = [...selected, asset];
194
+ // No modifier key - single select
195
+ setSelected([asset]);
196
+ onSelect?.([asset]);
166
197
  }
167
-
168
- setSelected(newSelected);
169
- onSelect?.(newSelected);
170
198
  };
171
199
 
172
200
  // Check if an asset is selected
@@ -272,7 +300,7 @@ export function AssetGallery({
272
300
  ${isSelected(asset as Asset) ? 'ring-2 ring-primary' : ''}
273
301
  flex flex-col min-w-[120px]
274
302
  `}
275
- onClick={() => handleSelect(asset as Asset)}
303
+ onClick={(e) => handleSelect(asset as Asset, e)}
276
304
  >
277
305
  <div
278
306
  className="relative w-full bg-muted/30"
@@ -296,11 +324,14 @@ export function AssetGallery({
296
324
  <p className="text-xs line-clamp-2 min-h-[2.5rem]" title={asset.name}>
297
325
  {asset.name}
298
326
  </p>
299
- {asset.fileSize && (
300
- <p className="text-xs text-muted-foreground mt-1">
301
- {formatFileSize(asset.fileSize)}
302
- </p>
303
- )}
327
+ <div className='flex justify-between items-center'>
328
+ {asset.fileSize && (
329
+ <p className="text-xs text-muted-foreground mt-1">
330
+ {formatFileSize(asset.fileSize)}
331
+ </p>
332
+ )}
333
+ <DetailPageButton id={asset.id} label={<Trans>Edit</Trans>} />
334
+ </div>
304
335
  </CardContent>
305
336
  </Card>
306
337
  ))
@@ -47,7 +47,7 @@ export function AssetPickerDialog({
47
47
  <div className="flex-grow py-4">
48
48
  <AssetGallery
49
49
  onSelect={handleAssetSelect}
50
- multiSelect={multiSelect}
50
+ multiSelect='manual'
51
51
  initialSelectedAssets={initialSelectedAssets}
52
52
  fixedHeight={true}
53
53
  />
@@ -12,9 +12,7 @@ interface AssetPreviewDialogProps {
12
12
  onOpenChange: (open: boolean) => void;
13
13
  asset: AssetWithTags;
14
14
  assets?: AssetWithTags[];
15
- editable?: boolean;
16
15
  customFields?: any[];
17
- onAssetChange?: (asset: Partial<AssetWithTags>) => void;
18
16
  }
19
17
 
20
18
  export function AssetPreviewDialog({
@@ -22,24 +20,20 @@ export function AssetPreviewDialog({
22
20
  onOpenChange,
23
21
  asset,
24
22
  assets,
25
- editable,
26
23
  customFields,
27
- onAssetChange,
28
24
  }: AssetPreviewDialogProps) {
29
25
  return (
30
26
  <Dialog open={open} onOpenChange={onOpenChange}>
31
27
  <DialogContent className="sm:max-w-[800px] lg:max-w-[95vw] w-[95vw] p-0">
32
28
  <DialogHeader className="p-6 pb-0">
33
29
  <DialogTitle>Asset</DialogTitle>
34
- <DialogDescription>Description goes here</DialogDescription>
30
+ <DialogDescription>Preview of {asset.name}</DialogDescription>
35
31
  </DialogHeader>
36
32
  <div className="h-full p-6">
37
33
  <AssetPreview
38
34
  asset={asset}
39
35
  assets={assets}
40
- editable={editable}
41
36
  customFields={customFields}
42
- onAssetChange={onAssetChange}
43
37
  />
44
38
  </div>
45
39
  </DialogContent>
@@ -0,0 +1,34 @@
1
+
2
+
3
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select.js";
4
+ import { PreviewPreset } from "./asset-preview.js";
5
+
6
+ export interface AssetPreviewSelectorProps {
7
+ size: PreviewPreset;
8
+ setSize: (size: PreviewPreset) => void;
9
+ width: number;
10
+ height: number;
11
+ }
12
+
13
+ export function AssetPreviewSelector({ size, setSize, width, height }: AssetPreviewSelectorProps) {
14
+ return (
15
+ <div className="flex items-center gap-2">
16
+ <Select value={size} onValueChange={value => setSize(value as PreviewPreset)}>
17
+ <SelectTrigger>
18
+ <SelectValue placeholder="Select size" />
19
+ </SelectTrigger>
20
+ <SelectContent>
21
+ <SelectItem value="tiny">Tiny</SelectItem>
22
+ <SelectItem value="thumb">Thumb</SelectItem>
23
+ <SelectItem value="small">Small</SelectItem>
24
+ <SelectItem value="medium">Medium</SelectItem>
25
+ <SelectItem value="large">Large</SelectItem>
26
+ <SelectItem value="full">Full Size</SelectItem>
27
+ </SelectContent>
28
+ </Select>
29
+ <p className="text-sm text-muted-foreground">
30
+ {width} x {height}
31
+ </p>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,128 @@
1
+ import { VendureImage } from '@/components/shared/vendure-image.js';
2
+ import { Button } from '@/components/ui/button.js';
3
+ import { Card, CardContent } from '@/components/ui/card.js';
4
+ import { AssetFragment } from '@/graphql/fragments.js';
5
+ import { cn } from '@/lib/utils.js';
6
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
7
+ import { useEffect, useRef, useState } from 'react';
8
+ import { useForm } from 'react-hook-form';
9
+ import { AssetPreviewSelector } from './asset-preview-selector.js';
10
+ import { AssetProperties } from './asset-properties.js';
11
+
12
+ export type PreviewPreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | '';
13
+
14
+
15
+ export type AssetWithTags = AssetFragment & { tags?: { value: string }[] };
16
+
17
+ interface AssetPreviewProps {
18
+ asset: AssetWithTags;
19
+ assets?: AssetWithTags[];
20
+ customFields?: any[];
21
+ }
22
+
23
+ export function AssetPreview({
24
+ asset,
25
+ assets,
26
+ customFields = [],
27
+ }: AssetPreviewProps) {
28
+ const [size, setSize] = useState<PreviewPreset>('medium');
29
+ const [width, setWidth] = useState(0);
30
+ const [height, setHeight] = useState(0);
31
+ const [centered, setCentered] = useState(true);
32
+ const [assetIndex, setAssetIndex] = useState(assets?.indexOf(asset) || 0);
33
+
34
+ const imageRef = useRef<HTMLImageElement>(null);
35
+ const containerRef = useRef<HTMLDivElement>(null);
36
+
37
+ const form = useForm({
38
+ defaultValues: {
39
+ name: asset.name,
40
+ tags: asset.tags?.map(t => t.value) || [],
41
+ },
42
+ });
43
+ const activeAsset = assets?.[assetIndex] ?? asset;
44
+
45
+ useEffect(() => {
46
+ if (assets?.length) {
47
+ const index = assets.findIndex(a => a.id === asset.id);
48
+ setAssetIndex(index === -1 ? 0 : index);
49
+ }
50
+ }, [assets, asset.id]);
51
+
52
+ useEffect(() => {
53
+ const handleResize = () => {
54
+ updateDimensions();
55
+ };
56
+
57
+ window.addEventListener('resize', handleResize);
58
+ return () => window.removeEventListener('resize', handleResize);
59
+ }, []);
60
+
61
+ const updateDimensions = () => {
62
+ if (!imageRef.current || !containerRef.current) return;
63
+
64
+ const img = imageRef.current;
65
+ const container = containerRef.current;
66
+ const imgWidth = img.naturalWidth;
67
+ const imgHeight = img.naturalHeight;
68
+ const containerWidth = container.offsetWidth;
69
+ const containerHeight = container.offsetHeight;
70
+ setWidth(imgWidth);
71
+ setHeight(imgHeight);
72
+ setCentered(imgWidth <= containerWidth && imgHeight <= containerHeight);
73
+ };
74
+
75
+ return (
76
+ <div className="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-4 h-full">
77
+ <div className="space-y-4">
78
+ <Card>
79
+ <CardContent className="pt-6 space-y-4">
80
+ <AssetProperties asset={activeAsset} />
81
+ <AssetPreviewSelector size={size} setSize={setSize} width={width} height={height} />
82
+ </CardContent>
83
+ </Card>
84
+ </div>
85
+
86
+ <div className="relative flex items-center justify-center bg-muted/30 rounded-lg">
87
+ {assets && assets.length > 1 && (
88
+ <>
89
+ <Button
90
+ variant="ghost"
91
+ size="icon"
92
+ className="absolute left-4 z-10"
93
+ onClick={() => setAssetIndex(i => i - 1)}
94
+ disabled={assetIndex === 0}
95
+ >
96
+ <ChevronLeft className="h-4 w-4" />
97
+ </Button>
98
+ <Button
99
+ variant="ghost"
100
+ size="icon"
101
+ className="absolute right-4 z-10"
102
+ onClick={() => setAssetIndex(i => i + 1)}
103
+ disabled={assetIndex === assets.length - 1}
104
+ >
105
+ <ChevronRight className="h-4 w-4" />
106
+ </Button>
107
+ </>
108
+ )}
109
+ <div
110
+ ref={containerRef}
111
+ className={cn(
112
+ 'relative',
113
+ centered && 'flex items-center justify-center',
114
+ )}
115
+ >
116
+ <VendureImage
117
+ ref={imageRef}
118
+ asset={activeAsset}
119
+ preset={size || undefined}
120
+ mode="resize"
121
+ onLoad={updateDimensions}
122
+ className="max-w-full max-h-full object-contain"
123
+ />
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,46 @@
1
+ import { formatFileSize } from "@/lib/utils.js";
2
+
3
+ import { Label } from "@/components/ui/label.js";
4
+ import { AssetFragment } from "@/graphql/fragments.js";
5
+ import { ExternalLink } from "lucide-react";
6
+
7
+ export interface AssetPropertiesProps {
8
+ asset: AssetFragment;
9
+ }
10
+
11
+ export function AssetProperties({ asset }: AssetPropertiesProps) {
12
+ return (
13
+ <div className="space-y-4">
14
+ <div>
15
+ <Label>Name</Label>
16
+ <p className="truncate text-sm text-muted-foreground">{asset.name}</p>
17
+ </div>
18
+ <div>
19
+ <Label>Source File</Label>
20
+ <a
21
+ href={asset.source}
22
+ target="_blank"
23
+ rel="noopener noreferrer"
24
+ className="text-sm text-primary hover:underline"
25
+ >
26
+ {asset.source.split('/').pop()}
27
+ <ExternalLink className="ml-1 h-3 w-3 inline" />
28
+ </a>
29
+ </div>
30
+
31
+ <div>
32
+ <Label>File Size</Label>
33
+ <p className="text-sm text-muted-foreground">
34
+ {formatFileSize(asset.fileSize)}
35
+ </p>
36
+ </div>
37
+
38
+ <div>
39
+ <Label>Dimensions</Label>
40
+ <p className="text-sm text-muted-foreground">
41
+ {asset.width} x {asset.height}
42
+ </p>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { cn } from '@/lib/utils.js';
3
3
 
4
- interface Point {
4
+ export interface Point {
5
5
  x: number;
6
6
  y: number;
7
7
  }
@@ -22,9 +22,10 @@ type CustomFieldConfig = ResultOf<typeof customFieldConfigFragment>;
22
22
  interface CustomFieldsFormProps {
23
23
  entityType: string;
24
24
  control: Control<any, any>;
25
+ formPathPrefix?: string;
25
26
  }
26
27
 
27
- export function CustomFieldsForm({ entityType, control }: CustomFieldsFormProps) {
28
+ export function CustomFieldsForm({ entityType, control, formPathPrefix }: CustomFieldsFormProps) {
28
29
  const {
29
30
  settings: { displayLanguage },
30
31
  } = useUserSettings();
@@ -39,7 +40,7 @@ export function CustomFieldsForm({ entityType, control }: CustomFieldsFormProps)
39
40
  {fieldDef.type === 'localeString' || fieldDef.type === 'localeText' ? (
40
41
  <TranslatableFormField
41
42
  control={control}
42
- name={`customFields.${fieldDef.name}`}
43
+ name={formPathPrefix ? `${formPathPrefix}.customFields.${fieldDef.name}` : `customFields.${fieldDef.name}`}
43
44
  render={({ field }) => (
44
45
  <FormItem>
45
46
  <FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
@@ -52,7 +53,7 @@ export function CustomFieldsForm({ entityType, control }: CustomFieldsFormProps)
52
53
  ) : (
53
54
  <FormField
54
55
  control={control}
55
- name={`customFields.${fieldDef.name}`}
56
+ name={formPathPrefix ? `${formPathPrefix}.customFields.${fieldDef.name}` : `customFields.${fieldDef.name}`}
56
57
  render={({ field }) => (
57
58
  <FormItem>
58
59
  <FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>