@sap-ux/preview-middleware 1.1.3 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +31 -29
  2. package/dist/base/config.d.ts +55 -2
  3. package/dist/base/config.js +158 -26
  4. package/dist/base/flp.d.ts +24 -0
  5. package/dist/base/flp.js +116 -18
  6. package/dist/client/adp/api-handler.js +5 -1
  7. package/dist/client/adp/api-handler.ts +11 -4
  8. package/dist/client/adp/controllers/ControllerExtension.controller.js +95 -35
  9. package/dist/client/adp/controllers/ControllerExtension.controller.ts +121 -42
  10. package/dist/client/adp/extend-controller.ts +1 -0
  11. package/dist/client/adp/quick-actions/common/add-controller-to-page.js +2 -2
  12. package/dist/client/adp/quick-actions/common/add-controller-to-page.ts +2 -2
  13. package/dist/client/adp/quick-actions/fe-v4/create-page-action.js +3 -2
  14. package/dist/client/adp/quick-actions/fe-v4/create-page-action.ts +3 -2
  15. package/dist/client/adp/quick-actions/fe-v4/create-table-action-config-change.js +3 -2
  16. package/dist/client/adp/quick-actions/fe-v4/create-table-action-config-change.ts +3 -2
  17. package/dist/client/adp/ui/ControllerExtension.fragment.xml +33 -6
  18. package/dist/client/adp/utils.js +73 -5
  19. package/dist/client/adp/utils.ts +84 -5
  20. package/dist/client/flp/WorkspaceConnector.js +7 -5
  21. package/dist/client/flp/WorkspaceConnector.ts +7 -5
  22. package/dist/client/flp/common.js +293 -1
  23. package/dist/client/flp/common.ts +344 -1
  24. package/dist/client/flp/sandbox1Init.js +187 -0
  25. package/dist/client/flp/sandbox1Init.ts +174 -0
  26. package/dist/client/flp/sandbox2AfterInit.js +66 -0
  27. package/dist/client/flp/sandbox2AfterInit.ts +63 -0
  28. package/dist/client/flp/sandbox2BeforeInit.js +67 -0
  29. package/dist/client/flp/sandbox2BeforeInit.ts +60 -0
  30. package/dist/client/messagebundle.properties +3 -0
  31. package/dist/types/index.d.ts +21 -0
  32. package/package.json +3 -3
  33. package/templates/flp/cdm.ejs +1 -1
  34. package/templates/flp/sandbox.ejs +1 -1
  35. package/templates/flp/sandbox2.ejs +12 -44
  36. package/dist/client/flp/init.js +0 -465
  37. 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
