@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
@@ -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,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
- }