@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.
- package/dist/bundle/chunks/{2SSKDMRQ-BVYTtZ4W.js → 2SSKDMRQ-BHHzW1a_.js} +2 -2
- package/dist/bundle/chunks/{AP7HFJJL-BDXyVf1-.js → AP7HFJJL-B5qPACUM.js} +1 -1
- package/dist/bundle/chunks/{WDYDFRGG-DP5Ikh83.js → WDYDFRGG-BfBnFCDT.js} +2 -2
- package/dist/bundle/chunks/{ar-DMK_eFVD.js → ar-ZYyYt3LW.js} +1 -1
- package/dist/bundle/chunks/{bg-_l7rTFuK.js → bg-BgDGBTKB.js} +1 -1
- package/dist/bundle/chunks/{common-operations-CVADEI6Z.js → common-operations-DaHPL4es.js} +732 -674
- package/dist/bundle/chunks/{cs-DMrGo78x.js → cs-BYGURbP6.js} +1 -1
- package/dist/bundle/chunks/{de-DEE86yVt.js → de-HdkR3fYi.js} +1 -1
- package/dist/bundle/chunks/{en-Cl1y9IIy.js → en-BAhByZZJ.js} +1 -1
- package/dist/bundle/chunks/{es-BxdaHguU.js → es-Dt0C52Mq.js} +1 -1
- package/dist/bundle/chunks/{fa-DmehFzkF.js → fa-CREXA3HL.js} +1 -1
- package/dist/bundle/chunks/{fr-OT8cvTDq.js → fr-Cz8kKMli.js} +1 -1
- package/dist/bundle/chunks/{he-BKP3NS5j.js → he-9NoIAj8m.js} +1 -1
- package/dist/bundle/chunks/{hr-BYAgAid3.js → hr-BwXcr71B.js} +1 -1
- package/dist/bundle/chunks/{hu-D5RzrlfJ.js → hu-DUz4OEIA.js} +1 -1
- package/dist/bundle/chunks/{it-DjrjGb8J.js → it-CAlaDmH7.js} +1 -1
- package/dist/bundle/chunks/{ja-CHHuYFJd.js → ja-2JxS6pFi.js} +1 -1
- package/dist/bundle/chunks/{ko-CIrLMXet.js → ko-BsFLDX_m.js} +1 -1
- package/dist/bundle/chunks/{locale-DlaghAWQ.js → locale-cLfB2IZc.js} +1 -1
- package/dist/bundle/chunks/{main-CcSc9LYG.js → main-MGWdQzqd.js} +3 -3
- package/dist/bundle/chunks/{nb-CBS2CxWA.js → nb-Dff7oHih.js} +1 -1
- package/dist/bundle/chunks/{ne-BKFoZX0n.js → ne-ByGdZmfQ.js} +1 -1
- package/dist/bundle/chunks/{nl-CioNp_N5.js → nl-DniFiVio.js} +1 -1
- package/dist/bundle/chunks/{pl-ChqS0pNw.js → pl-B5DHommF.js} +1 -1
- package/dist/bundle/chunks/{pt_BR-8wz_k8Ib.js → pt_BR-BAq7WTWG.js} +1 -1
- package/dist/bundle/chunks/{pt_PT-BsddGCVI.js → pt_PT-DL7eSU8R.js} +1 -1
- package/dist/bundle/chunks/{ro-DLZ4JjP9.js → ro-CGYxDEv8.js} +1 -1
- package/dist/bundle/chunks/{ru-CDB-oX6Y.js → ru-BduayFrD.js} +1 -1
- package/dist/bundle/chunks/{sv-C05330I8.js → sv-E3RLr9cy.js} +1 -1
- package/dist/bundle/chunks/{tr-BmdyAxJM.js → tr-pMuzyLjn.js} +1 -1
- package/dist/bundle/chunks/{uk-UHmyTWD9.js → uk-BFfpHemj.js} +1 -1
- package/dist/bundle/chunks/{uz-BPv1mncF.js → uz-DBzLLOoE.js} +1 -1
- package/dist/bundle/chunks/{zh_Hans-DYtmTSvd.js → zh_Hans-DDD5yH27.js} +1 -1
- package/dist/bundle/chunks/{zh_Hant-CdyREtHJ.js → zh_Hant-B9iOWieA.js} +1 -1
- package/dist/bundle/lib.js +653 -707
- package/dist/bundle/main.js +2 -2
- package/dist/vite/utils/get-active-plugin-info.d.ts +0 -29
- package/dist/vite/utils/get-active-plugin-info.js +1 -3
- package/dist/vite/vite-plugin-config.js +28 -2
- package/dist/vite/vite-plugin-dashboard-metadata.js +47 -15
- package/dist/vite/vite-plugin-vendure-dashboard.d.ts +0 -1
- package/dist/vite/vite-plugin-vendure-dashboard.js +0 -5
- package/package.json +3 -3
- package/src/i18n/locales/ar.po +4 -0
- package/src/i18n/locales/bg.po +4 -0
- package/src/i18n/locales/cs.po +4 -0
- package/src/i18n/locales/de.po +4 -0
- package/src/i18n/locales/en.po +4 -0
- package/src/i18n/locales/es.po +4 -0
- package/src/i18n/locales/fa.po +4 -0
- package/src/i18n/locales/fr.po +4 -0
- package/src/i18n/locales/he.po +4 -0
- package/src/i18n/locales/hr.po +4 -0
- package/src/i18n/locales/hu.po +4 -0
- package/src/i18n/locales/it.po +4 -0
- package/src/i18n/locales/ja.po +4 -0
- package/src/i18n/locales/ko.po +4 -0
- package/src/i18n/locales/nb.po +4 -0
- package/src/i18n/locales/ne.po +4 -0
- package/src/i18n/locales/nl.po +4 -0
- package/src/i18n/locales/pl.po +4 -0
- package/src/i18n/locales/pt_BR.po +4 -0
- package/src/i18n/locales/pt_PT.po +4 -0
- package/src/i18n/locales/ro.po +4 -0
- package/src/i18n/locales/ru.po +4 -0
- package/src/i18n/locales/sv.po +4 -0
- package/src/i18n/locales/tr.po +4 -0
- package/src/i18n/locales/uk.po +4 -0
- package/src/i18n/locales/uz.po +4 -0
- package/src/i18n/locales/zh_Hans.po +4 -0
- package/src/i18n/locales/zh_Hant.po +4 -0
- package/src/lib/components/shared/error-page.tsx +2 -2
- package/src/lib/components/shared/paginated-list-data-table.spec.tsx +151 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +11 -2
- package/src/lib/constants.ts +1 -0
- package/src/lib/framework/registry/registry-types.ts +0 -2
- package/src/lib/index.ts +0 -3
- package/src/lib/providers/auth.tsx +75 -7
- package/src/lib/providers/server-config.tsx +3 -2
- package/src/lib/providers/user-settings.spec.tsx +191 -0
- package/src/lib/providers/user-settings.tsx +30 -14
- package/src/lib/storage-keys.ts +3 -1
- package/src/lib/utils/is-forbidden-error.ts +9 -0
- package/src/lib/virtual.d.ts +0 -6
- package/dist/vite/vite-plugin-platform-promotion.d.ts +0 -25
- package/dist/vite/vite-plugin-platform-promotion.js +0 -83
- package/src/lib/framework/platform-promotion/platform-granted-capabilities.ts +0 -71
- package/src/lib/framework/platform-promotion/platform-promotion-types.ts +0 -5
- package/src/lib/hooks/use-platform-promotion.spec.tsx +0 -117
- 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',
|
|
175
|
-
queryFn: () => api.query(getSettingsStoreValueDocument, { 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(
|
|
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:
|
|
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
|
|
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 (
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
}, [
|
|
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(() => {
|
package/src/lib/storage-keys.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/virtual.d.ts
CHANGED
|
@@ -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,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
|
-
}
|