@vendure/dashboard 3.3.2 → 3.3.3

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 (37) hide show
  1. package/dist/plugin/utils/config-loader.d.ts +12 -1
  2. package/dist/plugin/utils/config-loader.js +25 -7
  3. package/dist/plugin/vite-plugin-vendure-dashboard.d.ts +8 -0
  4. package/dist/plugin/vite-plugin-vendure-dashboard.js +5 -1
  5. package/package.json +4 -4
  6. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +1 -4
  7. package/src/app/routes/_authenticated/_channels/channels.tsx +18 -0
  8. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +1 -5
  9. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -4
  10. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +1 -4
  11. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +1 -4
  12. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -5
  13. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +56 -74
  14. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +369 -0
  15. package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +435 -0
  16. package/src/app/routes/_authenticated/_products/components/product-option-select.tsx +117 -0
  17. package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +4 -2
  18. package/src/app/routes/_authenticated/_products/products_.$id.tsx +17 -3
  19. package/src/app/routes/_authenticated/_profile/profile.tsx +1 -4
  20. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -4
  21. package/src/lib/components/data-table/data-table-view-options.tsx +12 -2
  22. package/src/lib/components/data-table/data-table.tsx +9 -0
  23. package/src/lib/components/layout/channel-switcher.tsx +1 -2
  24. package/src/lib/components/shared/assigned-facet-values.tsx +13 -14
  25. package/src/lib/components/shared/entity-assets.tsx +140 -70
  26. package/src/lib/components/shared/paginated-list-data-table.tsx +10 -0
  27. package/src/lib/components/ui/button.tsx +1 -1
  28. package/src/lib/framework/form-engine/use-generated-form.tsx +1 -0
  29. package/src/lib/framework/page/list-page.tsx +2 -2
  30. package/src/lib/framework/page/use-detail-page.ts +7 -0
  31. package/src/lib/graphql/api.ts +10 -1
  32. package/src/lib/hooks/use-permissions.ts +4 -4
  33. package/src/lib/providers/auth.tsx +9 -3
  34. package/src/lib/providers/channel-provider.tsx +64 -24
  35. package/src/lib/providers/server-config.tsx +2 -2
  36. package/vite/utils/config-loader.ts +48 -13
  37. package/vite/vite-plugin-vendure-dashboard.ts +14 -4
@@ -1,7 +1,7 @@
1
1
  import { api } from '@/graphql/api.js';
2
2
  import { ResultOf, graphql } from '@/graphql/graphql.js';
3
- import { useQuery, useQueryClient } from '@tanstack/react-query';
4
3
  import { useAuth } from '@/hooks/use-auth.js';
4
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
5
5
  import * as React from 'react';
6
6
 
7
7
  // Define the channel fragment for reuse
