@vendure/dashboard 3.4.3-master-202509190229 → 3.4.3-master-202509230228

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 (60) hide show
  1. package/dist/vite/vite-plugin-config.js +1 -0
  2. package/package.json +4 -4
  3. package/src/app/routes/_authenticated/_administrators/administrators.tsx +1 -2
  4. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +39 -0
  5. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +18 -7
  6. package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +206 -0
  7. package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +226 -0
  8. package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +217 -0
  9. package/src/app/routes/_authenticated/_channels/channels.tsx +1 -2
  10. package/src/app/routes/_authenticated/_collections/collections.tsx +2 -16
  11. package/src/app/routes/_authenticated/_countries/countries.graphql.ts +2 -0
  12. package/src/app/routes/_authenticated/_countries/countries.tsx +1 -2
  13. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +1 -2
  14. package/src/app/routes/_authenticated/_customers/customers.tsx +1 -2
  15. package/src/app/routes/_authenticated/_facets/facets.tsx +0 -1
  16. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +302 -0
  17. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +16 -0
  18. package/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx +61 -0
  19. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +17 -10
  20. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +31 -0
  21. package/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx +50 -0
  22. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +17 -290
  23. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +7 -39
  24. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +4 -26
  25. package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +129 -0
  26. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +8 -0
  27. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +1 -2
  28. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +1 -2
  29. package/src/app/routes/_authenticated/_products/products.tsx +1 -2
  30. package/src/app/routes/_authenticated/_promotions/promotions.tsx +1 -2
  31. package/src/app/routes/_authenticated/_roles/components/permissions-table-grid.tsx +251 -0
  32. package/src/app/routes/_authenticated/_roles/roles.tsx +1 -2
  33. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -3
  34. package/src/app/routes/_authenticated/_sellers/sellers.tsx +1 -2
  35. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +1 -2
  36. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +1 -2
  37. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +1 -2
  38. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +1 -2
  39. package/src/app/routes/_authenticated/_zones/zones.tsx +1 -2
  40. package/src/lib/components/data-table/data-table-bulk-actions.tsx +5 -14
  41. package/src/lib/components/data-table/use-all-bulk-actions.ts +19 -0
  42. package/src/lib/components/data-table/use-generated-columns.tsx +12 -3
  43. package/src/lib/components/layout/nav-main.tsx +50 -25
  44. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
  45. package/src/lib/components/shared/asset/asset-gallery.tsx +83 -50
  46. package/src/lib/components/shared/detail-page-button.tsx +6 -4
  47. package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
  48. package/src/lib/components/shared/vendure-image.tsx +9 -1
  49. package/src/lib/framework/defaults.ts +24 -0
  50. package/src/lib/framework/extension-api/types/navigation.ts +8 -0
  51. package/src/lib/framework/layout-engine/page-layout.tsx +96 -9
  52. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +26 -0
  53. package/src/lib/framework/page/list-page.tsx +7 -0
  54. package/src/lib/hooks/use-custom-field-config.ts +19 -2
  55. package/src/lib/index.ts +7 -1
  56. package/src/lib/providers/channel-provider.tsx +22 -6
  57. package/src/lib/providers/server-config.tsx +1 -0
  58. package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +0 -33
  59. package/src/app/routes/_authenticated/_roles/components/permissions-grid.tsx +0 -120
  60. package/src/lib/components/shared/asset/focal-point-control.tsx +0 -57
@@ -8,7 +8,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
8
8
  import { PlusIcon } from 'lucide-react';
9
9
  import { DeleteZonesBulkAction } from './components/zone-bulk-actions.js';
10
10
  import { ZoneCountriesSheet } from './components/zone-countries-sheet.js';
11
- import { deleteZoneDocument, zoneListQuery } from './zones.graphql.js';
11
+ import { zoneListQuery } from './zones.graphql.js';
12
12
 
