@vendure/dashboard 3.4.3-master-202509180227 → 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 (54) hide show
  1. package/dist/vite/vite-plugin-config.js +1 -0
  2. package/package.json +11 -7
  3. package/src/app/common/duplicate-bulk-action.tsx +37 -23
  4. package/src/app/common/duplicate-entity-dialog.tsx +117 -0
  5. package/src/app/routes/_authenticated/_administrators/administrators.tsx +1 -2
  6. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +39 -0
  7. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +18 -7
  8. package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +206 -0
  9. package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +226 -0
  10. package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +217 -0
  11. package/src/app/routes/_authenticated/_channels/channels.tsx +1 -2
  12. package/src/app/routes/_authenticated/_collections/collections.tsx +2 -16
  13. package/src/app/routes/_authenticated/_countries/countries.tsx +1 -2
  14. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +1 -2
  15. package/src/app/routes/_authenticated/_customers/customers.tsx +1 -2
  16. package/src/app/routes/_authenticated/_facets/facets.tsx +0 -1
  17. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +1 -2
  18. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +1 -2
  19. package/src/app/routes/_authenticated/_products/products.tsx +1 -2
  20. package/src/app/routes/_authenticated/_promotions/promotions.tsx +1 -2
  21. package/src/app/routes/_authenticated/_roles/roles.tsx +1 -2
  22. package/src/app/routes/_authenticated/_sellers/sellers.tsx +1 -2
  23. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +1 -2
  24. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +1 -2
  25. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +1 -2
  26. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +1 -2
  27. package/src/app/routes/_authenticated/_zones/zones.tsx +1 -2
  28. package/src/lib/components/data-input/rich-text-input.tsx +2 -115
  29. package/src/lib/components/data-table/data-table-bulk-actions.tsx +5 -14
  30. package/src/lib/components/data-table/use-all-bulk-actions.ts +19 -0
  31. package/src/lib/components/data-table/use-generated-columns.tsx +12 -3
  32. package/src/lib/components/layout/nav-main.tsx +50 -25
  33. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
  34. package/src/lib/components/shared/asset/asset-gallery.tsx +83 -50
  35. package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
  36. package/src/lib/components/shared/rich-text-editor/image-dialog.tsx +223 -0
  37. package/src/lib/components/shared/rich-text-editor/link-dialog.tsx +151 -0
  38. package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +439 -0
  39. package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +338 -0
  40. package/src/lib/components/shared/rich-text-editor/table-delete-menu.tsx +104 -0
  41. package/src/lib/components/shared/rich-text-editor/table-edit-icons.tsx +225 -0
  42. package/src/lib/components/shared/vendure-image.tsx +9 -1
  43. package/src/lib/framework/defaults.ts +24 -0
  44. package/src/lib/framework/extension-api/types/navigation.ts +8 -0
  45. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +26 -0
  46. package/src/lib/framework/page/list-page.tsx +7 -0
  47. package/src/lib/graphql/common-operations.ts +19 -0
  48. package/src/lib/graphql/fragments.ts +23 -13
  49. package/src/lib/hooks/use-custom-field-config.ts +19 -2
  50. package/src/lib/index.ts +0 -1
  51. package/src/lib/providers/channel-provider.tsx +22 -6
  52. package/src/lib/providers/server-config.tsx +1 -0
  53. package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +0 -33
  54. package/src/lib/components/shared/asset/focal-point-control.tsx +0 -57
