@sap-ux/preview-middleware 1.1.2 → 1.2.0

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/base/flp.js CHANGED
@@ -13,7 +13,7 @@ import { isAppStudio, exposePort } from '@sap-ux/btp-utils';
13
13
  import { FeatureToggleAccess } from '@sap-ux/feature-toggle';
14
14
  import { deleteChange, readChanges, readLocalModulePaths, stripLocalModulesFromLrepResponse, writeChange } from './flex.js';
15
15
  import { generateImportList, mergeTestConfigDefaults } from './test.js';
16
- import { getFlpConfigWithDefaults, createFlpTemplateConfig, PREVIEW_URL, isFlexConnector, createTestTemplateConfig, addApp, getAppName, sanitizeRtaConfig, CARD_GENERATOR_DEFAULT, remapResourcesForPath } from './config.js';
16
+ import { getFlpConfigWithDefaults, createFlpTemplateConfig, PREVIEW_URL, isFlexConnector, createTestTemplateConfig, addApp, getAppName, sanitizeRtaConfig, CARD_GENERATOR_DEFAULT, remapResourcesForPath, generateSandboxAppConfig, qualifiesForNewSandbox } from './config.js';
17
17
  import { generateCdm } from './cdm.js';
18
18
  import { readFileSync } from 'node:fs';
19
19
  import { getIntegrationCard } from './utils/cards.js';
