@sap-ux/preview-middleware 1.2.0 → 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.
@@ -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
@@ -2,6 +2,7 @@
2
2
  import Button from 'sap/m/Button';
3
3
  import type Dialog from 'sap/m/Dialog';
4
4
  import Input from 'sap/m/Input';
5
+ import type RadioButtonGroup from 'sap/m/RadioButtonGroup';
5
6
 
6
7
  /** sap.ui.core */
7
8
  import type UI5Element from 'sap/ui/core/Element';
@@ -25,16 +26,16 @@ import { getResourceModel, getTextBundle, TextBundle } from '../../i18n.js';
25
26
  import { getControlById } from '../../utils/core.js';
26
27
  import { getError } from '../../utils/error.js';
27
28
  import { sendInfoCenterMessage } from '../../utils/info-center-message.js';
28
- import { getUi5Version, isLowerThanMinimalUi5Version } from '../../utils/version.js';
29
+ import { getUi5Version, isLowerThanMinimalUi5Version, type Ui5VersionInfo } from '../../utils/version.js';
29
30
  import type { CodeExtResponse, ControllersResponse } from '../api-handler.js';
30
31
  import { getExistingController, readControllers, writeChange, writeController } from '../api-handler.js';
31
32
  import CommandExecutor from '../command-executor.js';
32
33
  import type { DeferredExtendControllerData, ExtendControllerData } from '../extend-controller.js';
33
- import { checkForExistingChange, getControllerInfo } from '../utils.js';
34
+ import { checkForExistingChange, getControllerInfo, getPendingCodeExtViewIds } from '../utils.js';
34
35
  import BaseDialog from './BaseDialog.controller.js';
35
36
 
36
37
  interface ControllerExtensionService {
37
- add: (codeRef: string, viewId: string) => Promise<{ creation: string }>;
38
+ add: (codeRef: string, viewId: string, includeViewId?: boolean) => Promise<{ creation: string }>;
38
39
  }
39
40
 
