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

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 (43) 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.tsx +1 -2
  12. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +1 -2
  13. package/src/app/routes/_authenticated/_customers/customers.tsx +1 -2
  14. package/src/app/routes/_authenticated/_facets/facets.tsx +0 -1
  15. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +1 -2
  16. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +1 -2
  17. package/src/app/routes/_authenticated/_products/products.tsx +1 -2
  18. package/src/app/routes/_authenticated/_promotions/promotions.tsx +1 -2
  19. package/src/app/routes/_authenticated/_roles/roles.tsx +1 -2
  20. package/src/app/routes/_authenticated/_sellers/sellers.tsx +1 -2
  21. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +1 -2
  22. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +1 -2
  23. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +1 -2
  24. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +1 -2
  25. package/src/app/routes/_authenticated/_zones/zones.tsx +1 -2
  26. package/src/lib/components/data-table/data-table-bulk-actions.tsx +5 -14
  27. package/src/lib/components/data-table/use-all-bulk-actions.ts +19 -0
  28. package/src/lib/components/data-table/use-generated-columns.tsx +12 -3
  29. package/src/lib/components/layout/nav-main.tsx +50 -25
  30. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
  31. package/src/lib/components/shared/asset/asset-gallery.tsx +83 -50
  32. package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
  33. package/src/lib/components/shared/vendure-image.tsx +9 -1
  34. package/src/lib/framework/defaults.ts +24 -0
  35. package/src/lib/framework/extension-api/types/navigation.ts +8 -0
  36. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +26 -0
  37. package/src/lib/framework/page/list-page.tsx +7 -0
  38. package/src/lib/hooks/use-custom-field-config.ts +19 -2
  39. package/src/lib/index.ts +0 -1
  40. package/src/lib/providers/channel-provider.tsx +22 -6
  41. package/src/lib/providers/server-config.tsx +1 -0
  42. package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +0 -33
  43. package/src/lib/components/shared/asset/focal-point-control.tsx +0 -57
@@ -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
 
@@ -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
  }
@@ -37,30 +37,35 @@ export function registerDefaults() {
37
37
  title: 'Products',
38
38
  url: '/products',
39
39
  order: 100,
40
+ requiresPermission: ['ReadProduct', 'ReadCatalog'],
40
41
  },
41
42
  {
42
43
  id: 'product-variants',
43
44
  title: 'Product Variants',
44
45
  url: '/product-variants',
45
46
  order: 200,
47
+ requiresPermission: ['ReadProduct', 'ReadCatalog'],
46
48
  },
47
49
  {
48
50
  id: 'facets',
49
51
  title: 'Facets',
50
52
  url: '/facets',
51
53
  order: 300,
54
+ requiresPermission: ['ReadProduct', 'ReadCatalog'],
52
55
  },
53
56
  {
54
57
  id: 'collections',
55
58
  title: 'Collections',
56
59
  url: '/collections',
57
60
  order: 400,
61
+ requiresPermission: ['ReadCollection', 'ReadCatalog'],
58
62
  },
59
63
  {
60
64
  id: 'assets',
61
65
  title: 'Assets',
62
66
  url: '/assets',
63
67
  order: 500,
68
+ requiresPermission: ['ReadAsset', 'ReadCatalog'],
64
69
  },
65
70
  ],
66
71
  },
@@ -76,6 +81,7 @@ export function registerDefaults() {
76
81
  title: 'Orders',
77
82
  url: '/orders',
78
83
  order: 100,
84
+ requiresPermission: ['ReadOrder'],
79
85
  },
80
86
  ],
81
87
  },
@@ -91,12 +97,14 @@ export function registerDefaults() {
91
97
  title: 'Customers',
92
98
  url: '/customers',
93
99
  order: 100,
100
+ requiresPermission: ['ReadCustomer'],
94
101
  },
95
102
  {
96
103
  id: 'customer-groups',
97
104
  title: 'Customer Groups',
98
105
  url: '/customer-groups',
99
106
  order: 200,
107
+ requiresPermission: ['ReadCustomerGroup'],
100
108
  },
101
109
  ],
102
110
  },
@@ -112,6 +120,7 @@ export function registerDefaults() {
112
120
  title: 'Promotions',
113
121
  url: '/promotions',
114
122
  order: 100,
123
+ requiresPermission: ['ReadPromotion'],
115
124
  },
116
125
  ],
117
126
  },
@@ -127,18 +136,21 @@ export function registerDefaults() {
127
136
  title: 'Job Queue',
128
137
  url: '/job-queue',
129
138
  order: 100,
139
+ requiresPermission: ['ReadSystem'],
130
140
  },
