@vendure/dashboard 3.4.2-master-202509020230 → 3.4.2-master-202509030226

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 (31) hide show
  1. package/package.json +4 -4
  2. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +3 -0
  3. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +7 -7
  4. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +3 -3
  5. package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +2 -2
  6. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +4 -4
  7. package/src/app/routes/_authenticated/_products/components/add-option-group-dialog.tsx +127 -0
  8. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +41 -39
  9. package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +1 -33
  10. package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx +7 -42
  11. package/src/app/routes/_authenticated/_products/components/create-product-variants.tsx +38 -134
  12. package/src/app/routes/_authenticated/_products/components/option-groups-editor.tsx +180 -0
  13. package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +9 -39
  14. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +2 -2
  15. package/src/app/routes/_authenticated/_products/products.graphql.ts +136 -0
  16. package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -9
  17. package/src/app/routes/_authenticated/_products/products_.$id_.variants.tsx +405 -0
  18. package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +2 -2
  19. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +2 -2
  20. package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +3 -3
  21. package/src/lib/components/data-input/rich-text-input.tsx +8 -4
  22. package/src/lib/components/layout/channel-switcher.tsx +27 -6
  23. package/src/lib/components/layout/manage-languages-dialog.tsx +2 -2
  24. package/src/lib/components/shared/asset/asset-gallery.tsx +20 -2
  25. package/src/lib/components/shared/asset/asset-picker-dialog.tsx +5 -5
  26. package/src/lib/components/shared/assign-to-channel-dialog.tsx +2 -2
  27. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -2
  28. package/src/lib/graphql/api.ts +3 -1
  29. package/src/lib/hooks/use-permissions.ts +4 -4
  30. package/src/lib/providers/auth.tsx +8 -0
  31. package/src/lib/providers/channel-provider.tsx +48 -57
