Part 1 of this blog post introduced a quality management Fiori-like SAPUI5 app, and covered the details of creating an OData service for it. This part covers the steps taken to create the Fiori-like SAPUI5 quality management app from scratch.
The example app is for recording results for quality management. It could be used by the quality inspector to record results using a tablet, walking down the line.
The presented app is not a reference. Its purpose is to demonstrate key solutions involved in providing the required functionality.
This blog was made possible by itelligence Sp. z o.o. Thanks to Artur Wojciechowski and Piotr Pudelski for supporting the creation of this blog.
This tutorial shows how to use the SAP Web IDE with an on-premise SAP system. The system is connected to the SAP Web IDE using the SAP HANA Cloud Connector.
Follow the 'How-to setup the SAP HANA Cloud Connector for secure on-premise connectivity' tutorial to download and install the SAP HANA Cloud Connector (productive version recommended) on a host on the network of the application server.
Set on-premise resources and additional properties as prescribed in SAP Web IDE Developer Guide - Connecting Remote Systems:
URL Path | Access Policy |
---|---|
/sap/opu/odata | Path and all sub-paths |
/sap/bc/ui5_ui5 | Path and all sub-paths |
/sap/bc/adt | Path and all sub-paths |
/sap/bc/bsp | Path and all sub-paths |
/sap/hba | Path and all sub-paths |
/plugins/pluginrepository | Path and all sub-paths |
Git is used by the SAP Web IDE for version control. While not mandatory, connecting the SAP Web IDE project to git has the benefit of allowing experiments with the code. After testing an idea on a 'branch', it is very easy to switch to another state of the project on another 'branch'.
# git access (HTTPS)
SetEnv GIT_PROJECT_ROOT /home/git/opt/git
SetEnv GIT_HTTP_EXPORT_ALL
SetEnv REMOTE_USER=$REDIRECT_REMOTE_USER
ScriptAlias /git/ /usr/lib/git-core/git-http-backend/
<Directory "/usr/lib/git-core*">
Options ExecCGI Indexes
Order allow,deny
<IfVersion < 2.4>
Allow from all
</IfVersion>
<IfVersion >= 2.4>
Require all granted
</IfVersion>
</Directory>
#<LocationMatch "^/git/.*/git-receive-pack$">
<LocationMatch "^/git">
AuthType Basic
AuthName "Git Access"
# htpasswd [-c] /home/git/.htpasswd S1234564825
AuthUserFile /home/git/.htpasswd
Require valid-user
</LocationMatch>
cd /home/git/opt/git
mkdir ZTUT_QM_00.git
cd ZTUT_QM_00.git
git --bare init --shared
cd /tmp
mkdir ZTUT_QM_00.git
cd ZTUT_QM_00.git
touch .gitignore
git init
git add .
git commit -m "Initial commit"
git remote add origin https://S1234564825@yourserver.com/git/ZTUT_QM_00
git push --set-upstream origin master
The quality management result recording app is created using the 'SAP Fiori Master Master Detail Application' template in the SAP Web IDE.
Order the master list by order number:
Filter the inspection operation list to range >= 100 and <= 499:
These properties are available for inspection operations via the navigation property 'InspLot'.
this.byId("master2List").attachUpdateFinished(function(oEvent) {
var m2List = oEvent.oSource;
if (!m2List.getSelectedItem()) {
that.selectFirstItem();
oEventBus.publish("Master2", "LoadFinished", {
oListItem: m2List.getItems()[0]
});
}
});
Displaying inspection characteristic results, along with the option to edit, is implemented in custom control 'ResultControl'. The control ensures a clean interface and localisation of implementation code.
/* This file is part of ZTUT_QM_00 - SAPUI5 Tutoral App
scn.sap.com/people/laszlo.kajan/blog/2015/09/17/step-by-step-end-to-end-guide-to-building-a-sap-fiori-like-sapui5-app--part-2
*/
sap.ui.core.Control.extend("tut.qm.control.ResultControl", {
metadata: {
properties: {
closedch: {
type: "string"
},
code1ch: {
type: "string" /*, defaultValue: "0"*/
}, // "OK", "NOK"
okcode: "string",
nokcode: "string"
},
aggregations: {
"_hl": {
type: "sap.ui.layout.HorizontalLayout",
multiple: false,
visibility: "hidden"
}
},
events: {
"change": { // attachChange, detachChange, fireChange
parameters: {
property: "string",
bindingContext: "sap.ui.model.Context",
bindingPath: "string",
newValue: "any"
}
}
}
},
init: function() {
var hl = new sap.ui.layout.HorizontalLayout(this.getId() + "-hl", {});
this.setAggregation("_hl", hl, true); /* no re-rendering needed on property change */
hl.addContent(this._rs = new sap.m.ObjectStatus(this.getId() + "-resStat", {
text: undefined,
icon: undefined
}));
},
exit: function() {},
updateAllParts: function() {
var that = this;
var closedch = this.getClosedch();
var code1ch = this.getCode1ch();
var hl = this.getAggregation("_hl");
var initialCode1 = !code1ch;
var okcode = this.getOkcode();
var nokcode = this.getNokcode();
if (!this._rsw) {
hl.insertContent(this._rsw = new sap.m.RadioButtonGroup(this.getId() + "-resSw", {
columns: 1,
valueState: "None",
buttons: [{
text: "{i18n>evalAccept}"
}, {
text: "{i18n>evalReject}"
}],
selectedIndex: 2,
select: function(oEvent) {
var selidx = oEvent.getParameter("selectedIndex");
var newValue = selidx === 0 ? okcode : nokcode;
that._rsw.setValueState(selidx === 0 ? "Success" : "Error");
that._rs.setIcon("sap-icon://decision");
that.fireChange({
property: "code1ch",
bindingContext: that.getBindingContext(),
bindingPath: that.getBindingPath("code1ch"), // entity property name
newValue: newValue
});
}
}), 0);
}
if (!initialCode1) {
var selidx = (code1ch === okcode) ? 0 : 1;
this._rsw.setSelectedIndex(selidx);
this._rsw.setValueState(selidx === 0 ? "Success" : "Error");
} else {
this._rsw.setSelectedIndex(2);
this._rsw.setValueState("Success");
}
if (closedch === "X") {
if (this._rsw) {
this._rsw.setEnabled(false);
}
if (this._ri) {
this._ri.setEnabled(false);
}
this._rs.setIcon("sap-icon://locked");
} else {
if (this._rsw) {
this._rsw.setEnabled(true);
}
if (this._ri) {
this._ri.setEnabled(true);
}
this._rs.setIcon("");
}
},
renderer: {
render: function(oRm, oControl) {
oControl.updateAllParts();
oRm.write("<div");
oRm.writeControlData(oControl);
oRm.addClass("ResultControl");
oRm.writeClasses();
oRm.write(">");
oRm.renderControl(oControl.getAggregation("_hl"));
oRm.write("</div>");
}
}
});
/* This file is part of ZTUT_QM_00 - SAPUI5 Tutoral App
scn.sap.com/people/laszlo.kajan/blog/2015/09/17/step-by-step-end-to-end-guide-to-building-a-sap-fiori-like-sapui5-app--part-2
*/
<c:ResultControl change="onResultChanged" closedch="{ClosedCh}"
code1ch="{Code1Ch}" id="idDetailResultCont" nokcode="NOK" okcode="OK"/>
<style type="text/css">
.ResultControl .sapUiHLayoutChildWrapper {
vertical-align: middle;
}
.ResultControl .sapMObjStatus {
padding-left: 1em;
}
</style>
When the user enters inspection results by choosing 'Accept' or 'Reject', the following should take place:
/* This file is part of ZTUT_QM_00 - SAPUI5 Tutoral App
scn.sap.com/people/laszlo.kajan/blog/2015/09/17/step-by-step-end-to-end-guide-to-building-a-sap-fiori-like-sapui5-app--part-2
*/
sap.ui.define([
"sap/ui/base/Object",
"sap/ui/model/json/JSONModel"
], function(Object, JSONModel) {
"use strict";
return Object.extend("tut.qm.controller.Application", {
// This class serves as controller for the whole App
constructor: function(oComponent) {
this._oComponent = oComponent;
},
init: function() {
this._oAppModel = new JSONModel({
applicationController: this,
btnSaveEnabled: false,
busyForDetailChange: false,
errClassCh: "01", // Customization for insp. lot 030000000051
master2PageBusyIndicationDelay: undefined, // whatever is the element's default
sampleCloseOnSave: "" // [""|"X"] for testing
});
this._oComponent.setModel(this._oAppModel, "appProperties");
this._oDraftModel = new JSONModel({
//"key": {CharDescr: "desc1", ...} // contains changed characteristic entities
});
this._oComponent.setModel(this._oDraftModel, "draftModel");
}
});
});
/* This file is part of ZTUT_QM_00 - SAPUI5 Tutoral App
scn.sap.com/people/laszlo.kajan/blog/2015/09/17/step-by-step-end-to-end-guide-to-building-a-sap-fiori-like-sapui5-app--part-2
*/
this._oApplicationController = new tut.qm.controller.Application(this);
this._oApplicationController.init();
Add 'Cancel' and 'Save' buttons to the detail view. Bind the 'active' property of the buttons to 'appProperties>btnSaveEnabled'.
<Toolbar>
<ToolbarSpacer/>
<Button id="idBtnCancel" press="onCancelPressed" text="{i18n>btnCancel}" type="Reject"
enabled="{appProperties>/btnSaveEnabled}"/>
<Button id="idBtnSave" press="onSavePressed" text="{i18n>btnSave}" type="Accept"
enabled="{appProperties>/btnSaveEnabled}"/>
</Toolbar>
var oComponent = this.getOwnerComponent();
this._oAppModel = oComponent.getModel("appProperties");
this._oApplicationController = this._oAppModel.getProperty("/applicationController");
onResultChanged: function(oEvent) {
this._oApplicationController.onDetResultChanged(oEvent);
}
onDetResultChanged: function(oEvent) {
// Registering the change for later 'Save'
// bCon.sPath = "/InspLotOpChardetColl(Insplot='030000000005',Inspoper='0020',Inspchar='0010',ResNoSi='0000')"
var bCon = oEvent.getParameter("bindingContext"); // sap.ui.model.Context
var bKey = bCon.getModel().getKey(bCon, false);
//console.log(bKey);
if (!(this._oDraftModel.oData[bKey] instanceof Object)) {
var li = oEvent.oSource.getParent(); // list item
this._oDraftModel.setData(
function() {
var o = {};
o[bKey] = bCon.getProperty();
o[bKey]._listItem = li;
return o;
}(),
true);
li.setModel(this._oDraftModel);
}
this._oDraftModel.setProperty(bCon.sPath + "/" + oEvent.getParameter("bindingPath"),
oEvent.getParameter("newValue"), undefined, false); // no need to re-render bound element
}
setDetailChangingState: function(targetState) {
// No dialogues from here
if (!targetState) { // targetState = false
// reset item bindings to default (the parent model)
for (var key in this._oDraftModel.oData) {
var entry = this._oDraftModel.oData[key];
var li = entry._listItem;
if (li) {
li.setModel(undefined);
}
}
this._oDraftModel.setData({});
//
this._setDetailChangingStateHelper(targetState);
} else { // targetState = true
this._setDetailChangingStateHelper(targetState);
}
},
_setDetailChangingStateHelper: function(targetState) {
if (targetState) {
this._oAppModel.setProperty("/master2PageBusyIndicationDelay", 0);
this._oAppModel.setProperty("/busyForDetailChange", targetState);
} else { // oBool = false - save disable, not busy
this._oAppModel.setProperty("/busyForDetailChange", targetState);
this._oAppModel.setProperty("/master2PageBusyIndicationDelay", undefined);
}
this._oAppModel.setProperty("/btnSaveEnabled", targetState);
}
<mvc:View busy="{appProperties>/busyForDetailChange}" busyIndicatorDelay="{appProperties>/master2PageBusyIndicationDelay}"
controllerName="tut.qm.controller.Master2" xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m">
<Page class="sapUiFioriObjectPage" id="detailPage" navButtonPress="onNavBack"
showNavButton="{= ${device>/isPhone} && !${appProperties>/busyForDetailChange}}"
title="{i18n>detailTitle}">
After changes have been made to inspection characteristic results, the 'Cancel' button is active. If the user clicks 'Cancel', the original state of the controls is restored.
onCancelPressed: function(oEvent) {
this._oApplicationController.onDetailCancelPressed(oEvent);
}
onDetailCancelPressed: function(oEvent) {
this.discardDetailChanges();
}
sap.ui.define([
"sap/ui/base/Object",
"sap/ui/model/json/JSONModel",
"sap/m/MessageBox"
], function(Object, JSONModel, MessageBox) {
discardDetailChanges: function(fnAfterDiscard) {
var that = this;
// Show cancel dialogue, handle response, call fnAfterDiscard if set
if (this._oAppModel.getProperty("/btnSaveEnabled")) {
// Opens the confirmation dialog
var i18n = this._oComponent.getModel("i18n").getResourceBundle();
MessageBox.show(i18n.getText("msgDiscardChangesQuestion"), {
icon: MessageBox.Icon.WARNING,
title: i18n.getText("msgDiscardChangesTitle"),
actions: [MessageBox.Action.OK, MessageBox.Action.CANCEL],
onClose: function(oAction) {
if (oAction === MessageBox.Action.OK) {
// Perform clean-up actions
that.setDetailChangingState(false);
//
if (fnAfterDiscard) {
fnAfterDiscard();
}
}
}
});
} else {
if (fnAfterDiscard) {
fnAfterDiscard();
}
}
}
Save changes as one unit of work, one transaction. A batch OData request implements this.
onSavePressed: function(oEvent) {
this._oApplicationController.onDetailSavePressed(oEvent);
}
onDetailSavePressed: function(oEvent) {
this._oAppModel.setProperty("/btnSaveEnabled", false); // keep it busy still
//
var oModel = this._oComponent.getModel();
var batchChanges = [];
oModel.clearBatch();
var errClassCh = this._oAppModel.getProperty("/errClassCh");
for (var key in this._oDraftModel.oData) {
var entry = {};
var bCon = this._oDraftModel.createBindingContext("/" + key);
entry.ClosedCh = this._oAppModel.getProperty("/sampleCloseOnSave");
entry.EvaluatedCh = "X";
entry.ValidValsCh = "1";
entry.CodeGrp1Ch = bCon.getProperty("SelSet1");
if ((entry.Code1Ch = bCon.getProperty("Code1Ch")) === "OK") { // accept
entry.EvaluationCh = "A";
entry.ErrClassCh = "";
entry.NonconfCh = "";
} else { // reject
entry.EvaluationCh = "R";
entry.ErrClassCh = errClassCh;
entry.NonconfCh = "1";
}
batchChanges.push(oModel.createBatchOperation(bCon.sPath, "MERGE", entry, undefined));
}
//
this.setDetailChangingState(false); // also clears busyForDetailChange
// async
if (batchChanges.length) {
oModel.addBatchChangeOperations(batchChanges);
oModel.submitBatch(function(oData, oResponse, aErrorResponses) { // request successfully sent
// Homework: implement error handling
// 1869434 - Details for working with OData $batch
// Homework: internationalize the below texts
MessageBox.show(
oData.__batchResponses[0].__changeResponses.length +
(oData.__batchResponses[0].__changeResponses.length <= 1 ?
" characteristic" : " characteristics") + " updated", {
icon: sap.m.MessageBox.Icon.SUCCESS,
title: "Batch Update",
actions: [sap.m.MessageBox.Action.OK]
});
}, function(oError) { // invalid request
sap.m.MessageBox.show("Invalid batch update request", {
icon: sap.m.MessageBox.Icon.ERROR,
title: "Batch Update",
actions: [sap.m.MessageBox.Action.OK]
});
});
}
}
"Default changeset implementation allows only one operation"
The default implementation attempts to commit ('BAPI_TRANSACTION_COMMIT') each characteristic in the 'changeset' of the batch request. This is an error when multiple entries are present in the 'changeset', as the whole batch operation must consist exactly one unit of work, and be committed (or rolled back) as one.method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_BEGIN.
Z_CHANGESET = 'X'.
endmethod.
method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_END.
clear Z_CHANGESET.
endmethod.
METHOD /iwbep/if_sb_dpc_comm_services~commit_work.
IF z_changeset <> 'X'.
" Must not commit each entity if this request is part of a change set!
CALL METHOD super->/iwbep/if_sb_dpc_comm_services~commit_work
EXPORTING
iv_rfc_dest = iv_rfc_dest.
ENDIF.
ENDMETHOD.
If you look carefully, you can notice that when changes are saved, the old state flashes back on the controls for a short time. Why is this? Could you prevent this from happening? Clues: check if the model of the changed controls is (re)set in sync with the changing default data model (it is not; try to synchronize these actions).
Thank you for reading part 2 of this blog post. I hope you found it useful.
User | Count |
---|---|
1 | |
1 | |
1 | |
1 |