131
141
  {
132
142
  id: 'healthchecks',
133
143
  title: 'Healthchecks',
134
144
  url: '/healthchecks',
135
145
  order: 200,
146
+ requiresPermission: ['ReadSystem'],
136
147
  },
137
148
  {
138
149
  id: 'scheduled-tasks',
139
150
  title: 'Scheduled Tasks',
140
151
  url: '/scheduled-tasks',
141
152
  order: 300,
153
+ requiresPermission: ['ReadSystem'],
142
154
  },
143
155
  ],
144
156
  },
@@ -154,72 +166,84 @@ export function registerDefaults() {
154
166
  title: 'Sellers',
155
167
  url: '/sellers',
156
168
  order: 100,
169
+ requiresPermission: ['ReadSeller'],
157
170
  },
158
171
  {
159
172
  id: 'channels',
160
173
  title: 'Channels',
161
174
  url: '/channels',
162
175
  order: 200,
176
+ requiresPermission: ['ReadChannel'],
163
177
  },
164
178
  {
165
179
  id: 'stock-locations',
166
180
  title: 'Stock Locations',
167
181
  url: '/stock-locations',
168
182
  order: 300,
183
+ requiresPermission: ['ReadStockLocation'],
169
184
  },
170
185
  {
171
186
  id: 'administrators',
172
187
  title: 'Administrators',
173
188
  url: '/administrators',
174
189
  order: 400,
190
+ requiresPermission: ['ReadAdministrator'],
175
191
  },
176
192
  {
177
193
  id: 'roles',
178
194
  title: 'Roles',
179
195
  url: '/roles',
180
196
  order: 500,
197
+ requiresPermission: ['ReadAdministrator'],
181
198
  },
182
199
  {
183
200
  id: 'shipping-methods',
184
201
  title: 'Shipping Methods',
185
202
  url: '/shipping-methods',
186
203
  order: 600,
204
+ requiresPermission: ['ReadShippingMethod'],
187
205
  },
188
206
  {
189
207
  id: 'payment-methods',
190
208
  title: 'Payment Methods',
191
209
  url: '/payment-methods',
192
210
  order: 700,
211
+ requiresPermission: ['ReadPaymentMethod'],
193
212
  },
194
213
  {
195
214
  id: 'tax-categories',
196
215
  title: 'Tax Categories',
197
216
  url: '/tax-categories',
198
217
  order: 800,
218
+ requiresPermission: ['ReadTaxCategory'],
199
219
  },
200
220
  {
201
221
  id: 'tax-rates',
202
222
  title: 'Tax Rates',
203
223
  url: '/tax-rates',
204
224
  order: 900,
225
+ requiresPermission: ['ReadTaxRate'],
205
226
  },
206
227
  {
207
228
  id: 'countries',
208
229
  title: 'Countries',
209
230
  url: '/countries',
210
231
  order: 1000,
232
+ requiresPermission: ['ReadCountry'],
211
233
  },
212
234
  {
213
235
  id: 'zones',
214
236
  title: 'Zones',
215
237
  url: '/zones',
216
238
  order: 1100,
239
+ requiresPermission: ['ReadZone'],
217
240
  },
218
241
  {
219
242
  id: 'global-settings',
220
243
  title: 'Global Settings',
221
244
  url: '/global-settings',
222
245
  order: 1200,
246
+ requiresPermission: ['UpdateGlobalSettings'],
223
247
  },
224
248
  ],
225
249
  },
@@ -27,6 +27,10 @@ export interface DashboardRouteDefinition {
27
27
  * @description
28
28
  * Optional navigation menu item configuration to add this route to the nav menu
29
29
  * on the left side of the dashboard.
30
+ *
31
+ * The `sectionId` specifies which nav menu section (e.g. "catalog", "customers")
32
+ * this item should appear in. It can also point to custom nav menu sections that
33
+ * have been defined using the `navSections` extension property.
30
34
  */
31
35
  navMenuItem?: Partial<NavMenuItem> & { sectionId: string };
32
36
  /**
@@ -42,8 +46,12 @@ export interface DashboardRouteDefinition {
42
46
  * @description
43
47
  * Defines a custom navigation section in the dashboard sidebar.
44
48
  *
49
+ * Individual items can then be added to the section by defining routes in the
50
+ * `routes` property of your Dashboard extension.
51
+ *
45
52
  * @docsCategory extensions-api
46
53
  * @docsPage Navigation
54
+ * @docsWeight 0
47
55
  * @since 3.4.0
48
56
  */
49
57
  export interface DashboardNavSectionDefinition {