@sapui5/sap.suite.ui.generic.template 1.148.0 → 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 (81) 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/i18n/i18n_fr_CA.properties +2 -2
  4. package/src/sap/suite/ui/generic/template/AnalyticalListPage/manifest.json +1 -1
  5. package/src/sap/suite/ui/generic/template/Canvas/manifest.json +1 -1
  6. package/src/sap/suite/ui/generic/template/ListReport/controller/ControllerImplementation.js +6 -3
  7. package/src/sap/suite/ui/generic/template/ListReport/i18n/i18n_fr.properties +1 -1
  8. package/src/sap/suite/ui/generic/template/ListReport/manifest.json +1 -1
  9. package/src/sap/suite/ui/generic/template/ObjectPage/manifest.json +1 -1
  10. package/src/sap/suite/ui/generic/template/ObjectPage/view/fragments/Actions.fragment.xml +13 -13
  11. package/src/sap/suite/ui/generic/template/QuickCreate/manifest.json +1 -1
  12. package/src/sap/suite/ui/generic/template/QuickView/manifest.json +1 -1
  13. package/src/sap/suite/ui/generic/template/genericUtilities/controlStateWrapperFactory/SmartFilterBarWrapper.js +20 -1
  14. package/src/sap/suite/ui/generic/template/lib/AppComponent.js +1 -1
  15. package/src/sap/suite/ui/generic/template/lib/CRUDManager.js +0 -7
  16. package/src/sap/suite/ui/generic/template/lib/CommonEventHandlers.js +57 -6
  17. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillAIOrchestrator.js +216 -0
  18. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillApplyHandler.js +325 -0
  19. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillDialogController.js +358 -0
  20. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillFieldCollector.js +848 -0
  21. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillFormRenderer.js +292 -0
  22. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillHandler.js +652 -1071
  23. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillTableRenderer.js +940 -0
  24. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/fragments/EasyFillDialog.fragment.xml +41 -26
  25. package/src/sap/suite/ui/generic/template/lib/ai/EasyFilterBarHandler.js +90 -73
  26. package/src/sap/suite/ui/generic/template/lib/i18n/i18n.properties +35 -8
  27. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ar.properties +29 -7
  28. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_bg.properties +29 -7
  29. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ca.properties +29 -7
  30. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cnr.properties +29 -7
  31. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cs.properties +29 -7
  32. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cy.properties +29 -7
  33. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_da.properties +29 -7
  34. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_de.properties +29 -7
  35. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_el.properties +29 -7
  36. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en.properties +29 -7
  37. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en_GB.properties +29 -7
  38. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en_US_saprigi.properties +29 -7
  39. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_es.properties +29 -7
  40. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_es_MX.properties +29 -7
  41. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_et.properties +29 -7
  42. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fi.properties +29 -7
  43. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fr.properties +30 -8
  44. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fr_CA.properties +31 -9
  45. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hi.properties +29 -7
  46. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hr.properties +30 -8
  47. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hu.properties +29 -7
  48. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_id.properties +29 -7
  49. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_it.properties +29 -7
  50. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_iw.properties +29 -7
  51. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ja.properties +29 -7
  52. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_kk.properties +29 -7
  53. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ko.properties +29 -7
  54. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_lt.properties +29 -7
  55. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_lv.properties +29 -7
  56. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_mk.properties +29 -7
  57. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ms.properties +29 -7
  58. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_nl.properties +30 -8
  59. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_no.properties +29 -7
  60. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pl.properties +29 -7
  61. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pt.properties +29 -7
  62. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pt_PT.properties +29 -7
  63. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ro.properties +29 -7
  64. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ru.properties +29 -7
  65. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sh.properties +29 -7
  66. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sk.properties +29 -7
  67. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sl.properties +29 -7
  68. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sr.properties +29 -7
  69. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sv.properties +29 -7
  70. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_th.properties +29 -7
  71. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_tr.properties +29 -7
  72. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_uk.properties +29 -7
  73. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_vi.properties +29 -7
  74. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_zh_CN.properties +29 -7
  75. package/src/sap/suite/ui/generic/template/lib/i18n/i18n_zh_TW.properties +29 -7
  76. package/src/sap/suite/ui/generic/template/lib/navigation/NavigationController.js +1 -1
  77. package/src/sap/suite/ui/generic/template/library.js +1 -1
  78. package/src/sap/suite/ui/generic/template/themes/base/ObjectPage.less +30 -4
  79. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/ObjectPageSectionHandler.js +0 -282
  80. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/fragments/EasyFillNonUpdatableSmartForm.fragment.xml +0 -18
  81. package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/fragments/EasyFillUpdatebleSmartForm.fragment.xml +0 -15
