@vendure/dashboard 3.8.1-master-202610220213 → 3.8.1

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 (90) hide show
  1. package/dist/bundle/chunks/{2SSKDMRQ-BVYTtZ4W.js → 2SSKDMRQ-BHHzW1a_.js} +2 -2
  2. package/dist/bundle/chunks/{AP7HFJJL-BDXyVf1-.js → AP7HFJJL-B5qPACUM.js} +1 -1
  3. package/dist/bundle/chunks/{WDYDFRGG-DP5Ikh83.js → WDYDFRGG-BfBnFCDT.js} +2 -2
  4. package/dist/bundle/chunks/{ar-DMK_eFVD.js → ar-ZYyYt3LW.js} +1 -1
  5. package/dist/bundle/chunks/{bg-_l7rTFuK.js → bg-BgDGBTKB.js} +1 -1
  6. package/dist/bundle/chunks/{common-operations-CVADEI6Z.js → common-operations-DaHPL4es.js} +732 -674
  7. package/dist/bundle/chunks/{cs-DMrGo78x.js → cs-BYGURbP6.js} +1 -1
  8. package/dist/bundle/chunks/{de-DEE86yVt.js → de-HdkR3fYi.js} +1 -1
  9. package/dist/bundle/chunks/{en-Cl1y9IIy.js → en-BAhByZZJ.js} +1 -1
  10. package/dist/bundle/chunks/{es-BxdaHguU.js → es-Dt0C52Mq.js} +1 -1
  11. package/dist/bundle/chunks/{fa-DmehFzkF.js → fa-CREXA3HL.js} +1 -1
  12. package/dist/bundle/chunks/{fr-OT8cvTDq.js → fr-Cz8kKMli.js} +1 -1
  13. package/dist/bundle/chunks/{he-BKP3NS5j.js → he-9NoIAj8m.js} +1 -1
  14. package/dist/bundle/chunks/{hr-BYAgAid3.js → hr-BwXcr71B.js} +1 -1
  15. package/dist/bundle/chunks/{hu-D5RzrlfJ.js → hu-DUz4OEIA.js} +1 -1
  16. package/dist/bundle/chunks/{it-DjrjGb8J.js → it-CAlaDmH7.js} +1 -1
  17. package/dist/bundle/chunks/{ja-CHHuYFJd.js → ja-2JxS6pFi.js} +1 -1
  18. package/dist/bundle/chunks/{ko-CIrLMXet.js → ko-BsFLDX_m.js} +1 -1
  19. package/dist/bundle/chunks/{locale-DlaghAWQ.js → locale-cLfB2IZc.js} +1 -1
  20. package/dist/bundle/chunks/{main-CcSc9LYG.js → main-MGWdQzqd.js} +3 -3
  21. package/dist/bundle/chunks/{nb-CBS2CxWA.js → nb-Dff7oHih.js} +1 -1
  22. package/dist/bundle/chunks/{ne-BKFoZX0n.js → ne-ByGdZmfQ.js} +1 -1
  23. package/dist/bundle/chunks/{nl-CioNp_N5.js → nl-DniFiVio.js} +1 -1
  24. package/dist/bundle/chunks/{pl-ChqS0pNw.js → pl-B5DHommF.js} +1 -1
  25. package/dist/bundle/chunks/{pt_BR-8wz_k8Ib.js → pt_BR-BAq7WTWG.js} +1 -1
  26. package/dist/bundle/chunks/{pt_PT-BsddGCVI.js → pt_PT-DL7eSU8R.js} +1 -1
  27. package/dist/bundle/chunks/{ro-DLZ4JjP9.js → ro-CGYxDEv8.js} +1 -1
  28. package/dist/bundle/chunks/{ru-CDB-oX6Y.js → ru-BduayFrD.js} +1 -1
  29. package/dist/bundle/chunks/{sv-C05330I8.js → sv-E3RLr9cy.js} +1 -1
  30. package/dist/bundle/chunks/{tr-BmdyAxJM.js → tr-pMuzyLjn.js} +1 -1
  31. package/dist/bundle/chunks/{uk-UHmyTWD9.js → uk-BFfpHemj.js} +1 -1
  32. package/dist/bundle/chunks/{uz-BPv1mncF.js → uz-DBzLLOoE.js} +1 -1
  33. package/dist/bundle/chunks/{zh_Hans-DYtmTSvd.js → zh_Hans-DDD5yH27.js} +1 -1
  34. package/dist/bundle/chunks/{zh_Hant-CdyREtHJ.js → zh_Hant-B9iOWieA.js} +1 -1
  35. package/dist/bundle/lib.js +653 -707
  36. package/dist/bundle/main.js +2 -2
  37. package/dist/vite/utils/get-active-plugin-info.d.ts +0 -29
  38. package/dist/vite/utils/get-active-plugin-info.js +1 -3
  39. package/dist/vite/vite-plugin-config.js +28 -2
  40. package/dist/vite/vite-plugin-dashboard-metadata.js +47 -15
  41. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +0 -1
  42. package/dist/vite/vite-plugin-vendure-dashboard.js +0 -5
  43. package/package.json +3 -3
  44. package/src/i18n/locales/ar.po +4 -0
  45. package/src/i18n/locales/bg.po +4 -0
  46. package/src/i18n/locales/cs.po +4 -0
  47. package/src/i18n/locales/de.po +4 -0
  48. package/src/i18n/locales/en.po +4 -0
  49. package/src/i18n/locales/es.po +4 -0
  50. package/src/i18n/locales/fa.po +4 -0
  51. package/src/i18n/locales/fr.po +4 -0
  52. package/src/i18n/locales/he.po +4 -0
  53. package/src/i18n/locales/hr.po +4 -0
  54. package/src/i18n/locales/hu.po +4 -0
  55. package/src/i18n/locales/it.po +4 -0
  56. package/src/i18n/locales/ja.po +4 -0
  57. package/src/i18n/locales/ko.po +4 -0
  58. package/src/i18n/locales/nb.po +4 -0
  59. package/src/i18n/locales/ne.po +4 -0
  60. package/src/i18n/locales/nl.po +4 -0
  61. package/src/i18n/locales/pl.po +4 -0
  62. package/src/i18n/locales/pt_BR.po +4 -0
  63. package/src/i18n/locales/pt_PT.po +4 -0
  64. package/src/i18n/locales/ro.po +4 -0
  65. package/src/i18n/locales/ru.po +4 -0
  66. package/src/i18n/locales/sv.po +4 -0
  67. package/src/i18n/locales/tr.po +4 -0
  68. package/src/i18n/locales/uk.po +4 -0
  69. package/src/i18n/locales/uz.po +4 -0
  70. package/src/i18n/locales/zh_Hans.po +4 -0
  71. package/src/i18n/locales/zh_Hant.po +4 -0
  72. package/src/lib/components/shared/error-page.tsx +2 -2
  73. package/src/lib/components/shared/paginated-list-data-table.spec.tsx +151 -0
  74. package/src/lib/components/shared/paginated-list-data-table.tsx +11 -2
  75. package/src/lib/constants.ts +1 -0
  76. package/src/lib/framework/registry/registry-types.ts +0 -2
  77. package/src/lib/index.ts +0 -3
  78. package/src/lib/providers/auth.tsx +75 -7
  79. package/src/lib/providers/server-config.tsx +3 -2
  80. package/src/lib/providers/user-settings.spec.tsx +191 -0
  81. package/src/lib/providers/user-settings.tsx +30 -14
  82. package/src/lib/storage-keys.ts +3 -1
  83. package/src/lib/utils/is-forbidden-error.ts +9 -0
  84. package/src/lib/virtual.d.ts +0 -6
  85. package/dist/vite/vite-plugin-platform-promotion.d.ts +0 -25
  86. package/dist/vite/vite-plugin-platform-promotion.js +0 -83
  87. package/src/lib/framework/platform-promotion/platform-granted-capabilities.ts +0 -71
  88. package/src/lib/framework/platform-promotion/platform-promotion-types.ts +0 -5
  89. package/src/lib/hooks/use-platform-promotion.spec.tsx +0 -117
  90. package/src/lib/hooks/use-platform-promotion.ts +0 -117
