@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.
- package/package.json +1 -1
- package/src/sap/suite/ui/generic/template/.library +1 -1
- package/src/sap/suite/ui/generic/template/AnalyticalListPage/i18n/i18n_fr_CA.properties +2 -2
- package/src/sap/suite/ui/generic/template/AnalyticalListPage/manifest.json +1 -1
- package/src/sap/suite/ui/generic/template/Canvas/manifest.json +1 -1
- package/src/sap/suite/ui/generic/template/ListReport/controller/ControllerImplementation.js +6 -3
- package/src/sap/suite/ui/generic/template/ListReport/i18n/i18n_fr.properties +1 -1
- package/src/sap/suite/ui/generic/template/ListReport/manifest.json +1 -1
- package/src/sap/suite/ui/generic/template/ObjectPage/manifest.json +1 -1
- package/src/sap/suite/ui/generic/template/ObjectPage/view/fragments/Actions.fragment.xml +13 -13
- package/src/sap/suite/ui/generic/template/QuickCreate/manifest.json +1 -1
- package/src/sap/suite/ui/generic/template/QuickView/manifest.json +1 -1
- package/src/sap/suite/ui/generic/template/genericUtilities/controlStateWrapperFactory/SmartFilterBarWrapper.js +20 -1
- package/src/sap/suite/ui/generic/template/lib/AppComponent.js +1 -1
- package/src/sap/suite/ui/generic/template/lib/CRUDManager.js +0 -7
- package/src/sap/suite/ui/generic/template/lib/CommonEventHandlers.js +57 -6
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillAIOrchestrator.js +216 -0
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillApplyHandler.js +325 -0
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillDialogController.js +358 -0
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillFieldCollector.js +848 -0
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillFormRenderer.js +292 -0
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillHandler.js +652 -1071
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/EasyFillTableRenderer.js +940 -0
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/fragments/EasyFillDialog.fragment.xml +41 -26
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFilterBarHandler.js +90 -73
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n.properties +35 -8
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ar.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_bg.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ca.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cnr.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cs.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_cy.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_da.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_de.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_el.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en_GB.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_en_US_saprigi.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_es.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_es_MX.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_et.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fi.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fr.properties +30 -8
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_fr_CA.properties +31 -9
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hi.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hr.properties +30 -8
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_hu.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_id.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_it.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_iw.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ja.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_kk.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ko.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_lt.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_lv.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_mk.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ms.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_nl.properties +30 -8
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_no.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pl.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pt.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_pt_PT.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ro.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_ru.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sh.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sk.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sl.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sr.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_sv.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_th.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_tr.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_uk.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_vi.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_zh_CN.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/i18n/i18n_zh_TW.properties +29 -7
- package/src/sap/suite/ui/generic/template/lib/navigation/NavigationController.js +1 -1
- package/src/sap/suite/ui/generic/template/library.js +1 -1
- package/src/sap/suite/ui/generic/template/themes/base/ObjectPage.less +30 -4
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/ObjectPageSectionHandler.js +0 -282
- package/src/sap/suite/ui/generic/template/lib/ai/EasyFill/fragments/EasyFillNonUpdatableSmartForm.fragment.xml +0 -18
- 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
|
+
});
|