40
41
  type ControllerList = {
@@ -49,8 +50,14 @@ type ControllerModel = JSONModel & {
49
50
  getProperty(sPath: '/controllerExists'): boolean;
50
51
  getProperty(sPath: '/newControllerName'): string;
51
52
  getProperty(sPath: '/viewId'): string;
52
- getProperty(sPath: '/controllerPath'): string;
53
+ getProperty(sPath: '/baseControllerPath'): string;
54
+ getProperty(sPath: '/instanceControllerPath'): string;
53
55
  getProperty(sPath: '/controllerExtension'): string;
56
+ getProperty(sPath: '/isInstanceSpecific'): boolean;
57
+ getProperty(sPath: '/instanceSpecificVisibility'): boolean;
58
+ getProperty(sPath: '/baseControllerEnabled'): boolean;
59
+ getProperty(sPath: '/instanceControllerEnabled'): boolean;
60
+ getProperty(sPath: '/controllerTypeSelectedIndex'): number;
54
61
  };
55
62
 
56
63
  /**
@@ -59,8 +66,11 @@ type ControllerModel = JSONModel & {
59
66
  export default class ControllerExtension extends BaseDialog<ControllerModel> {
60
67
  /* The minimum version of UI5 framework which supports controller extensions. */
61
68
  private static readonly CONTROLLER_EXT_MIN_UI5_VERSION = { major: 1, minor: 135 };
69
+ /* The minimum version of UI5 framework which supports instance-specific controller extensions. */
70
+ private static readonly INSTANCE_SPECIFIC_MIN_UI5_VERSION = { major: 1, minor: 143 };
62
71
  public readonly data?: ExtendControllerData;
63
72
  private bundle: TextBundle;
73
+ private ui5Version: Ui5VersionInfo;
64
74
 
65
75
  constructor(
66
76
  name: string,
@@ -88,6 +98,7 @@ export default class ControllerExtension extends BaseDialog<ControllerModel> {
88
98
 
89
99
  const resourceModel = await getResourceModel('open.ux.preview.client');
90
100
  this.bundle = await getTextBundle();
101
+ this.ui5Version = await getUi5Version();
91
102
 
92
103
  await this.buildDialogData();
93
104
 
@@ -181,10 +192,12 @@ export default class ControllerExtension extends BaseDialog<ControllerModel> {
181
192
 
182
193
  const controllerName = this.model.getProperty('/newControllerName');
183
194
  const viewId = this.model.getProperty('/viewId');
195
+ const isInstanceSpecific = this.model.getProperty('/isInstanceSpecific');
184
196
 
185
- const controllerRef = {
197
+ const controllerRef: DeferredExtendControllerData = {
186
198
  codeRef: `coding/${controllerName}.js`,
187
- viewId
199
+ viewId,
200
+ instanceSpecific: isInstanceSpecific
188
201
  };
189
202
 
190
203
  if (this.data) {
@@ -193,16 +206,13 @@ export default class ControllerExtension extends BaseDialog<ControllerModel> {
193
206
  await this.createNewController(controllerName, controllerRef);
194
207
  }
195
208
 
196
- if (this.data && (await this.isControllerExtensionSupported())) {
209
+ if (this.data && this.isControllerExtensionSupported()) {
197
210
  await sendInfoCenterMessage({
198
211
  title: { key: 'ADP_CREATE_CONTROLLER_EXTENSION_TITLE' },
199
212
  description: { key: 'ADP_CREATE_CONTROLLER_EXTENSION', params: [controllerName] },
200
213
  type: MessageBarType.info
201
214
  });
202
215
  }
203
- } else {
204
- const controllerPath = this.model.getProperty('/controllerPath');
205
- window.open(`vscode://file${controllerPath}`);
206
216
  }
207
217
 
208
218
  this.handleDialogClose();
@@ -216,46 +226,68 @@ export default class ControllerExtension extends BaseDialog<ControllerModel> {
216
226
  const overlayControl = sap.ui.getCore().byId(selectorId) as unknown as ElementOverlay;
217
227
 
218
228
  const { controllerName, viewId } = getControllerInfo(overlayControl);
219
- const data = await this.getExistingController(controllerName);
229
+ const data = await this.getExistingController(controllerName, viewId);
220
230
 
221
- const hasPendingChangeForView = checkForExistingChange(
222
- this.rta,
223
- 'codeExt',
224
- 'selector.controllerName',
225
- controllerName
226
- );
231
+ if (!data) {
232
+ return;
233
+ }
227
234
 
228
- if (data) {
229
- if (hasPendingChangeForView) {
235
+ // Combine persisted (server) and pending (command stack) changes to determine whether a base
236
+ // page controller and/or an instance-specific controller for this view already exist.
237
+ const pendingViewIds = getPendingCodeExtViewIds(this.rta, controllerName);
238
+ const baseExists = data.baseControllerExists || pendingViewIds.some((id) => !id);
239
+ const instanceExists = data.instanceControllerExists || pendingViewIds.includes(viewId);
240
+
241
+ const showInstanceSpecificOption = this.isInstanceSpecificSupported();
242
+
243
+ if (!showInstanceSpecificOption) {
244
+ if (pendingViewIds.length > 0) {
230
245
  this.updateModelForExistingPendingChange();
231
- } else if (data?.controllerExists) {
232
- this.updateModelForExistingController(data);
246
+ } else if (data.baseControllerExists) {
247
+ this.updateModelForExistingController(data, true);
233
248
  } else {
234
- this.updateModelForNewController(viewId, data.isTsSupported);
235
-
249
+ this.updateModelForNewController(viewId, data.isTsSupported, false, false, false);
236
250
  await this.getControllers();
237
251
  }
252
+ return;
238
253
  }
254
+
255
+ if (baseExists && instanceExists) {
256
+ if (data.baseControllerExists || data.instanceControllerExists) {
257
+ this.updateModelForExistingController(data);
258
+ } else {
259
+ this.updateModelForExistingPendingChange();
260
+ }
261
+ return;
262
+ }
263
+
264
+ this.updateModelForNewController(viewId, data.isTsSupported, true, baseExists, instanceExists);
265
+ await this.getControllers();
239
266
  }
240
267
  /**
241
- * Updates the model properties for an existing controller.
268
+ * Updates the model properties for existing controller(s).
269
+ * Shows all persisted controllers (base and/or instance) in the existing-controller form, each with its own link to open in VS Code.
242
270
  *
243
- * @param {CodeExtResponse} data - Existing controller data from the server.
271
+ * @param data - Server response containing existence flags and file paths.
272
+ * @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.
244
273
  */
245
- private updateModelForExistingController(data: CodeExtResponse): void {
246
- const { controllerExists, controllerPath, controllerPathFromRoot, isRunningInBAS } = data;
247
-
248
- this.model.setProperty('/controllerExists', controllerExists);
249
- this.model.setProperty('/controllerPath', controllerPath);
250
- this.model.setProperty('/controllerPathFromRoot', controllerPathFromRoot);
274
+ private updateModelForExistingController(data: CodeExtResponse, showVsCodeButton = false): void {
275
+ this.model.setProperty('/controllerExists', true);
276
+ this.model.setProperty('/baseControllerExists', data.baseControllerExists);
277
+ this.model.setProperty('/baseControllerPath', data.baseControllerPath);
278
+ this.model.setProperty('/baseControllerPathFromRoot', data.baseControllerPathFromRoot);
279
+ this.model.setProperty('/instanceControllerExists', data.instanceControllerExists);
280
+ this.model.setProperty('/instanceControllerPath', data.instanceControllerPath);
281
+ this.model.setProperty('/instanceControllerPathFromRoot', data.instanceControllerPathFromRoot);
282
+ this.model.setProperty('/isRunningInBAS', data.isRunningInBAS);
251
283
  this.model.setProperty('/inputFormVisibility', false);
252
284
  this.model.setProperty('/pendingChangeFormVisibility', false);
253
285
  this.model.setProperty('/existingControllerFormVisibility', true);
254
286
 
255
- if (isRunningInBAS) {
256
- this.dialog.getBeginButton().setVisible(false);
257
- } else {
287
+ if (showVsCodeButton && !data.isRunningInBAS) {
258
288
  this.dialog.getBeginButton().setText('Open in VS Code').setEnabled(true);
289
+ } else {
290
+ this.dialog.getBeginButton().setVisible(false);
259
291
  }
260
292
  this.dialog.getEndButton().setText('Close');
261
293
  }
@@ -277,25 +309,41 @@ export default class ControllerExtension extends BaseDialog<ControllerModel> {
277
309
  *
278
310
  * @param {string} viewId - The view ID.
279
311
  * @param {boolean} isTsSupported - Whether TypeScript supported for the current project.
312
+ * @param {boolean} showInstanceSpecificOption - Whether to show the instance-specific radio button option.
313
+ * @param {boolean} baseExists - Whether a base page controller extension already exists.
314
+ * @param {boolean} instanceExists - Whether an instance-specific extension already exists for this view.
280
315
  */
281
- private updateModelForNewController(viewId: string, isTsSupported: boolean): void {
316
+ private updateModelForNewController(
317
+ viewId: string,
318
+ isTsSupported: boolean,
319
+ showInstanceSpecificOption: boolean,
320
+ baseExists: boolean,
321
+ instanceExists: boolean
322
+ ): void {
282
323
  this.model.setProperty('/viewId', viewId);
283
324
  this.model.setProperty('/controllerExtension', isTsSupported ? '.ts' : '.js');
284
325
  this.model.setProperty('/existingControllerFormVisibility', false);
285
326
  this.model.setProperty('/pendingChangeFormVisibility', false);
286
327
  this.model.setProperty('/inputFormVisibility', true);
328
+ this.model.setProperty('/instanceSpecificVisibility', showInstanceSpecificOption);
329
+ this.model.setProperty('/baseControllerEnabled', !baseExists);
330
+ this.model.setProperty('/instanceControllerEnabled', !instanceExists);
331
+ const selectedIndex = baseExists ? 1 : 0;
332
+ this.model.setProperty('/controllerTypeSelectedIndex', selectedIndex);
333
+ this.model.setProperty('/isInstanceSpecific', selectedIndex === 1);
287
334
  }
288
335
 
289
336
  /**
290
337
  * Retrieves existing controller data if found in the project's workspace.
291
338
  *
292
339
  * @param controllerName Controller name that exists in the view.
340
+ * @param viewId ID of the current view, used to detect an existing instance-specific extension.
293
341
  * @returns Returns existing controller data.
294
342
  */
295
- private async getExistingController(controllerName: string): Promise<CodeExtResponse | undefined> {
343
+ private async getExistingController(controllerName: string, viewId: string): Promise<CodeExtResponse | undefined> {
296
344
  let data: CodeExtResponse | undefined;
297
345
  try {
298
- data = await getExistingController(controllerName);
346
+ data = await getExistingController(controllerName, viewId);
299
347
  } catch (e) {
300
348
  const error = getError(e);
301
349
  await sendInfoCenterMessage({
@@ -337,7 +385,7 @@ export default class ControllerExtension extends BaseDialog<ControllerModel> {
337
385
  controllerName: string,
338
386
  controllerRef: DeferredExtendControllerData
339
387
  ): Promise<void> {
340
- if (await this.isControllerExtensionSupported()) {
388
+ if (this.isControllerExtensionSupported()) {
341
389
  await this.createControllerCommand(controllerName, controllerRef);
342
390
  return;
343
391
  }
@@ -346,7 +394,11 @@ export default class ControllerExtension extends BaseDialog<ControllerModel> {
346
394
 
347
395
  const service = await this.rta.getService<ControllerExtensionService>('controllerExtension');
348
396
 
349
- const change = await service.add(controllerRef.codeRef, controllerRef.viewId);
397
+ const change = await service.add(
398
+ controllerRef.codeRef,
399
+ controllerRef.viewId,
400
+ controllerRef.instanceSpecific
401
+ );
350
402
  change.creation = new Date().toISOString();
351
403
 
352
404
  await writeChange(change);
@@ -401,8 +453,35 @@ export default class ControllerExtension extends BaseDialog<ControllerModel> {
401
453
  });
402
454
  }
403
455
 
404
- private async isControllerExtensionSupported(): Promise<boolean> {
405
- const ui5Version = await getUi5Version();
406
- return !isLowerThanMinimalUi5Version(ui5Version, ControllerExtension.CONTROLLER_EXT_MIN_UI5_VERSION);
456
+ private isControllerExtensionSupported(): boolean {
457
+ return !isLowerThanMinimalUi5Version(this.ui5Version, ControllerExtension.CONTROLLER_EXT_MIN_UI5_VERSION);
458
+ }
459
+
460
+ /**
461
+ * Handles the selection change on the controller type for the radio button group.
462
+ *
463
+ * @param event Event
464
+ */
465
+ onControllerTypeSelectionChange(event: Event): void {
466
+ const group = event.getSource<RadioButtonGroup>();
467
+ this.model.setProperty('/isInstanceSpecific', group.getSelectedIndex() === 1);
468
+ }
469
+
470
+ /**
471
+ * Opens the base page controller extension file in VS Code.
472
+ */
473
+ onOpenBaseController(): void {
474
+ window.open(`vscode://file${this.model.getProperty('/baseControllerPath')}`);
475
+ }
476
+
477
+ /**
478
+ * Opens the instance-specific controller extension file in VS Code.
479
+ */
480
+ onOpenInstanceController(): void {
481
+ window.open(`vscode://file${this.model.getProperty('/instanceControllerPath')}`);
482
+ }
483
+
484
+ private isInstanceSpecificSupported(): boolean {
485
+ return !isLowerThanMinimalUi5Version(this.ui5Version, ControllerExtension.INSTANCE_SPECIFIC_MIN_UI5_VERSION);
407
486
  }
408
487
  }
@@ -12,6 +12,7 @@ export interface ExtendControllerData {
12
12
  export type DeferredExtendControllerData = {
13
13
  codeRef: string;
14
14
  viewId: string;
15
+ instanceSpecific: boolean;
15
16
  };
16
17
 
17
18
  /**
@@ -44,8 +44,8 @@ sap.ui.define(["sap/ui/dt/OverlayRegistry", "../../../utils/version", "../../uti
44
44
  const control = getRelevantControlFromActivePage(this.context.controlIndex, this.context.view, CONTROL_TYPES)[0];
45
45
  if (control) {
46
46
  const controlInfo = getControllerInfoForControl(control);
47
- const data = await getExistingController(controlInfo.controllerName);
48
- this.controllerExists = data?.controllerExists;
47
+ const data = await getExistingController(controlInfo.controllerName, controlInfo.viewId);
48
+ this.controllerExists = data?.baseControllerExists || data?.instanceControllerExists;
49
49
  const isActiveAction = isControllerExtensionEnabledForControl(control, isReuseComponent, this.context.flexSettings.isCloud);
50
50
  this.control = isActiveAction ? control : undefined;
51
51
  }
@@ -60,8 +60,8 @@ export class AddControllerToPageQuickAction
60
60
  )[0];
61
61
  if (control) {
62
62
  const controlInfo = getControllerInfoForControl(control);
63
- const data = await getExistingController(controlInfo.controllerName);
64
- this.controllerExists = data?.controllerExists;
63
+ const data = await getExistingController(controlInfo.controllerName, controlInfo.viewId);
64
+ this.controllerExists = data?.baseControllerExists || data?.instanceControllerExists;
65
65
  const isActiveAction = isControllerExtensionEnabledForControl(
66
66
  control,
67
67
  isReuseComponent,
@@ -40,8 +40,9 @@ sap.ui.define(["sap/ui/dt/OverlayRegistry", "../../dialog-factory", "../simple-q
40
40
  if (this.control) {
41
41
  const overlay = OverlayRegistry.getOverlay(this.control) || [];
42
42
  const controlInfo = getControllerInfoForControl(this.control);
43
- const data = await getExistingController(controlInfo.controllerName);
44
- const controllerPath = data.controllerPathFromRoot.replaceAll(/\//g, '.').replace(/\.[^.]+$/, '');
43
+ const data = await getExistingController(controlInfo.controllerName, controlInfo.viewId);
44
+ const controllerPathFromRoot = data.baseControllerPathFromRoot || data.instanceControllerPathFromRoot || '';
45
+ const controllerPath = controllerPathFromRoot.replaceAll('/', '.').replace(/\.[^.]+$/, '');
45
46
  await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.ADD_ACTION, undefined, {
46
47
  title: 'QUICK_ACTION_ADD_CUSTOM_PAGE_ACTION',
47
48
  controllerReference: controllerPath ? `.extension.${controllerPath}.<REPLACE_WITH_YOUR_HANDLER_NAME>` : '.extension.<ApplicationId.FolderName.ScriptFilename.methodName>',
@@ -43,8 +43,9 @@ export class AddPageActionQuickAction extends SimpleQuickActionDefinitionBase im
43
43
  if (this.control) {
44
44
  const overlay = OverlayRegistry.getOverlay(this.control) || [];
45
45
  const controlInfo = getControllerInfoForControl(this.control);
46
- const data = await getExistingController(controlInfo.controllerName);
47
- const controllerPath = data.controllerPathFromRoot.replaceAll(/\//g, '.').replace(/\.[^.]+$/, '');
46
+ const data = await getExistingController(controlInfo.controllerName, controlInfo.viewId);
47
+ const controllerPathFromRoot = data.baseControllerPathFromRoot || data.instanceControllerPathFromRoot || '';
48
+ const controllerPath = controllerPathFromRoot.replaceAll('/', '.').replace(/\.[^.]+$/, '');
48
49
  await DialogFactory.createDialog(
49
50
  overlay,
50
51
  this.context.rta,
@@ -50,8 +50,9 @@ sap.ui.define(["sap/ui/dt/OverlayRegistry", "../../dialog-factory", "../../../ut
50
50
  if (table) {
51
51
  const overlay = OverlayRegistry.getOverlay(table) || [];
52
52
  const controlInfo = getControllerInfoForControl(table);
53
- const data = await getExistingController(controlInfo.controllerName);
54
- const controllerPath = data.controllerPathFromRoot.replaceAll('/', '.').replace(/\.[^.]+$/, '');
53
+ const data = await getExistingController(controlInfo.controllerName, controlInfo.viewId);
54
+ const controllerPathFromRoot = data.baseControllerPathFromRoot || data.instanceControllerPathFromRoot || '';
55
+ const controllerPath = controllerPathFromRoot.replaceAll('/', '.').replace(/\.[^.]+$/, '');
55
56
  await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.ADD_ACTION, undefined, {
56
57
  title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION',
57
58
  controllerReference: controllerPath ? `.extension.${controllerPath}.<methodName>` : '.extension.<ApplicationId.FolderName.ScriptFilename.methodName>',
@@ -52,8 +52,9 @@ export class AddTableActionQuickAction extends TableQuickActionDefinitionBase im
52
52
  if (table) {
53
53
  const overlay = OverlayRegistry.getOverlay(table) || [];
54
54
  const controlInfo = getControllerInfoForControl(table);
55
- const data = await getExistingController(controlInfo.controllerName);
56
- const controllerPath = data.controllerPathFromRoot.replaceAll('/', '.').replace(/\.[^.]+$/, '');
55
+ const data = await getExistingController(controlInfo.controllerName, controlInfo.viewId);
56
+ const controllerPathFromRoot = data.baseControllerPathFromRoot || data.instanceControllerPathFromRoot || '';
57
+ const controllerPath = controllerPathFromRoot.replaceAll('/', '.').replace(/\.[^.]+$/, '');
57
58
  await DialogFactory.createDialog(
58
59
  overlay,
59
60
  this.context.rta,
@@ -1,10 +1,10 @@
1
- <Dialog id="controllerExtensionDialog"
1
+ <Dialog id="controllerExtensionDialog"
2
2
  xmlns:mvc="sap.ui.core.mvc"
3
3
  xmlns="sap.m"
4
4
  xmlns:core="sap.ui.core"
5
- xmlns:f="sap.ui.layout.form"
6
- title="Extend With Controller"
7
- contentWidth="450px"
5
+ xmlns:f="sap.ui.layout.form"
6
+ title="Extend With Controller"
7
+ contentWidth="450px"
8
8
  class="sapUiRTABorder">
9
9
  <content>
10
10
  <f:SimpleForm
@@ -15,12 +15,28 @@
15
15
  visible="{/inputFormVisibility}">
16
16
  <f:content>
17
17
  <Label text="Controller Name" />
18
- <Input
18
+ <Input
19
19
  id="controllerName"
20
20
  description="{/controllerExtension}"
21
21
  value="{/newControllerName}"
22
22
  liveChange="onControllerNameInputChange">
23
23
  </Input>
24
+ <Label text="{i18n>ADP_CONTROLLER_TYPE_SELECTION_LABEL}"
25
+ visible="{/instanceSpecificVisibility}" />
26
+ <RadioButtonGroup
27
+ id="controllerTypeGroup"
28
+ select="onControllerTypeSelectionChange"
29
+ selectedIndex="{/controllerTypeSelectedIndex}"
30
+ visible="{/instanceSpecificVisibility}">
31
+ <buttons>
32
+ <RadioButton id="baseControllerRadio"
33
+ text="{i18n>ADP_CONTROLLER_TYPE_BASE}"
34
+ enabled="{/baseControllerEnabled}" />
35
+ <RadioButton id="instanceControllerRadio"
36
+ text="{i18n>ADP_CONTROLLER_TYPE_INSTANCE}"
37
+ enabled="{/instanceControllerEnabled}" />
38
+ </buttons>
39
+ </RadioButtonGroup>
24
40
  </f:content>
25
41
  </f:SimpleForm>
26
42
  <f:SimpleForm
@@ -31,7 +47,18 @@
31
47
  singleContainerFullSize="false">
32
48
  <f:content>
33
49
  <Label text="{i18n>ADP_CONTROLLER_EXTENSION_EXISTS}" />
34
- <Text text="{/controllerPathFromRoot}" />
50
+
51
+ <Label text="{i18n>ADP_CONTROLLER_TYPE_BASE}" visible="{/baseControllerExists}" />
52
+ <Link text="{/baseControllerPathFromRoot}" press="onOpenBaseController"
53
+ visible="{= ${/baseControllerExists} &amp;&amp; !${/isRunningInBAS} }" />
54
+ <Text text="{/baseControllerPathFromRoot}"
55
+ visible="{= ${/baseControllerExists} &amp;&amp; ${/isRunningInBAS} }" />
56
+
57
+ <Label text="{i18n>ADP_CONTROLLER_TYPE_INSTANCE}" visible="{/instanceControllerExists}" />
58
+ <Link text="{/instanceControllerPathFromRoot}" press="onOpenInstanceController"
59
+ visible="{= ${/instanceControllerExists} &amp;&amp; !${/isRunningInBAS} }" />
60
+ <Text text="{/instanceControllerPathFromRoot}"
61
+ visible="{= ${/instanceControllerExists} &amp;&amp; ${/isRunningInBAS} }" />
35
62
  </f:content>
36
63
  </f:SimpleForm>
37
64
  <f:SimpleForm
@@ -107,6 +107,58 @@ sap.ui.define(["sap/ui/fl/Utils", "../utils/core", "../utils/changes", "../utils
107
107
  return typeof nestedProperty === 'string' ? nestedProperty.includes(propertyValue) : false;
108
108
  });
109
109
  }
110
+
111
+ /**
112
+ * Collects the `content.viewId` values of all pending `codeExt` changes in the RTA command stack for a
113
+ * given controller. A base page controller extension has no `viewId` (yields `undefined`), while an
114
+ * instance-specific extension yields the ID of the view it is bound to.
115
+ *
116
+ * @param {RuntimeAuthoring} rta - The RuntimeAuthoring instance to inspect.
117
+ * @param {string} controllerName - The controller name to match against `selector.controllerName`.
118
+ * @returns {(string | undefined)[]} The `content.viewId` of each matching pending codeExt change.
119
+ */
120
+ function getPendingCodeExtViewIds(rta, controllerName) {
121
+ const allCommands = rta.getCommandStack().getCommands();
122
+ const viewIds = [];
123
+ const collectFromCommand = command => {
124
+ getFlexChangeList(command).forEach(change => {
125
+ const changeDefinition = getChangeDefinition(change);
126
+ if (getNestedProperty(changeDefinition, 'changeType') !== 'codeExt') {
127
+ return;
128
+ }
129
+ if (getNestedProperty(changeDefinition, 'selector.controllerName') !== controllerName) {
130
+ return;
131
+ }
132
+ const viewId = getNestedProperty(changeDefinition, 'content.viewId');
133
+ viewIds.push(typeof viewId === 'string' ? viewId : undefined);
134
+ });
135
+ };
136
+ allCommands.forEach(command => {
137
+ if (typeof command.getCommands === 'function') {
138
+ command.getCommands().forEach(subCommand => {
139
+ if (subCommand?.getProperty('name') === 'codeExt') {
140
+ collectFromCommand(subCommand);
141
+ }
142
+ });
143
+ } else {
144
+ collectFromCommand(command);
145
+ }
146
+ });
147
+ return viewIds;
148
+ }
149
+ /**
150
+ * Resolves the controller name of a view, preferring the typed controller module name and falling
151
+ * back to the controller instance's metadata name. Returns an empty string when the view owns no
152
+ * controller.
153
+ *
154
+ * @param view The view for which to resolve the controller name.
155
+ * @returns The controller name (`module:...` or class name), or an empty string when none is resolvable.
156
+ */
157
+ function resolveControllerName(view) {
158
+ const moduleName = view?.getControllerModuleName?.();
159
+ return moduleName ? `module:${moduleName}` : view?.getController?.()?.getMetadata?.().getName() ?? '';
160
+ }
161
+
110
162
  /**
111
163
  * Gets controller name and view ID for the given UI5 control.
112
164
  *
@@ -114,13 +166,28 @@ sap.ui.define(["sap/ui/fl/Utils", "../utils/core", "../utils/changes", "../utils
114
166
  * @returns The controller name and view ID.
115
167
  */
116
168
  function getControllerInfoForControl(control) {
117
- const view = FlexUtils.getViewForControl(control);
118
- const moduleName = view?.getControllerModuleName?.();
119
- const controllerName = moduleName ? `module:${moduleName}` : view.getController()?.getMetadata().getName();
120
- const viewId = view.getId();
169
+ let view = FlexUtils.getViewForControl(control);
170
+ let controllerName = resolveControllerName(view);
171
+ while (!controllerName) {
172
+ const parent = view?.getParent?.();
173
+ if (!parent) {
174
+ break;
175
+ }
176
+ const parentView = FlexUtils.getViewForControl(parent);
177
+ if (!parentView || parentView === view) {
178
+ break;
179
+ }
180
+ view = parentView;
181
+ controllerName = resolveControllerName(view);
182
+ }
183
+
184
+ // viewId is intentionally the controller-bearing view's ID (which may be an ancestor of the
185
+ // original view). The change file binds controllerName and viewId as a pair — returning the
186
+ // innermost view's ID when it has no controller would create an inconsistent pair that the
187
+ // server could never match.
121
188
  return {
122
189
  controllerName,
123
- viewId
190
+ viewId: view.getId()
124
191
  };
125
192
  }
126
193
 
@@ -189,6 +256,7 @@ sap.ui.define(["sap/ui/fl/Utils", "../utils/core", "../utils/changes", "../utils
189
256
  __exports.checkForExistingChange = checkForExistingChange;
190
257
  __exports.getNestedProperty = getNestedProperty;
191
258
  __exports.matchesChangeProperty = matchesChangeProperty;
259
+ __exports.getPendingCodeExtViewIds = getPendingCodeExtViewIds;
192
260
  __exports.getControllerInfoForControl = getControllerInfoForControl;
193
261
  __exports.getControllerInfo = getControllerInfo;
194
262
  __exports.getReuseComponentChecker = getReuseComponentChecker;
@@ -113,11 +113,74 @@ export function matchesChangeProperty(command: FlexCommand, propertyPath: string
113
113
  return typeof nestedProperty === 'string' ? nestedProperty.includes(propertyValue) : false;
114
114
  });
115
115
  }
116
+
117
+ /**
118
+ * Collects the `content.viewId` values of all pending `codeExt` changes in the RTA command stack for a
119
+ * given controller. A base page controller extension has no `viewId` (yields `undefined`), while an
120
+ * instance-specific extension yields the ID of the view it is bound to.
121
+ *
122
+ * @param {RuntimeAuthoring} rta - The RuntimeAuthoring instance to inspect.
123
+ * @param {string} controllerName - The controller name to match against `selector.controllerName`.
124
+ * @returns {(string | undefined)[]} The `content.viewId` of each matching pending codeExt change.
125
+ */
126
+ export function getPendingCodeExtViewIds(rta: RuntimeAuthoring, controllerName: string): (string | undefined)[] {
127
+ const allCommands = rta.getCommandStack().getCommands();
128
+ const viewIds: (string | undefined)[] = [];
129
+
130
+ const collectFromCommand = (command: FlexCommand): void => {
131
+ getFlexChangeList(command).forEach((change) => {
132
+ const changeDefinition = getChangeDefinition(change);
133
+ if (getNestedProperty(changeDefinition, 'changeType') !== 'codeExt') {
134
+ return;
135
+ }
136
+ if (getNestedProperty(changeDefinition, 'selector.controllerName') !== controllerName) {
137
+ return;
138
+ }
139
+ const viewId = getNestedProperty(changeDefinition, 'content.viewId');
140
+ viewIds.push(typeof viewId === 'string' ? viewId : undefined);
141
+ });
142
+ };
143
+
144
+ allCommands.forEach((command: FlexCommand) => {
145
+ if (typeof command.getCommands === 'function') {
146
+ command.getCommands().forEach((subCommand: FlexCommand) => {
147
+ if (subCommand?.getProperty('name') === 'codeExt') {
148
+ collectFromCommand(subCommand);
149
+ }
150
+ });
151
+ } else {
152
+ collectFromCommand(command);
153
+ }
154
+ });
155
+
156
+ return viewIds;
157
+ }
158
+
116
159
  interface ControllerInfo {
117
160
  controllerName: string;
118
161
  viewId: string;
119
162
  }
120
163
 
164
+ interface ViewLike {
165
+ getId(): string;
166
+ getControllerModuleName?(): string | undefined;
167
+ getController?(): { getMetadata(): { getName(): string } } | undefined;
168
+ getParent?(): ManagedObject | null;
169
+ }
170
+
171
+ /**
172
+ * Resolves the controller name of a view, preferring the typed controller module name and falling
173
+ * back to the controller instance's metadata name. Returns an empty string when the view owns no
174
+ * controller.
175
+ *
176
+ * @param view The view for which to resolve the controller name.
177
+ * @returns The controller name (`module:...` or class name), or an empty string when none is resolvable.
178
+ */
179
+ function resolveControllerName(view: ViewLike): string {
180
+ const moduleName = view?.getControllerModuleName?.();
181
+ return moduleName ? `module:${moduleName}` : view?.getController?.()?.getMetadata?.().getName() ?? '';
182
+ }
183
+
121
184
  /**
122
185
  * Gets controller name and view ID for the given UI5 control.
123
186
  *
@@ -125,11 +188,27 @@ interface ControllerInfo {
125
188
  * @returns The controller name and view ID.
126
189
  */
127
190
  export function getControllerInfoForControl(control: ManagedObject): ControllerInfo {
128
- const view = FlexUtils.getViewForControl(control);
129
- const moduleName = view?.getControllerModuleName?.();
130
- const controllerName = moduleName ? `module:${moduleName}` : view.getController()?.getMetadata().getName();
131
- const viewId = view.getId();
132
- return { controllerName, viewId };
191
+ let view = FlexUtils.getViewForControl(control) as ViewLike;
192
+ let controllerName = resolveControllerName(view);
193
+
194
+ while (!controllerName) {
195
+ const parent = view?.getParent?.();
196
+ if (!parent) {
197
+ break;
198
+ }
199
+ const parentView = FlexUtils.getViewForControl(parent) as ViewLike;
200
+ if (!parentView || parentView === view) {
201
+ break;
202
+ }
203
+ view = parentView;
204
+ controllerName = resolveControllerName(view);
205
+ }
206
+
207
+ // viewId is intentionally the controller-bearing view's ID (which may be an ancestor of the
208
+ // original view). The change file binds controllerName and viewId as a pair — returning the
209
+ // innermost view's ID when it has no controller would create an inconsistent pair that the
210
+ // server could never match.
211
+ return { controllerName, viewId: view.getId() };
133
212
  }
134
213
 
135
214
  /**
@@ -42,6 +42,9 @@ ADP_ADD_FRAGMENT_WITH_TEMPLATE_NOTIFICATION = Note: The `{0}.fragment.xml` fragm
42
42
  ADP_CONTROLLER_EXTENSION_EXISTS = An existing controller extension has been found. Please use the previously created extension controller.
43
43
  ADP_CONTROLLER_PENDING_CHANGE_EXISTS = A pending change for controller extension has been found. Please use the previously created extension controller after saving the change or delete the pending change.
44
44
  ADP_CREATE_CONTROLLER_EXTENSION = Note: The `{0}` controller extension will be created once you save the change.
45
+ ADP_CONTROLLER_TYPE_SELECTION_LABEL = Which controller would you like to extend?
46
+ ADP_CONTROLLER_TYPE_BASE = Base Page Controller
47
+ ADP_CONTROLLER_TYPE_INSTANCE = This entity specific controller
45
48
  ADP_ADD_TWO_FRAGMENTS_WITH_TEMPLATE_NOTIFICATION = Note: The `{0}.fragment.xml` and `{1}.fragment.xml` fragments will be created once you save the changes.
46
49
  ADP_SYNC_VIEWS_MESSAGE = Synchronous views are detected for this application. Controller extensions are not supported for such views and will be disabled.
47
50
  ADP_QUICK_ACTION_DIALOG_OPEN_MESSAGE = This action is disabled because a dialog is already open.
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "bugs": {
11
11
  "url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Apreview-middleware"
12
12
  },
13
- "version": "1.2.0",
13
+ "version": "1.2.1",
14
14
  "license": "Apache-2.0",
15
15
  "author": "@SAP/ux-tools-team",
16
16
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "mem-fs-editor": "9.4.0",
29
29
  "qrcode": "1.5.4",
30
30
  "@sap/bas-sdk": "3.13.10",
31
- "@sap-ux/adp-tooling": "1.0.43",
31
+ "@sap-ux/adp-tooling": "1.0.44",
32
32
  "@sap-ux/btp-utils": "2.0.6",
33
33
  "@sap-ux/control-property-editor-sources": "npm:@sap-ux/control-property-editor@1.0.10",
34
34
  "@sap-ux/feature-toggle": "1.0.5",
@@ -54,7 +54,7 @@
54
54
  "nock": "14.0.16",
55
55
  "npm-run-all2": "9.0.2",
56
56
  "supertest": "7.2.2",
57
- "@private/preview-middleware-client": "npm:@sap-ux-private/preview-middleware-client@1.2.0",
57
+ "@private/preview-middleware-client": "npm:@sap-ux-private/preview-middleware-client@1.2.1",
58
58
  "@sap-ux-private/playwright": "1.0.6",
59
59
  "@sap-ux/axios-extension": "2.0.8",
60
60
  "@sap-ux/store": "2.0.6",