13
13
  export const Route = createFileRoute('/_authenticated/_zones/zones')({
14
14
  component: ZoneListPage,
@@ -20,7 +20,6 @@ function ZoneListPage() {
20
20
  <ListPage
21
21
  pageId="zone-list"
22
22
  listQuery={zoneListQuery}
23
- deleteMutation={deleteZoneDocument}
24
23
  route={Route}
25
24
  title="Zones"
26
25
  defaultVisibility={{
@@ -1,5 +1,4 @@
1
- 'use client';
2
-
1
+ import { useAllBulkActions } from '@/vdb/components/data-table/use-all-bulk-actions.js';
3
2
  import { Button } from '@/vdb/components/ui/button.js';
4
3
  import {
5
4
  DropdownMenu,
@@ -7,11 +6,8 @@ import {
7
6
  DropdownMenuItem,
8
7
  DropdownMenuTrigger,
9
8
  } from '@/vdb/components/ui/dropdown-menu.js';
10
- import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
11
9
  import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
12
10
  import { useFloatingBulkActions } from '@/vdb/hooks/use-floating-bulk-actions.js';
13
- import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
14
- import { usePage } from '@/vdb/hooks/use-page.js';
15
11
  import { Trans } from '@/vdb/lib/trans.js';
16
12
  import { Table } from '@tanstack/react-table';
17
13
  import { ChevronDown } from 'lucide-react';
@@ -26,9 +22,7 @@ export function DataTableBulkActions<TData>({
26
22
  table,
27
23
  bulkActions,
28
24
  }: Readonly<DataTableBulkActionsProps<TData>>) {
29
- const { pageId } = usePage();
30
- const pageBlock = usePageBlock();
31
- const blockId = pageBlock?.blockId;
25
+ const allBulkActions = useAllBulkActions(bulkActions);
32
26
 
33
27
  // Cache to store selected items across page changes
34
28
  const selectedItemsCache = useRef<Map<string, TData>>(new Map());
@@ -63,18 +57,15 @@ export function DataTableBulkActions<TData>({
63
57
  if (!shouldShow) {
64
58
  return null;
65
59
  }
66
- const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
67
- const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
68
- allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
69
60
 
70
61
  return (
71
62
  <div
72
63
  className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 fixed transform -translate-x-1/2 bg-white shadow-2xl rounded-md border z-50"
73
- style={{
74
- height: 'auto',
64
+ style={{
65
+ height: 'auto',
75
66
  maxHeight: '60px',
76
67
  bottom: position.bottom,
77
- left: position.left
68
+ left: position.left,
78
69
  }}
79
70
  >
80
71
  <span className="text-sm text-muted-foreground">
@@ -0,0 +1,19 @@
1
+ import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
2
+ import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
3
+ import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
4
+ import { usePage } from '@/vdb/hooks/use-page.js';
5
+
6
+ /**
7
+ * @description
8
+ * Augments the provided Bulk Actions with any user-defined actions for the current
9
+ * page & block, and returns all of the bulk actions sorted by the `order` property.
10
+ */
11
+ export function useAllBulkActions(bulkActions: BulkAction[]): BulkAction[] {
12
+ const { pageId } = usePage();
13
+ const pageBlock = usePageBlock();
14
+ const blockId = pageBlock?.blockId;
15
+ const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
16
+ const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
17
+ allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
18
+ return allBulkActions;
19
+ }
@@ -1,9 +1,11 @@
1
+ import { useAllBulkActions } from '@/vdb/components/data-table/use-all-bulk-actions.js';
1
2
  import { DisplayComponent } from '@/vdb/framework/component-registry/display-component.js';
2
3
  import {
3
4
  FieldInfo,
4
5
  getOperationVariablesFields,
5
6
  getTypeFieldInfo,
6
7
  } from '@/vdb/framework/document-introspection/get-document-structure.js';
8
+ import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
7
9
  import { api } from '@/vdb/graphql/api.js';
8
10
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
9
11
  import { TypedDocumentNode } from '@graphql-typed-document-node/core';
@@ -51,6 +53,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
51
53
  fields,
52
54
  customizeColumns,
53
55
  rowActions,
56
+ bulkActions,
54
57
  deleteMutation,
55
58
  additionalColumns,
56
59
  defaultColumnOrder,
@@ -62,6 +65,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
62
65
  fields: FieldInfo[];
63
66
  customizeColumns?: CustomizeColumnConfig<T>;
64
67
  rowActions?: RowAction<PaginatedListItemFields<T>>[];
68
+ bulkActions?: BulkAction[];
65
69
  deleteMutation?: TypedDocumentNode<any, any>;
66
70
  additionalColumns?: AdditionalColumns<T>;
67
71
  defaultColumnOrder?: Array<string | number | symbol>;
@@ -71,6 +75,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
71
75
  enableSorting?: boolean;
72
76
  }>) {
73
77
  const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
78
+ const allBulkActions = useAllBulkActions(bulkActions ?? []);
74
79
 
75
80
  const { columns, customFieldColumnNames } = useMemo(() => {
76
81
  const columnConfigs: Array<{ fieldInfo: FieldInfo; isCustomField: boolean }> = [];
@@ -169,8 +174,8 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
169
174
  finalColumns = [...orderedColumns, ...remainingColumns];
170
175
  }
171
176
 
172
- if (includeActionsColumn && (rowActions || deleteMutation)) {
173
- const rowActionColumn = getRowActions(rowActions, deleteMutation);
177
+ if (includeActionsColumn && (rowActions || deleteMutation || bulkActions)) {
178
+ const rowActionColumn = getRowActions(rowActions, deleteMutation, allBulkActions);
174
179
  if (rowActionColumn) {
175
180
  finalColumns.push(rowActionColumn);
176
181
  }
@@ -212,13 +217,14 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
212
217
  function getRowActions(
213
218
  rowActions?: RowAction<any>[],
214
219
  deleteMutation?: TypedDocumentNode<any, any>,
220
+ bulkActions?: BulkAction[],
215
221
  ): AccessorKeyColumnDef<any> | undefined {
216
222
  return {
217
223
  id: 'actions',
218
224
  accessorKey: 'actions',
219
225
  header: () => <Trans>Actions</Trans>,
220
226
  enableColumnFilter: false,
221
- cell: ({ row }) => {
227
+ cell: ({ row, table }) => {
222
228
  return (
223
229
  <DropdownMenu>
224
230
  <DropdownMenuTrigger asChild>
@@ -235,6 +241,9 @@ function getRowActions(
235
241
  {action.label}
236
242
  </DropdownMenuItem>
237
243
  ))}
244
+ {bulkActions?.map((action, index) => (
245
+ <action.component key={`bulk-action-${index}`} selection={[row]} table={table} />
246
+ ))}
238
247
  {deleteMutation && (
239
248
  <DeleteMutationRowAction deleteMutation={deleteMutation} row={row} />
240
249
  )}
@@ -14,6 +14,7 @@ import {
14
14
  NavMenuSection,
15
15
  NavMenuSectionPlacement,
16
16
  } from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
17
+ import { usePermissions } from '@/vdb/hooks/use-permissions.js';
17
18
  import { Link, useRouter, useRouterState } from '@tanstack/react-router';
18
19
  import { ChevronRight } from 'lucide-react';
19
20
  import * as React from 'react';
@@ -39,6 +40,7 @@ function escapeRegexChars(str: string): string {
39
40
  export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavMenuItem> }>) {
40
41
  const router = useRouter();
41
42
  const routerState = useRouterState();
43
+ const { hasPermissions } = usePermissions();
42
44
  const currentPath = routerState.location.pathname;
43
45
  const basePath = router.basepath || '';
44
46
 
@@ -46,16 +48,20 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
46
48
  const isPathActive = React.useCallback(
47
49
  (itemUrl: string) => {
48
50
  // Remove basepath prefix from current path for comparison
49
- const normalizedCurrentPath = basePath ? currentPath.replace(new RegExp(`^${escapeRegexChars(basePath)}`), '') : currentPath;
50
-
51
+ const normalizedCurrentPath = basePath
52
+ ? currentPath.replace(new RegExp(`^${escapeRegexChars(basePath)}`), '')
53
+ : currentPath;
54
+
51
55
  // Ensure normalized path starts with /
52
- const cleanPath = normalizedCurrentPath.startsWith('/') ? normalizedCurrentPath : `/${normalizedCurrentPath}`;
53
-
56
+ const cleanPath = normalizedCurrentPath.startsWith('/')
57
+ ? normalizedCurrentPath
58
+ : `/${normalizedCurrentPath}`;
59
+
54
60
  // Special handling for root path
55
61
  if (itemUrl === '/') {
56
62
  return cleanPath === '/' || cleanPath === '';
57
63
  }
58
-
64
+
59
65
  // For other paths, check exact match or prefix match
60
66
  return cleanPath === itemUrl || cleanPath.startsWith(`${itemUrl}/`);
61
67
  },
@@ -97,6 +103,20 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
97
103
  return activeTopSections;
98
104
  });
99
105
 
106
+ // Helper to check if an item is allowed based on permissions
107
+ const isItemAllowed = React.useCallback(
108
+ (item: NavMenuItem) => {
109
+ if (!item.requiresPermission) {
110
+ return true;
111
+ }
112
+ const permissions = Array.isArray(item.requiresPermission)
113
+ ? item.requiresPermission
114
+ : [item.requiresPermission];
115
+ return hasPermissions(permissions);
116
+ },
117
+ [hasPermissions],
118
+ );
119
+
100
120
  // Helper to build a sorted list of sections for a given placement, memoized for stability
101
121
  const getSortedSections = React.useCallback(
102
122
  (placement: NavMenuSectionPlacement) => {
@@ -104,13 +124,24 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
104
124
  .filter(item => item.placement === placement)
105
125
  .slice()
106
126
  .sort(sortByOrder)
107
- .map(section =>
108
- 'items' in section
109
- ? { ...section, items: section.items?.slice().sort(sortByOrder) }
110
- : section,
111
- );
127
+ .map(section => {
128
+ if ('items' in section) {
129
+ // Filter items based on permissions
130
+ const allowedItems = (section.items ?? []).filter(isItemAllowed).sort(sortByOrder);
131
+ return { ...section, items: allowedItems };
132
+ }
133
+ return section;
134
+ })
135
+ .filter(section => {
136
+ // Drop sections that have no items after permission filtering
137
+ if ('items' in section) {
138
+ return section.items && section.items.length > 0;
139
+ }
140
+ // For single items, check if they're allowed
141
+ return isItemAllowed(section as NavMenuItem);
142
+ });
112
143
  },
113
- [items],
144
+ [items, isItemAllowed],
114
145
  );
115
146
 
116
147
  const topSections = React.useMemo(() => getSortedSections('top'), [getSortedSections]);
@@ -154,11 +185,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
154
185
  return (
155
186
  <NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
156
187
  <SidebarMenuItem>
157
- <SidebarMenuButton
158
- tooltip={item.title}
159
- asChild
160
- isActive={isPathActive(item.url)}
161
- >
188
+ <SidebarMenuButton tooltip={item.title} asChild isActive={isPathActive(item.url)}>
162
189
  <Link to={item.url}>
163
190
  {item.icon && <item.icon />}
164
191
  <span>{item.title}</span>
@@ -220,11 +247,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
220
247
  return (
221
248
  <NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
222
249
  <SidebarMenuItem>
223
- <SidebarMenuButton
224
- tooltip={item.title}
225
- asChild
226
- isActive={isPathActive(item.url)}
227
- >
250
+ <SidebarMenuButton tooltip={item.title} asChild isActive={isPathActive(item.url)}>
228
251
  <Link to={item.url}>
229
252
  {item.icon && <item.icon />}
230
253
  <span>{item.title}</span>
@@ -287,10 +310,12 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
287
310
  </SidebarGroup>
288
311
 
289
312
  {/* Bottom sections - will be pushed to the bottom by CSS */}
290
- <SidebarGroup className="mt-auto">
291
- <SidebarGroupLabel>Administration</SidebarGroupLabel>
292
- <SidebarMenu>{bottomSections.map(renderBottomSection)}</SidebarMenu>
293
- </SidebarGroup>
313
+ {bottomSections.length ? (
314
+ <SidebarGroup className="mt-auto">
315
+ <SidebarGroupLabel>Administration</SidebarGroupLabel>
316
+ <SidebarMenu>{bottomSections.map(renderBottomSection)}</SidebarMenu>
317
+ </SidebarGroup>
318
+ ) : null}
294
319
  </>
295
320
  );
296
321
  }
@@ -17,7 +17,7 @@ export interface AssetFocalPointEditorProps {
17
17
  children?: React.ReactNode;
18
18
  }
19
19
 
20
- interface Point {
20
+ export interface Point {
21
21
  x: number;
22
22
  y: number;
23
23
  }
@@ -23,6 +23,8 @@ import { useDebounce } from '@uidotdev/usehooks';
23
23
  import { Loader2, Search, Upload, X } from 'lucide-react';
24
24
  import { useCallback, useState } from 'react';
25
25
  import { useDropzone } from 'react-dropzone';
26
+ import { tagListDocument } from '../../../../app/routes/_authenticated/_assets/assets.graphql.js';
27
+ import { AssetTagFilter } from '../../../../app/routes/_authenticated/_assets/components/asset-tag-filter.js';
26
28
  import { DetailPageButton } from '../detail-page-button.js';
27
29
  import { AssetBulkAction, AssetBulkActions } from './asset-bulk-actions.js';
28
30
 
@@ -74,7 +76,7 @@ export type Asset = AssetFragment;
74
76
  /**
75
77
  * @description
76
78
  * Props for the {@link AssetGallery} component.
77
- *
79
+ *
78
80
  * @docsCategory components
79
81
  * @docsPage AssetGallery
80
82
  */
@@ -134,16 +136,16 @@ export interface AssetGalleryProps {
134
136
  /**
135
137
  * @description
136
138
  * A component for displaying a gallery of assets.
137
- *
139
+ *
138
140
  * @example
139
141
  * ```tsx
140
142
  * <AssetGallery
141
- onSelect={handleAssetSelect}
142
- multiSelect="manual"
143
- initialSelectedAssets={initialSelectedAssets}
144
- fixedHeight={false}
145
- displayBulkActions={false}
146
- />
143
+ onSelect={handleAssetSelect}
144
+ multiSelect="manual"
145
+ initialSelectedAssets={initialSelectedAssets}
146
+ fixedHeight={false}
147
+ displayBulkActions={false}
148
+ />
147
149
  * ```
148
150
  *
149
151
  * @docsCategory components
@@ -169,9 +171,19 @@ export function AssetGallery({
169
171
  const debouncedSearch = useDebounce(search, 500);
170
172
  const [assetType, setAssetType] = useState<string>(AssetType.ALL);
171
173
  const [selected, setSelected] = useState<Asset[]>(initialSelectedAssets || []);
174
+ const [selectedTags, setSelectedTags] = useState<string[]>([]);
172
175
  const queryClient = useQueryClient();
173
176
 
174
- const queryKey = ['AssetGallery', page, pageSize, debouncedSearch, assetType];
177
+ const queryKey = ['AssetGallery', page, pageSize, debouncedSearch, assetType, selectedTags];
178
+
179
+ // Query for available tags to check if we should show the filter
180
+ const { data: tagsData } = useQuery({
181
+ queryKey: ['tags-check'],
182
+ queryFn: () => api.query(tagListDocument, { options: { take: 1 } }),
183
+ staleTime: 1000 * 60 * 5,
184
+ });
185
+
186
+ const hasTags = (tagsData?.tags.items?.length || 0) > 0;
175
187
 
176
188
  // Query for assets
177
189
  const { data, isLoading, refetch } = useQuery({
@@ -187,14 +199,20 @@ export function AssetGallery({
187
199
  filter.type = { eq: assetType };
188
200
  }
189
201
 
190
- return api.query(getAssetListDocument, {
191
- options: {
192
- skip: (page - 1) * pageSize,
193
- take: pageSize,
194
- filter: Object.keys(filter).length > 0 ? filter : undefined,
195
- sort: { createdAt: 'DESC' },
196
- },
197
- });
202
+ const options: any = {
203
+ skip: (page - 1) * pageSize,
204
+ take: pageSize,
205
+ filter: Object.keys(filter).length > 0 ? filter : undefined,
206
+ sort: { createdAt: 'DESC' },
207
+ };
208
+
209
+ // Add tag filtering if tags are provided
210
+ if (selectedTags && selectedTags.length > 0) {
211
+ options.tags = selectedTags;
212
+ options.tagsOperator = 'AND';
213
+ }
214
+
215
+ return api.query(getAssetListDocument, { options });
198
216
  },
199
217
  });
200
218
 
@@ -262,10 +280,17 @@ export function AssetGallery({
262
280
  // Check if an asset is selected
263
281
  const isSelected = (asset: Asset) => selected.some(a => a.id === asset.id);
264
282
 
283
+ // Handle tag changes
284
+ const handleTagsChange = (tags: string[]) => {
285
+ setSelectedTags(tags);
286
+ setPage(1); // Reset to page 1 when tags change
287
+ };
288
+
265
289
  // Clear filters
266
290
  const clearFilters = () => {
267
291
  setSearch('');
268
292
  setAssetType(AssetType.ALL);
293
+ setSelectedTags([]);
269
294
  setPage(1);
270
295
  };
271
296
 
@@ -294,40 +319,48 @@ export function AssetGallery({
294
319
  return (
295
320
  <div className={`relative flex flex-col w-full ${fixedHeight ? 'h-[600px]' : 'h-full'} ${className}`}>
296
321
  {showHeader && (
297
- <div className="flex flex-col md:flex-row gap-2 mb-4 flex-shrink-0">
298
- <div className="relative flex-grow flex items-center gap-2">
299
- <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
300
- <Input
301
- placeholder="Search assets..."
302
- value={search}
303
- onChange={e => setSearch(e.target.value)}
304
- className="pl-8"
305
- />
306
- {(search || assetType !== AssetType.ALL) && (
307
- <Button
308
- variant="ghost"
309
- size="sm"
310
- onClick={clearFilters}
311
- className="absolute right-0"
312
- >
313
- <X className="h-4 w-4 mr-1" /> Clear filters
314
- </Button>
315
- )}
322
+ <div className="space-y-4 mb-4 flex-shrink-0">
323
+ <div className="flex flex-col md:flex-row gap-2">
324
+ <div className="relative flex-grow flex items-center gap-2">
325
+ <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
326
+ <Input
327
+ placeholder="Search assets..."
328
+ value={search}
329
+ onChange={e => setSearch(e.target.value)}
330
+ className="pl-8"
331
+ />
332
+ {(search || assetType !== AssetType.ALL || selectedTags.length > 0) && (
333
+ <Button
334
+ variant="ghost"
335
+ size="sm"
336
+ onClick={clearFilters}
337
+ className="absolute right-0"
338
+ >
339
+ <X className="h-4 w-4 mr-1" /> Clear filters
340
+ </Button>
341
+ )}
342
+ </div>
343
+ <Select value={assetType} onValueChange={setAssetType}>
344
+ <SelectTrigger className="w-full md:w-[180px]">
345
+ <SelectValue placeholder="Asset type" />
346
+ </SelectTrigger>
347
+ <SelectContent>
348
+ <SelectItem value={AssetType.ALL}>All types</SelectItem>
349
+ <SelectItem value={AssetType.IMAGE}>Images</SelectItem>
350
+ <SelectItem value={AssetType.VIDEO}>Video</SelectItem>
351
+ <SelectItem value={AssetType.BINARY}>Binary</SelectItem>
352
+ </SelectContent>
353
+ </Select>
354
+ <Button onClick={openFileDialog} className="whitespace-nowrap">
355
+ <Upload className="h-4 w-4 mr-2" /> <Trans>Upload</Trans>
356
+ </Button>
316
357
  </div>
317
- <Select value={assetType} onValueChange={setAssetType}>
318
- <SelectTrigger className="w-full md:w-[180px]">
319
- <SelectValue placeholder="Asset type" />
320
- </SelectTrigger>
321
- <SelectContent>
322
- <SelectItem value={AssetType.ALL}>All types</SelectItem>
323
- <SelectItem value={AssetType.IMAGE}>Images</SelectItem>
324
- <SelectItem value={AssetType.VIDEO}>Video</SelectItem>
325
- <SelectItem value={AssetType.BINARY}>Binary</SelectItem>
326
- </SelectContent>
327
- </Select>
328
- <Button onClick={openFileDialog} className="whitespace-nowrap">
329
- <Upload className="h-4 w-4 mr-2" /> <Trans>Upload</Trans>
330
- </Button>
358
+
359
+ {hasTags && (
360
+ <div className="flex items-center -mt-2">
361
+ <AssetTagFilter selectedTags={selectedTags} onTagsChange={handleTagsChange} />
362
+ </div>
363
+ )}
331
364
  </div>
332
365
  )}
333
366
 
@@ -13,7 +13,7 @@ import { Button } from '../ui/button.js';
13
13
  * ```tsx
14
14
  * // Basic usage with ID (relative navigation)
15
15
  * <DetailPageButton id="123" label="Product Name" />
16
- *
16
+ *
17
17
  *
18
18
  * @example
19
19
  * ```tsx
@@ -36,18 +36,20 @@ export function DetailPageButton({
36
36
  label,
37
37
  disabled,
38
38
  search,
39
- }: {
39
+ className,
40
+ }: Readonly<{
40
41
  label: string | React.ReactNode;
41
42
  id?: string;
42
43
  href?: string;
43
44
  disabled?: boolean;
44
45
  search?: Record<string, string>;
45
- }) {
46
+ className?: string;
47
+ }>) {
46
48
  if (!id && !href) {
47
49
  return <span>{label}</span>;
48
50
  }
49
51
  return (
50
- <Button asChild variant="ghost" disabled={disabled}>
52
+ <Button asChild variant="ghost" disabled={disabled} className={className}>
51
53
  <Link to={href ?? `./${id}`} search={search ?? {}} preload={false}>
52
54
  {label}
53
55
  {!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
@@ -437,6 +437,7 @@ export function PaginatedListDataTable<
437
437
  fields,
438
438
  customizeColumns,
439
439
  rowActions,
440
+ bulkActions,
440
441
  deleteMutation,
441
442
  additionalColumns,
442
443
  defaultColumnOrder,
@@ -25,7 +25,7 @@ export interface AssetLike {
25
25
  * @docsPage VendureImage
26
26
  * @since 3.4.0
27
27
  */
28
- export type ImagePreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | null;
28
+ export type ImagePreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | 'full' | null;
29
29
 
30
30
  /**
31
31
  * @description
@@ -219,6 +219,10 @@ function getMinDimensions(preset?: ImagePreset, width?: number, height?: number)
219
219
  return { width: 300, height: 300 };
220
220
  case 'medium':
221
221
  return { width: 500, height: 500 };
222
+ case 'large':
223
+ return { width: 800, height: 800 };
224
+ case 'full':
225
+ return { width: undefined, height: undefined };
222
226
  }
223
227
  }
224
228
 
@@ -258,6 +262,10 @@ export function PlaceholderImage({
258
262
  width = 800;
259
263
  height = 800;
260
264
  break;
265
+ case 'full':
266
+ width = 1200;
267
+ height = 1200;
268
+ break;
261
269
  default:
262
270
  break;
263
271
  }