- 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.
@@ -125,12 +125,16 @@ sap.ui.define(["../utils/error"], function (___utils_errorjs) {
125
125
  * Checks for existing controller in the project's workspace
126
126
  *
127
127
  * @param controllerName Name of the controller
128
+ * @param viewId ID of the current view, used to detect an existing instance-specific extension
128
129
  * @returns {CodeExtResponse} Returns path to existing controller if found
129
130
  */
130
- async function getExistingController(controllerName) {
131
+ async function getExistingController(controllerName, viewId) {
131
132
  const params = new URLSearchParams({
132
133
  name: controllerName
133
134
  });
135
+ if (viewId) {
136
+ params.append('viewId', viewId);
137
+ }
134
138
  const url = `${ApiEndpoints.CODE_EXT}?${params.toString()}`;
135
139
  return request(url, RequestMethod.GET);
136
140
  }
@@ -31,9 +31,12 @@ export interface FragmentsResponse {
31
31
  }
32
32
 
33
33
  export interface CodeExtResponse {
34
- controllerExists: boolean;
35
- controllerPath: string;
36
- controllerPathFromRoot: string;
34
+ baseControllerExists: boolean;
35
+ baseControllerPath: string;
36
+ baseControllerPathFromRoot: string;
37
+ instanceControllerExists: boolean;
38
+ instanceControllerPath: string;
39
+ instanceControllerPathFromRoot: string;
37
40
  isRunningInBAS: boolean;
38
41
  isTsSupported: boolean;
39
42
  }
@@ -172,10 +175,14 @@ export async function getDataSourceAnnotationFileMap(): Promise<AnnotationDataSo
172
175
  * Checks for existing controller in the project's workspace
173
176
  *
174
177
  * @param controllerName Name of the controller
178
+ * @param viewId ID of the current view, used to detect an existing instance-specific extension
175
179
  * @returns {CodeExtResponse} Returns path to existing controller if found
176
180
  */
177
- export async function getExistingController(controllerName: string): Promise<CodeExtResponse> {
181
+ export async function getExistingController(controllerName: string, viewId?: string): Promise<CodeExtResponse> {
178
182
  const params = new URLSearchParams({ name: controllerName });
183
+ if (viewId) {
184
+ params.append('viewId', viewId);
185
+ }
179
186
  const url = `${ApiEndpoints.CODE_EXT}?${params.toString()}` as ApiEndpoints;
180
187
  return request<CodeExtResponse>(url, RequestMethod.GET);
181
188
  }
@@ -25,6 +25,7 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
25
25
  const CommandExecutor = _interopRequireDefault(__CommandExecutor);
26
26
  const checkForExistingChange = ___utilsjs["checkForExistingChange"];
27
27
  const getControllerInfo = ___utilsjs["getControllerInfo"];
28
+ const getPendingCodeExtViewIds = ___utilsjs["getPendingCodeExtViewIds"];
28
29
  const BaseDialog = _interopRequireDefault(__BaseDialog);
29
30
  /**
30
31
  * @namespace open.ux.preview.client.adp.controllers
@@ -47,6 +48,7 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
47
48
  this.setEscapeHandler();
48
49
  const resourceModel = await getResourceModel('open.ux.preview.client');
49
50
  this.bundle = await getTextBundle();
51
+ this.ui5Version = await getUi5Version();
50
52
  await this.buildDialogData();
51
53
  this.dialog.setModel(resourceModel, 'i18n');
52
54
  this.dialog.setModel(this.model);
@@ -106,16 +108,18 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
106
108
  source.setEnabled(false);
107
109
  const controllerName = this.model.getProperty('/newControllerName');
108
110
  const viewId = this.model.getProperty('/viewId');
111
+ const isInstanceSpecific = this.model.getProperty('/isInstanceSpecific');
109
112
  const controllerRef = {
110
113
  codeRef: `coding/${controllerName}.js`,
111
- viewId
114
+ viewId,
115
+ instanceSpecific: isInstanceSpecific
112
116
  };
113
117
  if (this.data) {
114
118
  this.data.deferred.resolve(controllerRef);
115
119
  } else {
116
120
  await this.createNewController(controllerName, controllerRef);
117
121
  }
118
- if (this.data && (await this.isControllerExtensionSupported())) {
122
+ if (this.data && this.isControllerExtensionSupported()) {
119
123
  await sendInfoCenterMessage({
120
124
  title: {
121
125
  key: 'ADP_CREATE_CONTROLLER_EXTENSION_TITLE'
@@ -127,9 +131,6 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
127
131
  type: MessageBarType.info
128
132
  });
129
133
  }
130
- } else {
131
- const controllerPath = this.model.getProperty('/controllerPath');
132
- window.open(`vscode://file${controllerPath}`);
133
134
  }
134
135
  this.handleDialogClose();
135
136
  },
@@ -143,41 +144,62 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
143
144
  controllerName,
144
145
  viewId
145
146
  } = getControllerInfo(overlayControl);
146
- const data = await this.getExistingController(controllerName);
147
- const hasPendingChangeForView = checkForExistingChange(this.rta, 'codeExt', 'selector.controllerName', controllerName);
148
- if (data) {
149
- if (hasPendingChangeForView) {
147
+ const data = await this.getExistingController(controllerName, viewId);
148
+ if (!data) {
149
+ return;
150
+ }
151
+
152
+ // Combine persisted (server) and pending (command stack) changes to determine whether a base
153
+ // page controller and/or an instance-specific controller for this view already exist.
154
+ const pendingViewIds = getPendingCodeExtViewIds(this.rta, controllerName);
155
+ const baseExists = data.baseControllerExists || pendingViewIds.some(id => !id);
156
+ const instanceExists = data.instanceControllerExists || pendingViewIds.includes(viewId);
157
+ const showInstanceSpecificOption = this.isInstanceSpecificSupported();
158
+ if (!showInstanceSpecificOption) {
159
+ if (pendingViewIds.length > 0) {
150
160
  this.updateModelForExistingPendingChange();
151
- } else if (data?.controllerExists) {
152
- this.updateModelForExistingController(data);
161
+ } else if (data.baseControllerExists) {
162
+ this.updateModelForExistingController(data, true);
153
163
  } else {
154
- this.updateModelForNewController(viewId, data.isTsSupported);
164
+ this.updateModelForNewController(viewId, data.isTsSupported, false, false, false);
155
165
  await this.getControllers();
156
166
  }
167
+ return;
168
+ }
169
+ if (baseExists && instanceExists) {
170
+ if (data.baseControllerExists || data.instanceControllerExists) {
171
+ this.updateModelForExistingController(data);
172
+ } else {
173
+ this.updateModelForExistingPendingChange();
174
+ }
175
+ return;
157
176
  }
177
+ this.updateModelForNewController(viewId, data.isTsSupported, true, baseExists, instanceExists);
178
+ await this.getControllers();
158
179
  },
159
180
  /**
160
- * Updates the model properties for an existing controller.
181
+ * Updates the model properties for existing controller(s).
182
+ * Shows all persisted controllers (base and/or instance) in the existing-controller form, each with its own link to open in VS Code.
161
183
  *
162
- * @param {CodeExtResponse} data - Existing controller data from the server.
184
+ * @param data - Server response containing existence flags and file paths.
185
+ * @param showVsCodeButton - When true (pre-1.143 single-controller path), shows an "Open in VS Code" begin-button instead of relying on the inline fragment links.
163
186
  */
164
- updateModelForExistingController: function _updateModelForExistingController(data) {
165
- const {
166
- controllerExists,
167
- controllerPath,
168
- controllerPathFromRoot,
169
- isRunningInBAS
170
- } = data;
171
- this.model.setProperty('/controllerExists', controllerExists);
172
- this.model.setProperty('/controllerPath', controllerPath);
173
- this.model.setProperty('/controllerPathFromRoot', controllerPathFromRoot);
187
+ updateModelForExistingController: function _updateModelForExistingController(data, showVsCodeButton = false) {
188
+ this.model.setProperty('/controllerExists', true);
189
+ this.model.setProperty('/baseControllerExists', data.baseControllerExists);
190
+ this.model.setProperty('/baseControllerPath', data.baseControllerPath);
191
+ this.model.setProperty('/baseControllerPathFromRoot', data.baseControllerPathFromRoot);
192
+ this.model.setProperty('/instanceControllerExists', data.instanceControllerExists);
193
+ this.model.setProperty('/instanceControllerPath', data.instanceControllerPath);
194
+ this.model.setProperty('/instanceControllerPathFromRoot', data.instanceControllerPathFromRoot);
195
+ this.model.setProperty('/isRunningInBAS', data.isRunningInBAS);
174
196
  this.model.setProperty('/inputFormVisibility', false);
175
197
  this.model.setProperty('/pendingChangeFormVisibility', false);
176
198
  this.model.setProperty('/existingControllerFormVisibility', true);
177
- if (isRunningInBAS) {
178
- this.dialog.getBeginButton().setVisible(false);
179
- } else {
199
+ if (showVsCodeButton && !data.isRunningInBAS) {
180
200
  this.dialog.getBeginButton().setText('Open in VS Code').setEnabled(true);
201
+ } else {
202
+ this.dialog.getBeginButton().setVisible(false);
181
203
  }
182
204
  this.dialog.getEndButton().setText('Close');
183
205
  },
@@ -196,24 +218,34 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
196
218
  *
197
219
  * @param {string} viewId - The view ID.
198
220
  * @param {boolean} isTsSupported - Whether TypeScript supported for the current project.
221
+ * @param {boolean} showInstanceSpecificOption - Whether to show the instance-specific radio button option.
222
+ * @param {boolean} baseExists - Whether a base page controller extension already exists.
223
+ * @param {boolean} instanceExists - Whether an instance-specific extension already exists for this view.
199
224
  */
200
- updateModelForNewController: function _updateModelForNewController(viewId, isTsSupported) {
225
+ updateModelForNewController: function _updateModelForNewController(viewId, isTsSupported, showInstanceSpecificOption, baseExists, instanceExists) {
201
226
  this.model.setProperty('/viewId', viewId);
202
227
  this.model.setProperty('/controllerExtension', isTsSupported ? '.ts' : '.js');
203
228
  this.model.setProperty('/existingControllerFormVisibility', false);
204
229
  this.model.setProperty('/pendingChangeFormVisibility', false);
205
230
  this.model.setProperty('/inputFormVisibility', true);
231
+ this.model.setProperty('/instanceSpecificVisibility', showInstanceSpecificOption);
232
+ this.model.setProperty('/baseControllerEnabled', !baseExists);
233
+ this.model.setProperty('/instanceControllerEnabled', !instanceExists);
234
+ const selectedIndex = baseExists ? 1 : 0;
235
+ this.model.setProperty('/controllerTypeSelectedIndex', selectedIndex);
236
+ this.model.setProperty('/isInstanceSpecific', selectedIndex === 1);
206
237
  },
207
238
  /**
208
239
  * Retrieves existing controller data if found in the project's workspace.
209
240
  *
210
241
  * @param controllerName Controller name that exists in the view.
242
+ * @param viewId ID of the current view, used to detect an existing instance-specific extension.
211
243
  * @returns Returns existing controller data.
212
244
  */
213
- getExistingController: async function _getExistingController(controllerName) {
245
+ getExistingController: async function _getExistingController(controllerName, viewId) {
214
246
  let data;
215
247
  try {
216
- data = await getExistingController(controllerName);
248
+ data = await getExistingController(controllerName, viewId);
217
249
  } catch (e) {
218
250
  const error = getError(e);
219
251
  await sendInfoCenterMessage({
@@ -255,7 +287,7 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
255
287
  * @param controllerRef Controller reference
256
288
  */
257
289
  createNewController: async function _createNewController(controllerName, controllerRef) {
258
- if (await this.isControllerExtensionSupported()) {
290
+ if (this.isControllerExtensionSupported()) {
259
291
  await this.createControllerCommand(controllerName, controllerRef);
260
292
  return;
261
293
  }
@@ -264,7 +296,7 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
264
296
  controllerName
265
297
  });
266
298
  const service = await this.rta.getService('controllerExtension');
267
- const change = await service.add(controllerRef.codeRef, controllerRef.viewId);
299
+ const change = await service.add(controllerRef.codeRef, controllerRef.viewId, controllerRef.instanceSpecific);
268
300
  change.creation = new Date().toISOString();
269
301
  await writeChange(change);
270
302
  await sendInfoCenterMessage({
@@ -315,9 +347,32 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
315
347
  type: MessageBarType.info
316
348
  });
317
349
  },
318
- isControllerExtensionSupported: async function _isControllerExtensionSupported() {
319
- const ui5Version = await getUi5Version();
320
- return !isLowerThanMinimalUi5Version(ui5Version, ControllerExtension.CONTROLLER_EXT_MIN_UI5_VERSION);
350
+ isControllerExtensionSupported: function _isControllerExtensionSupported() {
351
+ return !isLowerThanMinimalUi5Version(this.ui5Version, ControllerExtension.CONTROLLER_EXT_MIN_UI5_VERSION);
352
+ },
353
+ /**
354
+ * Handles the selection change on the controller type for the radio button group.
355
+ *
356
+ * @param event Event
357
+ */
358
+ onControllerTypeSelectionChange: function _onControllerTypeSelectionChange(event) {
359
+ const group = event.getSource();
360
+ this.model.setProperty('/isInstanceSpecific', group.getSelectedIndex() === 1);
361
+ },
362
+ /**
363
+ * Opens the base page controller extension file in VS Code.
364
+ */
365
+ onOpenBaseController: function _onOpenBaseController() {
366
+ window.open(`vscode://file${this.model.getProperty('/baseControllerPath')}`);
367
+ },
368
+ /**
369
+ * Opens the instance-specific controller extension file in VS Code.
370
+ */
371
+ onOpenInstanceController: function _onOpenInstanceController() {
372
+ window.open(`vscode://file${this.model.getProperty('/instanceControllerPath')}`);
373
+ },
374
+ isInstanceSpecificSupported: function _isInstanceSpecificSupported() {
375
+ return !isLowerThanMinimalUi5Version(this.ui5Version, ControllerExtension.INSTANCE_SPECIFIC_MIN_UI5_VERSION);
321
376
  }
322
377
  });
323
378
  /* The minimum version of UI5 framework which supports controller extensions. */
@@ -325,6 +380,11 @@ sap.ui.define(["sap/ui/core/library", "sap/ui/model/json/JSONModel", "open/ux/pr
325
380
  major: 1,
326
381
  minor: 135
327
382
  };
383
+ /* The minimum version of UI5 framework which supports instance-specific controller extensions. */
384
+ ControllerExtension.INSTANCE_SPECIFIC_MIN_UI5_VERSION = {
385
+ major: 1,
386
+ minor: 143
387
+ };
328
388
  return ControllerExtension;
329
389
  });
330
390
  //# sourceMappingURL=ControllerExtension.controller.js.map