@sapui5/sap.suite.ui.generic.template 1.148.1 → 1.148.2

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 (74) hide show
  1. package/package.json +1 -1
  2. package/src/sap/suite/ui/generic/template/.library +1 -1
  3. package/src/sap/suite/ui/generic/template/AnalyticalListPage/manifest.json +1 -1
  4. package/src/sap/suite/ui/generic/template/Canvas/manifest.json +1 -1
  5. package/src/sap/suite/ui/generic/template/ListReport/controller/ControllerImplementation.js +6 -3
  6. package/src/sap/suite/ui/generic/template/ListReport/manifest.json +1 -1
  7. package/src/sap/suite/ui/generic/template/ObjectPage/manifest.json +1 -1
  8. package/src/sap/suite/ui/generic/template/QuickCreate/manifest.json +1 -1
  9. package/src/sap/suite/ui/generic/template/QuickView/manifest.json +1 -1
  10. package/src/sap/suite/ui/generic/template/genericUtilities/controlStateWrapperFactory/SmartFilterBarWrapper.js +20 -1
  11. package/src/sap/suite/ui/generic/template/lib/AppComponent.js +1 -1
  12. package/src/sap/suite/ui/generic/template/lib/CRUDManager.js +0 -7
  13. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillAIOrchestrator.js +43 -12
  14. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillApplyHandler.js +67 -218
  15. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillDialogController.js +55 -8
  16. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillFieldCollector.js +214 -35
  17. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillFormRenderer.js +93 -18
  18. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillHandler.js +426 -48
  19. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillTableRenderer.js +512 -63
  20. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/fragments/EasyFillDialog.fragment.xml +33 -22
  21. package/src/sap/suite/ui/generic/template/lib/ai/EasyFilterBarHandler.js +90 -73
  22. package/src/sap/suite/ui/generic/template/lib/i18n/i18n.properties +24 -0
  23. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ar.properties +15 -7
  24. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_bg.properties +15 -7
  25. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ca.properties +15 -7
  26. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cnr.properties +15 -7
  27. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cs.properties +15 -7
  28. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cy.properties +15 -7
  29. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_da.properties +15 -7
  30. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_de.properties +19 -11
  31. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_el.properties +15 -7
  32. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en.properties +15 -7
  33. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en_GB.properties +15 -7
  34. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en_US_saprigi.properties +15 -7
  35. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_es.properties +15 -7
  36. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_es_MX.properties +15 -7
  37. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_et.properties +15 -7
  38. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fi.properties +15 -7
  39. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fr.properties +16 -8
  40. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fr_CA.properties +16 -8
  41. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hi.properties +15 -7
  42. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hr.properties +16 -8
  43. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hu.properties +15 -7
  44. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_id.properties +15 -7
  45. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_it.properties +15 -7
  46. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_iw.properties +15 -7
  47. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ja.properties +15 -7
  48. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_kk.properties +15 -7
  49. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ko.properties +15 -7
  50. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_lt.properties +15 -7
  51. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_lv.properties +15 -7
  52. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_mk.properties +15 -7
  53. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ms.properties +15 -7
  54. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_nl.properties +15 -7
  55. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_no.properties +15 -7
  56. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pl.properties +15 -7
  57. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pt.properties +15 -7
  58. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pt_PT.properties +15 -7
  59. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ro.properties +15 -7
  60. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ru.properties +15 -7
  61. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sh.properties +15 -7
  62. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sk.properties +15 -7
  63. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sl.properties +15 -7
  64. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sr.properties +15 -7
  65. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sv.properties +15 -7
  66. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_th.properties +15 -7
  67. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_tr.properties +15 -7
  68. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_uk.properties +15 -7
  69. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_vi.properties +15 -7
  70. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_zh_CN.properties +15 -7
  71. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_zh_TW.properties +15 -7
  72. package/src/sap/suite/ui/generic/template/lib/navigation/NavigationController.js +1 -1
  73. package/src/sap/suite/ui/generic/template/library.js +1 -1
  74. package/src/sap/suite/ui/generic/template/themes/base/ObjectPage.less +22 -1