@@ -234,7 +234,7 @@ export function AssetGallery({
234
234
  };
235
235
 
236
236
  return (
237
- <div className={`relative flex flex-col w-full ${fixedHeight ? 'h-[600px]' : ''} ${className}`}>
237
+ <div className={`relative flex flex-col w-full ${fixedHeight ? 'h-[600px]' : 'h-full'} ${className}`}>
238
238
  {showHeader && (
239
239
  <div className="flex flex-col md:flex-row gap-2 mb-4 flex-shrink-0">
240
240
  <div className="relative flex-grow flex items-center gap-2">
@@ -328,7 +328,25 @@ export function AssetGallery({
328
328
  />
329
329
  {selectable && (
330
330
  <div className="absolute top-2 left-2">
331
- <Checkbox checked={isSelected(asset as Asset)} />
331
+ <Checkbox
332
+ checked={isSelected(asset as Asset)}
333
+ onClick={e => {
334
+ e.stopPropagation();
335
+ const isCurrentlySelected = selected.some(
336
+ a => a.id === asset.id,
337
+ );
338
+ let newSelected: Asset[];
339
+
340
+ if (isCurrentlySelected) {
341
+ newSelected = selected.filter(a => a.id !== asset.id);
342
+ } else {
343
+ newSelected = [...selected, asset as Asset];
344
+ }
345
+
346
+ setSelected(newSelected);
347
+ onSelect?.(newSelected);
348
+ }}
349
+ />
332
350
  </div>
333
351
  )}
334
352
  </div>
@@ -39,22 +39,22 @@ export function AssetPickerDialog({
39
39
 
40
40
  return (
41
41
  <Dialog open={open} onOpenChange={onClose}>
42
- <DialogContent className="sm:max-w-[800px] lg:max-w-[1000px] max-h-[80vh] flex flex-col">
43
- <DialogHeader>
42
+ <DialogContent className="sm:max-w-[800px] lg:max-w-[1000px] h-[85vh] p-0 flex flex-col">
43
+ <DialogHeader className="px-6 pt-6">
44
44
  <DialogTitle>{multiSelect ? title : title.replace('Assets', 'Asset')}</DialogTitle>
45
45
  </DialogHeader>
46
46
 
47
- <div className="flex-grow py-4">
47
+ <div className="flex-1 overflow-y-auto px-6 pt-1">
48
48
  <AssetGallery
49
49
  onSelect={handleAssetSelect}
50
50
  multiSelect="manual"
51
51
  initialSelectedAssets={initialSelectedAssets}
52
- fixedHeight={true}
52
+ fixedHeight={false}
53
53
  displayBulkActions={false}
54
54
  />
55
55
  </div>
56
56
 
57
- <DialogFooter>
57
+ <DialogFooter className="px-6 pb-6 pt-4 border-t">
58
58
  <Button variant="outline" onClick={onClose}>
59
59
  Cancel
60
60
  </Button>
@@ -56,10 +56,10 @@ export function AssignToChannelDialog({
56
56
  }: Readonly<AssignToChannelDialogProps>) {
57
57
  const { i18n } = useLingui();
58
58
  const [selectedChannelId, setSelectedChannelId] = useState<string>('');
59
- const { channels, selectedChannel } = useChannel();
59
+ const { channels, activeChannel } = useChannel();
60
60
 
61
61
  // Filter out the currently selected channel from available options
62
- const availableChannels = channels.filter(channel => channel.id !== selectedChannel?.id);
62
+ const availableChannels = channels.filter(channel => channel.id !== activeChannel?.id);
63
63
 
64
64
  const { mutate, isPending } = useMutation({
65
65
  mutationFn,
@@ -42,7 +42,7 @@ export function RemoveFromChannelBulkAction({
42
42
  errorMessage,
43
43
  }: Readonly<RemoveFromChannelBulkActionProps>) {
44
44
  const { refetchPaginatedList } = usePaginatedList();
45
- const { selectedChannel } = useChannel();
45
+ const { activeChannel } = useChannel();
46
46
  const { i18n } = useLingui();
47
47
  const { mutate } = useMutation({
48
48
  mutationFn,
@@ -63,7 +63,7 @@ export function RemoveFromChannelBulkAction({
63
63
  },
64
64
  });
65
65
 
66
- if (!selectedChannel) {
66
+ if (!activeChannel) {
67
67
  return null;
68
68
  }
69
69
 
@@ -8,6 +8,8 @@ const API_URL =
8
8
  (uiConfig.api.port !== 'auto' ? `:${uiConfig.api.port}` : '') +
9
9
  `/${uiConfig.api.adminApiPath}`;
10
10
 
11
+ export const SELECTED_CHANNEL_TOKEN_KEY = 'vendure-selected-channel-token';
12
+
11
13
  export type Variables = object;
12
14
  export type RequestDocument = string | DocumentNode;
13
15
 
@@ -15,7 +17,7 @@ const awesomeClient = new AwesomeGraphQLClient({
15
17
  endpoint: API_URL,
16
18
  fetch: async (url: string, options: RequestInit = {}) => {
17
19
  // Get the active channel token from localStorage
18
- const channelToken = localStorage.getItem('vendure-selected-channel-token');
20
+ const channelToken = localStorage.getItem(SELECTED_CHANNEL_TOKEN_KEY);
19
21
  const headers = new Headers(options.headers);
20
22
 
21
23
  if (channelToken) {
@@ -18,18 +18,18 @@ import { useChannel } from './use-channel.js';
18
18
  */
19
19
  export function usePermissions() {
20
20
  const { channels } = useAuth();
21
- const { selectedChannelId } = useChannel();
21
+ const { activeChannel } = useChannel();
22
22
 
23
23
  function hasPermissions(permissions: string[]) {
24
24
  if (permissions.length === 0) {
25
25
  return true;
26
26
  }
27
27
  // Use the selected channel instead of settings.activeChannelId
28
- const activeChannel = (channels ?? []).find(channel => channel.id === selectedChannelId);
29
- if (!activeChannel) {
28
+ const selectedChannel = (channels ?? []).find(channel => channel.id === activeChannel?.id);
29
+ if (!selectedChannel) {
30
30
  return false;
31
31
  }
32
- return permissions.some(permission => activeChannel.permissions.includes(permission as Permission));
32
+ return permissions.some(permission => selectedChannel.permissions.includes(permission as Permission));
33
33
  }
34
34
 
35
35
  return { hasPermissions };
@@ -21,6 +21,7 @@ export interface AuthContext {
21
21
  logout: (onSuccess?: () => void) => Promise<void>;
22
22
  user: ResultOf<typeof CurrentUserQuery>['activeAdministrator'] | undefined;
23
23
  channels: NonNullable<ResultOf<typeof CurrentUserQuery>['me']>['channels'] | undefined;
24
+ refreshCurrentUser: () => void;
24
25
  }
25
26
 
26
27
  const LoginMutation = graphql(`
@@ -174,6 +175,12 @@ export function AuthProvider({ children }: Readonly<{ children: React.ReactNode
174
175
  }
175
176
  }, [isLoading, currentUserData, currentUserError, status, isLoginLogoutInProgress]);
176
177
 
178
+ const refreshCurrentUser = () => {
179
+ queryClient.invalidateQueries({
180
+ queryKey: ['currentUser'],
181
+ });
182
+ };
183
+
177
184
  return (
178
185
  <AuthContext.Provider
179
186
  value={{
@@ -184,6 +191,7 @@ export function AuthProvider({ children }: Readonly<{ children: React.ReactNode
184
191
  channels: currentUserData?.me?.channels,
185
192
  login,
186
193
  logout,
194
+ refreshCurrentUser,
187
195
  }}
188
196
  >
189
197
  {children}
@@ -1,6 +1,7 @@
1
- import { api } from '@/vdb/graphql/api.js';
2
- import { ResultOf, graphql } from '@/vdb/graphql/graphql.js';
1
+ import { api, SELECTED_CHANNEL_TOKEN_KEY } from '@/vdb/graphql/api.js';
2
+ import { graphql, ResultOf } from '@/vdb/graphql/graphql.js';
3
3
  import { useAuth } from '@/vdb/hooks/use-auth.js';
4
+ import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
4
5
  import { useQuery, useQueryClient } from '@tanstack/react-query';
5
6
  import * as React from 'react';
6
7
 
@@ -51,33 +52,37 @@ type Channel = ResultOf<typeof channelFragment>;
51
52
  * @since 3.3.0
52
53
  */
53
54
  export interface ChannelContext {
54
- activeChannel: ActiveChannel | undefined;
55
- channels: Channel[];
56
- selectedChannelId: string | undefined;
57
- selectedChannel: Channel | undefined;
58
55
  isLoading: boolean;
59
- setSelectedChannel: (channelId: string) => void;
56
+ channels: Channel[];
57
+ activeChannel: ActiveChannel | undefined;
58
+ setActiveChannel: (channelId: string) => void;
59
+ refreshChannels: () => void;
60
+ }
61
+
62
+ /**
63
+ * Sets the channel token in localStorage, which is then used by the `api`
64
+ * object to ensure we add the correct token header to all API calls.
65
+ */
66
+ function setChannelTokenInLocalStorage(channelToken: string) {
67
+ try {
68
+ localStorage.setItem(SELECTED_CHANNEL_TOKEN_KEY, channelToken);
69
+ } catch (e) {
70
+ console.error('Failed to store selected channel in localStorage', e);
71
+ }
60
72
  }
61
73
 
62
74
  // Create the context
63
75
  export const ChannelContext = React.createContext<ChannelContext | undefined>(undefined);
64
76
 
65
- // Local storage key for the selected channel
66
- const SELECTED_CHANNEL_KEY = 'vendure-selected-channel';
67
- const SELECTED_CHANNEL_TOKEN_KEY = 'vendure-selected-channel-token';
68
-
69
77
  export function ChannelProvider({ children }: Readonly<{ children: React.ReactNode }>) {
70
78
  const queryClient = useQueryClient();
71
- const { channels: userChannels, isAuthenticated } = useAuth();
79
+ const {
80
+ setActiveChannelId,
81
+ settings: { activeChannelId },
82
+ } = useUserSettings();
83
+ const { channels: userChannels, isAuthenticated, refreshCurrentUser } = useAuth();
72
84
  const [selectedChannelId, setSelectedChannelId] = React.useState<string | undefined>(() => {
73
- // Initialize from localStorage if available
74
- try {
75
- const storedChannelId = localStorage.getItem(SELECTED_CHANNEL_KEY);
76
- return storedChannelId || undefined;
77
- } catch (e) {
78
- console.error('Failed to load selected channel from localStorage', e);
79
- return undefined;
80
- }
85
+ return activeChannelId;
81
86
  });
82
87
 
83
88
  // Fetch all available channels
@@ -90,15 +95,15 @@ export function ChannelProvider({ children }: Readonly<{ children: React.ReactNo
90
95
 
91
96
  // Filter channels based on user permissions
92
97
  const channels = React.useMemo(() => {
93
- // If user has specific channels assigned (non-superadmin), use those
98
+ // If user has specific channels assigned, use those
94
99
  if (userChannels && userChannels.length > 0) {
95
100
  // Map user channels to match the Channel type structure
96
101
  return userChannels.map(ch => {
97
102
  const fullChannelData = channelsData?.channels.items.find(c => c.id === ch.id);
98
103
  return {
99
104
  id: ch.id,
100
- code: ch.code,
101
- token: ch.token,
105
+ code: fullChannelData?.code ?? ch.code,
106
+ token: fullChannelData?.token ?? ch.token,
102
107
  defaultLanguageCode: fullChannelData?.defaultLanguageCode || 'en',
103
108
  defaultCurrencyCode: fullChannelData?.defaultCurrencyCode || 'USD',
104
109
  pricesIncludeTax: fullChannelData?.pricesIncludeTax || false,
@@ -106,25 +111,19 @@ export function ChannelProvider({ children }: Readonly<{ children: React.ReactNo
106
111
  };
107
112
  });
108
113
  }
109
- // Otherwise use all channels (superadmin)
114
+ // Otherwise use all channels
110
115
  return channelsData?.channels.items || [];
111
116
  }, [userChannels, channelsData?.channels.items]);
112
117
 
113
118
  // Set the selected channel and update localStorage
114
119
  const setSelectedChannel = React.useCallback(
115
120
  (channelId: string) => {
116
- try {
117
- // Find the channel to get its token
118
- const channel = channels.find(c => c.id === channelId);
119
- if (channel) {
120
- // Store channel ID and token in localStorage
121
- localStorage.setItem(SELECTED_CHANNEL_KEY, channelId);
122
- localStorage.setItem(SELECTED_CHANNEL_TOKEN_KEY, channel.token);
123
- setSelectedChannelId(channelId);
124
- queryClient.invalidateQueries();
125
- }
126
- } catch (e) {
127
- console.error('Failed to set selected channel', e);
121
+ const channel = channels.find(c => c.id === channelId);
122
+ if (channel) {
123
+ setChannelTokenInLocalStorage(channel.token);
124
+ setSelectedChannelId(channelId);
125
+ setActiveChannelId(channelId);
126
+ queryClient.invalidateQueries();
128
127
  }
129
128
  },
130
129
  [queryClient, channels],
@@ -136,44 +135,36 @@ export function ChannelProvider({ children }: Readonly<{ children: React.ReactNo
136
135
  const validChannelIds = channels.map(c => c.id);
137
136
 
138
137
  // If selected channel is not valid for this user, reset it
139
- if (selectedChannelId && !validChannelIds.includes(selectedChannelId)) {
138
+ if (selectedChannelId && validChannelIds.length && !validChannelIds.includes(selectedChannelId)) {
140
139
  setSelectedChannelId(undefined);
141
- try {
142
- localStorage.removeItem(SELECTED_CHANNEL_KEY);
143
- localStorage.removeItem(SELECTED_CHANNEL_TOKEN_KEY);
144
- } catch (e) {
145
- console.error('Failed to remove selected channel from localStorage', e);
146
- }
147
140
  }
148
141
 
149
142
  // If no selected channel is set, use the first available channel
150
143
  if (!selectedChannelId && channels.length > 0) {
151
144
  const defaultChannel = channels[0];
152
145
  setSelectedChannelId(defaultChannel.id);
153
- try {
154
- localStorage.setItem(SELECTED_CHANNEL_KEY, defaultChannel.id);
155
- localStorage.setItem(SELECTED_CHANNEL_TOKEN_KEY, defaultChannel.token);
156
- } catch (e) {
157
- console.error('Failed to store selected channel in localStorage', e);
158
- }
146
+ setChannelTokenInLocalStorage(defaultChannel.token);
159
147
  }
160
148
  }, [selectedChannelId, channels]);
161
149
 
162
- const activeChannel = channelsData?.activeChannel;
163
150
  const isLoading = isChannelsLoading;
164
151
 
165
152
  // Find the selected channel from the list of channels
166
- const selectedChannel = React.useMemo(() => {
167
- return channels.find(channel => channel.id === selectedChannelId);
168
- }, [channels, selectedChannelId]);
153
+ const selectedChannel = channelsData?.activeChannel;
154
+
155
+ const refreshChannels = () => {
156
+ refreshCurrentUser();
157
+ queryClient.invalidateQueries({
158
+ queryKey: ['channels', isAuthenticated],
159
+ });
160
+ };
169
161
 
170
162
  const contextValue: ChannelContext = {
171
- activeChannel,
172
163
  channels,
173
- selectedChannelId,
174
- selectedChannel,
164
+ activeChannel: selectedChannel,
175
165
  isLoading,
176
- setSelectedChannel,
166
+ setActiveChannel: setSelectedChannel,
167
+ refreshChannels,
177
168
  };
178
169
 
179
170
  return <ChannelContext.Provider value={contextValue}>{children}</ChannelContext.Provider>;