@vendure/dashboard 3.4.2-master-202508290230 → 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.
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +3 -0
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +7 -7
- package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +3 -3
- package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +4 -4
- package/src/app/routes/_authenticated/_products/components/add-option-group-dialog.tsx +127 -0
- package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +41 -39
- package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +1 -33
- package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx +7 -42
- package/src/app/routes/_authenticated/_products/components/create-product-variants.tsx +38 -134
- package/src/app/routes/_authenticated/_products/components/option-groups-editor.tsx +180 -0
- package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +9 -39
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_products/products.graphql.ts +136 -0
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -9
- package/src/app/routes/_authenticated/_products/products_.$id_.variants.tsx +405 -0
- package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +3 -3
- package/src/lib/components/data-input/rich-text-input.tsx +8 -4
- package/src/lib/components/layout/channel-switcher.tsx +27 -6
- package/src/lib/components/layout/manage-languages-dialog.tsx +2 -2
- package/src/lib/components/shared/asset/asset-gallery.tsx +20 -2
- package/src/lib/components/shared/asset/asset-picker-dialog.tsx +5 -5
- package/src/lib/components/shared/assign-to-channel-dialog.tsx +2 -2
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -2
- package/src/lib/graphql/api.ts +3 -1
- package/src/lib/hooks/use-permissions.ts +4 -4
- package/src/lib/providers/auth.tsx +8 -0
- 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
|
|
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]
|
|
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-
|
|
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={
|
|
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,
|
|
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 !==
|
|
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 {
|
|
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 (!
|
|
66
|
+
if (!activeChannel) {
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
|
package/src/lib/graphql/api.ts
CHANGED
|
@@ -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(
|
|
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 {
|
|
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
|
|
29
|
-
if (!
|
|
28
|
+
const selectedChannel = (channels ?? []).find(channel => channel.id === activeChannel?.id);
|
|
29
|
+
if (!selectedChannel) {
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
|
-
return permissions.some(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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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 =
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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>;
|