@@ -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 {
@@ -5,15 +5,41 @@ import { globalRegistry } from '../registry/global-registry.js';
5
5
  // Define the placement options for navigation sections
6
6
  export type NavMenuSectionPlacement = 'top' | 'bottom';
7
7
 
8
+ /**
9
+ * @description
10
+ * The base configuration for navigation items and sections of the main app nav bar.
11
+ *
12
+ * @docsCategory extensions-api
13
+ * @docsPage Navigation
14
+ * @since 3.4.0
15
+ */
8
16
  interface NavMenuBaseItem {
9
17
  id: string;
10
18
  title: string;
11
19
  icon?: LucideIcon;
12
20
  order?: number;
13
21
  placement?: NavMenuSectionPlacement;
22
+ /**
23
+ * @description
24
+ * This can be used to restrict the menu item to the given
25
+ * permission or permissions.
26
+ */
27
+ requiresPermission?: string | string[];
14
28
  }
15
29
 
30
+ /**
31
+ * @description
32
+ * Defines an items in the navigation menu.
33
+ *
34
+ * @docsCategory extensions-api
35
+ * @docsPage Navigation
36
+ * @since 3.4.0
37
+ */
16
38
  export interface NavMenuItem extends NavMenuBaseItem {
39
+ /**
40
+ * @description
41
+ * The url of the route which this nav item links to.
42
+ */
17
43
  url: string;
18
44
  }
19
45
 
@@ -38,6 +38,13 @@ export interface ListPageProps<
38
38
  route: AnyRoute | (() => AnyRoute);
39
39
  title: string | React.ReactElement;
40
40
  listQuery: T;
41
+ /**
42
+ * @description
43
+ * Providing the `deleteMutation` will automatically add a "delete" menu item to the
44
+ * actions column dropdown. Note that if this table already has a "delete" bulk action,
45
+ * you don't need to additionally provide a delete mutation, because the bulk action
46
+ * will be added to the action column dropdown already.
47
+ */
41
48
  deleteMutation?: TypedDocumentNode<any, { id: string }>;
42
49
  transformVariables?: (variables: V) => V;
43
50
  onSearchTermChange?: (searchTerm: string) => NonNullable<V['options']>['filter'];
@@ -1,3 +1,5 @@
1
+ import { configArgDefinitionFragment } from '@/vdb/graphql/fragments.js';
2
+
1
3
  import { graphql } from './graphql.js';
2
4
 
3
5
  export const duplicateEntityDocument = graphql(`
@@ -16,3 +18,20 @@ export const duplicateEntityDocument = graphql(`
16
18
  }
17
19
  }
18
20
  `);
21
+
22
+ export const getEntityDuplicatorsDocument = graphql(
23
+ `
24
+ query GetEntityDuplicators {
25
+ entityDuplicators {
26
+ code
27
+ description
28
+ requiresPermission
29
+ forEntities
30
+ args {
31
+ ...ConfigArgDefinition
32
+ }
33
+ }
34
+ }
35
+ `,
36
+ [configArgDefinitionFragment],
37
+ );
@@ -34,23 +34,32 @@ export const configurableOperationFragment = graphql(`
34
34
 
35
35
  export type ConfigurableOperationFragment = ResultOf<typeof configurableOperationFragment>;
36
36
 