@@ -0,0 +1,940 @@
1
+ sap.ui.define([
2
+ "sap/ui/comp/smartfield/SmartField",
3
+ "sap/m/GenericTag",
4
+ "sap/m/MessageStrip",
5
+ "sap/m/Text",
6
+ "sap/m/Title",
7
+ "sap/m/Table",
8
+ "sap/m/Column",
9
+ "sap/m/ColumnListItem",
10
+ "sap/m/VBox",
11
+ "sap/m/HBox",
12
+ "sap/m/ScrollContainer",
13
+ "sap/m/SegmentedButton",
14
+ "sap/m/SegmentedButtonItem",
15
+ "sap/uxap/ObjectPageSection",
16
+ "sap/uxap/ObjectPageSubSection",
17
+ "sap/ui/core/CustomData",
18
+ "sap/suite/ui/generic/template/genericUtilities/FeLogger",
19
+ "sap/suite/ui/generic/template/genericUtilities/FeError"
20
+ ], function(SmartField, GenericTag, MessageStrip, Text, Title, Table, Column, ColumnListItem, VBox, HBox, ScrollContainer, SegmentedButton, SegmentedButtonItem, ObjectPageSection, ObjectPageSubSection, CustomData, FeLogger, FeError) {
21
+ "use strict";
22
+
23
+ var sClassName = "lib.ai.EasyFill.EasyFillTableRenderer";
24
+ var oLogger = new FeLogger(sClassName).getLogger();
25
+ var iMaxPreviewRows = 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
+
33
+ /**
34
+ * EasyFillTableRenderer
35
+ *
36
+ * Responsible for rendering the SmartTable collection preview in the EasyFill dialog.
37
+ * For each collection (navigation property) with LLM-suggested row updates, this module:
38
+ *
39
+ * 1. Creates transient OData row contexts for preview rendering.
40
+ * 2. Creates a "Previous Values" read-only table and a "New Values" editable SmartField table.
41
+ * 3. Renders a SegmentedButton toggle between the two views.
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.
45
+ *
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).
52
+ */
53
+ return {
54
+
55
+ /**
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.
59
+ *
60
+ * @param {object} mCollectionUpdates - map of { navProperty: [ rowUpdate ] } from AI response
61
+ * @param {object} mFieldMapping - full LLM field mapping (used to get item properties and descriptions)
62
+ * @param {Array} aTableValidationErrors - validation errors from value-help checks
63
+ * @param {object} oController - MVC controller (used to byId the ObjectPageLayout)
64
+ * @param {object} oRb - i18n resource bundle
65
+ * @param {object} mCollectionState - collection tracking state { mCollectionRowContexts, mCollectionRowContextsByPath, mCollectionProperties, mCollectionAllowsCreation }
66
+ * @param {object} oTableStagingState - mutable table staging state { mStagedRows, mPendingNewRowContexts }
67
+ * @param {object} oObjectPageModel - the OData model used to create transient staging contexts
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.
73
+ */
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");
78
+ return;
79
+ }
80
+
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
+ });
127
+
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;
131
+
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
+
163
+ Object.keys(mCollectionUpdates).forEach(function(sCollectionName) {
164
+ var oCollectionMeta = mFieldMapping[sCollectionName];
165
+ if (!oCollectionMeta || !oCollectionMeta.itemProperties) {
166
+ return;
167
+ }
168
+
169
+ var aPropertyNames = Object.keys(oCollectionMeta.itemProperties || {});
170
+ if (!aPropertyNames.length) {
171
+ return;
172
+ }
173
+ var aPreviewRows = (oTableStagingState.mStagedRows[sCollectionName] || []).filter(function(oStagedRow) {
174
+ return oStagedRow.isPreviewVisible;
175
+ });
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) {
184
+ return;
185
+ }
186
+
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
192
+ var aPreviousRows = aPreviewRows.map(function(oPreviewRow) {
193
+ return that._buildCollectionPreviewRowFromContext(aPropertyNames, oPreviewRow.realContext);
194
+ });
195
+ var oPreviousTable = that._createPreviewTable(aPropertyNames, oCollectionMeta.itemProperties, aPreviousRows);
196
+ oPreviousTable.setVisible(false);
197
+
198
+ // New values table (editable SmartFields, initially visible)
199
+ // Includes both updated existing rows and new rows
200
+ var oNewTable = that._createEditableNewTable(
201
+ aPropertyNames,
202
+ oCollectionMeta.itemProperties,
203
+ aPreviewRows,
204
+ aNewRows,
205
+ sCollectionName,
206
+ mCollectionState,
207
+ fnOnTableFieldChange
208
+ );
209
+
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" });
219
+
220
+ // Toggle button between "Previous" and "New" views
221
+ var oSegmentedButton = new SegmentedButton({
222
+ selectedKey: "new",
223
+ width: "auto",
224
+ items: [
225
+ new SegmentedButtonItem({ key: "previous", text: oRb.getText("EASY_FILL_TABLE_VIEW_PREVIOUS"), width: "auto" }),
226
+ new SegmentedButtonItem({ key: "new", text: oRb.getText("EASY_FILL_TABLE_VIEW_NEW"), width: "auto" })
227
+ ],
228
+ selectionChange: function() {
229
+ var sKey = oSegmentedButton.getSelectedKey();
230
+ oPreviousTable.setVisible(sKey === "previous");
231
+ oNewTable.setVisible(sKey === "new");
232
+ if (oReadonlyTag) {
233
+ oReadonlyTag.setVisible(sKey === "new");
234
+ }
235
+ oTitleControl.setText(sCollectionName_label + " (" + (sKey === "previous" ? iExistingRowCount : iTotalCount) + ")");
236
+ }
237
+ });
238
+ var aRightItems = oReadonlyTag ? [oReadonlyTag, oSegmentedButton] : [oSegmentedButton];
239
+ var oHeader = new HBox({
240
+ justifyContent: "SpaceBetween",
241
+ alignItems: "Center",
242
+ wrap: "Wrap",
243
+ items: [oTitleControl].concat(aRightItems)
244
+ }).addStyleClass("sapUiSmallMarginTop sapUiSmallMarginBottom");
245
+
246
+ var oTableVBox = new VBox({
247
+ items: [oHeader, oPreviousTable, oNewTable]
248
+ }).addStyleClass("sapSmartTemplatesObjectPageEasyFillTableSection");
249
+
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)");
326
+ });
327
+ },
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
+
337
+ /**
338
+ * Resets and clears all transient collection contexts created for table preview staging.
339
+ *
340
+ * @param {object} oTableStagingState - table staging state { mStagedRows, mPendingNewRowContexts }
341
+ * @param {object} oObjectPageModel - the OData model
342
+ */
343
+ resetStagedCollectionContexts: function(oTableStagingState, oObjectPageModel) {
344
+ var aTransientPaths = [];
345
+
346
+ Object.keys((oTableStagingState && oTableStagingState.mStagedRows) || {}).forEach(function(sCollectionName) {
347
+ (oTableStagingState.mStagedRows[sCollectionName] || []).forEach(function(oStagedRow) {
348
+ var oTransientContext = oStagedRow.transientContext;
349
+ var sTransientPath = oTransientContext && oTransientContext.getPath && oTransientContext.getPath();
350
+ if (sTransientPath) {
351
+ aTransientPaths.push(sTransientPath);
352
+ }
353
+ });
354
+ });
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
+
366
+ if (aTransientPaths.length && oObjectPageModel) {
367
+ oObjectPageModel.resetChanges(aTransientPaths, false, true);
368
+ }
369
+
370
+ this._clearStagedCollectionState(oTableStagingState);
371
+ oLogger.debug("resetStagedCollectionContexts: transient table contexts reset");
372
+ },
373
+
374
+ // ─── Private Helpers ─────────────────────────────────────────────────────
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
+
422
+ _initializeStagedCollectionRows: function(mCollectionUpdates, mFieldMapping, mCollectionState, oTableStagingState, oObjectPageModel) {
423
+ var that = this;
424
+ this._clearStagedCollectionState(oTableStagingState);
425
+
426
+ Object.keys(mCollectionUpdates || {}).forEach(function(sCollectionName) {
427
+ var oCollectionMeta = mFieldMapping[sCollectionName];
428
+ var aRowUpdates = mCollectionUpdates[sCollectionName] || [];
429
+ var aCollectionRowContexts = mCollectionState.mCollectionRowContexts[sCollectionName] || [];
430
+ var mCollectionPropertyMap = mCollectionState.mCollectionProperties[sCollectionName] || Object.create(null);
431
+ var oEntitySet = mCollectionState.mCollectionEntitySets && mCollectionState.mCollectionEntitySets[sCollectionName];
432
+ var aPropertyNames = Object.keys((oCollectionMeta && oCollectionMeta.itemProperties) || {});
433
+ var bAllowsCreation = !!(mCollectionState.mCollectionAllowsCreation &&
434
+ mCollectionState.mCollectionAllowsCreation[sCollectionName]);
435
+
436
+ if (!oCollectionMeta || !oCollectionMeta.entitySet || !aPropertyNames.length) {
437
+ return;
438
+ }
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
+
450
+ var mUpdatesByIdentity = Object.create(null);
451
+ aExistingRowUpdates.forEach(function(oRowUpdate, iUpdateIndex) {
452
+ var oResolvedUpdate = that._resolveCollectionRowContext(sCollectionName, oRowUpdate, mCollectionState);
453
+ mUpdatesByIdentity[that._getCollectionRowIdentity(oResolvedUpdate, oRowUpdate, iUpdateIndex)] = oRowUpdate;
454
+ });
455
+
456
+ oTableStagingState.mStagedRows[sCollectionName] = [];
457
+
458
+ aCollectionRowContexts.forEach(function(oRealContext, iRowIndex) {
459
+ var oResolvedRow = { context: oRealContext, rowIndex: iRowIndex };
460
+ var sIdentity = that._getCollectionRowIdentity(oResolvedRow, null, iRowIndex);
461
+ var oRowUpdate = mUpdatesByIdentity[sIdentity];
462
+ var bIsPreviewVisible = iRowIndex < iMaxPreviewRows;
463
+
464
+ if (!bIsPreviewVisible && !oRowUpdate) {
465
+ return;
466
+ }
467
+
468
+ var oTransientContext = that._createTransientCollectionContext(oCollectionMeta.entitySet, oObjectPageModel);
469
+ if (!oTransientContext) {
470
+ return;
471
+ }
472
+
473
+ var oRealRowObject = oRealContext.getObject && oRealContext.getObject();
474
+ that._seedTransientContextFromRowObject(oTransientContext, oRealRowObject);
475
+ that._applyRowUpdateToTransientContext(oTransientContext, oRowUpdate, mCollectionPropertyMap, oRealRowObject, oEntitySet);
476
+
477
+ oTableStagingState.mStagedRows[sCollectionName].push({
478
+ realContext: oRealContext,
479
+ transientContext: oTransientContext,
480
+ update: oRowUpdate,
481
+ rowIndex: iRowIndex,
482
+ propertyNames: aPropertyNames,
483
+ isPreviewVisible: bIsPreviewVisible
484
+ });
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
+ });
507
+ });
508
+ },
509
+
510
+ /**
511
+ * Creates a read-only sap.m.Table for displaying table row data (used for "Previous Values" view).
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).
516
+ *
517
+ * @param {Array<string>} aPropertyNames - ordered list of property names to show as columns
518
+ * @param {object} oItemProperties - map of { propName: { description, dataType } }
519
+ * @param {Array<object>} aRows - array of plain row value objects
520
+ * @param {function} [fnCellFormatter] - optional custom cell formatter (oRow, sPropName, sRawValue) → string
521
+ * @returns {sap.m.ScrollContainer} ScrollContainer wrapping the table with horizontal scrolling enabled
522
+ */
523
+ _createPreviewTable: function(aPropertyNames, oItemProperties, aRows, fnCellFormatter) {
524
+ var aColumns = aPropertyNames.map(function(sPropertyName) {
525
+ return new Column({
526
+ header: new Text({
527
+ text: (oItemProperties[sPropertyName] && oItemProperties[sPropertyName].description) || sPropertyName
528
+ })
529
+ });
530
+ });
531
+
532
+ var aItems = aRows.map(function(oRow) {
533
+ return new ColumnListItem({
534
+ cells: aPropertyNames.map(function(sPropertyName) {
535
+ var sText = fnCellFormatter
536
+ ? fnCellFormatter(oRow, sPropertyName, this._stringifyCellValue(oRow[sPropertyName]))
537
+ : this._stringifyCellValue(oRow[sPropertyName]);
538
+ return new Text({ text: sText });
539
+ }.bind(this))
540
+ });
541
+ }.bind(this));
542
+
543
+ var oTable = new Table({
544
+ columns: aColumns,
545
+ items: aItems,
546
+ fixedLayout: false,
547
+ autoPopinMode: false
548
+ }).addStyleClass("sapSmartTemplatesObjectPageEasyFillTablePreview");
549
+
550
+ return new ScrollContainer({
551
+ horizontal: true,
552
+ vertical: false,
553
+ width: "100%",
554
+ content: [oTable]
555
+ });
556
+ },
557
+
558
+ /**
559
+ * Creates an editable sap.m.Table for displaying AI-suggested row updates.
560
+ * Editable cells are rendered as SmartField controls bound to the OData row context.
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).
566
+ *
567
+ * @param {Array<string>} aPropertyNames - ordered list of property names to show as columns
568
+ * @param {object} oItemProperties - map of { propName: { description, dataType } }
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
571
+ * @param {string} sCollectionName - navigation property name (used to look up property metadata)
572
+ * @param {object} mCollectionState - collection tracking state (used to get property metadata)
573
+ * @param {function} fnOnTableFieldChange - callback when a cell value changes
574
+ * @returns {sap.m.ScrollContainer} ScrollContainer wrapping the table with horizontal scrolling enabled
575
+ */
576
+ _createEditableNewTable: function(aPropertyNames, oItemProperties, aPreviewRows, aNewRowContexts, sCollectionName, mCollectionState, fnOnTableFieldChange) {
577
+ var aColumns = aPropertyNames.map(function(sPropName) {
578
+ return new Column({
579
+ header: new Text({
580
+ text: (oItemProperties[sPropName] && oItemProperties[sPropName].description) || sPropName
581
+ })
582
+ });
583
+ });
584
+
585
+ var mCollectionPropertyMap = mCollectionState.mCollectionProperties[sCollectionName] || Object.create(null);
586
+ var oEntitySet = mCollectionState.mCollectionEntitySets && mCollectionState.mCollectionEntitySets[sCollectionName];
587
+ var that = this;
588
+
589
+ // Rows for existing rows with AI-proposed updates
590
+ var aExistingItems = aPreviewRows.map(function(oPreviewRow) {
591
+ var oContext = oPreviewRow.transientContext;
592
+ var oRowObject = oPreviewRow.realContext && oPreviewRow.realContext.getObject && oPreviewRow.realContext.getObject();
593
+
594
+ var aCells = aPropertyNames.map(function(sPropName) {
595
+ if (!oContext) {
596
+ return new Text({ text: "" });
597
+ }
598
+
599
+ var oPropertyMeta = mCollectionPropertyMap[sPropName];
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
+ );
606
+ var oSmartField = new SmartField({
607
+ value: "{" + sPropName + "}",
608
+ editable: bEditable,
609
+ enabled: bEditable,
610
+ contextEditable: bEditable,
611
+ change: function() {
612
+ if (typeof fnOnTableFieldChange === "function") {
613
+ fnOnTableFieldChange();
614
+ }
615
+ },
616
+ editableChanged: function(oEvent) {
617
+ if (oEvent.getParameter("editable") !== bEditable) {
618
+ oEvent.getSource().setEditable(bEditable);
619
+ }
620
+ }
621
+ });
622
+
623
+ if (bHasUneditableAiProp) {
624
+ that._markSmartFieldAsWarning(oSmartField);
625
+ }
626
+
627
+ return oSmartField;
628
+ });
629
+
630
+ var oItem = new ColumnListItem({ cells: aCells });
631
+ if (oContext) {
632
+ oItem.setBindingContext(oContext);
633
+ }
634
+ return oItem;
635
+ });
636
+
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({
680
+ columns: aColumns,
681
+ items: aExistingItems.concat(aNewItems),
682
+ fixedLayout: false,
683
+ autoPopinMode: false
684
+ }).addStyleClass("sapSmartTemplatesObjectPageEasyFillTablePreview");
685
+
686
+ return new ScrollContainer({
687
+ horizontal: true,
688
+ vertical: false,
689
+ width: "100%",
690
+ content: [oTable]
691
+ });
692
+ },
693
+
694
+ /**
695
+ * Builds a plain preview row object from a given OData context.
696
+ *
697
+ * @param {Array<string>} aPropertyNames - property names to include
698
+ * @param {object} oContext - OData context providing the property values
699
+ * @returns {object} plain row data object
700
+ */
701
+ _buildCollectionPreviewRowFromContext: function(aPropertyNames, oContext) {
702
+ var oPreviewRow = {};
703
+
704
+ aPropertyNames.forEach(function(sPropertyName) {
705
+ oPreviewRow[sPropertyName] = oContext ? oContext.getProperty(sPropertyName) : "";
706
+ });
707
+
708
+ return oPreviewRow;
709
+ },
710
+
711
+ _createTransientCollectionContext: function(sEntitySetName, oObjectPageModel) {
712
+ if (!sEntitySetName || !oObjectPageModel) {
713
+ return null;
714
+ }
715
+
716
+ return oObjectPageModel.createEntry("/" + sEntitySetName, {
717
+ groupId: "EasyFillFETransientChanges",
718
+ changeSetId: "EasyFillFETransientChanges"
719
+ });
720
+ },
721
+
722
+ _seedTransientContextFromRowObject: function(oTransientContext, oRowObject) {
723
+ Object.keys(oRowObject || {}).forEach(function(sPropertyName) {
724
+ var vValue = oRowObject[sPropertyName];
725
+ if (vValue === null || vValue === undefined || typeof vValue !== "object" || vValue instanceof Date) {
726
+ oTransientContext.setProperty(sPropertyName, vValue);
727
+ }
728
+ });
729
+ },
730
+
731
+ _applyRowUpdateToTransientContext: function(oTransientContext, oRowUpdate, mCollectionPropertyMap, oRealRowObject, oEntitySet) {
732
+ var that = this;
733
+ Object.keys(oRowUpdate || {}).forEach(function(sPropertyName) {
734
+ if (sPropertyName === "_rowIndex" || sPropertyName === "_contextPath") {
735
+ return;
736
+ }
737
+
738
+ var oPropertyMeta = mCollectionPropertyMap[sPropertyName];
739
+ if (!that._isEditableProperty(oPropertyMeta, oRealRowObject, oEntitySet)) {
740
+ return;
741
+ }
742
+
743
+ oTransientContext.setProperty(sPropertyName, oRowUpdate[sPropertyName]);
744
+ });
745
+ },
746
+
747
+ _clearStagedCollectionState: function(oTableStagingState) {
748
+ Object.keys((oTableStagingState && oTableStagingState.mStagedRows) || {}).forEach(function(sCollectionName) {
749
+ delete oTableStagingState.mStagedRows[sCollectionName];
750
+ });
751
+ Object.keys((oTableStagingState && oTableStagingState.mPendingNewRowContexts) || {}).forEach(function(sCollectionName) {
752
+ delete oTableStagingState.mPendingNewRowContexts[sCollectionName];
753
+ });
754
+ },
755
+
756
+ /**
757
+ * Resolves an OData row context for a given collection row update.
758
+ * Prefers context path lookup; falls back to row index.
759
+ *
760
+ * @param {string} sCollectionName - navigation property name
761
+ * @param {object} oRowUpdate - the AI row update object ({ _contextPath?, _rowIndex? })
762
+ * @param {object} mCollectionState - collection tracking state
763
+ * @returns {{ context, rowIndex }|null}
764
+ */
765
+ _resolveCollectionRowContext: function(sCollectionName, oRowUpdate, mCollectionState) {
766
+ var mByPath = mCollectionState.mCollectionRowContextsByPath[sCollectionName] || Object.create(null);
767
+ var sContextPath = oRowUpdate && oRowUpdate._contextPath;
768
+
769
+ // Prefer context path (more stable across refreshes)
770
+ if (typeof sContextPath === "string" && mByPath[sContextPath]) {
771
+ return mByPath[sContextPath];
772
+ }
773
+
774
+ // Fallback to row index
775
+ var iRowIndex = oRowUpdate && oRowUpdate._rowIndex;
776
+ var aRowContexts = mCollectionState.mCollectionRowContexts[sCollectionName] || [];
777
+ if (typeof iRowIndex === "number" && aRowContexts[iRowIndex]) {
778
+ return { context: aRowContexts[iRowIndex], rowIndex: iRowIndex };
779
+ }
780
+
781
+ return null;
782
+ },
783
+
784
+ /**
785
+ * Returns a stable string identity for a collection row, used as a map key.
786
+ * Priority: context path → _contextPath from update → _rowIndex → fallback index string.
787
+ *
788
+ * @param {object} oResolvedRow - { context, rowIndex } or null
789
+ * @param {object} oRowUpdate - the AI row update object
790
+ * @param {number} iFallbackIndex - fallback numeric index
791
+ * @returns {string}
792
+ */
793
+ _getCollectionRowIdentity: function(oResolvedRow, oRowUpdate, iFallbackIndex) {
794
+ if (oResolvedRow && oResolvedRow.context && oResolvedRow.context.getPath) {
795
+ return oResolvedRow.context.getPath();
796
+ }
797
+ if (oRowUpdate && typeof oRowUpdate._contextPath === "string") {
798
+ return oRowUpdate._contextPath;
799
+ }
800
+ if (oRowUpdate && typeof oRowUpdate._rowIndex === "number") {
801
+ return "index:" + String(oRowUpdate._rowIndex);
802
+ }
803
+ return "row:" + String(iFallbackIndex);
804
+ },
805
+
806
+ /**
807
+ * Converts a raw cell value to a display string for read-only table cells.
808
+ * Handles null/undefined, Date objects, and primitives.
809
+ *
810
+ * @param {*} vValue
811
+ * @returns {string}
812
+ */
813
+ _stringifyCellValue: function(vValue) {
814
+ if (vValue === null || vValue === undefined) {
815
+ return "";
816
+ }
817
+ if (vValue instanceof Date) {
818
+ return vValue.toISOString();
819
+ }
820
+ return String(vValue);
821
+ },
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
+
862
+ /**
863
+ * Checks whether a collection property is editable given the current row data object.
864
+ * Delegates to the same annotation checks used in EasyFillFieldCollector.isEditableByContextObject.
865
+ * Duplicated here to keep TableRenderer self-contained (no circular dependency).
866
+ *
867
+ * @param {object} oPropertyMeta - OData property metadata
868
+ * @param {object} oRowObject - plain row data object
869
+ * @returns {boolean}
870
+ */
871
+ _isEditableProperty: function(oPropertyMeta, oRowObject, oEntitySet) {
872
+ if (!oPropertyMeta) {
873
+ return false;
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
+
892
+ var sUpdatable = oPropertyMeta["sap:updatable"];
893
+ if (sUpdatable) {
894
+ return sUpdatable !== "false";
895
+ }
896
+ var oAnnotation = oPropertyMeta["com.sap.vocabularies.Common.v1.FieldControl"];
897
+ if (oAnnotation && oAnnotation["EnumMember"]) {
898
+ return oAnnotation["EnumMember"] !== "com.sap.vocabularies.Common.v1.FieldControlType/ReadOnly";
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).
911
+ var sFieldControl = oPropertyMeta["sap:field-control"];
912
+ if (sFieldControl) {
913
+ if (!oRowObject || !(sFieldControl in oRowObject)) {
914
+ return false;
915
+ }
916
+ var iFieldControlValue = oRowObject[sFieldControl];
917
+ return iFieldControlValue !== 0 && iFieldControlValue !== 1;
918
+ }
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
+ }));
938
+ }
939
+ };
940
+ });