@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/README.md +31 -29
- package/dist/base/config.d.ts +55 -2
- package/dist/base/config.js +158 -26
- package/dist/base/flp.d.ts +24 -0
- package/dist/base/flp.js +116 -18
- package/dist/client/flp/WorkspaceConnector.js +7 -5
- package/dist/client/flp/WorkspaceConnector.ts +7 -5
- package/dist/client/flp/common.js +293 -1
- package/dist/client/flp/common.ts +344 -1
- package/dist/client/flp/sandbox1Init.js +187 -0
- package/dist/client/flp/sandbox1Init.ts +174 -0
- package/dist/client/flp/sandbox2AfterInit.js +66 -0
- package/dist/client/flp/sandbox2AfterInit.ts +63 -0
- package/dist/client/flp/sandbox2BeforeInit.js +67 -0
- package/dist/client/flp/sandbox2BeforeInit.ts +60 -0
- package/dist/types/index.d.ts +21 -0
- package/package.json +6 -6
- package/templates/flp/cdm.ejs +1 -1
- package/templates/flp/sandbox.ejs +1 -1
- package/templates/flp/sandbox2.ejs +12 -44
- package/dist/client/flp/init.js +0 -465
- package/dist/client/flp/init.ts +0 -498
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
12
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
10
|
-
const
|
|
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(
|
|
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(
|
|
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(
|
|
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
|