@@ -63,10 +63,11 @@ export const ChannelContext = React.createContext<ChannelContext | undefined>(un
63
63
 
64
64
  // Local storage key for the selected channel
65
65
  const SELECTED_CHANNEL_KEY = 'vendure-selected-channel';
66
+ const SELECTED_CHANNEL_TOKEN_KEY = 'vendure-selected-channel-token';
66
67
 
67
68
  export function ChannelProvider({ children }: { children: React.ReactNode }) {
68
- const { isAuthenticated } = useAuth();
69
69
  const queryClient = useQueryClient();
70
+ const { channels: userChannels, isAuthenticated } = useAuth();
70
71
  const [selectedChannelId, setSelectedChannelId] = React.useState<string | undefined>(() => {
71
72
  // Initialize from localStorage if available
72
73
  try {
@@ -79,44 +80,83 @@ export function ChannelProvider({ children }: { children: React.ReactNode }) {
79
80
  });
80
81
 
81
82
  // Fetch all available channels
82
- const { data: channelsData, isLoading: isChannelsLoading, refetch: refetchChannels } = useQuery({
83
- queryKey: ['channels'],
83
+ const { data: channelsData, isLoading: isChannelsLoading } = useQuery({
84
+ queryKey: ['channels', isAuthenticated],
84
85
  queryFn: () => api.query(ChannelsQuery),
85
86
  retry: false,
87
+ enabled: isAuthenticated,
86
88
  });
87
89
 
88
- React.useEffect(() => {
89
- if (isAuthenticated) {
90
- // Refetch channels when authenticated
91
- refetchChannels();
90
+ // Filter channels based on user permissions
91
+ const channels = React.useMemo(() => {
92
+ // If user has specific channels assigned (non-superadmin), use those
93
+ if (userChannels && userChannels.length > 0) {
94
+ // Map user channels to match the Channel type structure
95
+ return userChannels.map(ch => ({
96
+ id: ch.id,
97
+ code: ch.code,
98
+ token: ch.token,
99
+ defaultLanguageCode:
100
+ channelsData?.channels.items.find(c => c.id === ch.id)?.defaultLanguageCode || 'en',
101
+ defaultCurrencyCode:
102
+ channelsData?.channels.items.find(c => c.id === ch.id)?.defaultCurrencyCode || 'USD',
103
+ pricesIncludeTax:
104
+ channelsData?.channels.items.find(c => c.id === ch.id)?.pricesIncludeTax || false,
105
+ }));
92
106
  }
93
- }, [isAuthenticated, refetchChannels])
107
+ // Otherwise use all channels (superadmin)
108
+ return channelsData?.channels.items || [];
109
+ }, [userChannels, channelsData?.channels.items]);
94
110
 
95
111
  // Set the selected channel and update localStorage
96
- const setSelectedChannel = React.useCallback((channelId: string) => {
97
- try {
98
- // Store in localStorage
99
- localStorage.setItem(SELECTED_CHANNEL_KEY, channelId);
100
- setSelectedChannelId(channelId);
101
- queryClient.invalidateQueries();
102
- } catch (e) {
103
- console.error('Failed to set selected channel', e);
104
- }
105
- }, []);
112
+ const setSelectedChannel = React.useCallback(
113
+ (channelId: string) => {
114
+ try {
115
+ // Find the channel to get its token
116
+ const channel = channels.find(c => c.id === channelId);
117
+ if (channel) {
118
+ // Store channel ID and token in localStorage
119
+ localStorage.setItem(SELECTED_CHANNEL_KEY, channelId);
120
+ localStorage.setItem(SELECTED_CHANNEL_TOKEN_KEY, channel.token);
121
+ setSelectedChannelId(channelId);
122
+ queryClient.invalidateQueries();
123
+ }
124
+ } catch (e) {
125
+ console.error('Failed to set selected channel', e);
126
+ }
127
+ },
128
+ [queryClient, channels],
129
+ );
106
130
 
107
131
  // If no selected channel is set but we have an active channel, use that
132
+ // Also validate that the selected channel is accessible to the user
108
133
  React.useEffect(() => {
109
- if (!selectedChannelId && channelsData?.activeChannel?.id) {
110
- setSelectedChannelId(channelsData.activeChannel.id);
134
+ const validChannelIds = channels.map(c => c.id);
135
+
136
+ // If selected channel is not valid for this user, reset it
137
+ if (selectedChannelId && !validChannelIds.includes(selectedChannelId)) {
138
+ setSelectedChannelId(undefined);
139
+ try {
140
+ localStorage.removeItem(SELECTED_CHANNEL_KEY);
141
+ localStorage.removeItem(SELECTED_CHANNEL_TOKEN_KEY);
142
+ } catch (e) {
143
+ console.error('Failed to remove selected channel from localStorage', e);
144
+ }
145
+ }
146
+
147
+ // If no selected channel is set, use the first available channel
148
+ if (!selectedChannelId && channels.length > 0) {
149
+ const defaultChannel = channels[0];
150
+ setSelectedChannelId(defaultChannel.id);
111
151
  try {
112
- localStorage.setItem(SELECTED_CHANNEL_KEY, channelsData.activeChannel.id);
152
+ localStorage.setItem(SELECTED_CHANNEL_KEY, defaultChannel.id);
153
+ localStorage.setItem(SELECTED_CHANNEL_TOKEN_KEY, defaultChannel.token);
113
154
  } catch (e) {
114
155
  console.error('Failed to store selected channel in localStorage', e);
115
156
  }
116
157
  }
117
- }, [selectedChannelId, channelsData]);
158
+ }, [selectedChannelId, channels]);
118
159
 
119
- const channels = channelsData?.channels.items || [];
120
160
  const activeChannel = channelsData?.activeChannel;
121
161
  const isLoading = isChannelsLoading;
122
162
 
@@ -270,14 +270,14 @@ export const ServerConfigProvider = ({ children }: { children: React.ReactNode }
270
270
  enabled: !!user?.id,
271
271
  staleTime: 1000,
272
272
  });
273
- const value: ServerConfig = {
273
+ const value: ServerConfig | null = data?.globalSettings ? {
274
274
  availableLanguages: data?.globalSettings.availableLanguages ?? [],
275
275
  moneyStrategyPrecision: data?.globalSettings.serverConfig.moneyStrategyPrecision ?? 2,
276
276
  orderProcess: data?.globalSettings.serverConfig.orderProcess ?? [],
277
277
  permittedAssetTypes: data?.globalSettings.serverConfig.permittedAssetTypes ?? [],
278
278
  permissions: data?.globalSettings.serverConfig.permissions ?? [],
279
279
  entityCustomFields: data?.globalSettings.serverConfig.entityCustomFields ?? [],
280
- };
280
+ } : null;
281
281
 
282
282
  return <ServerConfigContext.Provider value={value}>{children}</ServerConfigContext.Provider>;
283
283
  };
@@ -36,6 +36,7 @@ export interface ConfigLoaderOptions {
36
36
  tempDir: string;
37
37
  vendureConfigExport?: string;
38
38
  logger?: Logger;
39
+ reportCompilationErrors?: boolean;
39
40
  }
40
41
 
41
42
  export interface LoadVendureConfigResult {
@@ -67,7 +68,12 @@ export async function loadVendureConfig(options: ConfigLoaderOptions): Promise<L
67
68
  const configFileName = path.basename(vendureConfigPath);
68
69
  const inputRootDir = path.dirname(vendureConfigPath);
69
70
  await fs.remove(outputPath);
70
- const pluginInfo = await compileFile(inputRootDir, vendureConfigPath, outputPath, logger);
71
+ const pluginInfo = await compileFile({
72
+ inputRootDir,
73
+ inputPath: vendureConfigPath,
74
+ outputDir: outputPath,
75
+ logger,
76
+ });
71
77
  const compiledConfigFilePath = pathToFileURL(path.join(outputPath, configFileName)).href.replace(
72
78
  /.ts$/,
73
79
  '.js',
@@ -147,7 +153,14 @@ async function findTsConfigPaths(
147
153
  );
148
154
  }
149
155
  logger.debug(
150
- `Found tsconfig paths in ${tsConfigPath}: ${JSON.stringify({ baseUrl: tsConfigBaseUrl, paths }, null, 2)}`,
156
+ `Found tsconfig paths in ${tsConfigPath}: ${JSON.stringify(
157
+ {
158
+ baseUrl: tsConfigBaseUrl,
159
+ paths,
160
+ },
161
+ null,
162
+ 2,
163
+ )}`,
151
164
  );
152
165
  return { baseUrl: tsConfigBaseUrl, paths };
153
166
  }
@@ -167,15 +180,27 @@ async function findTsConfigPaths(
167
180
  return undefined;
168
181
  }
169
182
 
170
- export async function compileFile(
171
- inputRootDir: string,
172
- inputPath: string,
173
- outputDir: string,
174
- logger: Logger = defaultLogger,
183
+ type CompileFileOptions = {
184
+ inputRootDir: string;
185
+ inputPath: string;
186
+ outputDir: string;
187
+ logger?: Logger;
188
+ compiledFiles?: Set<string>;
189
+ isRoot?: boolean;
190
+ pluginInfo?: PluginInfo[];
191
+ reportCompilationErrors?: boolean;
192
+ };
193
+
194
+ export async function compileFile({
195
+ inputRootDir,
196
+ inputPath,
197
+ outputDir,
198
+ logger = defaultLogger,
175
199
  compiledFiles = new Set<string>(),
176
200
  isRoot = true,
177
- pluginInfo: PluginInfo[] = [],
178
- ): Promise<PluginInfo[]> {
201
+ pluginInfo = [],
202
+ reportCompilationErrors = false,
203
+ }: CompileFileOptions): Promise<PluginInfo[]> {
179
204
  const absoluteInputPath = path.resolve(inputPath);
180
205
  if (compiledFiles.has(absoluteInputPath)) {
181
206
  return pluginInfo;
@@ -325,7 +350,15 @@ export async function compileFile(
325
350
  // Recursively collect all files that need to be compiled
326
351
  for (const importPath of importPaths) {
327
352
  // Pass rootTsConfigInfo down, but set isRoot to false
328
- await compileFile(inputRootDir, importPath, outputDir, logger, compiledFiles, false, pluginInfo);
353
+ await compileFile({
354
+ inputRootDir,
355
+ inputPath: importPath,
356
+ outputDir,
357
+ logger,
358
+ compiledFiles,
359
+ isRoot: false,
360
+ pluginInfo,
361
+ });
329
362
  }
330
363
 
331
364
  // If this is the root file (the one that started the compilation),
@@ -370,10 +403,12 @@ export async function compileFile(
370
403
  logger.info(`Emitting compiled files to ${outputDir}`);
371
404
  const emitResult = program.emit();
372
405
 
373
- const hasEmitErrors = reportDiagnostics(program, emitResult, logger);
406
+ if (reportCompilationErrors) {
407
+ const hasEmitErrors = reportDiagnostics(program, emitResult, logger);
374
408
 
375
- if (hasEmitErrors) {
376
- throw new Error('TypeScript compilation failed with errors.');
409
+ if (hasEmitErrors) {
410
+ throw new Error('TypeScript compilation failed with errors.');
411
+ }
377
412
  }
378
413
 
379
414
  logger.info(`Successfully compiled ${allFiles.length} files to ${outputDir}`);
@@ -1,4 +1,3 @@
1
- import { lingui } from '@lingui/vite-plugin';
2
1
  import tailwindcss from '@tailwindcss/vite';
3
2
  import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
4
3
  import react from '@vitejs/plugin-react';
@@ -10,8 +9,7 @@ import { configLoaderPlugin } from './vite-plugin-config-loader.js';
10
9
  import { viteConfigPlugin } from './vite-plugin-config.js';
11
10
  import { dashboardMetadataPlugin } from './vite-plugin-dashboard-metadata.js';
12
11
  import { gqlTadaPlugin } from './vite-plugin-gql-tada.js';
13
- import { themeVariablesPlugin } from './vite-plugin-theme.js';
14
- import { ThemeVariablesPluginOptions } from './vite-plugin-theme.js';
12
+ import { ThemeVariablesPluginOptions, themeVariablesPlugin } from './vite-plugin-theme.js';
15
13
  import { UiConfigPluginOptions, uiConfigPlugin } from './vite-plugin-ui-config.js';
16
14
 
17
15
  /**
@@ -37,6 +35,14 @@ export type VitePluginVendureDashboardOptions = {
37
35
  gqlTadaOutputPath?: string;
38
36
  tempCompilationDir?: string;
39
37
  disableTansStackRouterPlugin?: boolean;
38
+ /**
39
+ * @description
40
+ * If set to `true`, compilation errors during the build process will be reported and
41
+ * the build will fail.
42
+ *
43
+ * @default false
44
+ */
45
+ reportCompilationErrors?: boolean;
40
46
  } & UiConfigPluginOptions &
41
47
  ThemeVariablesPluginOptions;
42
48
 
@@ -74,7 +80,11 @@ export function vendureDashboardPlugin(options: VitePluginVendureDashboardOption
74
80
  }),
75
81
  themeVariablesPlugin({ theme: options.theme }),
76
82
  tailwindcss(),
77
- configLoaderPlugin({ vendureConfigPath: normalizedVendureConfigPath, tempDir }),
83
+ configLoaderPlugin({
84
+ vendureConfigPath: normalizedVendureConfigPath,
85
+ tempDir,
86
+ reportCompilationErrors: options.reportCompilationErrors,
87
+ }),
78
88
  viteConfigPlugin({ packageRoot }),
79
89
  adminApiSchemaPlugin(),
80
90
  dashboardMetadataPlugin({ rootDir: tempDir }),