@@ -1,4 +1,5 @@
1
- import { LS_KEY_USER_SETTINGS } from '@/vdb/constants.js';
1
+ import { LS_KEY_USER_SETTINGS, SETTINGS_STORE_KEY_USER_SETTINGS } from '@/vdb/constants.js';
2
+ import { isForbiddenError } from '@/vdb/utils/is-forbidden-error.js';
2
3
  import { QueryClient, useMutation, useQuery } from '@tanstack/react-query';
3
4
  import { ColumnFiltersState } from '@tanstack/react-table';
4
5
  import React, { createContext, useEffect, useRef, useState } from 'react';
@@ -136,8 +137,6 @@ export interface UserSettingsContextType {
136
137
 
137
138
  export const UserSettingsContext = createContext<UserSettingsContextType | undefined>(undefined);
138
139
 
139
- const SETTINGS_STORE_KEY = 'vendure.dashboard.userSettings';
140
-
141
140
  interface UserSettingsProviderProps {
142
141
  queryClient?: QueryClient;
143
142
  children: React.ReactNode;
@@ -171,8 +170,8 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ quer
171
170
  isError: serverSettingsErrored,
172
171
  error,
173
172
  } = useQuery({
174
- queryKey: ['user-settings', SETTINGS_STORE_KEY],
175
- queryFn: () => api.query(getSettingsStoreValueDocument, { key: SETTINGS_STORE_KEY }),
173
+ queryKey: ['user-settings', SETTINGS_STORE_KEY_USER_SETTINGS],
174
+ queryFn: () => api.query(getSettingsStoreValueDocument, { key: SETTINGS_STORE_KEY_USER_SETTINGS }),
176
175
  retry: false,
177
176
  staleTime: 0,
178
177
  enabled: settingsStoreIsAvailable,
@@ -181,7 +180,9 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ quer
181
180
  useEffect(() => {
182
181
  if (
183
182
  settingsStoreIsAvailable &&
184
- error?.message.includes('Settings store field not registered: vendure.dashboard.userSettings')
183
+ error?.message.includes(
184
+ `Settings store field not registered: ${SETTINGS_STORE_KEY_USER_SETTINGS}`,
185
+ )
185
186
  ) {
186
187
  logSettingsStoreWarning();
187
188
  setSettingsStoreIsAvailable(false);
@@ -192,7 +193,7 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ quer
192
193
  const saveToServerMutation = useMutation({
193
194
  mutationFn: (settingsToSave: UserSettings) =>
194
195
  api.mutate(setSettingsStoreValueDocument, {
195
- input: { key: SETTINGS_STORE_KEY, value: settingsToSave },
196
+ input: { key: SETTINGS_STORE_KEY_USER_SETTINGS, value: settingsToSave },
196
197
  }),
197
198
  onSuccess: (_, settingsSaved) => {
198
199
  // Only update serverSettings after successful save
@@ -205,9 +206,11 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ quer
205
206
  },
206
207
  });
207
208
 
208
- // Initialize settings from server if available
209
+ // Initialize settings from the first successful read of the server value. Until then
210
+ // `serverSettings` stays null, which stops the save effect below from writing this
211
+ // browser's local copy over the stored value.
209
212
  useEffect(() => {
210
- if (isReady) {
213
+ if (serverSettings) {
211
214
  return;
212
215
  }
213
216
  if (serverSettingsLoaded) {
@@ -230,13 +233,26 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({ quer
230
233
  setServerSettings(settings);
231
234
  setIsReady(true);
232
235
  }
233
- } else if (serverSettingsErrored) {
234
- // Fetch failed and won't retry (retry: false); fall back to local settings and mark
235
- // ready so consumers gating on `settingsReady` don't hang forever.
236
- setServerSettings(settings);
236
+ return;
237
+ }
238
+ // A FORBIDDEN read (no session yet) initializes nothing: login() re-reads the settings.
239
+ // A signed-in user whose channel token grants no permissions gets FORBIDDEN too, and the
240
+ // settings stay not ready until a later read succeeds, e.g. after a channel switch.
241
+ if (serverSettingsErrored && !isForbiddenError(error)) {
242
+ // The read won't retry (retry: false). Mark ready so consumers gating on
243
+ // `settingsReady` don't hang forever, but leave `serverSettings` unset: the local
244
+ // copy was never read from the server, so it must not be saved over the stored
245
+ // value. A later successful read still initializes the settings.
237
246
  setIsReady(true);
238
247
  }
239
- }, [serverSettingsLoaded, serverSettingsErrored, serverSettingsResponse, settings, isReady]);
248
+ }, [
249
+ serverSettings,
250
+ serverSettingsLoaded,
251
+ serverSettingsErrored,
252
+ error,
253
+ serverSettingsResponse,
254
+ settings,
255
+ ]);
240
256
 
241
257
  // Save settings to localStorage whenever they change
242
258
  useEffect(() => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The local storage keys, kept in their own module so the Playwright suite can
2
+ * The local storage and settings store keys, kept in their own module so the Playwright suite can
3
3
  * import them. They cannot live in `constants.ts`, because that imports
4
4
  * `schema-enums.js`, which imports `virtual:admin-api-schema` — a module only
5
5
  * the Vite build provides. Importing `constants.ts` from Playwright's Node
@@ -14,3 +14,5 @@ export const LS_KEY_USER_SETTINGS = 'vendure-user-settings';
14
14
  export const LS_KEY_SELECTED_CHANNEL_TOKEN = 'vendure-selected-channel-token';
15
15
  export const LS_KEY_SHIPPING_TEST_ORDER = 'vendure-shipping-test-order';
16
16
  export const LS_KEY_SHIPPING_TEST_ADDRESS = 'vendure-shipping-test-address';
17
+ /** The settings store key under which the server stores the user settings. */
18
+ export const SETTINGS_STORE_KEY_USER_SETTINGS = 'vendure.dashboard.userSettings';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Whether a GraphQL request failed because the request has no session or lacks permission.
3
+ * `api.query`/`api.mutate` throw a `GraphQLRequestError`, which carries the Vendure error
4
+ * code in `extensions.code`.
5
+ */
6
+ export function isForbiddenError(error: unknown): boolean {
7
+ const code = (error as { extensions?: { code?: unknown } } | null)?.extensions?.code;
8
+ return code === 'FORBIDDEN' || code === 'UNAUTHORIZED';
9
+ }
@@ -13,12 +13,6 @@ declare module 'virtual:plugin-translations' {
13
13
  export default translations;
14
14
  }
15
15
 
16
- declare module 'virtual:vendure-platform-promotion' {
17
- import { DashboardPromotionV1 } from '@/vdb/framework/platform-promotion/platform-promotion-types.js';
18
- export const dashboardPromotion: DashboardPromotionV1;
19
- export const licenseGuardConfigured: boolean;
20
- }
21
-
22
16
  declare module 'virtual:vendure-ui-config' {
23
17
  import { LanguageCode } from '@vendure/core';
24
18
 
@@ -1,25 +0,0 @@
1
- import { DashboardPromotionV1 } from '@vendure/common/lib/platform-promotion';
2
- import { Plugin } from 'vite';
3
- export declare const PLATFORM_CATALOG_URL = "https://api.vendure.io/v1/catalog";
4
- export declare const PLATFORM_CATALOG_TIMEOUT_MS = 2000;
5
- /**
6
- * Set this environment variable to `true` to skip the catalog fetch. The build then uses
7
- * the snapshot that ships with `@vendure/common`.
8
- */
9
- export declare const SKIP_PLATFORM_CATALOG_FETCH_ENV = "VENDURE_DASHBOARD_SKIP_PLATFORM_CATALOG_FETCH";
10
- /**
11
- * Fetches the `dashboardPromotion` data from the Vendure catalog one time. If the fetch
12
- * is skipped, fails, takes longer than the timeout or returns an unknown version, it
13
- * returns the snapshot that ships with `@vendure/common`. It never throws.
14
- */
15
- export declare function loadDashboardPromotion(log?: (message: string) => void): Promise<DashboardPromotionV1>;
16
- /**
17
- * This Vite plugin supplies the `virtual:vendure-platform-promotion` module. It holds the
18
- * Platform catalog data (`dashboardPromotion`) that the Dashboard uses to show locked
19
- * Platform items, and `licenseGuardConfigured`, which is `true` when the Vendure config
20
- * contains the License Guard plugin.
21
- *
22
- * The data is read at build time (and at dev server start), so the browser makes no
23
- * request to `api.vendure.io`.
24
- */
25
- export declare function platformPromotionPlugin(): Plugin;
@@ -1,83 +0,0 @@
1
- import { isDashboardPromotionV1 } from '@vendure/common/lib/platform-promotion';
2
- import { platformPromotionSnapshot } from '@vendure/common/lib/platform-promotion-snapshot';
3
- import { getActivePluginNames } from './utils/get-active-plugin-info.js';
4
- import { getConfigLoaderApi } from './vite-plugin-config-loader.js';
5
- const virtualModuleId = 'virtual:vendure-platform-promotion';
6
- const resolvedVirtualModuleId = `\0${virtualModuleId}`;
7
- export const PLATFORM_CATALOG_URL = 'https://api.vendure.io/v1/catalog';
8
- export const PLATFORM_CATALOG_TIMEOUT_MS = 2000;
9
- /**
10
- * Set this environment variable to `true` to skip the catalog fetch. The build then uses
11
- * the snapshot that ships with `@vendure/common`.
12
- */
13
- export const SKIP_PLATFORM_CATALOG_FETCH_ENV = 'VENDURE_DASHBOARD_SKIP_PLATFORM_CATALOG_FETCH';
14
- const LICENSE_GUARD_PLUGIN_NAME = 'LicenseGuardPlugin';
15
- /**
16
- * Fetches the `dashboardPromotion` data from the Vendure catalog one time. If the fetch
17
- * is skipped, fails, takes longer than the timeout or returns an unknown version, it
18
- * returns the snapshot that ships with `@vendure/common`. It never throws.
19
- */
20
- export async function loadDashboardPromotion(log = () => undefined) {
21
- if (process.env[SKIP_PLATFORM_CATALOG_FETCH_ENV] === 'true') {
22
- return platformPromotionSnapshot;
23
- }
24
- try {
25
- const response = await fetch(PLATFORM_CATALOG_URL, {
26
- signal: AbortSignal.timeout(PLATFORM_CATALOG_TIMEOUT_MS),
27
- });
28
- if (!response.ok) {
29
- throw new Error(`HTTP ${response.status}`);
30
- }
31
- const body = (await response.json());
32
- if (!isDashboardPromotionV1(body === null || body === void 0 ? void 0 : body.dashboardPromotion)) {
33
- throw new Error('the response has no valid dashboardPromotion with version 1');
34
- }
35
- return body.dashboardPromotion;
36
- }
37
- catch (e) {
38
- const reason = e instanceof Error ? e.message : String(e);
39
- log(`Could not fetch the Platform catalog (${reason}). Using the bundled snapshot.`);
40
- return platformPromotionSnapshot;
41
- }
42
- }
43
- /**
44
- * This Vite plugin supplies the `virtual:vendure-platform-promotion` module. It holds the
45
- * Platform catalog data (`dashboardPromotion`) that the Dashboard uses to show locked
46
- * Platform items, and `licenseGuardConfigured`, which is `true` when the Vendure config
47
- * contains the License Guard plugin.
48
- *
49
- * The data is read at build time (and at dev server start), so the browser makes no
50
- * request to `api.vendure.io`.
51
- */
52
- export function platformPromotionPlugin() {
53
- let configLoaderApi;
54
- let dashboardPromotion;
55
- return {
56
- name: 'vendure:platform-promotion',
57
- configResolved({ plugins }) {
58
- configLoaderApi = getConfigLoaderApi(plugins);
59
- },
60
- buildStart() {
61
- dashboardPromotion !== null && dashboardPromotion !== void 0 ? dashboardPromotion : (dashboardPromotion = loadDashboardPromotion(message => this.warn(message)));
62
- },
63
- resolveId(id) {
64
- if (id === virtualModuleId) {
65
- return resolvedVirtualModuleId;
66
- }
67
- },
68
- async load(id) {
69
- if (id === resolvedVirtualModuleId) {
70
- dashboardPromotion !== null && dashboardPromotion !== void 0 ? dashboardPromotion : (dashboardPromotion = loadDashboardPromotion(message => this.warn(message)));
71
- const [promotion, { vendureConfig }] = await Promise.all([
72
- dashboardPromotion,
73
- configLoaderApi.getVendureConfig(),
74
- ]);
75
- const licenseGuardConfigured = getActivePluginNames(vendureConfig).has(LICENSE_GUARD_PLUGIN_NAME);
76
- return [
77
- `export const dashboardPromotion = ${JSON.stringify(promotion)};`,
78
- `export const licenseGuardConfigured = ${JSON.stringify(licenseGuardConfigured)};`,
79
- ].join('\n');
80
- }
81
- },
82
- };
83
- }
@@ -1,71 +0,0 @@
1
- import { globalRegistry } from '../registry/global-registry.js';
2
-
3
- export interface PlatformGrantedCapabilitiesStore {
4
- capabilities: ReadonlySet<string> | undefined;
5
- listeners: Set<() => void>;
6
- }
7
-
8
- globalRegistry.register('platformGrantedCapabilities', {
9
- capabilities: undefined,
10
- listeners: new Set(),
11
- });
12
-
13
- /**
14
- * @description
15
- * Reports the Vendure Platform capability keys that the current license grants. The
16
- * Dashboard shows a locked item for each promoted Platform capability that is not in
17
- * this set. The License Guard Dashboard extension calls this function. Each call
18
- * replaces the earlier set.
19
- *
20
- * The Dashboard applies these rules:
21
- *
22
- * - The Vendure config has no `LicenseGuardPlugin`: the granted set is empty, and the
23
- * Dashboard shows all locked items on the first render.
24
- * - The Vendure config has the `LicenseGuardPlugin` and this function was called: the
25
- * Dashboard uses the reported set.
26
- * - The Vendure config has the `LicenseGuardPlugin` and this function was not called
27
- * (yet): the Dashboard shows no locked items.
28
- *
29
- * Call it before the first render if you can, for example at the top level of the
30
- * extension entry file, or from a `customProviders` component that loads the grants.
31
- *
32
- * @example
33
- * ```ts
34
- * import { defineDashboardExtension, setPlatformGrantedCapabilities } from '@vendure/dashboard';
35
- *
36
- * setPlatformGrantedCapabilities(['org.accounts', 'quote.requests']);
37
- *
38
- * defineDashboardExtension({});
39
- * ```
40
- *
41
- * @docsCategory extensions-api
42
- * @docsPage PlatformPromotion
43
- * @since 3.8.1
44
- */
45
- export function setPlatformGrantedCapabilities(capabilities: readonly string[]) {
46
- const store = globalRegistry.get('platformGrantedCapabilities');
47
- store.capabilities = new Set(capabilities);
48
- for (const listener of store.listeners) {
49
- listener();
50
- }
51
- }
52
-
53
- /**
54
- * @description
55
- * Returns the capability keys that were reported with {@link setPlatformGrantedCapabilities},
56
- * or `undefined` if nothing was reported.
57
- *
58
- * @internal
59
- */
60
- export function getReportedPlatformGrantedCapabilities(): ReadonlySet<string> | undefined {
61
- return globalRegistry.get('platformGrantedCapabilities').capabilities;
62
- }
63
-
64
- /**
65
- * @internal
66
- */
67
- export function subscribeToPlatformGrantedCapabilities(listener: () => void): () => void {
68
- const { listeners } = globalRegistry.get('platformGrantedCapabilities');
69
- listeners.add(listener);
70
- return () => listeners.delete(listener);
71
- }
@@ -1,5 +0,0 @@
1
- export type {
2
- DashboardPromotionCapabilityV1,
3
- DashboardPromotionV1,
4
- PlatformTier,
5
- } from '@vendure/common/lib/platform-promotion';
@@ -1,117 +0,0 @@
1
- import { act } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { renderToStaticMarkup } from 'react-dom/server';
4
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
-
6
- import { DashboardPromotionV1 } from '@/vdb/framework/platform-promotion/platform-promotion-types.js';
7
- import { globalRegistry } from '@/vdb/framework/registry/global-registry.js';
8
-
9
- (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
10
-
11
- const dashboardPromotion: DashboardPromotionV1 = {
12
- version: 1,
13
- navSections: [],
14
- navItems: [],
15
- pageBlocks: [],
16
- actions: [],
17
- capabilities: [
18
- {
19
- key: 'org.accounts',
20
- tier: 'starter',
21
- title: 'Company accounts',
22
- summary: 'Summary',
23
- benefits: ['One', 'Two', 'Three'],
24
- cta: 'trial',
25
- },
26
- ],
27
- };
28
-
29
- async function importWithLicenseGuard(licenseGuardConfigured: boolean) {
30
- vi.resetModules();
31
- vi.doMock('virtual:vendure-platform-promotion', () => ({
32
- dashboardPromotion,
33
- licenseGuardConfigured,
34
- }));
35
- const hook = await import('./use-platform-promotion.js');
36
- const extensionPoint =
37
- await import('@/vdb/framework/platform-promotion/platform-granted-capabilities.js');
38
- return { ...hook, ...extensionPoint };
39
- }
40
-
41
- describe('usePlatformPromotion', () => {
42
- afterEach(() => {
43
- vi.unstubAllGlobals();
44
- });
45
-
46
- beforeEach(() => {
47
- // No test may reach a real host.
48
- vi.stubGlobal(
49
- 'fetch',
50
- vi.fn(() => Promise.reject(new Error('Unexpected fetch'))),
51
- );
52
- const store = globalRegistry.get('platformGrantedCapabilities');
53
- if (store) {
54
- store.capabilities = undefined;
55
- }
56
- });
57
-
58
- it('License Guard not configured: the granted set is empty and all items are locked at once', async () => {
59
- const { getPlatformPromotion } = await importWithLicenseGuard(false);
60
-
61
- const state = getPlatformPromotion();
62
- expect(state.licenseGuardConfigured).toBe(false);
63
- expect(state.hideAll).toBe(false);
64
- expect(state.grantedCapabilities.size).toBe(0);
65
- expect(state.isLocked('org.accounts')).toBe(true);
66
- expect(state.capabilities.get('org.accounts')?.tier).toBe('starter');
67
- });
68
-
69
- it('License Guard configured and it reports a granted set: the set is used', async () => {
70
- const { getPlatformPromotion, setPlatformGrantedCapabilities } = await importWithLicenseGuard(true);
71
-
72
- setPlatformGrantedCapabilities(['org.accounts']);
73
-
74
- const state = getPlatformPromotion();
75
- expect(state.hideAll).toBe(false);
76
- expect([...state.grantedCapabilities]).toEqual(['org.accounts']);
77
- expect(state.isLocked('org.accounts')).toBe(false);
78
- expect(state.isLocked('quote.requests')).toBe(true);
79
- });
80
-
81
- it('License Guard configured but it reports nothing: all locked items are hidden', async () => {
82
- const { getPlatformPromotion } = await importWithLicenseGuard(true);
83
-
84
- const state = getPlatformPromotion();
85
- expect(state.hideAll).toBe(true);
86
- expect(state.isLocked('org.accounts')).toBe(false);
87
- });
88
-
89
- it('renders again when License Guard reports the granted set', async () => {
90
- const { usePlatformPromotion, setPlatformGrantedCapabilities } = await importWithLicenseGuard(true);
91
- const states: Array<{ hideAll: boolean; locked: boolean }> = [];
92
- function Probe() {
93
- const { hideAll, isLocked } = usePlatformPromotion();
94
- states.push({ hideAll, locked: isLocked('quote.requests') });
95
- return null;
96
- }
97
- const container = document.createElement('div');
98
- const root = createRoot(container);
99
-
100
- await act(async () => root.render(<Probe />));
101
- await act(async () => setPlatformGrantedCapabilities(['org.accounts']));
102
- await act(async () => root.unmount());
103
-
104
- expect(states[0]).toEqual({ hideAll: true, locked: false });
105
- expect(states[states.length - 1]).toEqual({ hideAll: false, locked: true });
106
- });
107
-
108
- it('renders with renderToStaticMarkup', async () => {
109
- const { usePlatformPromotion } = await importWithLicenseGuard(false);
110
- function Probe() {
111
- const { isLocked } = usePlatformPromotion();
112
- return <span>{String(isLocked('org.accounts'))}</span>;
113
- }
114
-
115
- expect(renderToStaticMarkup(<Probe />)).toBe('<span>true</span>');
116
- });
117
- });
@@ -1,117 +0,0 @@
1
- import {
2
- getReportedPlatformGrantedCapabilities,
3
- subscribeToPlatformGrantedCapabilities,
4
- } from '@/vdb/framework/platform-promotion/platform-granted-capabilities.js';
5
- import {
6
- DashboardPromotionCapabilityV1,
7
- DashboardPromotionV1,
8
- } from '@/vdb/framework/platform-promotion/platform-promotion-types.js';
9
- import { useSyncExternalStore } from 'react';
10
- import { dashboardPromotion, licenseGuardConfigured } from 'virtual:vendure-platform-promotion';
11
-
12
- /**
13
- * @description
14
- * The data that the Dashboard needs to show locked Vendure Platform items. It is
15
- * returned by {@link usePlatformPromotion}.
16
- *
17
- * @docsCategory extensions-api
18
- * @docsPage PlatformPromotion
19
- * @since 3.8.1
20
- */
21
- export interface PlatformPromotionState {
22
- /**
23
- * @description
24
- * The Platform catalog data, read at build time. If the build could not fetch it,
25
- * this is the snapshot that ships with `@vendure/common`.
26
- */
27
- promotion: DashboardPromotionV1;
28
- /**
29
- * @description
30
- * `promotion.capabilities`, keyed by capability key.
31
- */
32
- capabilities: ReadonlyMap<string, DashboardPromotionCapabilityV1>;
33
- /**
34
- * @description
35
- * `true` when the Vendure config contains the `LicenseGuardPlugin`.
36
- */
37
- licenseGuardConfigured: boolean;
38
- /**
39
- * @description
40
- * The capability keys that the license grants. Empty when License Guard is not configured.
41
- */
42
- grantedCapabilities: ReadonlySet<string>;
43
- /**
44
- * @description
45
- * `true` when the Dashboard must show no locked items: License Guard is configured
46
- * but it did not report a granted set.
47
- */
48
- hideAll: boolean;
49
- /**
50
- * @description
51
- * Returns `true` when the Dashboard shows the locked item for the capability:
52
- * `hideAll` is `false` and the capability is not granted.
53
- */
54
- isLocked: (capability: string) => boolean;
55
- }
56
-
57
- const EMPTY_SET: ReadonlySet<string> = new Set();
58
- const capabilities = new Map<string, DashboardPromotionCapabilityV1>(
59
- dashboardPromotion.capabilities.map(c => [c.key, c]),
60
- );
61
- let cachedState: PlatformPromotionState | undefined;
62
- let cachedReported: ReadonlySet<string> | undefined;
63
-
64
- function createState(reported: ReadonlySet<string> | undefined): PlatformPromotionState {
65
- const hideAll = licenseGuardConfigured && !reported;
66
- // Without License Guard nothing can grant a capability, so a reported set is not used.
67
- const grantedCapabilities = (licenseGuardConfigured && reported) || EMPTY_SET;
68
- return {
69
- promotion: dashboardPromotion,
70
- capabilities,
71
- licenseGuardConfigured,
72
- grantedCapabilities,
73
- hideAll,
74
- isLocked: capability => !hideAll && !grantedCapabilities.has(capability),
75
- };
76
- }
77
-
78
- /**
79
- * @description
80
- * Returns the current {@link PlatformPromotionState} outside of React.
81
- * Use {@link usePlatformPromotion} in components.
82
- *
83
- * @docsCategory extensions-api
84
- * @docsPage PlatformPromotion
85
- * @since 3.8.1
86
- */
87
- export function getPlatformPromotion(): PlatformPromotionState {
88
- const reported = getReportedPlatformGrantedCapabilities();
89
- if (!cachedState || reported !== cachedReported) {
90
- cachedState = createState(reported);
91
- cachedReported = reported;
92
- }
93
- return cachedState;
94
- }
95
-
96
- /**
97
- * @description
98
- * Returns the {@link PlatformPromotionState}. The component renders again when
99
- * {@link setPlatformGrantedCapabilities} reports a new granted set.
100
- *
101
- * @example
102
- * ```tsx
103
- * const { promotion, isLocked } = usePlatformPromotion();
104
- * const lockedNavItems = promotion.navItems.filter(item => item.promoted && isLocked(item.capability));
105
- * ```
106
- *
107
- * @docsCategory extensions-api
108
- * @docsPage PlatformPromotion
109
- * @since 3.8.1
110
- */
111
- export function usePlatformPromotion(): PlatformPromotionState {
112
- return useSyncExternalStore(
113
- subscribeToPlatformGrantedCapabilities,
114
- getPlatformPromotion,
115
- getPlatformPromotion,
116
- );
117
- }