37
- export const configurableOperationDefFragment = graphql(`
38
- fragment ConfigurableOperationDef on ConfigurableOperationDefinition {
39
- args {
40
- name
41
- type
42
- required
43
- defaultValue
44
- list
45
- ui
46
- label
47
- description
48
- }
49
- code
37
+ export const configArgDefinitionFragment = graphql(`
38
+ fragment ConfigArgDefinition on ConfigArgDefinition {
39
+ name
40
+ type
41
+ required
42
+ defaultValue
43
+ list
44
+ ui
45
+ label
50
46
  description
51
47
  }
52
48
  `);
53
49
 
50
+ export const configurableOperationDefFragment = graphql(
51
+ `
52
+ fragment ConfigurableOperationDef on ConfigurableOperationDefinition {
53
+ args {
54
+ ...ConfigArgDefinition
55
+ }
56
+ code
57
+ description
58
+ }
59
+ `,
60
+ [configArgDefinitionFragment],
61
+ );
62
+
54
63
  export const errorResultFragment = graphql(`
55
64
  fragment ErrorResult on ErrorResult {
56
65
  errorCode
@@ -59,3 +68,4 @@ export const errorResultFragment = graphql(`
59
68
  `);
60
69
 
61
70
  export type ConfigurableOperationDefFragment = ResultOf<typeof configurableOperationDefFragment>;
71
+ export type ConfigArgDefFragment = ResultOf<typeof configArgDefinitionFragment>;
@@ -1,10 +1,27 @@
1
+ import { usePermissions } from '@/vdb/hooks/use-permissions.js';
2
+ import { CustomFieldConfig } from '@/vdb/providers/server-config.js';
3
+
1
4
  import { useServerConfig } from './use-server-config.js';
2
5
 
3
- export function useCustomFieldConfig(entityType: string) {
6
+ /**
7
+ * @description
8
+ * Returns the custom field config for the given entity type (e.g. 'Product').
9
+ * Also filters out any custom fields that the current active user does not
10
+ * have permissions to access.
11
+ *
12
+ * @docsCategory hooks
13
+ * @since 3.4.0
14
+ */
15
+ export function useCustomFieldConfig(entityType: string): CustomFieldConfig[] {
4
16
  const serverConfig = useServerConfig();
17
+ const { hasPermissions } = usePermissions();
5
18
  if (!serverConfig) {
6
19
  return [];
7
20
  }
8
21
  const customFieldConfig = serverConfig.entityCustomFields.find(field => field.entityName === entityType);
9
- return customFieldConfig?.customFields;
22
+ return (
23
+ customFieldConfig?.customFields?.filter(config => {
24
+ return config.requiresPermission ? hasPermissions(config.requiresPermission) : true;
25
+ }) ?? []
26
+ );
10
27
  }
package/src/lib/index.ts CHANGED
@@ -70,7 +70,6 @@ export * from './components/shared/asset/asset-preview-dialog.js';
70
70
  export * from './components/shared/asset/asset-preview-selector.js';
71
71
  export * from './components/shared/asset/asset-preview.js';
72
72
  export * from './components/shared/asset/asset-properties.js';
73
- export * from './components/shared/asset/focal-point-control.js';
74
73
  export * from './components/shared/assign-to-channel-bulk-action.js';
75
74
  export * from './components/shared/assign-to-channel-dialog.js';
76
75
  export * from './components/shared/assigned-facet-values.js';
@@ -19,7 +19,7 @@ const channelFragment = graphql(`
19
19
  `);
20
20
 
21
21
  // Query to get all available channels and the active channel
22
- const ChannelsQuery = graphql(
22
+ const activeChannelDocument = graphql(
23
23
  `
24
24
  query ChannelInformation {
25
25
  activeChannel {
@@ -28,6 +28,14 @@ const ChannelsQuery = graphql(
28
28
  id
29
29
  }
30
30
  }
31
+ }
32
+ `,
33
+ [channelFragment],
34
+ );
35
+
36
+ const channelsDocument = graphql(
37
+ `
38
+ query ChannelInformation {
31
39
  channels {
32
40
  items {
33
41
  ...ChannelInfo
@@ -40,7 +48,7 @@ const ChannelsQuery = graphql(
40
48
  );
41
49
 
42
50
  // Define the type for a channel
43
- type ActiveChannel = ResultOf<typeof ChannelsQuery>['activeChannel'];
51
+ type ActiveChannel = ResultOf<typeof activeChannelDocument>['activeChannel'];
44
52
  type Channel = ResultOf<typeof channelFragment>;
45
53
 
46
54
  /**
@@ -106,10 +114,18 @@ export function ChannelProvider({ children }: Readonly<{ children: React.ReactNo
106
114
  return activeChannelId;
107
115
  });
108
116
 
117
+ // Fetch active channel
118
+ const { data: activeChannelData, isLoading: isActiveChannelLoading } = useQuery({
119
+ queryKey: ['activeChannel', isAuthenticated],
120
+ queryFn: () => api.query(activeChannelDocument),
121
+ retry: false,
122
+ enabled: isAuthenticated,
123
+ });
124
+
109
125
  // Fetch all available channels
110
- const { data: channelsData, isLoading: isChannelsLoading } = useQuery({
126
+ const { data: channelsData } = useQuery({
111
127
  queryKey: ['channels', isAuthenticated],
112
- queryFn: () => api.query(ChannelsQuery),
128
+ queryFn: () => api.query(channelsDocument),
113
129
  retry: false,
114
130
  enabled: isAuthenticated,
115
131
  });
@@ -168,10 +184,10 @@ export function ChannelProvider({ children }: Readonly<{ children: React.ReactNo
168
184
  }
169
185
  }, [selectedChannelId, channels]);
170
186
 
171
- const isLoading = isChannelsLoading;
187
+ const isLoading = isActiveChannelLoading;
172
188
 
173
189
  // Find the selected channel from the list of channels
174
- const selectedChannel = channelsData?.activeChannel;
190
+ const selectedChannel = activeChannelData?.activeChannel;
175
191
 
176
192
  const refreshChannels = () => {
177
193
  refreshCurrentUser();
@@ -250,6 +250,7 @@ export const getServerConfigDocument = graphql(
250
250
  );
251
251
 
252
252
  type QueryResult = ResultOf<typeof getServerConfigDocument>['globalSettings']['serverConfig'];
253
+ export type CustomFieldConfig = QueryResult['entityCustomFields'][number]['customFields'][number];
253
254
 
254
255
  export interface ServerConfig {
255
256
  availableLanguages: string[];
@@ -1,33 +0,0 @@
1
- import { ResultOf } from 'gql.tada';
2
- import { useState } from 'react';
3
-
4
- import { collectionListDocument } from '../collections.graphql.js';
5
- import { MoveCollectionsDialog } from './move-collections-dialog.js';
6
-
7
- type Collection = ResultOf<typeof collectionListDocument>['collections']['items'][number];
8
-
9
- export function useMoveSingleCollection() {
10
- const [moveDialogOpen, setMoveDialogOpen] = useState(false);
11
- const [collectionsToMove, setCollectionsToMove] = useState<Collection[]>([]);
12
-
13
- const handleMoveClick = (collection: Collection) => {
14
- setCollectionsToMove([collection]);
15
- setMoveDialogOpen(true);
16
- };
17
-
18
- const MoveDialog = () => (
19
- <MoveCollectionsDialog
20
- open={moveDialogOpen}
21
- onOpenChange={setMoveDialogOpen}
22
- collectionsToMove={collectionsToMove}
23
- onSuccess={() => {
24
- // The dialog will handle invalidating queries internally
25
- }}
26
- />
27
- );
28
-
29
- return {
30
- handleMoveClick,
31
- MoveDialog,
32
- };
33
- }
@@ -1,57 +0,0 @@
1
- import { cn } from '@/vdb/lib/utils.js';
2
- import { useEffect, useState } from 'react';
3
-
4
- export interface Point {
5
- x: number;
6
- y: number;
7
- }
8
-
9
- interface FocalPointControlProps {
10
- width: number;
11
- height: number;
12
- point: Point;
13
- onChange: (point: Point) => void;
14
- }
15
-
16
- export function FocalPointControl({ width, height, point, onChange }: Readonly<FocalPointControlProps>) {
17
- const [dragging, setDragging] = useState(false);
18
-
19
- useEffect(() => {
20
- if (!dragging) return;
21
-
22
- const handleMouseMove = (e: MouseEvent) => {
23
- const rect = (e.target as HTMLDivElement)?.getBoundingClientRect();
24
- const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
25
- const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
26
- onChange({ x, y });
27
- };
28
-
29
- const handleMouseUp = () => {
30
- setDragging(false);
31
- };
32
-
33
- document.addEventListener('mousemove', handleMouseMove);
34
- document.addEventListener('mouseup', handleMouseUp);
35
-
36
- return () => {
37
- document.removeEventListener('mousemove', handleMouseMove);
38
- document.removeEventListener('mouseup', handleMouseUp);
39
- };
40
- }, [dragging, onChange]);
41
-
42
- return (
43
- <div className="absolute inset-0 cursor-crosshair" onMouseDown={() => setDragging(true)}>
44
- <div
45
- className={cn(
46
- 'absolute w-6 h-6 border-2 border-white rounded-full -translate-x-1/2 -translate-y-1/2',
47
- 'shadow-[0_0_0_1px_rgba(0,0,0,0.3)]',
48
- dragging && 'scale-75',
49
- )}
50
- style={{
51
- left: `${point.x * width}px`,
52
- top: `${point.y * height}px`,
53
- }}
54
- />
55
- </div>
56
- );
57
- }