@@ -1,5 +1,7 @@
1
1
  sap.ui.define([
2
2
  "sap/ui/comp/smartfield/SmartField",
3
+ "sap/m/GenericTag",
4
+ "sap/m/MessageStrip",
3
5
  "sap/m/Text",
4
6
  "sap/m/Title",
5
7
  "sap/m/Table",
@@ -7,69 +9,157 @@ sap.ui.define([
7
9
  "sap/m/ColumnListItem",
8
10
  "sap/m/VBox",
9
11
  "sap/m/HBox",
12
+ "sap/m/ScrollContainer",
10
13
  "sap/m/SegmentedButton",
11
14
  "sap/m/SegmentedButtonItem",
15
+ "sap/uxap/ObjectPageSection",
16
+ "sap/uxap/ObjectPageSubSection",
17
+ "sap/ui/core/CustomData",
12
18
  "sap/suite/ui/generic/template/genericUtilities/FeLogger",
13
19
  "sap/suite/ui/generic/template/genericUtilities/FeError"
14
- ], function(SmartField, Text, Title, Table, Column, ColumnListItem, VBox, HBox, SegmentedButton, SegmentedButtonItem, FeLogger, FeError) {
20
+ ], function(SmartField, GenericTag, MessageStrip, Text, Title, Table, Column, ColumnListItem, VBox, HBox, ScrollContainer, SegmentedButton, SegmentedButtonItem, ObjectPageSection, ObjectPageSubSection, CustomData, FeLogger, FeError) {
15
21
  "use strict";
16
22
 
17
23
  var sClassName = "lib.ai.EasyFill.EasyFillTableRenderer";
18
24
  var oLogger = new FeLogger(sClassName).getLogger();
19
25
  var iMaxPreviewRows = 20;
20
26
 
27
+ /** CustomData key used to identify dynamically-added table sections in the ObjectPageLayout. */
28
+ var sTableSectionMarker = "easyfillTableSection";
29
+
30
+ /** CustomData key used to identify the condensed-mode wrapper VBox in the Filled Fields SubSection. */
31
+ var sCondensedWrapperMarker = "easyfillCondensedWrapper";
32
+
21
33
  /**
22
34
  * EasyFillTableRenderer
23
35
  *
24
36
  * Responsible for rendering the SmartTable collection preview in the EasyFill dialog.
25
37
  * For each collection (navigation property) with LLM-suggested row updates, this module:
26
38
  *
27
- * 1. Creates transient OData row contexts for preview rendering.
39
+ * 1. Creates transient OData row contexts for preview rendering.
28
40
  * 2. Creates a "Previous Values" read-only table and a "New Values" editable SmartField table.
29
41
  * 3. Renders a SegmentedButton toggle between the two views.
30
- * 4. Provides reset logic to discard staged values if the user cancels.
42
+ * 4. Injects each collection as an ObjectPageSection into the shared EasyFillObjectPageLayout
43
+ * so the OPL header and anchor bar remain visible while scrolling through table content.
44
+ * 5. Provides reset logic to discard staged values if the user cancels.
31
45
  *
32
- * The table preview container (VBox#EasyFillTablePreviewContainer) in the dialog XML is populated
33
- * programmatically no table rows are declared in the fragment XML.
46
+ * Create mode:
47
+ * - Rows without _rowIndex or _contextPath are treated as new rows.
48
+ * - New rows are rendered as editable SmartField rows highlighted with "Information" row highlight.
49
+ * - Transient contexts for new rows are stored in oTableStagingState.mPendingNewRowContexts.
50
+ * - On confirm, EasyFillApplyHandler activates these contexts via submitChanges.
51
+ * - Only one new row per table is created (extra new rows are suppressed upstream).
34
52
  */
35
53
  return {
36
54
 
37
55
  /**
38
- * Renders collection (SmartTable) previews into the EasyFillTablePreviewContainer VBox.
39
- * For each collection with AI-suggested updates:
40
- * - Creates transient staging contexts for the preview rows
41
- * - Creates a Previous/New table pair with a toggle SegmentedButton
42
- * - Appends the section to the container
56
+ * Renders collection (SmartTable) previews as ObjectPageSections appended to the
57
+ * EasyFillObjectPageLayout. Each collection becomes its own anchored section so that
58
+ * the OPL header stays visible when the user edits table fields.
43
59
  *
44
60
  * @param {object} mCollectionUpdates - map of { navProperty: [ rowUpdate ] } from AI response
45
61
  * @param {object} mFieldMapping - full LLM field mapping (used to get item properties and descriptions)
46
62
  * @param {Array} aTableValidationErrors - validation errors from value-help checks
47
- * @param {object} oController - MVC controller (used to byId the container)
63
+ * @param {object} oController - MVC controller (used to byId the ObjectPageLayout)
48
64
  * @param {object} oRb - i18n resource bundle
49
- * @param {object} mCollectionState - collection tracking state { mCollectionRowContexts, mCollectionRowContextsByPath, mCollectionProperties }
50
- * @param {object} oTableStagingState - mutable table staging state { mStagedRows }
65
+ * @param {object} mCollectionState - collection tracking state { mCollectionRowContexts, mCollectionRowContextsByPath, mCollectionProperties, mCollectionAllowsCreation }
66
+ * @param {object} oTableStagingState - mutable table staging state { mStagedRows, mPendingNewRowContexts }
51
67
  * @param {object} oObjectPageModel - the OData model used to create transient staging contexts
52
68
  * @param {function} fnOnTableFieldChange - callback invoked when a preview table cell changes
69
+ * @param {boolean} [bReuseStaging=false] - when true, skip re-initializing transient contexts (preserves user edits on mode switch)
70
+ * @param {object} [mCollectionsWithRejectedNewRows] - map of { navProperty: "all" | "mixed" } where
71
+ * AI proposed new rows for a non-creatable table. "all" = only new rows proposed (no diff table shown);
72
+ * "mixed" = some existing-row updates plus rejected new rows.
53
73
  */
54
- renderCollectionPreviews: function(mCollectionUpdates, mFieldMapping, aTableValidationErrors, oController, oRb, mCollectionState, oTableStagingState, oObjectPageModel, fnOnTableFieldChange) {
55
- var oTablePreviewContainer = oController.byId("EasyFillTablePreviewContainer");
56
- if (!oTablePreviewContainer) {
57
- oLogger.warning("renderCollectionPreviews: EasyFillTablePreviewContainer not found");
74
+ renderCollectionPreviews: function(mCollectionUpdates, mFieldMapping, aTableValidationErrors, oController, oRb, mCollectionState, oTableStagingState, oObjectPageModel, fnOnTableFieldChange, bReuseStaging, mCollectionsWithRejectedNewRows) {
75
+ var oObjectPageLayout = oController.byId("EasyFillObjectPageLayout");
76
+ if (!oObjectPageLayout) {
77
+ oLogger.warning("renderCollectionPreviews: EasyFillObjectPageLayout not found");
58
78
  return;
59
79
  }
60
80
 
61
- // Remove all previously rendered table sections
62
- oTablePreviewContainer.destroyItems();
81
+ // Remove previously injected table content before re-rendering.
82
+ // On mode switch (bReuseStaging=true) cleanup is done BEFORE the model update
83
+ // in onSelectionChange, so we skip it here to avoid operating on the already-fresh OPL.
84
+ // On first render (bReuseStaging=false) we do the cleanup here.
85
+ if (!bReuseStaging) {
86
+ this._removeTableSections(oObjectPageLayout);
87
+ }
88
+
89
+ if (!bReuseStaging) {
90
+ this._initializeStagedCollectionRows(
91
+ mCollectionUpdates,
92
+ mFieldMapping,
93
+ mCollectionState,
94
+ oTableStagingState,
95
+ oObjectPageModel
96
+ );
97
+ }
98
+
99
+ // Determine mode from the dialog model.
100
+ var oDialogModel = oController.getView().getModel("easyFillDialogModel");
101
+ var sMode = oDialogModel ? oDialogModel.getProperty("/easyFillMode") : "";
102
+ var bIsCondensedMode = sMode === "CondensedMode";
103
+
104
+ // Find the target sections by inspecting the binding context of each live OPL section.
105
+ // This is the only reliable approach: model-index ↔ getSections()-index mapping is
106
+ // broken whenever manually-added sections (OP table sections) are still present in the
107
+ // aggregation alongside newly-created bound sections.
108
+ // Bound sections always have a "easyFillDialogModel" binding context; manually-added
109
+ // ones never do, so there is no ambiguity.
110
+ var oFilledFieldsSubSection = null; // condensed mode: add table as block inside here
111
+ var oUnfilledFieldsSection = null; // OP mode: insert standalone section before this
112
+ oObjectPageLayout.getSections().forEach(function(oSection) {
113
+ var oCtx = oSection.getBindingContext("easyFillDialogModel");
114
+ if (!oCtx) { return; }
115
+ var sTitle = oCtx.getProperty("title");
116
+ if (bIsCondensedMode && sTitle === "{@i18n>EASY_FILL_FILLED_FIELDS}") {
117
+ oFilledFieldsSubSection = oSection.getSubSections()[0] || null;
118
+ }
119
+ // Keep a handle to the Unfilled Fields section in both modes.
120
+ // In condensed mode with only non-updatable fields, there is no
121
+ // Filled Fields section; table previews are rendered as standalone
122
+ // sections and must still be inserted BEFORE Unfilled Fields.
123
+ if (sTitle === "{@i18n>EASY_FILL_UNFILLED_FIELDS}") {
124
+ oUnfilledFieldsSection = oSection;
125
+ }
126
+ });
63
127
 
64
- this._initializeStagedCollectionRows(
65
- mCollectionUpdates,
66
- mFieldMapping,
67
- mCollectionState,
68
- oTableStagingState,
69
- oObjectPageModel
70
- );
128
+ // iInsertOffset keeps multiple consecutive tables in the correct order when
129
+ // inserting standalone sections before the Unfilled Fields section (OP mode).
130
+ var iInsertOffset = 0;
71
131
 
72
132
  var that = this;
133
+ var oRejected = mCollectionsWithRejectedNewRows || {};
134
+
135
+ // Render "all rejected" tables: AI only proposed new rows, creation not allowed.
136
+ // These collections have no entry in mCollectionUpdates so they need their own loop.
137
+ Object.keys(oRejected).forEach(function(sCollectionName) {
138
+ if (oRejected[sCollectionName] !== "all") {
139
+ return; // "mixed" is handled below alongside the diff table
140
+ }
141
+ var oCollectionMeta = mFieldMapping[sCollectionName];
142
+ var sTitle = (oCollectionMeta && oCollectionMeta.description) || sCollectionName;
143
+ var oStrip = new MessageStrip({
144
+ text: oRb.getText("EASY_FILL_TABLE_NEW_ROW_NOT_ALLOWED"),
145
+ type: "Warning",
146
+ showIcon: true
147
+ });
148
+ var oNotifVBox = new VBox({ items: [oStrip] });
149
+ oNotifVBox.addCustomData(new CustomData({ key: sTableSectionMarker, value: "true" }));
150
+ var oNotifSubSection = new ObjectPageSubSection({ title: sTitle, titleUppercase: false, blocks: [oNotifVBox] });
151
+ var oNotifSection = new ObjectPageSection({ title: sTitle, titleUppercase: false, subSections: [oNotifSubSection] });
152
+ oNotifSection.addCustomData(new CustomData({ key: sTableSectionMarker, value: "true" }));
153
+ if (oUnfilledFieldsSection) {
154
+ var iRejectedAt = oObjectPageLayout.indexOfSection(oUnfilledFieldsSection);
155
+ oObjectPageLayout.insertSection(oNotifSection, iRejectedAt + iInsertOffset);
156
+ iInsertOffset++;
157
+ } else {
158
+ oObjectPageLayout.addSection(oNotifSection);
159
+ }
160
+ oLogger.debug("renderCollectionPreviews: rejected-all new rows for '" + sCollectionName + "'");
161
+ });
162
+
73
163
  Object.keys(mCollectionUpdates).forEach(function(sCollectionName) {
74
164
  var oCollectionMeta = mFieldMapping[sCollectionName];
75
165
  if (!oCollectionMeta || !oCollectionMeta.itemProperties) {
@@ -83,12 +173,22 @@ sap.ui.define([
83
173
  var aPreviewRows = (oTableStagingState.mStagedRows[sCollectionName] || []).filter(function(oStagedRow) {
84
174
  return oStagedRow.isPreviewVisible;
85
175
  });
86
-
87
- if (!aPreviewRows.length) {
176
+ var aNewRows = oTableStagingState.mPendingNewRowContexts[sCollectionName] || [];
177
+ var bHasUneditableChanges = aPreviewRows.some(function(oPreviewRow) {
178
+ return oPreviewRow.update && that._hasUneditableCollectionChanges(oPreviewRow.update, sCollectionName, {
179
+ context: oPreviewRow.realContext,
180
+ rowIndex: oPreviewRow.rowIndex
181
+ }, mCollectionState);
182
+ });
183
+ if (!aPreviewRows.length && !aNewRows.length) {
88
184
  return;
89
185
  }
90
186
 
91
- // Previous values table (read-only, initially hidden)
187
+ var iExistingRowCount = aPreviewRows.length;
188
+ var iNewRowCount = aNewRows.length;
189
+ var iTotalCount = iExistingRowCount + iNewRowCount;
190
+
191
+ // Previous values table (read-only, initially hidden) — only existing rows
92
192
  var aPreviousRows = aPreviewRows.map(function(oPreviewRow) {
93
193
  return that._buildCollectionPreviewRowFromContext(aPropertyNames, oPreviewRow.realContext);
94
194
  });
@@ -96,20 +196,26 @@ sap.ui.define([
96
196
  oPreviousTable.setVisible(false);
97
197
 
98
198
  // New values table (editable SmartFields, initially visible)
199
+ // Includes both updated existing rows and new rows
99
200
  var oNewTable = that._createEditableNewTable(
100
201
  aPropertyNames,
101
202
  oCollectionMeta.itemProperties,
102
203
  aPreviewRows,
204
+ aNewRows,
103
205
  sCollectionName,
104
206
  mCollectionState,
105
207
  fnOnTableFieldChange
106
208
  );
107
209
 
108
- // Section title showing collection label and row count
109
- var oSectionTitle = new Title({
110
- level: "H5",
111
- text: (oCollectionMeta.description || sCollectionName) + " (" + aPreviewRows.length + ")"
112
- });
210
+ var oReadonlyTag = bHasUneditableChanges
211
+ ? new GenericTag({
212
+ text: oRb.getText("EASYFILL_TABLE_READONLY_TAG"),
213
+ status: "Warning"
214
+ }).addStyleClass("sapUiTinyMarginBegin")
215
+ : null;
216
+
217
+ var sCollectionName_label = oCollectionMeta.description || sCollectionName;
218
+ var oTitleControl = new Title({ text: sCollectionName_label + " (" + iTotalCount + ")", titleStyle: "H4" });
113
219
 
114
220
  // Toggle button between "Previous" and "New" views
115
221
  var oSegmentedButton = new SegmentedButton({
@@ -123,27 +229,115 @@ sap.ui.define([
123
229
  var sKey = oSegmentedButton.getSelectedKey();
124
230
  oPreviousTable.setVisible(sKey === "previous");
125
231
  oNewTable.setVisible(sKey === "new");
232
+ if (oReadonlyTag) {
233
+ oReadonlyTag.setVisible(sKey === "new");
234
+ }
235
+ oTitleControl.setText(sCollectionName_label + " (" + (sKey === "previous" ? iExistingRowCount : iTotalCount) + ")");
126
236
  }
127
237
  });
128
-
238
+ var aRightItems = oReadonlyTag ? [oReadonlyTag, oSegmentedButton] : [oSegmentedButton];
129
239
  var oHeader = new HBox({
130
240
  justifyContent: "SpaceBetween",
131
- items: [oSectionTitle, oSegmentedButton]
132
- }).addStyleClass("sapUiSmallMarginTop");
241
+ alignItems: "Center",
242
+ wrap: "Wrap",
243
+ items: [oTitleControl].concat(aRightItems)
244
+ }).addStyleClass("sapUiSmallMarginTop sapUiSmallMarginBottom");
133
245
 
134
- var oSection = new VBox({
246
+ var oTableVBox = new VBox({
135
247
  items: [oHeader, oPreviousTable, oNewTable]
136
248
  }).addStyleClass("sapSmartTemplatesObjectPageEasyFillTableSection");
137
249
 
138
- oTablePreviewContainer.addItem(oSection);
139
- oLogger.debug("renderCollectionPreviews: rendered collection '" + sCollectionName + "' with " + aPreviewRows.length + " rows");
250
+ // "mixed" rejection: AI also proposed new rows but creation is not allowed.
251
+ // Show a warning strip after the toggle header, before the tables.
252
+ if (oRejected[sCollectionName] === "mixed") {
253
+ var oRejectionStrip = new MessageStrip({
254
+ text: oRb.getText("EASY_FILL_TABLE_NEW_ROW_NOT_ALLOWED"),
255
+ type: "Warning",
256
+ showIcon: true
257
+ });
258
+ oTableVBox.insertItem(oRejectionStrip, 1); // after toggle header, before tables
259
+ }
260
+
261
+ if (bIsCondensedMode && oFilledFieldsSubSection) {
262
+ // ── Condensed mode ────────────────────────────────────────────────────────
263
+ // Wrap ALL current SubSection blocks together with the table VBox in a
264
+ // single outer VBox, then make that the only block in the SubSection.
265
+ // BlockLayout always renders a single block at 100% width, so this prevents
266
+ // the table from ever appearing side-by-side with the SmartForm regardless
267
+ // of how wide the dialog is resized.
268
+ oTableVBox.addCustomData(new CustomData({ key: sTableSectionMarker, value: "true" }));
269
+
270
+ // Check if a wrapper VBox already exists (multiple collections: re-use it)
271
+ var aExistingBlocks = oFilledFieldsSubSection.getBlocks();
272
+ var oExistingWrapper = aExistingBlocks.find(function(oBlock) {
273
+ return oBlock.getCustomData && oBlock.getCustomData().some(function(oData) {
274
+ return oData.getKey() === sCondensedWrapperMarker;
275
+ });
276
+ });
277
+
278
+ if (oExistingWrapper) {
279
+ // Append the new table VBox to the already-created wrapper
280
+ oExistingWrapper.addItem(oTableVBox);
281
+ } else {
282
+ // First table: pull all current blocks out of the SubSection,
283
+ // place them inside a new wrapper VBox alongside the table VBox,
284
+ // and add the wrapper as the sole block.
285
+ var aOriginalBlocks = aExistingBlocks.slice();
286
+ aOriginalBlocks.forEach(function(oBlock) {
287
+ oFilledFieldsSubSection.removeBlock(oBlock);
288
+ });
289
+ var oWrapper = new VBox({
290
+ width: "100%",
291
+ items: aOriginalBlocks.concat([oTableVBox])
292
+ });
293
+ oWrapper.addCustomData(new CustomData({ key: sCondensedWrapperMarker, value: "true" }));
294
+ oFilledFieldsSubSection.addBlock(oWrapper);
295
+ }
296
+ } else {
297
+ // ── OP mode ───────────────────────────────────────────────────────────────
298
+ // Wrap in a standalone ObjectPageSection so the anchor bar shows the table
299
+ // name. Insert just before the Unfilled Fields section (if present) so the
300
+ // order is: annotation sections → table sections → Unfilled Fields.
301
+ var oSubSection = new ObjectPageSubSection({
302
+ title: sCollectionName_label,
303
+ titleUppercase: false,
304
+ showTitle: false,
305
+ blocks: [oTableVBox]
306
+ });
307
+ var oOpSection = new ObjectPageSection({
308
+ title: sCollectionName_label,
309
+ titleUppercase: false,
310
+ subSections: [oSubSection]
311
+ });
312
+ oOpSection.addCustomData(new CustomData({ key: sTableSectionMarker, value: "true" }));
313
+
314
+ if (oUnfilledFieldsSection) {
315
+ // Use indexOfSection on the live control — this is the actual aggregation
316
+ // index regardless of how many manually-added sections are mixed in.
317
+ var iInsertAt = oObjectPageLayout.indexOfSection(oUnfilledFieldsSection);
318
+ oObjectPageLayout.insertSection(oOpSection, iInsertAt + iInsertOffset);
319
+ iInsertOffset++;
320
+ } else {
321
+ oObjectPageLayout.addSection(oOpSection);
322
+ }
323
+ }
324
+ oLogger.debug("renderCollectionPreviews: rendered collection '" + sCollectionName + "' with " +
325
+ iExistingRowCount + " existing row(s) and " + iNewRowCount + " new row(s)");
140
326
  });
141
327
  },
142
328
 
329
+ /**
330
+ * Public alias for _removeTableSections, called from EasyFillHandler before model update
331
+ * on mode switch so the OPL aggregation is clean when the sections binding recreates controls.
332
+ */
333
+ removeTableSections: function(oObjectPageLayout) {
334
+ this._removeTableSections(oObjectPageLayout);
335
+ },
336
+
143
337
  /**
144
338
  * Resets and clears all transient collection contexts created for table preview staging.
145
339
  *
146
- * @param {object} oTableStagingState - table staging state { mStagedRows }
340
+ * @param {object} oTableStagingState - table staging state { mStagedRows, mPendingNewRowContexts }
147
341
  * @param {object} oObjectPageModel - the OData model
148
342
  */
149
343
  resetStagedCollectionContexts: function(oTableStagingState, oObjectPageModel) {
@@ -159,6 +353,16 @@ sap.ui.define([
159
353
  });
160
354
  });
161
355
 
356
+ // Also reset pending new-row contexts (these use the same deferred group)
357
+ Object.keys((oTableStagingState && oTableStagingState.mPendingNewRowContexts) || {}).forEach(function(sCollectionName) {
358
+ (oTableStagingState.mPendingNewRowContexts[sCollectionName] || []).forEach(function(oNewRowContext) {
359
+ var sPath = oNewRowContext && oNewRowContext.getPath && oNewRowContext.getPath();
360
+ if (sPath) {
361
+ aTransientPaths.push(sPath);
362
+ }
363
+ });
364
+ });
365
+
162
366
  if (aTransientPaths.length && oObjectPageModel) {
163
367
  oObjectPageModel.resetChanges(aTransientPaths, false, true);
164
368
  }
@@ -169,6 +373,52 @@ sap.ui.define([
169
373
 
170
374
  // ─── Private Helpers ─────────────────────────────────────────────────────
171
375
 
376
+ /**
377
+ * Removes all ObjectPageSections that were dynamically added by renderCollectionPreviews.
378
+ * Identified by the custom data marker key set during creation.
379
+ *
380
+ * @param {sap.uxap.ObjectPageLayout} oObjectPageLayout
381
+ */
382
+ _removeTableSections: function(oObjectPageLayout) {
383
+ // OP mode: remove marked standalone ObjectPageSections.
384
+ oObjectPageLayout.getSections().filter(function(oSection) {
385
+ return oSection.getCustomData().some(function(oData) {
386
+ return oData.getKey() === sTableSectionMarker;
387
+ });
388
+ }).forEach(function(oSection) {
389
+ oObjectPageLayout.removeSection(oSection);
390
+ oSection.destroy();
391
+ });
392
+
393
+ // Condensed mode: find and unwrap the wrapper VBox, restoring the original
394
+ // SmartForm block(s) directly into their SubSection, then destroy the wrapper.
395
+ oObjectPageLayout.getSections().forEach(function(oSection) {
396
+ oSection.getSubSections().forEach(function(oSubSec) {
397
+ oSubSec.getBlocks().filter(function(oBlock) {
398
+ return oBlock.getCustomData && oBlock.getCustomData().some(function(oData) {
399
+ return oData.getKey() === sCondensedWrapperMarker;
400
+ });
401
+ }).forEach(function(oWrapper) {
402
+ // Separate original (non-table) items from table VBox items
403
+ var aItems = oWrapper.getItems().slice();
404
+ aItems.forEach(function(oItem) {
405
+ var bIsTableVBox = oItem.getCustomData && oItem.getCustomData().some(function(oData) {
406
+ return oData.getKey() === sTableSectionMarker;
407
+ });
408
+ oWrapper.removeItem(oItem);
409
+ if (bIsTableVBox) {
410
+ oItem.destroy();
411
+ } else {
412
+ oSubSec.addBlock(oItem); // restore SmartForm
413
+ }
414
+ });
415
+ oSubSec.removeBlock(oWrapper);
416
+ oWrapper.destroy();
417
+ });
418
+ });
419
+ });
420
+ },
421
+
172
422
  _initializeStagedCollectionRows: function(mCollectionUpdates, mFieldMapping, mCollectionState, oTableStagingState, oObjectPageModel) {
173
423
  var that = this;
174
424
  this._clearStagedCollectionState(oTableStagingState);
@@ -178,14 +428,27 @@ sap.ui.define([
178
428
  var aRowUpdates = mCollectionUpdates[sCollectionName] || [];
179
429
  var aCollectionRowContexts = mCollectionState.mCollectionRowContexts[sCollectionName] || [];
180
430
  var mCollectionPropertyMap = mCollectionState.mCollectionProperties[sCollectionName] || Object.create(null);
431
+ var oEntitySet = mCollectionState.mCollectionEntitySets && mCollectionState.mCollectionEntitySets[sCollectionName];
181
432
  var aPropertyNames = Object.keys((oCollectionMeta && oCollectionMeta.itemProperties) || {});
433
+ var bAllowsCreation = !!(mCollectionState.mCollectionAllowsCreation &&
434
+ mCollectionState.mCollectionAllowsCreation[sCollectionName]);
182
435
 
183
436
  if (!oCollectionMeta || !oCollectionMeta.entitySet || !aPropertyNames.length) {
184
437
  return;
185
438
  }
186
439
 
440
+ // Separate existing-row updates from new-row creates
441
+ var aExistingRowUpdates = aRowUpdates.filter(function(oRow) {
442
+ return typeof oRow._contextPath === "string" || typeof oRow._rowIndex === "number";
443
+ });
444
+ var aNewRowData = bAllowsCreation
445
+ ? aRowUpdates.filter(function(oRow) {
446
+ return typeof oRow._contextPath !== "string" && typeof oRow._rowIndex !== "number";
447
+ })
448
+ : [];
449
+
187
450
  var mUpdatesByIdentity = Object.create(null);
188
- aRowUpdates.forEach(function(oRowUpdate, iUpdateIndex) {
451
+ aExistingRowUpdates.forEach(function(oRowUpdate, iUpdateIndex) {
189
452
  var oResolvedUpdate = that._resolveCollectionRowContext(sCollectionName, oRowUpdate, mCollectionState);
190
453
  mUpdatesByIdentity[that._getCollectionRowIdentity(oResolvedUpdate, oRowUpdate, iUpdateIndex)] = oRowUpdate;
191
454
  });
@@ -209,28 +472,53 @@ sap.ui.define([
209
472
 
210
473
  var oRealRowObject = oRealContext.getObject && oRealContext.getObject();
211
474
  that._seedTransientContextFromRowObject(oTransientContext, oRealRowObject);
212
- that._applyRowUpdateToTransientContext(oTransientContext, oRowUpdate, mCollectionPropertyMap, oRealRowObject);
475
+ that._applyRowUpdateToTransientContext(oTransientContext, oRowUpdate, mCollectionPropertyMap, oRealRowObject, oEntitySet);
213
476
 
214
477
  oTableStagingState.mStagedRows[sCollectionName].push({
215
478
  realContext: oRealContext,
216
479
  transientContext: oTransientContext,
480
+ update: oRowUpdate,
217
481
  rowIndex: iRowIndex,
218
482
  propertyNames: aPropertyNames,
219
483
  isPreviewVisible: bIsPreviewVisible
220
484
  });
221
485
  });
486
+
487
+ // Create and seed transient contexts for new rows
488
+ oTableStagingState.mPendingNewRowContexts[sCollectionName] = [];
489
+ aNewRowData.forEach(function(oNewRow) {
490
+ var oNewContext = that._createTransientCollectionContext(oCollectionMeta.entitySet, oObjectPageModel);
491
+ if (!oNewContext) {
492
+ return;
493
+ }
494
+ // Seed only editable (non-key, non-computed) properties from the AI data
495
+ Object.keys(oNewRow).forEach(function(sPropName) {
496
+ if (sPropName === "_rowIndex" || sPropName === "_contextPath") {
497
+ return;
498
+ }
499
+ var oPropertyMeta = mCollectionPropertyMap[sPropName];
500
+ if (oPropertyMeta && !that._isKeyProperty(oPropertyMeta) &&
501
+ typeof oNewRow[sPropName] !== "object") {
502
+ oNewContext.setProperty(sPropName, oNewRow[sPropName]);
503
+ }
504
+ });
505
+ oTableStagingState.mPendingNewRowContexts[sCollectionName].push(oNewContext);
506
+ });
222
507
  });
223
508
  },
224
509
 
225
510
  /**
226
511
  * Creates a read-only sap.m.Table for displaying table row data (used for "Previous Values" view).
227
512
  * All cells are rendered as plain Text controls.
513
+ * The table is wrapped in a sap.m.ScrollContainer to enable horizontal scrolling when the
514
+ * dialog is narrower than the table content (autoPopinMode is disabled to prevent columns
515
+ * from collapsing instead of scrolling).
228
516
  *
229
517
  * @param {Array<string>} aPropertyNames - ordered list of property names to show as columns
230
518
  * @param {object} oItemProperties - map of { propName: { description, dataType } }
231
519
  * @param {Array<object>} aRows - array of plain row value objects
232
520
  * @param {function} [fnCellFormatter] - optional custom cell formatter (oRow, sPropName, sRawValue) → string
233
- * @returns {sap.m.Table}
521
+ * @returns {sap.m.ScrollContainer} ScrollContainer wrapping the table with horizontal scrolling enabled
234
522
  */
235
523
  _createPreviewTable: function(aPropertyNames, oItemProperties, aRows, fnCellFormatter) {
236
524
  var aColumns = aPropertyNames.map(function(sPropertyName) {
@@ -252,27 +540,40 @@ sap.ui.define([
252
540
  });
253
541
  }.bind(this));
254
542
 
255
- return new Table({
543
+ var oTable = new Table({
256
544
  columns: aColumns,
257
545
  items: aItems,
258
546
  fixedLayout: false,
259
- autoPopinMode: true
547
+ autoPopinMode: false
260
548
  }).addStyleClass("sapSmartTemplatesObjectPageEasyFillTablePreview");
549
+
550
+ return new ScrollContainer({
551
+ horizontal: true,
552
+ vertical: false,
553
+ width: "100%",
554
+ content: [oTable]
555
+ });
261
556
  },
262
557
 
263
558
  /**
264
559
  * Creates an editable sap.m.Table for displaying AI-suggested row updates.
265
560
  * Editable cells are rendered as SmartField controls bound to the OData row context.
266
561
  * Non-editable cells are rendered as plain Text controls.
562
+ * New rows (proposed by the AI for creation) are highlighted with "Information" row color.
563
+ * The table is wrapped in a sap.m.ScrollContainer to enable horizontal scrolling when the
564
+ * dialog is narrower than the table content (autoPopinMode is disabled to prevent columns
565
+ * from collapsing instead of scrolling).
267
566
  *
268
567
  * @param {Array<string>} aPropertyNames - ordered list of property names to show as columns
269
568
  * @param {object} oItemProperties - map of { propName: { description, dataType } }
270
- * @param {Array<object>} aPreviewRows - enriched row objects with context, newValues, etc.
569
+ * @param {Array<object>} aPreviewRows - enriched row objects with context, newValues, etc. (existing rows)
570
+ * @param {Array<object>} aNewRowContexts - transient OData contexts for AI-proposed new rows
271
571
  * @param {string} sCollectionName - navigation property name (used to look up property metadata)
272
572
  * @param {object} mCollectionState - collection tracking state (used to get property metadata)
273
- * @returns {sap.m.Table}
573
+ * @param {function} fnOnTableFieldChange - callback when a cell value changes
574
+ * @returns {sap.m.ScrollContainer} ScrollContainer wrapping the table with horizontal scrolling enabled
274
575
  */
275
- _createEditableNewTable: function(aPropertyNames, oItemProperties, aPreviewRows, sCollectionName, mCollectionState, fnOnTableFieldChange) {
576
+ _createEditableNewTable: function(aPropertyNames, oItemProperties, aPreviewRows, aNewRowContexts, sCollectionName, mCollectionState, fnOnTableFieldChange) {
276
577
  var aColumns = aPropertyNames.map(function(sPropName) {
277
578
  return new Column({
278
579
  header: new Text({
@@ -282,9 +583,11 @@ sap.ui.define([
282
583
  });
283
584
 
284
585
  var mCollectionPropertyMap = mCollectionState.mCollectionProperties[sCollectionName] || Object.create(null);
586
+ var oEntitySet = mCollectionState.mCollectionEntitySets && mCollectionState.mCollectionEntitySets[sCollectionName];
285
587
  var that = this;
286
588
 
287
- var aItems = aPreviewRows.map(function(oPreviewRow) {
589
+ // Rows for existing rows with AI-proposed updates
590
+ var aExistingItems = aPreviewRows.map(function(oPreviewRow) {
288
591
  var oContext = oPreviewRow.transientContext;
289
592
  var oRowObject = oPreviewRow.realContext && oPreviewRow.realContext.getObject && oPreviewRow.realContext.getObject();
290
593
 
@@ -294,9 +597,12 @@ sap.ui.define([
294
597
  }
295
598
 
296
599
  var oPropertyMeta = mCollectionPropertyMap[sPropName];
297
- var bEditable = that._isEditableProperty(oPropertyMeta, oRowObject);
298
-
299
- // Use a SmartField so value-help and type-specific rendering are supported
600
+ var bEditable = that._isEditableProperty(oPropertyMeta, oRowObject, oEntitySet);
601
+ var bHasUneditableAiProp = !!(
602
+ oPreviewRow.update &&
603
+ Object.prototype.hasOwnProperty.call(oPreviewRow.update, sPropName) &&
604
+ !bEditable
605
+ );
300
606
  var oSmartField = new SmartField({
301
607
  value: "{" + sPropName + "}",
302
608
  editable: bEditable,
@@ -308,13 +614,16 @@ sap.ui.define([
308
614
  }
309
615
  },
310
616
  editableChanged: function(oEvent) {
311
- // Keep table SmartField editability deterministic during preview
312
617
  if (oEvent.getParameter("editable") !== bEditable) {
313
618
  oEvent.getSource().setEditable(bEditable);
314
619
  }
315
620
  }
316
621
  });
317
622
 
623
+ if (bHasUneditableAiProp) {
624
+ that._markSmartFieldAsWarning(oSmartField);
625
+ }
626
+
318
627
  return oSmartField;
319
628
  });
320
629
 
@@ -325,12 +634,61 @@ sap.ui.define([
325
634
  return oItem;
326
635
  });
327
636
 
328
- return new Table({
637
+ // Rows for AI-proposed new entries — all non-key properties are editable
638
+ // sEntitySetName is set explicitly on SmartField so it can resolve OData metadata
639
+ // even though it lives inside a plain sap.m.Table (not a SmartTable).
640
+ var sEntitySetName = (oEntitySet && oEntitySet.name) || sCollectionName;
641
+ var aNewItems = (aNewRowContexts || []).map(function(oNewContext) {
642
+ var aCells = aPropertyNames.map(function(sPropName) {
643
+ if (!oNewContext) {
644
+ return new Text({ text: "" });
645
+ }
646
+ var oPropertyMeta = mCollectionPropertyMap[sPropName];
647
+ // On a new row: key fields and computed fields are not user-editable;
648
+ // all other fields are editable so the user can review and adjust.
649
+ var bEditable = oPropertyMeta
650
+ ? !that._isKeyProperty(oPropertyMeta) && that._isEditableProperty(oPropertyMeta, {}, oEntitySet)
651
+ : false;
652
+ var oSmartField = new SmartField({
653
+ value: "{" + sPropName + "}",
654
+ entitySet: sEntitySetName,
655
+ editable: bEditable,
656
+ enabled: bEditable,
657
+ contextEditable: bEditable,
658
+ change: function() {
659
+ if (typeof fnOnTableFieldChange === "function") {
660
+ fnOnTableFieldChange();
661
+ }
662
+ },
663
+ editableChanged: function(oEvent) {
664
+ if (oEvent.getParameter("editable") !== bEditable) {
665
+ oEvent.getSource().setEditable(bEditable);
666
+ }
667
+ }
668
+ });
669
+ return oSmartField;
670
+ });
671
+ var oItem = new ColumnListItem({
672
+ cells: aCells,
673
+ highlight: "Information"
674
+ });
675
+ oItem.setBindingContext(oNewContext);
676
+ return oItem;
677
+ });
678
+
679
+ var oTable = new Table({
329
680
  columns: aColumns,
330
- items: aItems,
681
+ items: aExistingItems.concat(aNewItems),
331
682
  fixedLayout: false,
332
- autoPopinMode: true
683
+ autoPopinMode: false
333
684
  }).addStyleClass("sapSmartTemplatesObjectPageEasyFillTablePreview");
685
+
686
+ return new ScrollContainer({
687
+ horizontal: true,
688
+ vertical: false,
689
+ width: "100%",
690
+ content: [oTable]
691
+ });
334
692
  },
335
693
 
336
694
  /**
@@ -370,7 +728,7 @@ sap.ui.define([
370
728
  });
371
729
  },
372
730
 
373
- _applyRowUpdateToTransientContext: function(oTransientContext, oRowUpdate, mCollectionPropertyMap, oRealRowObject) {
731
+ _applyRowUpdateToTransientContext: function(oTransientContext, oRowUpdate, mCollectionPropertyMap, oRealRowObject, oEntitySet) {
374
732
  var that = this;
375
733
  Object.keys(oRowUpdate || {}).forEach(function(sPropertyName) {
376
734
  if (sPropertyName === "_rowIndex" || sPropertyName === "_contextPath") {
@@ -378,7 +736,7 @@ sap.ui.define([
378
736
  }
379
737
 
380
738
  var oPropertyMeta = mCollectionPropertyMap[sPropertyName];
381
- if (!that._isEditableProperty(oPropertyMeta, oRealRowObject)) {
739
+ if (!that._isEditableProperty(oPropertyMeta, oRealRowObject, oEntitySet)) {
382
740
  return;
383
741
  }
384
742
 
@@ -390,6 +748,9 @@ sap.ui.define([
390
748
  Object.keys((oTableStagingState && oTableStagingState.mStagedRows) || {}).forEach(function(sCollectionName) {
391
749
  delete oTableStagingState.mStagedRows[sCollectionName];
392
750
  });
751
+ Object.keys((oTableStagingState && oTableStagingState.mPendingNewRowContexts) || {}).forEach(function(sCollectionName) {
752
+ delete oTableStagingState.mPendingNewRowContexts[sCollectionName];
753
+ });
393
754
  },
394
755
 
395
756
  /**
@@ -459,6 +820,45 @@ sap.ui.define([
459
820
  return String(vValue);
460
821
  },
461
822
 
823
+ _markSmartFieldAsWarning: function(oSmartField) {
824
+ if (!oSmartField) {
825
+ return;
826
+ }
827
+ /*Adding the style class 'sapMInputBaseContentWrapper' to make the uneditable field,
828
+ in the table appear visually like an input field */
829
+ oSmartField.addStyleClass("sapUiEasyFillWarningCell sapMInputBaseContentWrapper");
830
+ },
831
+
832
+ /**
833
+ * Checks whether the AI proposed at least one change for the row that cannot be applied
834
+ * because the target property is not editable in the current row context.
835
+ *
836
+ * @param {object} oRowUpdate - AI row update object
837
+ * @param {string} sCollectionName - navigation property name
838
+ * @param {object} oResolvedRow - resolved row context metadata
839
+ * @param {object} mCollectionState - collection tracking state
840
+ * @returns {boolean}
841
+ */
842
+ _hasUneditableCollectionChanges: function(oRowUpdate, sCollectionName, oResolvedRow, mCollectionState) {
843
+ if (!oRowUpdate) {
844
+ return false;
845
+ }
846
+
847
+ var mCollectionPropertyMap = mCollectionState.mCollectionProperties[sCollectionName] || Object.create(null);
848
+ var oEntitySet = mCollectionState.mCollectionEntitySets && mCollectionState.mCollectionEntitySets[sCollectionName];
849
+ var oContext = oResolvedRow && oResolvedRow.context;
850
+ var oRowObject = oContext && oContext.getObject && oContext.getObject();
851
+ var that = this;
852
+
853
+ return Object.keys(oRowUpdate).some(function(sPropName) {
854
+ if (sPropName === "_rowIndex" || sPropName === "_contextPath") {
855
+ return false;
856
+ }
857
+
858
+ return !that._isEditableProperty(mCollectionPropertyMap[sPropName], oRowObject, oEntitySet);
859
+ });
860
+ },
861
+
462
862
  /**
463
863
  * Checks whether a collection property is editable given the current row data object.
464
864
  * Delegates to the same annotation checks used in EasyFillFieldCollector.isEditableByContextObject.
@@ -468,10 +868,27 @@ sap.ui.define([
468
868
  * @param {object} oRowObject - plain row data object
469
869
  * @returns {boolean}
470
870
  */
471
- _isEditableProperty: function(oPropertyMeta, oRowObject) {
871
+ _isEditableProperty: function(oPropertyMeta, oRowObject, oEntitySet) {
472
872
  if (!oPropertyMeta) {
473
873
  return false;
474
874
  }
875
+
876
+ // Check entity-set-level sap:updatable (entire entity set is read-only)
877
+ if (oEntitySet) {
878
+ var sEntitySetUpdatable = oEntitySet["sap:updatable"];
879
+ if (sEntitySetUpdatable === "false") {
880
+ return false;
881
+ }
882
+ // sap:updatable-path: resolved against the row context object (boolean property)
883
+ var sEntitySetUpdatablePath = oEntitySet["sap:updatable-path"];
884
+ if (sEntitySetUpdatablePath && oRowObject) {
885
+ var bUpdatableFromPath = oRowObject[sEntitySetUpdatablePath];
886
+ if (bUpdatableFromPath === false || bUpdatableFromPath === "false") {
887
+ return false;
888
+ }
889
+ }
890
+ }
891
+
475
892
  var sUpdatable = oPropertyMeta["sap:updatable"];
476
893
  if (sUpdatable) {
477
894
  return sUpdatable !== "false";
@@ -480,12 +897,44 @@ sap.ui.define([
480
897
  if (oAnnotation && oAnnotation["EnumMember"]) {
481
898
  return oAnnotation["EnumMember"] !== "com.sap.vocabularies.Common.v1.FieldControlType/ReadOnly";
482
899
  }
900
+ // Dynamic FieldControl via Path (e.g. { "Path": "SomeField_fc" })
901
+ // FieldControl values: 0=Hidden, 1=ReadOnly, 3=Optional, 7=Mandatory
902
+ if (oAnnotation && oAnnotation["Path"]) {
903
+ var iDynamicFieldControl = oRowObject && oRowObject[oAnnotation["Path"]];
904
+ if (iDynamicFieldControl !== undefined && iDynamicFieldControl !== null) {
905
+ return iDynamicFieldControl !== 0 && iDynamicFieldControl !== 1;
906
+ }
907
+ }
908
+ // Check "sap:field-control" dynamic path (value 1 = read-only)
909
+ // If the _fc property is defined in metadata but its runtime value is absent from
910
+ // the row object (not fetched / not in $select), treat as non-editable (safe fallback).
483
911
  var sFieldControl = oPropertyMeta["sap:field-control"];
484
912
  if (sFieldControl) {
485
- var iFieldControlValue = oRowObject && oRowObject[sFieldControl];
486
- return iFieldControlValue !== 1;
913
+ if (!oRowObject || !(sFieldControl in oRowObject)) {
914
+ return false;
915
+ }
916
+ var iFieldControlValue = oRowObject[sFieldControl];
917
+ return iFieldControlValue !== 0 && iFieldControlValue !== 1;
487
918
  }
488
919
  return true;
920
+ },
921
+
922
+ /**
923
+ * Returns true if an OData property is a key field (sap:key="true" or isKey: true).
924
+ * Key fields are not user-editable when creating new rows.
925
+ *
926
+ * @param {object} oPropertyMeta - OData property metadata
927
+ * @returns {boolean}
928
+ */
929
+ _isKeyProperty: function(oPropertyMeta) {
930
+ if (!oPropertyMeta) {
931
+ return false;
932
+ }
933
+ return oPropertyMeta.isKey === true ||
934
+ oPropertyMeta["sap:key"] === "true" ||
935
+ !!(oPropertyMeta.extensions && oPropertyMeta.extensions.some(function(ext) {
936
+ return ext.name === "key" && ext.value === "true";
937
+ }));
489
938
  }
490
939
  };
491
- });
940
+ });