@@ -89,7 +89,7 @@ export class FlpSandbox {
89
89
  this.createFlexHandler();
90
90
  this.flpConfig.libs ??= await this.hasLocateReuseLibsScript();
91
91
  const id = manifest['sap.app']?.id ?? '';
92
- this.templateConfig = createFlpTemplateConfig(this.flpConfig, manifest, resources, adp !== undefined);
92
+ this.templateConfig = createFlpTemplateConfig(this.flpConfig, manifest, resources, adp !== undefined, this.projectType !== 'EDMXBackend', this.logger);
93
93
  this.adp = adp;
94
94
  this.manifest = manifest;
95
95
  await addApp(this.templateConfig, manifest, {
@@ -161,14 +161,15 @@ export class FlpSandbox {
161
161
  * Also deletes the ABAP connector in case of a CAP project.
162
162
  * Deletes all connectors if UI5 version is < 1.84 and served from npmjs.
163
163
  *
164
+ * @param config - the template config to mutate (must be a per-request clone, not the shared this.templateConfig)
164
165
  * @param ui5VersionMajor - the major version of UI5
165
166
  * @param ui5VersionMinor - the minor version of UI5
166
167
  * @param isCDN - whether the UI5 sources are served from CDN
167
168
  * @private
168
169
  */
169
- checkDeleteConnectors(ui5VersionMajor, ui5VersionMinor, isCDN) {
170
+ checkDeleteConnectors(config, ui5VersionMajor, ui5VersionMinor, isCDN) {
170
171
  if (ui5VersionMajor === 1 && ui5VersionMinor < 84) {
171
- this.templateConfig.ui5.flex = this.templateConfig.ui5?.flex?.filter((connector) => isFlexConnector(connector));
172
+ config.ui5.flex = config.ui5?.flex?.filter((connector) => isFlexConnector(connector));
172
173
  this.logger.debug(`The Fiori Tools local connector (WorkspaceConnector) is not being used because the current UI5 version does not support it.${isCDN ? 'The Fiori Tools fake connector (FakeLrepConnector) will be used instead.' : ''} `);
173
174
  if (!isCDN) {
174
175
  this.logger.warn(`The preview with virtual endpoints does not support flex changes for the current UI5 version ${ui5VersionMajor}.${ui5VersionMinor} from npmjs. Consider using a proxy to load UI5 resources from the CDN (e.g., https://ui5.sap.com), or upgrade the UI5 version in the yaml configuration to at least 1.84.`);
@@ -177,14 +178,6 @@ export class FlpSandbox {
177
178
  else {
178
179
  this.logger.debug(`The Fiori Tools local connector (WorkspaceConnector) is being used.`);
179
180
  }
180
- if (this.projectType === 'CAPJava' || this.projectType === 'CAPNodejs') {
181
- this.templateConfig.ui5.flex = this.templateConfig.ui5?.flex?.filter((connector) => !isFlexConnector(connector) ||
182
- (isFlexConnector(connector) && !connector.url?.startsWith('/sap/bc/lrep')));
183
- this.logger.debug(`The ABAP connector is not being used because the current project type is '${this.projectType}'.`);
184
- }
185
- else {
186
- this.logger.debug(`The ABAP connector is being used.`);
187
- }
188
181
  }
189
182
  /**
190
183
  * Generates the FLP sandbox for an editor.
@@ -201,7 +194,6 @@ export class FlpSandbox {
201
194
  await this.setApplicationDependencies();
202
195
  this.templateConfig.baseUrl = req['ui5-patched-router']?.baseUrl ?? '';
203
196
  const ui5Version = await this.getUi5Version(req.protocol, req.headers.host, this.templateConfig.baseUrl);
204
- this.checkDeleteConnectors(ui5Version.major, ui5Version.minor, ui5Version.isCdn);
205
197
  if (ui5Version.major === 1 && ui5Version.minor <= 71) {
206
198
  this.removeAsyncHintsRequests();
207
199
  }
@@ -209,6 +201,8 @@ export class FlpSandbox {
209
201
  this.removeFlexExtensionPointEnabled();
210
202
  }
211
203
  const config = structuredClone(this.templateConfig);
204
+ config.ui5.versionMajor = ui5Version.major;
205
+ this.checkDeleteConnectors(config, ui5Version.major, ui5Version.minor, ui5Version.isCdn);
212
206
  if (!config.ui5.libs.includes('sap.ui.rta')) {
213
207
  // sap.ui.rta needs to be added to the list of preload libs for variants management and adaptation projects
214
208
  config.ui5.libs += ',sap.ui.rta';
@@ -226,7 +220,7 @@ export class FlpSandbox {
226
220
  config.features = FeatureToggleAccess.getAllFeatureToggles();
227
221
  const appId = this.manifest['sap.app']?.id ?? '';
228
222
  remapResourcesForPath(config, editor.path, appId);
229
- return render(this.getSandboxTemplate(ui5Version), config);
223
+ return render(await this.getSandboxTemplate(ui5Version), config);
230
224
  }
231
225
  /**
232
226
  * Sets application dependencies in the template configuration.
@@ -359,15 +353,56 @@ export class FlpSandbox {
359
353
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
360
354
  this.templateConfig.baseUrl = ('ui5-patched-router' in req && req['ui5-patched-router']?.baseUrl) || '';
361
355
  const ui5Version = await this.getUi5VersionFromRequest(req, this.templateConfig.baseUrl);
362
- this.checkDeleteConnectors(ui5Version.major, ui5Version.minor, ui5Version.isCdn);
356
+ this.templateConfig.ui5.versionMajor = ui5Version.major;
357
+ this.checkDeleteConnectors(this.templateConfig, ui5Version.major, ui5Version.minor, ui5Version.isCdn);
363
358
  if (ui5Version.major === 1 && ui5Version.minor < 120) {
364
359
  this.removeFlexExtensionPointEnabled();
365
360
  }
366
361
  //for consistency reasons, we also add the baseUrl to the HTML here, although it is only used in editor mode
367
- const html = render(this.getSandboxTemplate(ui5Version), this.templateConfig);
362
+ const html = render(await this.getSandboxTemplate(ui5Version), this.templateConfig);
368
363
  this.sendResponse(res, 'text/html', 200, html);
369
364
  }
370
365
  }
366
+ /**
367
+ * Handler for GET requests to fioriSandboxAppConfig.json (Sandbox 2.0).
368
+ * Serves a virtual config, optionally merged with a user-provided file.
369
+ * If a real HTML file exists at the FLP path, no virtual config is served.
370
+ *
371
+ * @param req incoming request
372
+ * @param res server response
373
+ * @param next next middleware function
374
+ * @param configJsonPath project-relative path to fioriSandboxAppConfig.json
375
+ */
376
+ async sandboxAppConfigGetHandler(req, res, next, configJsonPath) {
377
+ const baseUrl = (this.templateConfig.baseUrl =
378
+ ('ui5-patched-router' in req && req['ui5-patched-router']?.baseUrl) || '');
379
+ const file = await this.project.byPath(`${baseUrl}${this.flpConfig.path}`);
380
+ if (file) {
381
+ this.logger.info(`HTML file returned at '${this.flpConfig.path}'. No virtual 'fioriSandboxAppConfig.json' will be served.`);
382
+ next();
383
+ return;
384
+ }
385
+ // If the legacy fioriSandboxConfig.json exists, sandbox 2 is disabled — no config to serve.
386
+ const legacyFile = await this.project.byPath(`${baseUrl}/appconfig/fioriSandboxConfig.json`);
387
+ if (legacyFile) {
388
+ next();
389
+ return;
390
+ }
391
+ // check for user-provided fioriSandboxAppConfig.json and merge if present
392
+ const userConfigFile = await this.project.byPath(`${baseUrl}${configJsonPath}`);
393
+ let config = generateSandboxAppConfig(this.templateConfig, this.flpConfig, this.adp !== undefined, this.projectType !== 'EDMXBackend', this.logger);
394
+ if (userConfigFile) {
395
+ const userConfig = JSON.parse(await userConfigFile.getString());
396
+ config = {
397
+ ...config,
398
+ ...userConfig,
399
+ // beforeFlpStart and afterFlpStart must always point to our hooks
400
+ beforeFlpStart: config.beforeFlpStart,
401
+ afterFlpStart: config.afterFlpStart
402
+ };
403
+ }
404
+ this.sendResponse(res, 'application/json', 200, JSON.stringify(config));
405
+ }
371
406
  /**
372
407
  * Add routes for html and scripts required for a local FLP.
373
408
  */
@@ -378,6 +413,35 @@ export class FlpSandbox {
378
413
  this.router.get(this.flpConfig.path, async (req, res, next) => {
379
414
  await this.flpGetHandler(req, res, next);
380
415
  });
416
+ // add routes for fioriSandboxAppConfig.json (Sandbox 2.0) — only when sandbox 2 is not explicitly disabled.
417
+ // The config must be served at the directory of every sandbox HTML endpoint,
418
+ // since each endpoint fetches fioriSandboxAppConfig.json relative to its own location.
419
+ if (this.flpConfig.useNewSandbox === true) {
420
+ const sandboxDirs = new Set();
421
+ sandboxDirs.add(dirname(this.flpConfig.path));
422
+ if (this.cardGenerator?.path) {
423
+ const cardGeneratorPath = this.cardGenerator.path.startsWith('/')
424
+ ? this.cardGenerator.path
425
+ : `/${this.cardGenerator.path}`;
426
+ sandboxDirs.add(dirname(cardGeneratorPath));
427
+ }
428
+ if (this.rta) {
429
+ for (const editor of this.rta.endpoints) {
430
+ sandboxDirs.add(dirname(editor.path));
431
+ }
432
+ }
433
+ // Register one route per directory rather than passing an array to router.get()
434
+ // sandboxAppConfigGetHandler needs the concrete configJsonPath to look up the user-provided
435
+ // file via project.byPath(), and connect.IncomingMessage does not expose req.path,
436
+ // so we cannot derive the path from the request inside the handler.
437
+ for (const dir of sandboxDirs) {
438
+ const configJsonPath = `${dir}/fioriSandboxAppConfig.json`;
439
+ this.logger.debug(`Add route for ${configJsonPath}`);
440
+ this.router.get(configJsonPath, async (req, res, next) => {
441
+ await this.sandboxAppConfigGetHandler(req, res, next, configJsonPath);
442
+ });
443
+ }
444
+ }
381
445
  }
382
446
  /**
383
447
  * Adds a middleware route for the Card Generator in the FLP sandbox.
@@ -463,6 +527,11 @@ export class FlpSandbox {
463
527
  this.flpConfig.enhancedHomePage = this.templateConfig.enhancedHomePage = false;
464
528
  this.logger.warn(`Feature enhancedHomePage disabled: UI5 version: ${version} not supported.`);
465
529
  }
530
+ // enhancedHomePage (CDM) is not supported with Sandbox 2 — fall back to sandbox 1
531
+ if (this.flpConfig.enhancedHomePage && qualifiesForNewSandbox({ major, minor, patch, label, isCdn })) {
532
+ this.flpConfig.useNewSandbox = false;
533
+ this.logger.warn(`New FLP Sandbox disabled: enhancedHomePage is not supported with Sandbox 2.`);
534
+ }
466
535
  return {
467
536
  major,
468
537
  minor,
@@ -473,13 +542,22 @@ export class FlpSandbox {
473
542
  }
474
543
  /**
475
544
  * Read the sandbox template file based on the given UI5 version.
545
+ * Also checks for a legacy 'appconfig/fioriSandboxConfig.json' and falls back to Sandbox 1 if found.
476
546
  *
477
547
  * @param ui5Version - the UI5 version
478
548
  * @returns the template for the sandbox HTML file
479
549
  */
480
- getSandboxTemplate(ui5Version) {
550
+ async getSandboxTemplate(ui5Version) {
481
551
  this.logger.info(`Using sandbox template for UI5 version: ${ui5Version.major}.${ui5Version.minor}.${ui5Version.patch}${ui5Version.label ? `-${ui5Version.label}` : ''}.`);
482
- const filePrefix = ui5Version.major > 1 || ui5Version.label?.includes('legacy-free') ? '2' : '';
552
+ const qualifies = qualifiesForNewSandbox(ui5Version);
553
+ const legacyConfigPresent = qualifies
554
+ ? await this.hasLegacySandboxConfig(ui5Version, this.templateConfig.baseUrl)
555
+ : false;
556
+ const useNewSandbox = qualifies && this.flpConfig.useNewSandbox === true && !legacyConfigPresent;
557
+ if (qualifies && !useNewSandbox && !this.flpConfig.enhancedHomePage) {
558
+ this.logger.info('New FLP Sandbox disabled in configuration.');
559
+ }
560
+ const filePrefix = useNewSandbox ? '2' : '';
483
561
  const template = this.flpConfig.enhancedHomePage ? 'cdm' : 'sandbox';
484
562
  return readFileSync(join(__dirname, `../../templates/flp/${template}${filePrefix}.ejs`), 'utf-8');
485
563
  }
@@ -495,6 +573,26 @@ export class FlpSandbox {
495
573
  }
496
574
  }
497
575
  }
576
+ /**
577
+ * Checks if a legacy 'appconfig/fioriSandboxConfig.json' file is present when using the new FLP Sandbox.
578
+ * If found, warns the user to migrate and returns true so the caller can fall back to the classic Sandbox
579
+ * for this request.
580
+ *
581
+ * @param ui5Version - the resolved UI5 version
582
+ * @param baseUrl - the base URL of the current request
583
+ * @returns true if the legacy file is present and sandbox 2 should be suppressed
584
+ * @private
585
+ */
586
+ async hasLegacySandboxConfig(ui5Version, baseUrl) {
587
+ if (qualifiesForNewSandbox(ui5Version) && this.flpConfig.useNewSandbox === true) {
588
+ const legacyFile = await this.project.byPath(`${baseUrl}/appconfig/fioriSandboxConfig.json`);
589
+ if (legacyFile) {
590
+ this.logger.warn(`Found legacy file at 'appconfig/fioriSandboxConfig.json'. Falling back to the classic Sandbox. Please migrate your application configuration first.`);
591
+ return true;
592
+ }
593
+ }
594
+ return false;
595
+ }
498
596
  /**
499
597
  * For UI5 versions below 1.120, flexExtensionPointEnabled must be removed from the application
500
598
  * dependencies manifest. Older UI5 versions cannot handle this property at bootstrap time.
@@ -8,8 +8,10 @@ sap.ui.define(["sap/base/util/merge", "sap/ui/fl/write/api/connectors/ObjectStor
8
8
  const getUi5Version = ___utils_versionjs["getUi5Version"];
9
9
  const isLowerThanMinimalUi5Version = ___utils_versionjs["isLowerThanMinimalUi5Version"];
10
10
  const getAdditionalChangeInfo = ___utils_additional_change_infojs["getAdditionalChangeInfo"];
11
- const baseUrl = document.getElementById('sap-ui-bootstrap')?.dataset.openUxPreviewBaseUrl ?? '';
12
- const changesApiPath = `${baseUrl}${CHANGES_API_PATH_STATIC}`;
11
+ const getChangesApiPath = () => {
12
+ const baseUrl = document.getElementById('sap-ui-bootstrap')?.dataset.openUxPreviewBaseUrl ?? '';
13
+ return `${baseUrl}${CHANGES_API_PATH_STATIC}`;
14
+ };
13
15
  const connector = merge({}, ObjectStorageConnector, {
14
16
  layers: [Layer.VENDOR, Layer.CUSTOMER_BASE],
15
17
  storage: {
@@ -33,7 +35,7 @@ sap.ui.define(["sap/base/util/merge", "sap/ui/fl/write/api/connectors/ObjectStor
33
35
  change,
34
36
  additionalChangeInfo
35
37
  };
36
- return fetch(changesApiPath, {
38
+ return fetch(getChangesApiPath(), {
37
39
  method: 'POST',
38
40
  body: JSON.stringify(body, null, 2),
39
41
  headers: {
@@ -49,7 +51,7 @@ sap.ui.define(["sap/base/util/merge", "sap/ui/fl/write/api/connectors/ObjectStor
49
51
  // exceptions in the listener call are ignored
50
52
  }
51
53
  }
52
- return fetch(changesApiPath, {
54
+ return fetch(getChangesApiPath(), {
53
55
  method: 'DELETE',
54
56
  body: JSON.stringify({
55
57
  fileName: key
@@ -66,7 +68,7 @@ sap.ui.define(["sap/base/util/merge", "sap/ui/fl/write/api/connectors/ObjectStor
66
68
  // not implemented
67
69
  },
68
70
  getItems: async function () {
69
- const response = await fetch(changesApiPath, {
71
+ const response = await fetch(getChangesApiPath(), {
70
72
  method: 'GET',
71
73
  headers: {
72
74
  'content-type': 'application/json'
@@ -6,8 +6,10 @@ import { CHANGES_API_PATH as CHANGES_API_PATH_STATIC, getFlexSettings } from './
6
6
  import { getUi5Version, isLowerThanMinimalUi5Version } from '../utils/version.js';
7
7
  import { getAdditionalChangeInfo } from '../utils/additional-change-info.js';
8
8
 
9
- const baseUrl = document.getElementById('sap-ui-bootstrap')?.dataset.openUxPreviewBaseUrl ?? '';
10
- const changesApiPath = `${baseUrl}${CHANGES_API_PATH_STATIC}`;
9
+ const getChangesApiPath = (): string => {
10
+ const baseUrl = document.getElementById('sap-ui-bootstrap')?.dataset.openUxPreviewBaseUrl ?? '';
11
+ return `${baseUrl}${CHANGES_API_PATH_STATIC}`;
12
+ };
11
13
 
12
14
  const connector = merge({}, ObjectStorageConnector, {
13
15
  layers: [Layer.VENDOR, Layer.CUSTOMER_BASE],
@@ -36,7 +38,7 @@ const connector = merge({}, ObjectStorageConnector, {
36
38
  additionalChangeInfo
37
39
  };
38
40
 
39
- return fetch(changesApiPath, {
41
+ return fetch(getChangesApiPath(), {
40
42
  method: 'POST',
41
43
  body: JSON.stringify(body, null, 2),
42
44
  headers: {
@@ -53,7 +55,7 @@ const connector = merge({}, ObjectStorageConnector, {
53
55
  }
54
56
  }
55
57
 
56
- return fetch(changesApiPath, {
58
+ return fetch(getChangesApiPath(), {
57
59
  method: 'DELETE',
58
60
  body: JSON.stringify({ fileName: key }),
59
61
  headers: {
@@ -68,7 +70,7 @@ const connector = merge({}, ObjectStorageConnector, {
68
70
  // not implemented
69
71
  },
70
72
  getItems: async function (): Promise<FlexChange[]> {
71
- const response = await fetch(changesApiPath, {
73
+ const response = await fetch(getChangesApiPath(), {
72
74
  method: 'GET',
73
75
  headers: {
74
76
  'content-type': 'application/json'
@@ -1,10 +1,157 @@
1
1
  "use strict";
2
2
 
3
- sap.ui.define([], function () {
3
+ sap.ui.define(["sap/base/Log", "open/ux/preview/client/thirdparty/@sap-ux-private/control-property-editor-common", "../utils/error", "../utils/version", "../utils/info-center-message"], function (Log, ___sap_ux_private_control_property_editor_common, ___utils_errorjs, ___utils_versionjs, ___utils_info_center_messagejs) {
4
4
  "use strict";
5
5
 
6
+ const MessageBarType = ___sap_ux_private_control_property_editor_common["MessageBarType"];
7
+ const getError = ___utils_errorjs["getError"];
8
+ const isLowerThanMinimalUi5Version = ___utils_versionjs["isLowerThanMinimalUi5Version"];
9
+ const sendInfoCenterMessage = ___utils_info_center_messagejs["sendInfoCenterMessage"];
10
+ const CONTROLLER_EXTENSION_PATH_REGEX = /\/changes\/coding\/.+\.(js|ts)/;
11
+
12
+ /**
13
+ * Extracts an Error object from a global error event.
14
+ * Handles both synchronous errors (ErrorEvent) and unhandled promise rejections (PromiseRejectionEvent).
15
+ *
16
+ * @param {GlobalErrorEvent} event - The global error or unhandled rejection event.
17
+ * @returns {Error | undefined} The extracted Error instance, or undefined if no Error could be extracted.
18
+ */
19
+ function extractError(event) {
20
+ if ('error' in event && event.error instanceof Error) {
21
+ return event.error;
22
+ }
23
+ if ('reason' in event && event.reason instanceof Error) {
24
+ return event.reason;
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ /**
30
+ * Reports controller extension errors to the Info Center.
31
+ * Filters events by checking if the stack trace contains 'ControllerExtension',
32
+ * and sends matching errors as error-level messages to the Info Center panel.
33
+ *
34
+ * @param {GlobalErrorEvent} event - The global error or unhandled rejection event.
35
+ */
36
+ const reportControllerExtensionErrorToInfoCenter = event => {
37
+ const error = extractError(event);
38
+ const stackTrace = error?.stack ?? '';
39
+ if (!CONTROLLER_EXTENSION_PATH_REGEX.test(stackTrace)) {
40
+ return;
41
+ }
42
+ void sendInfoCenterMessage({
43
+ title: {
44
+ key: 'CONTROLLER_EXTENSION_UNHANDLED_ERROR_TITLE'
45
+ },
46
+ description: error?.message ?? '',
47
+ type: MessageBarType.error,
48
+ details: stackTrace
49
+ });
50
+ };
51
+
52
+ /**
53
+ * Registers global event listeners for uncaught errors and unhandled promise rejections
54
+ * to detect and report controller extension errors to the Info Center.
55
+ */
56
+ function registerForControllerExtensionErrors() {
57
+ globalThis.addEventListener('error', reportControllerExtensionErrorToInfoCenter);
58
+ globalThis.addEventListener('unhandledrejection', reportControllerExtensionErrorToInfoCenter);
59
+ }
6
60
  const CHANGES_API_PATH = '/preview/api/changes';
7
61
 
62
+ /**
63
+ * SAPUI5 delivered namespaces from https://ui5.sap.com/#/api/sap
64
+ */
65
+ const UI5_LIBS = ['sap.apf', 'sap.base', 'sap.chart', 'sap.collaboration', 'sap.f', 'sap.fe', 'sap.fileviewer', 'sap.gantt', 'sap.landvisz', 'sap.m', 'sap.ndc', 'sap.ovp', 'sap.rules', 'sap.suite', 'sap.tnt', 'sap.ui', 'sap.uiext', 'sap.ushell', 'sap.uxap', 'sap.viz', 'sap.webanalytics', 'sap.zen'];
66
+ /**
67
+ * Check whether a specific dependency is a custom library, and if yes, add it to the map.
68
+ *
69
+ * @param dependency dependency from the manifest
70
+ * @param customLibs map containing the required custom libraries
71
+ */
72
+ function addKeys(dependency, customLibs) {
73
+ Object.keys(dependency).forEach(function (key) {
74
+ if (!UI5_LIBS.some(function (substring) {
75
+ return key === substring || key.startsWith(substring + '.');
76
+ })) {
77
+ customLibs[key] = true;
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Check whether a specific ComponentUsage is a custom component, and if yes, add it to the map.
84
+ *
85
+ * @param compUsages ComponentUsage from the manifest
86
+ * @param customLibs map containing the required custom libraries
87
+ */
88
+ function getComponentUsageNames(compUsages, customLibs) {
89
+ const compNames = Object.keys(compUsages).map(function (compUsageKey) {
90
+ return compUsages[compUsageKey].name;
91
+ });
92
+ compNames.forEach(function (key) {
93
+ if (!UI5_LIBS.some(function (substring) {
94
+ return key === substring || key.startsWith(substring + '.');
95
+ })) {
96
+ customLibs[key] = true;
97
+ }
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Fetch the manifest for all the given application urls and generate a string containing all required custom library ids.
103
+ *
104
+ * @param appUrls urls pointing to included applications
105
+ * @returns Promise of a comma separated list of all required libraries.
106
+ */
107
+ async function getManifestLibs(appUrls) {
108
+ const result = {};
109
+ const promises = [];
110
+ for (const url of appUrls) {
111
+ promises.push(fetch(`${url}/manifest.json`).then(async resp => {
112
+ const manifest = await resp.json();
113
+ if (manifest) {
114
+ if (manifest['sap.ui5']?.dependencies) {
115
+ if (manifest['sap.ui5'].dependencies.libs) {
116
+ addKeys(manifest['sap.ui5'].dependencies.libs, result);
117
+ }
118
+ if (manifest['sap.ui5'].dependencies.components) {
119
+ addKeys(manifest['sap.ui5'].dependencies.components, result);
120
+ }
121
+ }
122
+ if (manifest['sap.ui5']?.componentUsages) {
123
+ getComponentUsageNames(manifest['sap.ui5'].componentUsages, result);
124
+ }
125
+ }
126
+ }));
127
+ }
128
+ return Promise.all(promises).then(() => Object.keys(result).join(','));
129
+ }
130
+
131
+ /**
132
+ * Register the custom libraries and their url with the UI5 loader.
133
+ *
134
+ * @param dataFromAppIndex data returned from the app index service
135
+ */
136
+ function registerModules(dataFromAppIndex) {
137
+ Object.keys(dataFromAppIndex).forEach(function (moduleDefinitionKey) {
138
+ const moduleDefinition = dataFromAppIndex[moduleDefinitionKey];
139
+ if (moduleDefinition?.dependencies) {
140
+ moduleDefinition.dependencies.forEach(function (dependency) {
141
+ if (dependency.url && dependency.url.length > 0 && dependency.type === 'UI5LIB') {
142
+ Log.info('Registering Library ' + dependency.componentId + ' from server ' + dependency.url);
143
+ const compId = dependency.componentId.replaceAll('.', '/');
144
+ const config = {
145
+ paths: {}
146
+ };
147
+ config.paths[compId] = dependency.url;
148
+ sap.ui.loader.config(config);
149
+ }
150
+ });
151
+ }
152
+ });
153
+ }
154
+
8
155
  /**
9
156
  * Retrieves Flex settings from a 'sap-ui-bootstrap' element's data attribute.
10
157
  * Parses the 'data-open-ux-preview-flex-settings' attribute as JSON.
@@ -20,11 +167,156 @@ sap.ui.define([], function () {
20
167
  }
21
168
  return result;
22
169
  }
170
+
171
+ /**
172
+ * Fetch the app state from the given application urls, then reset the app state.
173
+ *
174
+ * @param container the UShell container
175
+ */
176
+ async function resetAppState(container) {
177
+ const urlParams = new URLSearchParams(globalThis.location.hash);
178
+ const appStateValue = urlParams.get('sap-iapp-state') ?? urlParams.get('/?sap-iapp-state');
179
+ if (appStateValue) {
180
+ const appStateService = await container.getServiceAsync('AppState');
181
+ appStateService.deleteAppState(appStateValue);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Fetch the manifest from the given application urls, then parse them for custom libs, and finally request their urls.
187
+ *
188
+ * @param appUrls application urls
189
+ * @param urlParams URLSearchParams object
190
+ * @returns returns a promise when the registration is completed.
191
+ */
192
+ async function registerComponentDependencyPaths(appUrls, urlParams) {
193
+ const libs = await getManifestLibs(appUrls);
194
+ if (libs && libs.length > 0) {
195
+ let url = '/sap/bc/ui2/app_index/ui5_app_info?id=' + encodeURIComponent(libs);
196
+ const sapClient = urlParams.get('sap-client');
197
+ if (sapClient?.length === 3 && /^\d+$/.test(sapClient)) {
198
+ url = url + '&sap-client=' + sapClient;
199
+ }
200
+ const response = await fetch(url);
201
+ try {
202
+ registerModules(await response.json());
203
+ } catch (error) {
204
+ Log.error(`Registering of reuse libs failed. Error:${error}`);
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Handle higher layer changes when starting UI Adaptation.
211
+ * When RTA detects higher layer changes an error with Reload triggered text is thrown, the RTA instance is destroyed and the application is reloaded.
212
+ * For UI5 version lower than 1.84.0 RTA is showing a popup with notification text about the detection of higher layer changes.
213
+ *
214
+ * @param error the error thrown when there are higher layer changes when starting UI Adaptation.
215
+ * @param ui5VersionInfo ui5 version info
216
+ */
217
+ async function handleHigherLayerChanges(error, ui5VersionInfo) {
218
+ const err = getError(error);
219
+ if (err.message.includes('Reload triggered')) {
220
+ if (!isLowerThanMinimalUi5Version(ui5VersionInfo, {
221
+ major: 1,
222
+ minor: 84
223
+ })) {
224
+ await sendInfoCenterMessage({
225
+ title: {
226
+ key: 'HIGHER_LAYER_CHANGES_TITLE'
227
+ },
228
+ description: {
229
+ key: 'HIGHER_LAYER_CHANGES_INFO_MESSAGE'
230
+ },
231
+ type: MessageBarType.warning
232
+ });
233
+ }
234
+ globalThis.location.reload();
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Starts UI adaptation (RTA) for the given component instance.
240
+ * Contains the inner logic shared between sandbox 1 (sandbox1Init.ts) and sandbox 2 (sandbox2AfterInit.ts).
241
+ *
242
+ * @param view - the component instance returned by AppLifeCycle attachAppLoaded
243
+ * @param flexSettings - the parsed flex settings
244
+ * @param ui5VersionInfo - current UI5 version
245
+ */
246
+ function startRtaForAppInstance(view, flexSettings, ui5VersionInfo) {
247
+ const {
248
+ pluginScript,
249
+ ...flexSettingsWithoutPlugin
250
+ } = flexSettings;
251
+ const libs = [];
252
+ if (isLowerThanMinimalUi5Version(ui5VersionInfo, {
253
+ major: 1,
254
+ minor: 72
255
+ })) {
256
+ libs.push('open/ux/preview/client/flp/initRta');
257
+ } else {
258
+ libs.push('sap/ui/rta/api/startAdaptation');
259
+ }
260
+ if (pluginScript) {
261
+ libs.push(pluginScript);
262
+ }
263
+ const options = {
264
+ rootControl: view,
265
+ validateAppVersion: false,
266
+ flexSettings: flexSettingsWithoutPlugin
267
+ };
268
+ sap.ui.require(libs, async function (startAdaptation, pluginScript) {
269
+ try {
270
+ await startAdaptation(options, pluginScript);
271
+ } catch (error) {
272
+ await sendInfoCenterMessage({
273
+ title: {
274
+ key: 'FLP_ADAPTATION_START_FAILED_TITLE'
275
+ },
276
+ description: getError(error).message,
277
+ type: MessageBarType.error
278
+ });
279
+ await handleHigherLayerChanges(error, ui5VersionInfo);
280
+ }
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Dynamically adds a "Generate Card" action to the SAP Fiori Launchpad for the given component instance.
286
+ *
287
+ * @param componentInstance - The component instance for which the card generation action is added.
288
+ * @param container - The SAP Fiori Launchpad container used to access services.
289
+ */
290
+ function addCardGenerationUserAction(componentInstance, container) {
291
+ sap.ui.require(['sap/cards/ap/generator/CardGenerator'], async CardGenerator => {
292
+ const extensionService = await container.getServiceAsync('Extension');
293
+ const controlProperties = {
294
+ icon: 'sap-icon://add',
295
+ id: 'generate_card',
296
+ text: 'Generate Card',
297
+ tooltip: 'Generate Card',
298
+ press: () => {
299
+ CardGenerator.initializeAsync(componentInstance);
300
+ }
301
+ };
302
+ const parameters = {
303
+ controlType: 'sap.ushell.ui.launchpad.ActionItem'
304
+ };
305
+ const generateCardAction = await extensionService.createUserAction(controlProperties, parameters);
306
+ generateCardAction.showForCurrentApp();
307
+ });
308
+ }
23
309
  var __exports = {
24
310
  __esModule: true
25
311
  };
312
+ __exports.registerForControllerExtensionErrors = registerForControllerExtensionErrors;
26
313
  __exports.CHANGES_API_PATH = CHANGES_API_PATH;
27
314
  __exports.getFlexSettings = getFlexSettings;
315
+ __exports.resetAppState = resetAppState;
316
+ __exports.registerComponentDependencyPaths = registerComponentDependencyPaths;
317
+ __exports.handleHigherLayerChanges = handleHigherLayerChanges;
318
+ __exports.startRtaForAppInstance = startRtaForAppInstance;
319
+ __exports.addCardGenerationUserAction = addCardGenerationUserAction;
28
320
  return __exports;
29
321
  });
30
322
  //# sourceMappingURL=common.js.map