├── .reuse └── dep5 ├── BP ├── .eslintrc ├── .gitignore ├── .npmrc ├── package.json ├── tsconfig.json ├── ui5.yaml ├── webapp │ ├── Component.js │ ├── controller │ │ ├── App.controller.js │ │ ├── BaseController.js │ │ ├── Detail.controller.js │ │ ├── DetailObjectNotFound.controller.js │ │ ├── DetailObjectNotFound.js │ │ ├── ErrorHandler.js │ │ ├── ListSelector.js │ │ ├── Master.controller.js │ │ └── NotFound.controller.js │ ├── i18n │ │ └── i18n.properties │ ├── index.html │ ├── localService │ │ ├── metadata.xml │ │ └── mockserver.js │ ├── manifest.json │ ├── model │ │ ├── formatter.js │ │ └── models.js │ ├── test.html │ ├── test │ │ ├── initMockServer.js │ │ ├── integration │ │ │ ├── AllJourneys.js │ │ │ ├── BusyJourney.js │ │ │ ├── BusyJourneyPhone.js │ │ │ ├── MasterJourney.js │ │ │ ├── NavigationJourney.js │ │ │ ├── NavigationJourneyPhone.js │ │ │ ├── NotFoundJourney.js │ │ │ ├── NotFoundJourneyPhone.js │ │ │ ├── PhoneJourneys.js │ │ │ ├── arrangements │ │ │ │ └── Startup.js │ │ │ ├── opaTests.qunit.html │ │ │ ├── opaTests.qunit.js │ │ │ ├── opaTestsPhone.qunit.html │ │ │ ├── opaTestsPhone.qunit.js │ │ │ └── pages │ │ │ │ ├── App.js │ │ │ │ ├── Browser.js │ │ │ │ ├── Common.js │ │ │ │ ├── Detail.js │ │ │ │ ├── Master.js │ │ │ │ └── NotFound.js │ │ ├── mockServer.html │ │ ├── testsuite.qunit.html │ │ ├── testsuite.qunit.js │ │ └── unit │ │ │ ├── AllTests.js │ │ │ ├── controller │ │ │ └── ListSelector.js │ │ │ ├── model │ │ │ ├── formatter.js │ │ │ └── models.js │ │ │ ├── unitTests.qunit.html │ │ │ └── unitTests.qunit.js │ └── view │ │ ├── App.view.xml │ │ ├── Detail.view.xml │ │ ├── DetailObjectNotFound.view.xml │ │ ├── Master.view.xml │ │ ├── NotFound.view.xml │ │ └── ViewSettingsDialog.fragment.xml └── xs-app.json ├── LICENSE ├── LICENSES └── Apache-2.0.txt ├── Mission.md ├── README.md ├── RefApp.md ├── doc └── img │ ├── AssignRoleCollection.png │ ├── BizappDevSpace.png │ ├── BuildAndDeploy.png │ ├── CFLoginBizapp.png │ ├── CICD-Credentials.png │ ├── Destination.png │ ├── FindCommand.png │ ├── RunCICD.png │ ├── Subscription.png │ ├── Webhook.png │ ├── gitclone.png │ ├── images.png │ ├── simpleUI.drawio.png │ └── solution_diagram.png ├── mta.yaml ├── package.json ├── resources └── BP-content.zip └── xs-security.json /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: cloud-extension-html5-sample 3 | Upstream-Contact: LakshmiCR (lakshmi.c.rajeev@sap.com) 4 | Source: https://github.com/SAP-samples/cloud-extension-html5-sample 5 | Disclaimer: The code in this project may include calls to APIs (“API Calls”) of 6 | SAP or third-party products or services developed outside of this project 7 | (“External Products”). 8 | “APIs” means application programming interfaces, as well as their respective 9 | specifications and implementing code that allows software to communicate with 10 | other software. 11 | API Calls to External Products are not licensed under the open source license 12 | that governs this project. The use of such API Calls and related External 13 | Products are subject to applicable additional agreements with the relevant 14 | provider of the External Products. In no event shall the open source license 15 | that governs this project grant any rights in or to any External Products,or 16 | alter, expand or supersede any terms of the applicable additional agreements. 17 | If you have a valid license agreement with SAP for the use of a particular SAP 18 | External Product, then you may make use of any API Calls included in this 19 | project’s code for that SAP External Product, subject to the terms of such 20 | license agreement. If you do not have a valid license agreement for the use of 21 | a particular SAP External Product, then you may only make use of any API Calls 22 | in this project for that SAP External Product for your internal, non-productive 23 | and non-commercial test and evaluation of such API Calls. Nothing herein grants 24 | you any rights to use or access any SAP External Product, or provide any third 25 | parties the right to use of access any SAP External Product, through API Calls. 26 | 27 | Files: * 28 | Copyright: 2020 SAP SE or an SAP affiliate company and cloud-extension-html5-sample 29 | License: Apache-2.0 -------------------------------------------------------------------------------- /BP/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@sap/ui5-jsdocs"], 3 | "extends": ["plugin:@sap/ui5-jsdocs/recommended", "eslint:recommended"] 4 | } 5 | -------------------------------------------------------------------------------- /BP/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /BP/.npmrc: -------------------------------------------------------------------------------- 1 | @sap:registry = "https://npm.sap.com" -------------------------------------------------------------------------------- /BP/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BP", 3 | "version": "0.0.1", 4 | "devDependencies": { 5 | "@sapui5/ts-types": "1.71.x", 6 | "@sap/ui5-builder-webide-extension": "1.0.x", 7 | "bestzip": "2.1.7", 8 | "rimraf": "3.0.2" 9 | }, 10 | "scripts": { 11 | "build": "npm run clean && ui5 build --include-task=generateManifestBundle generateCachebusterInfo && npm run zip", 12 | "zip": "cd dist && npx bestzip ../BP-content.zip *", 13 | "clean": "npx rimraf BP-content.zip dist" 14 | }, 15 | "ui5": { 16 | "dependencies": [ 17 | "@sap/ui5-builder-webide-extension" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BP/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "none", 4 | "noEmit": true, 5 | "checkJs": true, 6 | "allowJs": true, 7 | "types": [ 8 | "@sapui5/ts-types" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BP/ui5.yaml: -------------------------------------------------------------------------------- 1 | specVersion: '1.0' 2 | metadata: 3 | name: BP 4 | type: application 5 | resources: 6 | configuration: 7 | propertiesFileSourceEncoding: UTF-8 8 | builder: 9 | customTasks: 10 | - name: webide-extension-task-updateManifestJson 11 | afterTask: generateVersionInfo 12 | configuration: 13 | appFolder: webapp 14 | destDir: dist 15 | - name: webide-extension-task-resources 16 | afterTask: webide-extension-task-updateManifestJson 17 | configuration: 18 | nameSpace: ns 19 | - name: webide-extension-task-copyFile 20 | afterTask: webide-extension-task-resources 21 | configuration: 22 | srcFile: "/xs-app.json" 23 | destFile: "/xs-app.json" 24 | -------------------------------------------------------------------------------- /BP/webapp/Component.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/UIComponent", 3 | "sap/ui/Device", 4 | "./model/models", 5 | "./controller/ListSelector", 6 | "./controller/ErrorHandler" 7 | ], function (UIComponent, Device, models, ListSelector, ErrorHandler) { 8 | "use strict"; 9 | 10 | return UIComponent.extend("ns.BP.Component", { 11 | 12 | metadata : { 13 | manifest : "json" 14 | }, 15 | 16 | /** 17 | * The component is initialized by UI5 automatically during the startup of the app and calls the init method once. 18 | * In this method, the device models are set and the router is initialized. 19 | * @public 20 | * @override 21 | */ 22 | init : function () { 23 | this.oListSelector = new ListSelector(); 24 | this._oErrorHandler = new ErrorHandler(this); 25 | 26 | // set the device model 27 | this.setModel(models.createDeviceModel(), "device"); 28 | 29 | // call the base component's init function and create the App view 30 | UIComponent.prototype.init.apply(this, arguments); 31 | 32 | // create the views based on the url/hash 33 | this.getRouter().initialize(); 34 | }, 35 | 36 | /** 37 | * The component is destroyed by UI5 automatically. 38 | * In this method, the ListSelector and ErrorHandler are destroyed. 39 | * @public 40 | * @override 41 | */ 42 | destroy : function () { 43 | this.oListSelector.destroy(); 44 | this._oErrorHandler.destroy(); 45 | // call the base component's destroy function 46 | UIComponent.prototype.destroy.apply(this, arguments); 47 | }, 48 | 49 | /** 50 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy 51 | * design mode class should be set, which influences the size appearance of some controls. 52 | * @public 53 | * @return {string} css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set 54 | */ 55 | getContentDensityClass : function() { 56 | if (this._sContentDensityClass === undefined) { 57 | // check whether FLP has already set the content density class; do nothing in this case 58 | // eslint-disable-next-line sap-no-proprietary-browser-api 59 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) { 60 | this._sContentDensityClass = ""; 61 | } else if (!Device.support.touch) { // apply "compact" mode if touch is not supported 62 | this._sContentDensityClass = "sapUiSizeCompact"; 63 | } else { 64 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table 65 | this._sContentDensityClass = "sapUiSizeCozy"; 66 | } 67 | } 68 | return this._sContentDensityClass; 69 | } 70 | 71 | }); 72 | }); -------------------------------------------------------------------------------- /BP/webapp/controller/App.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel" 4 | ], function (BaseController, JSONModel) { 5 | "use strict"; 6 | 7 | return BaseController.extend("ns.BP.controller.App", { 8 | 9 | onInit : function () { 10 | var oViewModel, 11 | fnSetAppNotBusy, 12 | iOriginalBusyDelay = this.getView().getBusyIndicatorDelay(); 13 | 14 | oViewModel = new JSONModel({ 15 | busy : true, 16 | delay : 0, 17 | layout : "OneColumn", 18 | previousLayout : "", 19 | actionButtonsInfo : { 20 | midColumn : { 21 | fullScreen : false 22 | } 23 | } 24 | }); 25 | this.setModel(oViewModel, "appView"); 26 | 27 | fnSetAppNotBusy = function() { 28 | oViewModel.setProperty("/busy", false); 29 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 30 | }; 31 | 32 | // since then() has no "reject"-path attach to the MetadataFailed-Event to disable the busy indicator in case of an error 33 | this.getOwnerComponent().getModel().metadataLoaded().then(fnSetAppNotBusy); 34 | this.getOwnerComponent().getModel().attachMetadataFailed(fnSetAppNotBusy); 35 | 36 | // apply content density mode to root view 37 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass()); 38 | } 39 | 40 | }); 41 | }); -------------------------------------------------------------------------------- /BP/webapp/controller/BaseController.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/mvc/Controller", 3 | "sap/ui/core/routing/History" 4 | ], function (Controller, History) { 5 | "use strict"; 6 | 7 | return Controller.extend("ns.BP.controller.BaseController", { 8 | /** 9 | * Convenience method for accessing the router in every controller of the application. 10 | * @public 11 | * @returns {sap.ui.core.routing.Router} the router for this component 12 | */ 13 | getRouter : function () { 14 | return this.getOwnerComponent().getRouter(); 15 | }, 16 | 17 | /** 18 | * Convenience method for getting the view model by name in every controller of the application. 19 | * @public 20 | * @param {string} sName the model name 21 | * @returns {sap.ui.model.Model} the model instance 22 | */ 23 | getModel : function (sName) { 24 | return this.getView().getModel(sName); 25 | }, 26 | 27 | /** 28 | * Convenience method for setting the view model in every controller of the application. 29 | * @public 30 | * @param {sap.ui.model.Model} oModel the model instance 31 | * @param {string} sName the model name 32 | * @returns {sap.ui.mvc.View} the view instance 33 | */ 34 | setModel : function (oModel, sName) { 35 | return this.getView().setModel(oModel, sName); 36 | }, 37 | 38 | /** 39 | * Convenience method for getting the resource bundle. 40 | * @public 41 | * @returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component 42 | */ 43 | getResourceBundle : function () { 44 | return this.getOwnerComponent().getModel("i18n").getResourceBundle(); 45 | }, 46 | 47 | /** 48 | * Event handler for navigating back. 49 | * It there is a history entry we go one step back in the browser history 50 | * If not, it will replace the current entry of the browser history with the master route. 51 | * @public 52 | */ 53 | onNavBack : function() { 54 | var sPreviousHash = History.getInstance().getPreviousHash(); 55 | 56 | if (sPreviousHash !== undefined) { 57 | // eslint-disable-next-line sap-no-history-manipulation 58 | history.go(-1); 59 | } else { 60 | this.getRouter().navTo("master", {}, true); 61 | } 62 | } 63 | 64 | }); 65 | 66 | }); -------------------------------------------------------------------------------- /BP/webapp/controller/Detail.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel", 4 | "../model/formatter", 5 | "sap/m/library" 6 | ], function (BaseController, JSONModel, formatter, mobileLibrary) { 7 | "use strict"; 8 | 9 | // shortcut for sap.m.URLHelper 10 | var URLHelper = mobileLibrary.URLHelper; 11 | 12 | return BaseController.extend("ns.BP.controller.Detail", { 13 | 14 | formatter: formatter, 15 | 16 | /* =========================================================== */ 17 | /* lifecycle methods */ 18 | /* =========================================================== */ 19 | 20 | onInit : function () { 21 | // Model used to manipulate control states. The chosen values make sure, 22 | // detail page is busy indication immediately so there is no break in 23 | // between the busy indication for loading the view's meta data 24 | var oViewModel = new JSONModel({ 25 | busy : false, 26 | delay : 0, 27 | lineItemListTitle : this.getResourceBundle().getText("detailLineItemTableHeading") 28 | }); 29 | 30 | this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this); 31 | 32 | this.setModel(oViewModel, "detailView"); 33 | 34 | this.getOwnerComponent().getModel().metadataLoaded().then(this._onMetadataLoaded.bind(this)); 35 | }, 36 | 37 | /* =========================================================== */ 38 | /* event handlers */ 39 | /* =========================================================== */ 40 | 41 | /** 42 | * Event handler when the share by E-Mail button has been clicked 43 | * @public 44 | */ 45 | onSendEmailPress : function () { 46 | var oViewModel = this.getModel("detailView"); 47 | 48 | URLHelper.triggerEmail( 49 | null, 50 | oViewModel.getProperty("/shareSendEmailSubject"), 51 | oViewModel.getProperty("/shareSendEmailMessage") 52 | ); 53 | }, 54 | 55 | 56 | /** 57 | * Updates the item count within the line item table's header 58 | * @param {object} oEvent an event containing the total number of items in the list 59 | * @private 60 | */ 61 | onListUpdateFinished : function (oEvent) { 62 | var sTitle, 63 | iTotalItems = oEvent.getParameter("total"), 64 | oViewModel = this.getModel("detailView"); 65 | 66 | // only update the counter if the length is final 67 | if (this.byId("lineItemsList").getBinding("items").isLengthFinal()) { 68 | if (iTotalItems) { 69 | sTitle = this.getResourceBundle().getText("detailLineItemTableHeadingCount", [iTotalItems]); 70 | } else { 71 | //Display 'Line Items' instead of 'Line items (0)' 72 | sTitle = this.getResourceBundle().getText("detailLineItemTableHeading"); 73 | } 74 | oViewModel.setProperty("/lineItemListTitle", sTitle); 75 | } 76 | }, 77 | 78 | /* =========================================================== */ 79 | /* begin: internal methods */ 80 | /* =========================================================== */ 81 | 82 | /** 83 | * Binds the view to the object path and expands the aggregated line items. 84 | * @function 85 | * @param {sap.ui.base.Event} oEvent pattern match event in route 'object' 86 | * @private 87 | */ 88 | _onObjectMatched : function (oEvent) { 89 | var sObjectId = oEvent.getParameter("arguments").objectId; 90 | this.getModel("appView").setProperty("/layout", "TwoColumnsMidExpanded"); 91 | this.getModel().metadataLoaded().then( function() { 92 | var sObjectPath = this.getModel().createKey("BusinessPartnerSet", { 93 | BusinessPartnerID : sObjectId 94 | }); 95 | this._bindView("/" + sObjectPath); 96 | }.bind(this)); 97 | }, 98 | 99 | /** 100 | * Binds the view to the object path. Makes sure that detail view displays 101 | * a busy indicator while data for the corresponding element binding is loaded. 102 | * @function 103 | * @param {string} sObjectPath path to the object to be bound to the view. 104 | * @private 105 | */ 106 | _bindView : function (sObjectPath) { 107 | // Set busy indicator during view binding 108 | var oViewModel = this.getModel("detailView"); 109 | 110 | // If the view was not bound yet its not busy, only if the binding requests data it is set to busy again 111 | oViewModel.setProperty("/busy", false); 112 | 113 | this.getView().bindElement({ 114 | path : sObjectPath, 115 | events: { 116 | change : this._onBindingChange.bind(this), 117 | dataRequested : function () { 118 | oViewModel.setProperty("/busy", true); 119 | }, 120 | dataReceived: function () { 121 | oViewModel.setProperty("/busy", false); 122 | } 123 | } 124 | }); 125 | }, 126 | 127 | _onBindingChange : function () { 128 | var oView = this.getView(), 129 | oElementBinding = oView.getElementBinding(); 130 | 131 | // No data for the binding 132 | if (!oElementBinding.getBoundContext()) { 133 | this.getRouter().getTargets().display("detailObjectNotFound"); 134 | // if object could not be found, the selection in the master list 135 | // does not make sense anymore. 136 | this.getOwnerComponent().oListSelector.clearMasterListSelection(); 137 | return; 138 | } 139 | 140 | var sPath = oElementBinding.getPath(), 141 | oResourceBundle = this.getResourceBundle(), 142 | oObject = oView.getModel().getObject(sPath), 143 | sObjectId = oObject.BusinessPartnerID, 144 | sObjectName = oObject.CompanyName, 145 | oViewModel = this.getModel("detailView"); 146 | 147 | this.getOwnerComponent().oListSelector.selectAListItem(sPath); 148 | 149 | oViewModel.setProperty("/shareSendEmailSubject", 150 | oResourceBundle.getText("shareSendEmailObjectSubject", [sObjectId])); 151 | oViewModel.setProperty("/shareSendEmailMessage", 152 | oResourceBundle.getText("shareSendEmailObjectMessage", [sObjectName, sObjectId, location.href])); 153 | }, 154 | 155 | _onMetadataLoaded : function () { 156 | // Store original busy indicator delay for the detail view 157 | var iOriginalViewBusyDelay = this.getView().getBusyIndicatorDelay(), 158 | oViewModel = this.getModel("detailView"), 159 | oLineItemTable = this.byId("lineItemsList"), 160 | iOriginalLineItemTableBusyDelay = oLineItemTable.getBusyIndicatorDelay(); 161 | 162 | // Make sure busy indicator is displayed immediately when 163 | // detail view is displayed for the first time 164 | oViewModel.setProperty("/delay", 0); 165 | oViewModel.setProperty("/lineItemTableDelay", 0); 166 | 167 | oLineItemTable.attachEventOnce("updateFinished", function() { 168 | // Restore original busy indicator delay for line item table 169 | oViewModel.setProperty("/lineItemTableDelay", iOriginalLineItemTableBusyDelay); 170 | }); 171 | 172 | // Binding the view will set it to not busy - so the view is always busy if it is not bound 173 | oViewModel.setProperty("/busy", true); 174 | // Restore original busy indicator delay for the detail view 175 | oViewModel.setProperty("/delay", iOriginalViewBusyDelay); 176 | }, 177 | 178 | /** 179 | * Set the full screen mode to false and navigate to master page 180 | */ 181 | onCloseDetailPress: function () { 182 | this.getModel("appView").setProperty("/actionButtonsInfo/midColumn/fullScreen", false); 183 | // No item should be selected on master after detail page is closed 184 | this.getOwnerComponent().oListSelector.clearMasterListSelection(); 185 | this.getRouter().navTo("master"); 186 | }, 187 | 188 | /** 189 | * Toggle between full and non full screen mode. 190 | */ 191 | toggleFullScreen: function () { 192 | var bFullScreen = this.getModel("appView").getProperty("/actionButtonsInfo/midColumn/fullScreen"); 193 | this.getModel("appView").setProperty("/actionButtonsInfo/midColumn/fullScreen", !bFullScreen); 194 | if (!bFullScreen) { 195 | // store current layout and go full screen 196 | this.getModel("appView").setProperty("/previousLayout", this.getModel("appView").getProperty("/layout")); 197 | this.getModel("appView").setProperty("/layout", "MidColumnFullScreen"); 198 | } else { 199 | // reset to previous layout 200 | this.getModel("appView").setProperty("/layout", this.getModel("appView").getProperty("/previousLayout")); 201 | } 202 | } 203 | }); 204 | 205 | }); -------------------------------------------------------------------------------- /BP/webapp/controller/DetailObjectNotFound.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController" 3 | ], function (BaseController) { 4 | "use strict"; 5 | 6 | return BaseController.extend("ns.BP.controller.DetailObjectNotFound", {}); 7 | }); -------------------------------------------------------------------------------- /BP/webapp/controller/DetailObjectNotFound.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController" 3 | ], function (BaseController) { 4 | "use strict"; 5 | 6 | return BaseController.extend("ns.BP.controller.DetailObjectNotFound", {}); 7 | }); 8 | -------------------------------------------------------------------------------- /BP/webapp/controller/ErrorHandler.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/base/Object", 3 | "sap/m/MessageBox" 4 | ], function (UI5Object, MessageBox) { 5 | "use strict"; 6 | 7 | return UI5Object.extend("ns.BP.controller.ErrorHandler", { 8 | 9 | /** 10 | * Handles application errors by automatically attaching to the model events and displaying errors when needed. 11 | * @class 12 | * @param {sap.ui.core.UIComponent} oComponent reference to the app's component 13 | * @public 14 | * @alias ns.BP.controller.ErrorHandler 15 | */ 16 | constructor : function (oComponent) { 17 | this._oResourceBundle = oComponent.getModel("i18n").getResourceBundle(); 18 | this._oComponent = oComponent; 19 | this._oModel = oComponent.getModel(); 20 | this._bMessageOpen = false; 21 | this._sErrorText = this._oResourceBundle.getText("errorText"); 22 | 23 | this._oModel.attachMetadataFailed(function (oEvent) { 24 | var oParams = oEvent.getParameters(); 25 | this._showServiceError(oParams.response); 26 | }, this); 27 | 28 | this._oModel.attachRequestFailed(function (oEvent) { 29 | var oParams = oEvent.getParameters(); 30 | // An entity that was not found in the service is also throwing a 404 error in oData. 31 | // We already cover this case with a notFound target so we skip it here. 32 | // A request that cannot be sent to the server is a technical error that we have to handle though 33 | if (oParams.response.statusCode !== "404" || (oParams.response.statusCode === 404 && oParams.response.responseText.indexOf("Cannot POST") === 0)) { 34 | this._showServiceError(oParams.response); 35 | } 36 | }, this); 37 | }, 38 | 39 | /** 40 | * Shows a {@link sap.m.MessageBox} when a service call has failed. 41 | * Only the first error message will be display. 42 | * @param {string} sDetails a technical error to be displayed on request 43 | * @private 44 | */ 45 | _showServiceError : function (sDetails) { 46 | if (this._bMessageOpen) { 47 | return; 48 | } 49 | this._bMessageOpen = true; 50 | MessageBox.error( 51 | this._sErrorText, 52 | { 53 | id : "serviceErrorMessageBox", 54 | details : sDetails, 55 | styleClass : this._oComponent.getContentDensityClass(), 56 | actions : [MessageBox.Action.CLOSE], 57 | onClose : function () { 58 | this._bMessageOpen = false; 59 | }.bind(this) 60 | } 61 | ); 62 | } 63 | 64 | }); 65 | 66 | }); -------------------------------------------------------------------------------- /BP/webapp/controller/ListSelector.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/base/Object", 3 | "sap/base/Log" 4 | ], function (BaseObject, Log) { 5 | "use strict"; 6 | 7 | return BaseObject.extend("ns.BP.controller.ListSelector", { 8 | 9 | /** 10 | * Provides a convenience API for selecting list items. All the functions will wait until the initial load of the a List passed to the instance by the setBoundMasterList 11 | * function. 12 | * @class 13 | * @public 14 | * @alias ns.BP.controller.ListSelector 15 | */ 16 | 17 | constructor : function () { 18 | this._oWhenListHasBeenSet = new Promise(function (fnResolveListHasBeenSet) { 19 | this._fnResolveListHasBeenSet = fnResolveListHasBeenSet; 20 | }.bind(this)); 21 | // This promise needs to be created in the constructor, since it is allowed to 22 | // invoke selectItem functions before calling setBoundMasterList 23 | this.oWhenListLoadingIsDone = new Promise(function (fnResolve, fnReject) { 24 | // Used to wait until the setBound masterList function is invoked 25 | this._oWhenListHasBeenSet 26 | .then(function (oList) { 27 | oList.getBinding("items").attachEventOnce("dataReceived", 28 | function () { 29 | if (this._oList.getItems().length) { 30 | fnResolve({ 31 | list : oList 32 | }); 33 | } else { 34 | // No items in the list 35 | fnReject({ 36 | list : oList 37 | }); 38 | } 39 | }.bind(this) 40 | ); 41 | }.bind(this)); 42 | }.bind(this)); 43 | }, 44 | 45 | /** 46 | * A bound list should be passed in here. Should be done, before the list has received its initial data from the server. 47 | * May only be invoked once per ListSelector instance. 48 | * @param {sap.m.List} oList The list all the select functions will be invoked on. 49 | * @public 50 | */ 51 | setBoundMasterList : function (oList) { 52 | this._oList = oList; 53 | this._fnResolveListHasBeenSet(oList); 54 | }, 55 | 56 | /** 57 | * Tries to select and scroll to a list item with a matching binding context. If there are no items matching the binding context or the ListMode is none, 58 | * no selection/scrolling will happen 59 | * @param {string} sBindingPath the binding path matching the binding path of a list item 60 | * @public 61 | */ 62 | selectAListItem : function (sBindingPath) { 63 | 64 | this.oWhenListLoadingIsDone.then( 65 | function () { 66 | var oList = this._oList, 67 | oSelectedItem; 68 | 69 | if (oList.getMode() === "None") { 70 | return; 71 | } 72 | 73 | oSelectedItem = oList.getSelectedItem(); 74 | 75 | // skip update if the current selection is already matching the object path 76 | if (oSelectedItem && oSelectedItem.getBindingContext().getPath() === sBindingPath) { 77 | return; 78 | } 79 | 80 | oList.getItems().some(function (oItem) { 81 | if (oItem.getBindingContext() && oItem.getBindingContext().getPath() === sBindingPath) { 82 | oList.setSelectedItem(oItem); 83 | return true; 84 | } 85 | }); 86 | }.bind(this), 87 | function () { 88 | Log.warning("Could not select the list item with the path" + sBindingPath + " because the list encountered an error or had no items"); 89 | } 90 | ); 91 | }, 92 | 93 | /** 94 | * Removes all selections from master list. 95 | * Does not trigger 'selectionChange' event on master list, though. 96 | * @public 97 | */ 98 | clearMasterListSelection : function () { 99 | //use promise to make sure that 'this._oList' is available 100 | this._oWhenListHasBeenSet.then(function () { 101 | this._oList.removeSelections(true); 102 | }.bind(this)); 103 | } 104 | }); 105 | }); -------------------------------------------------------------------------------- /BP/webapp/controller/Master.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel", 4 | "sap/ui/model/Filter", 5 | "sap/ui/model/Sorter", 6 | "sap/ui/model/FilterOperator", 7 | "sap/m/GroupHeaderListItem", 8 | "sap/ui/Device", 9 | "sap/ui/core/Fragment", 10 | "../model/formatter" 11 | ], function (BaseController, JSONModel, Filter, Sorter, FilterOperator, GroupHeaderListItem, Device, Fragment, formatter) { 12 | "use strict"; 13 | 14 | return BaseController.extend("ns.BP.controller.Master", { 15 | 16 | formatter: formatter, 17 | 18 | /* =========================================================== */ 19 | /* lifecycle methods */ 20 | /* =========================================================== */ 21 | 22 | /** 23 | * Called when the master list controller is instantiated. It sets up the event handling for the master/detail communication and other lifecycle tasks. 24 | * @public 25 | */ 26 | onInit : function () { 27 | // Control state model 28 | var oList = this.byId("list"), 29 | oViewModel = this._createViewModel(), 30 | // Put down master list's original value for busy indicator delay, 31 | // so it can be restored later on. Busy handling on the master list is 32 | // taken care of by the master list itself. 33 | iOriginalBusyDelay = oList.getBusyIndicatorDelay(); 34 | 35 | this._oGroupFunctions = { 36 | BusinessPartnerRole : function(oContext) { 37 | var iNumber = oContext.getProperty('BusinessPartnerRole'), 38 | key, text; 39 | if (iNumber <= 20) { 40 | key = "LE20"; 41 | text = this.getResourceBundle().getText("masterGroup1Header1"); 42 | } else { 43 | key = "GT20"; 44 | text = this.getResourceBundle().getText("masterGroup1Header2"); 45 | } 46 | return { 47 | key: key, 48 | text: text 49 | }; 50 | }.bind(this) 51 | }; 52 | 53 | this._oList = oList; 54 | // keeps the filter and search state 55 | this._oListFilterState = { 56 | aFilter : [], 57 | aSearch : [] 58 | }; 59 | 60 | this.setModel(oViewModel, "masterView"); 61 | // Make sure, busy indication is showing immediately so there is no 62 | // break after the busy indication for loading the view's meta data is 63 | // ended (see promise 'oWhenMetadataIsLoaded' in AppController) 64 | oList.attachEventOnce("updateFinished", function(){ 65 | // Restore original busy indicator delay for the list 66 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 67 | }); 68 | 69 | this.getView().addEventDelegate({ 70 | onBeforeFirstShow: function () { 71 | this.getOwnerComponent().oListSelector.setBoundMasterList(oList); 72 | }.bind(this) 73 | }); 74 | 75 | this.getRouter().getRoute("master").attachPatternMatched(this._onMasterMatched, this); 76 | this.getRouter().attachBypassed(this.onBypassed, this); 77 | }, 78 | 79 | /* =========================================================== */ 80 | /* event handlers */ 81 | /* =========================================================== */ 82 | 83 | /** 84 | * After list data is available, this handler method updates the 85 | * master list counter 86 | * @param {sap.ui.base.Event} oEvent the update finished event 87 | * @public 88 | */ 89 | onUpdateFinished : function (oEvent) { 90 | // update the master list object counter after new data is loaded 91 | this._updateListItemCount(oEvent.getParameter("total")); 92 | }, 93 | 94 | /** 95 | * Event handler for the master search field. Applies current 96 | * filter value and triggers a new search. If the search field's 97 | * 'refresh' button has been pressed, no new search is triggered 98 | * and the list binding is refresh instead. 99 | * @param {sap.ui.base.Event} oEvent the search event 100 | * @public 101 | */ 102 | onSearch : function (oEvent) { 103 | if (oEvent.getParameters().refreshButtonPressed) { 104 | // Search field's 'refresh' button has been pressed. 105 | // This is visible if you select any master list item. 106 | // In this case no new search is triggered, we only 107 | // refresh the list binding. 108 | this.onRefresh(); 109 | return; 110 | } 111 | 112 | var sQuery = oEvent.getParameter("query"); 113 | 114 | if (sQuery) { 115 | this._oListFilterState.aSearch = [new Filter("CompanyName", FilterOperator.Contains, sQuery)]; 116 | } else { 117 | this._oListFilterState.aSearch = []; 118 | } 119 | this._applyFilterSearch(); 120 | 121 | }, 122 | 123 | /** 124 | * Event handler for refresh event. Keeps filter, sort 125 | * and group settings and refreshes the list binding. 126 | * @public 127 | */ 128 | onRefresh : function () { 129 | this._oList.getBinding("items").refresh(); 130 | }, 131 | 132 | /** 133 | * Event handler for the filter, sort and group buttons to open the ViewSettingsDialog. 134 | * @param {sap.ui.base.Event} oEvent the button press event 135 | * @public 136 | */ 137 | onOpenViewSettings : function (oEvent) { 138 | var sDialogTab = "filter"; 139 | if (oEvent.getSource() instanceof sap.m.Button) { 140 | var sButtonId = oEvent.getSource().getId(); 141 | if (sButtonId.match("sort")) { 142 | sDialogTab = "sort"; 143 | } else if (sButtonId.match("group")) { 144 | sDialogTab = "group"; 145 | } 146 | } 147 | // load asynchronous XML fragment 148 | if (!this.byId("viewSettingsDialog")) { 149 | Fragment.load({ 150 | id: this.getView().getId(), 151 | name: "ns.BP.view.ViewSettingsDialog", 152 | controller: this 153 | }).then(function(oDialog){ 154 | // connect dialog to the root view of this component (models, lifecycle) 155 | this.getView().addDependent(oDialog); 156 | oDialog.addStyleClass(this.getOwnerComponent().getContentDensityClass()); 157 | oDialog.open(sDialogTab); 158 | }.bind(this)); 159 | } else { 160 | this.byId("viewSettingsDialog").open(sDialogTab); 161 | } 162 | }, 163 | 164 | /** 165 | * Event handler called when ViewSettingsDialog has been confirmed, i.e. 166 | * has been closed with 'OK'. In the case, the currently chosen filters, sorters or groupers 167 | * are applied to the master list, which can also mean that they 168 | * are removed from the master list, in case they are 169 | * removed in the ViewSettingsDialog. 170 | * @param {sap.ui.base.Event} oEvent the confirm event 171 | * @public 172 | */ 173 | onConfirmViewSettingsDialog : function (oEvent) { 174 | var aFilterItems = oEvent.getParameters().filterItems, 175 | aFilters = [], 176 | aCaptions = []; 177 | 178 | // update filter state: 179 | // combine the filter array and the filter string 180 | aFilterItems.forEach(function (oItem) { 181 | switch (oItem.getKey()) { 182 | case "Filter1" : 183 | aFilters.push(new Filter("BusinessPartnerRole", FilterOperator.LE, 100)); 184 | break; 185 | case "Filter2" : 186 | aFilters.push(new Filter("BusinessPartnerRole", FilterOperator.GT, 100)); 187 | break; 188 | default : 189 | break; 190 | } 191 | aCaptions.push(oItem.getText()); 192 | }); 193 | 194 | this._oListFilterState.aFilter = aFilters; 195 | this._updateFilterBar(aCaptions.join(", ")); 196 | this._applyFilterSearch(); 197 | this._applySortGroup(oEvent); 198 | }, 199 | 200 | /** 201 | * Apply the chosen sorter and grouper to the master list 202 | * @param {sap.ui.base.Event} oEvent the confirm event 203 | * @private 204 | */ 205 | _applySortGroup: function (oEvent) { 206 | var mParams = oEvent.getParameters(), 207 | sPath, 208 | bDescending, 209 | aSorters = []; 210 | // apply sorter to binding 211 | // (grouping comes before sorting) 212 | if (mParams.groupItem) { 213 | sPath = mParams.groupItem.getKey(); 214 | bDescending = mParams.groupDescending; 215 | var vGroup = this._oGroupFunctions[sPath]; 216 | aSorters.push(new Sorter(sPath, bDescending, vGroup)); 217 | } 218 | sPath = mParams.sortItem.getKey(); 219 | bDescending = mParams.sortDescending; 220 | aSorters.push(new Sorter(sPath, bDescending)); 221 | this._oList.getBinding("items").sort(aSorters); 222 | }, 223 | 224 | /** 225 | * Event handler for the list selection event 226 | * @param {sap.ui.base.Event} oEvent the list selectionChange event 227 | * @public 228 | */ 229 | onSelectionChange : function (oEvent) { 230 | var oList = oEvent.getSource(), 231 | bSelected = oEvent.getParameter("selected"); 232 | 233 | // skip navigation when deselecting an item in multi selection mode 234 | if (!(oList.getMode() === "MultiSelect" && !bSelected)) { 235 | // get the list item, either from the listItem parameter or from the event's source itself (will depend on the device-dependent mode). 236 | this._showDetail(oEvent.getParameter("listItem") || oEvent.getSource()); 237 | } 238 | }, 239 | 240 | /** 241 | * Event handler for the bypassed event, which is fired when no routing pattern matched. 242 | * If there was an object selected in the master list, that selection is removed. 243 | * @public 244 | */ 245 | onBypassed : function () { 246 | this._oList.removeSelections(true); 247 | }, 248 | 249 | /** 250 | * Used to create GroupHeaders with non-capitalized caption. 251 | * These headers are inserted into the master list to 252 | * group the master list's items. 253 | * @param {Object} oGroup group whose text is to be displayed 254 | * @public 255 | * @returns {sap.m.GroupHeaderListItem} group header with non-capitalized caption. 256 | */ 257 | createGroupHeader : function (oGroup) { 258 | return new GroupHeaderListItem({ 259 | title : oGroup.text, 260 | upperCase : false 261 | }); 262 | }, 263 | 264 | /** 265 | * Event handler for navigating back. 266 | * We navigate back in the browser historz 267 | * @public 268 | */ 269 | onNavBack : function() { 270 | // eslint-disable-next-line sap-no-history-manipulation 271 | history.go(-1); 272 | }, 273 | 274 | /* =========================================================== */ 275 | /* begin: internal methods */ 276 | /* =========================================================== */ 277 | 278 | 279 | _createViewModel : function() { 280 | return new JSONModel({ 281 | isFilterBarVisible: false, 282 | filterBarLabel: "", 283 | delay: 0, 284 | title: this.getResourceBundle().getText("masterTitleCount", [0]), 285 | noDataText: this.getResourceBundle().getText("masterListNoDataText"), 286 | sortBy: "CompanyName", 287 | groupBy: "None" 288 | }); 289 | }, 290 | 291 | _onMasterMatched : function() { 292 | //Set the layout property of the FCL control to 'OneColumn' 293 | this.getModel("appView").setProperty("/layout", "OneColumn"); 294 | }, 295 | 296 | /** 297 | * Shows the selected item on the detail page 298 | * On phones a additional history entry is created 299 | * @param {sap.m.ObjectListItem} oItem selected Item 300 | * @private 301 | */ 302 | _showDetail : function (oItem) { 303 | var bReplace = !Device.system.phone; 304 | // set the layout property of FCL control to show two columns 305 | this.getModel("appView").setProperty("/layout", "TwoColumnsMidExpanded"); 306 | this.getRouter().navTo("object", { 307 | objectId : oItem.getBindingContext().getProperty("BusinessPartnerID") 308 | }, bReplace); 309 | }, 310 | 311 | /** 312 | * Sets the item count on the master list header 313 | * @param {integer} iTotalItems the total number of items in the list 314 | * @private 315 | */ 316 | _updateListItemCount : function (iTotalItems) { 317 | var sTitle; 318 | // only update the counter if the length is final 319 | if (this._oList.getBinding("items").isLengthFinal()) { 320 | sTitle = this.getResourceBundle().getText("masterTitleCount", [iTotalItems]); 321 | this.getModel("masterView").setProperty("/title", sTitle); 322 | } 323 | }, 324 | 325 | /** 326 | * Internal helper method to apply both filter and search state together on the list binding 327 | * @private 328 | */ 329 | _applyFilterSearch : function () { 330 | var aFilters = this._oListFilterState.aSearch.concat(this._oListFilterState.aFilter), 331 | oViewModel = this.getModel("masterView"); 332 | this._oList.getBinding("items").filter(aFilters, "Application"); 333 | // changes the noDataText of the list in case there are no filter results 334 | if (aFilters.length !== 0) { 335 | oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataWithFilterOrSearchText")); 336 | } else if (this._oListFilterState.aSearch.length > 0) { 337 | // only reset the no data text to default when no new search was triggered 338 | oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataText")); 339 | } 340 | }, 341 | 342 | /** 343 | * Internal helper method that sets the filter bar visibility property and the label's caption to be shown 344 | * @param {string} sFilterBarText the selected filter value 345 | * @private 346 | */ 347 | _updateFilterBar : function (sFilterBarText) { 348 | var oViewModel = this.getModel("masterView"); 349 | oViewModel.setProperty("/isFilterBarVisible", (this._oListFilterState.aFilter.length > 0)); 350 | oViewModel.setProperty("/filterBarLabel", this.getResourceBundle().getText("masterFilterBarText", [sFilterBarText])); 351 | } 352 | 353 | }); 354 | 355 | }); -------------------------------------------------------------------------------- /BP/webapp/controller/NotFound.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController" 3 | ], function (BaseController) { 4 | "use strict"; 5 | 6 | return BaseController.extend("ns.BP.controller.NotFound", { 7 | 8 | onInit: function () { 9 | this.getRouter().getTarget("notFound").attachDisplay(this._onNotFoundDisplayed, this); 10 | }, 11 | 12 | _onNotFoundDisplayed : function () { 13 | this.getModel("appView").setProperty("/layout", "OneColumn"); 14 | } 15 | }); 16 | }); -------------------------------------------------------------------------------- /BP/webapp/i18n/i18n.properties: -------------------------------------------------------------------------------- 1 | # This is the resource bundle for BusinessPartner 2 | # __ldi.translation.uuid= 3 | 4 | #XTIT: Application name 5 | appTitle=BusinessPartner 6 | 7 | #YDES: Application description 8 | appDescription= 9 | 10 | #~~~ Master View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | #XTIT: Master view title with placeholder for the number of items 13 | masterTitleCount=BusinessPartnerSet ({0}) 14 | 15 | #XTOL: Tooltip for the search field 16 | masterSearchTooltip=Enter an BusinessPartnerSet name or a part of it. 17 | 18 | #XBLI: text for a list with no data 19 | masterListNoDataText=No BusinessPartnerSetPlural are currently available 20 | 21 | #XBLI: text for a list with no data with filter or search 22 | masterListNoDataWithFilterOrSearchText=No matching BusinessPartnerSetPlural found 23 | 24 | #XSEL: Option to sort the master list by CompanyName 25 | masterSort1=Sort By CompanyName 26 | 27 | #XSEL: Option to sort the master list by BusinessPartnerRole 28 | masterSort2=Sort By BusinessPartnerRole 29 | 30 | #XSEL: Option to filter the master list by BusinessPartnerRole 31 | masterFilterName=BusinessPartnerRole 32 | 33 | #XSEL: Option to not filter the master list 34 | masterFilterNone=none 35 | 36 | 37 | #XSEL: Option to filter the master list by EmailAddress if the value is less than 100 38 | masterFilter1=100 EmailAddress 39 | 40 | #XSEL: Option to filter the master list by EmailAddress if the value is greater than 100 41 | masterFilter2=100 EmailAddress 42 | 43 | #YMSG: Filter text that is displayed above the master list 44 | masterFilterBarText=Filtered by {0} 45 | 46 | #XSEL: Option to not group the master list 47 | masterGroupNone=(Not grouped) 48 | 49 | #XSEL: Option to group the master list by BusinessPartnerRole 50 | masterGroup1=BusinessPartnerRole Group 51 | 52 | #XGRP: Group header BusinessPartnerRole 53 | masterGroup1Header1=BusinessPartnerRole 20 or less 54 | 55 | #XGRP: Group header BusinessPartnerRole 56 | masterGroup1Header2=BusinessPartnerRole higher than 20 57 | 58 | #~~~ Detail View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 59 | 60 | #XTOL: Icon Tab Bar Info 61 | detailIconTabBarInfo=Info 62 | 63 | #XTOL: Icon Tab Bar Attachments 64 | detailIconTabBarAttachments=Attachments 65 | 66 | #XTOL: Tooltip text for close column button 67 | closeColumn=Close 68 | 69 | #XBLI: Text for the ToProducts table with no data 70 | detailLineItemTableNoDataText=No ToProductsPlural 71 | 72 | #XTIT: Title of the ToProducts table 73 | detailLineItemTableHeading=Products 74 | 75 | #XTIT: Title of the ToProducts table 76 | detailLineItemTableHeadingCount= Products ({0}) 77 | 78 | #XGRP: Title for the Name column in the ToProducts table 79 | detailLineItemTableIDColumn=Product Name 80 | 81 | #XGRP: Title for the Price column in the ToProducts table 82 | detailLineItemTableUnitNumberColumn=Price 83 | 84 | #XTIT: Send E-Mail subject 85 | shareSendEmailObjectSubject=Email subject including object identifier PLEASE REPLACE ACCORDING TO YOUR USE CASE {0} 86 | 87 | #YMSG: Send E-Mail message 88 | shareSendEmailObjectMessage=Email body PLEASE REPLACE ACCORDING TO YOUR USE CASE {0} (id: {1})\r\n{2} 89 | 90 | #XBUT: Text for the send e-mail button 91 | sendEmail=Send E-Mail 92 | 93 | #XTIT: Title text for the price 94 | priceTitle=BusinessPartnerRole 95 | 96 | #~~~ Not Found View ~~~~~~~~~~~~~~~~~~~~~~~ 97 | 98 | #XTIT: Not found view title 99 | notFoundTitle=Not Found 100 | 101 | #YMSG: The BusinessPartnerSet not found text is displayed when there is no BusinessPartnerSet with this id 102 | noObjectFoundText=This BusinessPartnerSet is not available 103 | 104 | #YMSG: The not found text is displayed when there was an error loading the resource (404 error) 105 | notFoundText=The requested resource was not found 106 | 107 | #~~~ Not Available View ~~~~~~~~~~~~~~~~~~~~~~~ 108 | 109 | #XTIT: Master view title 110 | notAvailableViewTitle=BusinessPartnerSet 111 | 112 | #~~~ Error Handling ~~~~~~~~~~~~~~~~~~~~~~~ 113 | 114 | #YMSG: Error dialog description 115 | errorText=Sorry, a technical error occurred! Please try again later. -------------------------------------------------------------------------------- /BP/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BusinessPartner 7 | 8 | 19 | 20 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /BP/webapp/localService/mockserver.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/util/MockServer", 3 | "sap/ui/model/json/JSONModel", 4 | "sap/base/Log", 5 | "sap/base/util/UriParameters" 6 | ], function (MockServer, JSONModel, Log, UriParameters) { 7 | "use strict"; 8 | 9 | var oMockServer, 10 | _sAppPath = "ns/BP/", 11 | _sJsonFilesPath = _sAppPath + "localService/mockdata"; 12 | 13 | var oMockServerInterface = { 14 | 15 | /** 16 | * Initializes the mock server asynchronously. 17 | * You can configure the delay with the URL parameter "serverDelay". 18 | * The local mock data in this folder is returned instead of the real data for testing. 19 | * @protected 20 | * @param {object} [oOptionsParameter] init parameters for the mockserver 21 | * @returns{Promise} a promise that is resolved when the mock server has been started 22 | */ 23 | init : function (oOptionsParameter) { 24 | var oOptions = oOptionsParameter || {}; 25 | 26 | return new Promise(function(fnResolve, fnReject) { 27 | var sManifestUrl = sap.ui.require.toUrl(_sAppPath + "manifest.json"), 28 | oManifestModel = new JSONModel(sManifestUrl); 29 | 30 | oManifestModel.attachRequestCompleted(function () { 31 | var oUriParameters = new UriParameters(window.location.href), 32 | // parse manifest for local metadata URI 33 | sJsonFilesUrl = sap.ui.require.toUrl(_sJsonFilesPath), 34 | oMainDataSource = oManifestModel.getProperty("/sap.app/dataSources/mainService"), 35 | sMetadataUrl = sap.ui.require.toUrl(_sAppPath + oMainDataSource.settings.localUri), 36 | // ensure there is a trailing slash 37 | sMockServerUrl = /.*\/$/.test(oMainDataSource.uri) ? oMainDataSource.uri : oMainDataSource.uri + "/"; 38 | // ensure the URL to be relative to the application 39 | sMockServerUrl = sMockServerUrl && new URI(sMockServerUrl).absoluteTo(sap.ui.require.toUrl(_sAppPath)).toString(); 40 | 41 | // create a mock server instance or stop the existing one to reinitialize 42 | if (!oMockServer) { 43 | oMockServer = new MockServer({ 44 | rootUri: sMockServerUrl 45 | }); 46 | } else { 47 | oMockServer.stop(); 48 | } 49 | 50 | // configure mock server with the given options or a default delay of 0.5s 51 | MockServer.config({ 52 | autoRespond : true, 53 | autoRespondAfter : (oOptions.delay || oUriParameters.get("serverDelay") || 500) 54 | }); 55 | 56 | // simulate all requests using mock data 57 | oMockServer.simulate(sMetadataUrl, { 58 | sMockdataBaseUrl : sJsonFilesUrl, 59 | bGenerateMissingMockData : true 60 | }); 61 | 62 | var aRequests = oMockServer.getRequests(); 63 | 64 | // compose an error response for each request 65 | var fnResponse = function (iErrCode, sMessage, aRequest) { 66 | aRequest.response = function(oXhr){ 67 | oXhr.respond(iErrCode, {"Content-Type": "text/plain;charset=utf-8"}, sMessage); 68 | }; 69 | }; 70 | 71 | // simulate metadata errors 72 | if (oOptions.metadataError || oUriParameters.get("metadataError")) { 73 | aRequests.forEach(function (aEntry) { 74 | if (aEntry.path.toString().indexOf("$metadata") > -1) { 75 | fnResponse(500, "metadata Error", aEntry); 76 | } 77 | }); 78 | } 79 | 80 | // simulate request errors 81 | var sErrorParam = oOptions.errorType || oUriParameters.get("errorType"), 82 | iErrorCode = sErrorParam === "badRequest" ? 400 : 500; 83 | if (sErrorParam) { 84 | aRequests.forEach(function (aEntry) { 85 | fnResponse(iErrorCode, sErrorParam, aEntry); 86 | }); 87 | } 88 | 89 | // custom mock behaviour may be added here 90 | 91 | // set requests and start the server 92 | oMockServer.setRequests(aRequests); 93 | oMockServer.start(); 94 | 95 | Log.info("Running the app with mock data"); 96 | fnResolve(); 97 | }); 98 | 99 | oManifestModel.attachRequestFailed(function () { 100 | var sError = "Failed to load application manifest"; 101 | 102 | Log.error(sError); 103 | fnReject(new Error(sError)); 104 | }); 105 | }); 106 | }, 107 | 108 | /** 109 | * @public returns the mockserver of the app, should be used in integration tests 110 | * @returns {sap.ui.core.util.MockServer} the mockserver instance 111 | */ 112 | getMockServer : function () { 113 | return oMockServer; 114 | } 115 | }; 116 | 117 | return oMockServerInterface; 118 | }); -------------------------------------------------------------------------------- /BP/webapp/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "_version": "1.12.0", 3 | "sap.app": { 4 | "id": "ns.BP", 5 | "type": "application", 6 | "i18n": "i18n/i18n.properties", 7 | "title": "{{appTitle}}", 8 | "description": "{{appDescription}}", 9 | "applicationVersion": { 10 | "version": "1.0.0" 11 | }, 12 | "ach": "set-ach", 13 | "resources": "resources.json", 14 | "dataSources": { 15 | "mainService": { 16 | "uri": "sap/opu/odata/iwbep/GWSAMPLE_BASIC/", 17 | "type": "OData", 18 | "settings": { 19 | "odataVersion": "2.0", 20 | "localUri": "localService/metadata.xml" 21 | } 22 | } 23 | } 24 | }, 25 | "sap.fiori": { 26 | "registrationIds": [], 27 | "archeType": "transactional" 28 | }, 29 | "sap.ui": { 30 | "technology": "UI5", 31 | "icons": { 32 | "icon": "sap-icon://detail-view", 33 | "favIcon": "", 34 | "phone": "", 35 | "phone@2": "", 36 | "tablet": "", 37 | "tablet@2": "" 38 | }, 39 | "deviceTypes": { 40 | "desktop": true, 41 | "tablet": true, 42 | "phone": true 43 | } 44 | }, 45 | "sap.ui5": { 46 | "rootView": { 47 | "viewName": "ns.BP.view.App", 48 | "type": "XML", 49 | "async": true, 50 | "id": "app" 51 | }, 52 | "dependencies": { 53 | "minUI5Version": "1.66.0", 54 | "libs": { 55 | "sap.ui.core": {}, 56 | "sap.m": {}, 57 | "sap.f": {} 58 | } 59 | }, 60 | "contentDensities": { 61 | "compact": true, 62 | "cozy": true 63 | }, 64 | "models": { 65 | "i18n": { 66 | "type": "sap.ui.model.resource.ResourceModel", 67 | "settings": { 68 | "bundleName": "ns.BP.i18n.i18n" 69 | } 70 | }, 71 | "": { 72 | "dataSource": "mainService", 73 | "preload": true, 74 | "settings": { 75 | "defaultBindingMode": "TwoWay", 76 | "defaultCountMode": "Inline", 77 | "refreshAfterChange": false 78 | } 79 | } 80 | }, 81 | "routing": { 82 | "config": { 83 | "routerClass": "sap.f.routing.Router", 84 | "viewType": "XML", 85 | "viewPath": "ns.BP.view", 86 | "controlId": "layout", 87 | "controlAggregation": "beginColumnPages", 88 | "bypassed": { 89 | "target": "notFound" 90 | }, 91 | "async": true 92 | }, 93 | "routes": [ 94 | { 95 | "pattern": "", 96 | "name": "master", 97 | "target": "master" 98 | }, 99 | { 100 | "pattern": "BusinessPartnerSet/{objectId}", 101 | "name": "object", 102 | "target": [ 103 | "master", 104 | "object" 105 | ] 106 | } 107 | ], 108 | "targets": { 109 | "master": { 110 | "viewName": "Master", 111 | "viewLevel": 1, 112 | "viewId": "master" 113 | }, 114 | "object": { 115 | "viewName": "Detail", 116 | "viewId": "detail", 117 | "viewLevel": 1, 118 | "controlAggregation": "midColumnPages" 119 | }, 120 | "detailObjectNotFound": { 121 | "viewName": "DetailObjectNotFound", 122 | "viewId": "detailObjectNotFound", 123 | "controlAggregation": "midColumnPages" 124 | }, 125 | "notFound": { 126 | "viewName": "NotFound", 127 | "viewId": "notFound" 128 | } 129 | } 130 | } 131 | }, 132 | "sap.cloud": { 133 | "public": true, 134 | "service": "bp_ServiceManaged" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /BP/webapp/model/formatter.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([], function () { 2 | "use strict"; 3 | 4 | return { 5 | /** 6 | * Rounds the currency value to 2 digits 7 | * 8 | * @public 9 | * @param {string} sValue value to be formatted 10 | * @returns {string} formatted currency value with 2 digits 11 | */ 12 | currencyValue : function (sValue) { 13 | if (!sValue) { 14 | return ""; 15 | } 16 | 17 | return parseFloat(sValue).toFixed(2); 18 | } 19 | }; 20 | }); -------------------------------------------------------------------------------- /BP/webapp/model/models.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/model/json/JSONModel", 3 | "sap/ui/Device" 4 | ], function (JSONModel, Device) { 5 | "use strict"; 6 | 7 | return { 8 | createDeviceModel : function () { 9 | var oModel = new JSONModel(Device); 10 | oModel.setDefaultBindingMode("OneWay"); 11 | return oModel; 12 | } 13 | }; 14 | }); -------------------------------------------------------------------------------- /BP/webapp/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Testing Overview 5 | 6 | 7 | 8 | 9 |

Testing Overview

10 |

This is an overview page of various ways to test the generated app during development.
Choose one of the access points below to launch the app as a standalone application.

11 | 12 | 23 | 24 | -------------------------------------------------------------------------------- /BP/webapp/test/initMockServer.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "../localService/mockserver", 3 | "sap/m/MessageBox" 4 | ], function (mockserver, MessageBox) { 5 | "use strict"; 6 | 7 | var aMockservers = []; 8 | 9 | // initialize the mock server 10 | aMockservers.push(mockserver.init()); 11 | 12 | Promise.all(aMockservers).catch(function (oError) { 13 | MessageBox.error(oError.message); 14 | }).finally(function () { 15 | // initialize the embedded component on the HTML page 16 | sap.ui.require(["sap/ui/core/ComponentSupport"]); 17 | }); 18 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/AllJourneys.js: -------------------------------------------------------------------------------- 1 | // We cannot provide stable mock data out of the template. 2 | // If you introduce mock data, by adding .json files in your webapp/localService/mockdata folder you have to provide the following minimum data: 3 | // * At least 3 BusinessPartnerSet in the list 4 | // * All 3 BusinessPartnerSet have at least one ToProducts 5 | 6 | sap.ui.define([ 7 | "sap/ui/test/Opa5", 8 | "./arrangements/Startup", 9 | "./MasterJourney", 10 | "./NavigationJourney", 11 | "./NotFoundJourney", 12 | "./BusyJourney" 13 | ], function (Opa5, Startup) { 14 | "use strict"; 15 | Opa5.extendConfig({ 16 | arrangements: new Startup(), 17 | viewNamespace: "ns.BP.view.", 18 | autoWait: true 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /BP/webapp/test/integration/BusyJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "sap/ui/Device", 6 | "./pages/App", 7 | "./pages/Master" 8 | ], function (opaTest, Device) { 9 | "use strict"; 10 | 11 | var iDelay = (Device.browser.msie || Device.browser.edge) ? 1500 : 1000; 12 | 13 | QUnit.module("Desktop busy indication"); 14 | 15 | opaTest("Should see a global busy indication while loading the metadata", function (Given, When, Then) { 16 | // Arrangements 17 | Given.iStartMyApp({delay : iDelay}); 18 | 19 | // Assertions 20 | Then.onTheAppPage.iShouldSeeTheBusyIndicator(); 21 | }); 22 | 23 | opaTest("Should see a busy indication on the master after loading the metadata", function (Given, When, Then) { 24 | // Assertions 25 | Then.onTheMasterPage.iShouldSeeTheBusyIndicator(); 26 | 27 | // Cleanup 28 | Then.iTeardownMyApp(); 29 | }); 30 | 31 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/BusyJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "sap/ui/Device", 6 | "./pages/App", 7 | "./pages/Master" 8 | ], function (opaTest, Device) { 9 | "use strict"; 10 | 11 | var iDelay = (Device.browser.msie || Device.browser.edge) ? 1500 : 1000; 12 | 13 | QUnit.module("Phone busy indication"); 14 | 15 | opaTest("Should see a global busy indication while loading the metadata", function (Given, When, Then) { 16 | // Arrangements 17 | Given.iStartMyApp({delay : iDelay}); 18 | 19 | // Assertions 20 | Then.onTheAppPage.iShouldSeeTheBusyIndicator(); 21 | }); 22 | 23 | opaTest("Should see a busy indication on the master after loading the metadata", function (Given, When, Then) { 24 | // Assertions 25 | Then.onTheMasterPage.iShouldSeeTheBusyIndicator(); 26 | 27 | //Cleanup 28 | Then.iTeardownMyApp(); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /BP/webapp/test/integration/MasterJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Master" 6 | ], function (opaTest) { 7 | "use strict"; 8 | 9 | QUnit.module("Master List"); 10 | 11 | opaTest("Should see the master list with all entries", function (Given, When, Then) { 12 | // Arrangements 13 | Given.iStartMyApp(); 14 | 15 | // Assertions 16 | Then.onTheMasterPage.iShouldSeeTheList(). 17 | and.theListShouldHaveAllEntries(). 18 | and.theHeaderShouldDisplayAllEntries(). 19 | and.theListShouldContainOnlyFormattedUnitNumbers(); 20 | }); 21 | 22 | opaTest("Search for the First object should deliver results that contain the firstObject in the name", function (Given, When, Then) { 23 | //Actions 24 | When.onTheMasterPage.iSearchForTheFirstObject(); 25 | 26 | // Assertions 27 | Then.onTheMasterPage.theListShowsOnlyObjectsWithTheSearchStringInTheirTitle(); 28 | }); 29 | 30 | opaTest("Entering something that cannot be found into search field and pressing search field's refresh should leave the list as it was", function (Given, When, Then) { 31 | //Actions 32 | When.onTheMasterPage.iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh(); 33 | 34 | // Assertions 35 | Then.onTheMasterPage.theListHasEntries(); 36 | }); 37 | 38 | opaTest("Entering something that cannot be found into search field and pressing 'search' should display the list's 'not found' message", function (Given, When, Then) { 39 | //Actions 40 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 41 | 42 | // Assertions 43 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(). 44 | and.theListHeaderDisplaysZeroHits(); 45 | }); 46 | 47 | opaTest("Should display items again if the searchfield is emptied", function (Given, When, Then) { 48 | //Actions 49 | When.onTheMasterPage.iClearTheSearch(); 50 | 51 | // Assertions 52 | Then.onTheMasterPage.theListShouldHaveAllEntries(); 53 | }); 54 | 55 | opaTest("MasterList Sorting on Name", function(Given, When, Then) { 56 | // Actions 57 | When.onTheMasterPage.iSortTheListOnName(); 58 | 59 | // Assertions 60 | Then.onTheMasterPage.theListShouldBeSortedAscendingOnName(); 61 | }); 62 | 63 | opaTest("MasterList Filtering on UnitNumber less than 100", function(Given, When, Then) { 64 | // Action 65 | When.onTheMasterPage.iFilterTheListOnUnitNumber(); 66 | 67 | // Assertion 68 | Then.onTheMasterPage.theListShouldBeFilteredOnUnitNumber(); 69 | }); 70 | 71 | opaTest("MasterList remove filter should display all items", function(Given, When, Then) { 72 | // Action 73 | When.onTheMasterPage.iOpenViewSettingsDialog(). 74 | and.iPressResetInViewSelectionDialog(). 75 | and.iPressOKInViewSelectionDialog(); 76 | 77 | // Assertion 78 | Then.onTheMasterPage.theListShouldHaveAllEntries(); 79 | }); 80 | 81 | 82 | opaTest("MasterList Sorting on UnitNumber", function(Given, When, Then) { 83 | // Actions 84 | When.onTheMasterPage.iSortTheListOnUnitNumber(); 85 | 86 | // Assertions 87 | Then.onTheMasterPage.theListShouldBeSortedAscendingOnUnitNumber(); 88 | }); 89 | 90 | opaTest("MasterList grouping created group headers", function(Given, When, Then) { 91 | // Action 92 | When.onTheMasterPage.iGroupTheList(); 93 | 94 | // Assertion 95 | Then.onTheMasterPage.theListShouldContainAGroupHeader(); 96 | }); 97 | 98 | opaTest("Remove grouping from MasterList delivers initial list", function(Given, When, Then) { 99 | // Action 100 | When.onTheMasterPage.iRemoveListGrouping(); 101 | 102 | // Assertion 103 | Then.onTheMasterPage.theListShouldNotContainGroupHeaders(). 104 | and.theListShouldHaveAllEntries(); 105 | }); 106 | 107 | opaTest("Grouping the master list and sorting it should deliver the initial list", function(Given, When, Then) { 108 | // Action 109 | When.onTheMasterPage.iGroupTheList(). 110 | and.iSortTheListOnUnitNumber(); 111 | 112 | // Assertion 113 | Then.onTheMasterPage.theListShouldContainAGroupHeader(); 114 | 115 | // Cleanup 116 | Then.iTeardownMyApp(); 117 | }); 118 | 119 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/NavigationJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Master", 6 | "./pages/Detail", 7 | "./pages/Browser", 8 | "./pages/App" 9 | ], function (opaTest) { 10 | "use strict"; 11 | 12 | QUnit.module("Desktop navigation"); 13 | 14 | opaTest("Should navigate on press", function (Given, When, Then) { 15 | // Arrangements 16 | Given.iStartMyApp(); 17 | 18 | // Actions 19 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1). 20 | and.iPressOnTheObjectAtPosition(1); 21 | 22 | // Assertions 23 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(). 24 | and.iShouldSeeHeaderActionButtons(); 25 | Then.onTheBrowserPage.iShouldSeeTheHashForTheRememberedObject(); 26 | }); 27 | 28 | opaTest("Should press full screen toggle button: The app shows one column", function (Given, When, Then) { 29 | // Actions 30 | When.onTheDetailPage.iPressTheHeaderActionButton("enterFullScreen"); 31 | 32 | // Assertions 33 | Then.onTheAppPage.theAppShowsFCLDesign("MidColumnFullScreen"); 34 | Then.onTheDetailPage.iShouldSeeTheFullScreenToggleButton("exitFullScreen"); 35 | }); 36 | 37 | opaTest("Should press full screen toggle button: The app shows two columns", function (Given, When, Then) { 38 | // Actions 39 | When.onTheDetailPage.iPressTheHeaderActionButton("exitFullScreen"); 40 | 41 | // Assertions 42 | Then.onTheAppPage.theAppShowsFCLDesign("TwoColumnsMidExpanded"); 43 | Then.onTheDetailPage.iShouldSeeTheFullScreenToggleButton("enterFullScreen"); 44 | }); 45 | 46 | opaTest("Should react on hash change", function (Given, When, Then) { 47 | // Actions 48 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1); 49 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 50 | 51 | // Assertions 52 | Then.onTheDetailPage.iShouldSeeTheRememberedObject().and.iShouldSeeNoBusyIndicator(); 53 | Then.onTheMasterPage.theRememberedListItemShouldBeSelected(); 54 | }); 55 | 56 | 57 | opaTest("Detail Page Shows Object Details", function (Given, When, Then) { 58 | 59 | // Assertions 60 | Then.onTheDetailPage.iShouldSeeTheObjectLineItemsList(). 61 | and.theDetailViewShouldContainOnlyFormattedUnitNumbers(). 62 | and.theLineItemsListShouldHaveTheCorrectNumberOfItems(). 63 | and.theLineItemsHeaderShouldDisplayTheAmountOfEntries(); 64 | 65 | }); 66 | 67 | opaTest("Navigate to an object not on the client: no item should be selected and the object page should be displayed", function (Given, When, Then) { 68 | //Actions 69 | When.onTheMasterPage.iRememberAnIdOfAnObjectThatsNotInTheList(); 70 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 71 | 72 | // Assertions 73 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(); 74 | }); 75 | 76 | opaTest("Should press close column button: The app shows one columns", function (Given, When, Then) { 77 | // Actions 78 | When.onTheDetailPage.iPressTheHeaderActionButton("closeColumn"); 79 | 80 | // Assertions 81 | Then.onTheAppPage.theAppShowsFCLDesign("OneColumn"); 82 | Then.onTheMasterPage.theListShouldHaveNoSelection(); 83 | 84 | // Cleanup 85 | Then.iTeardownMyApp(); 86 | }); 87 | 88 | opaTest("Start the App and simulate metadata error: MessageBox should be shown", function (Given, When, Then) { 89 | //Arrangement 90 | Given.iStartMyApp({ 91 | delay : 1000, 92 | metadataError : true 93 | }); 94 | 95 | // Assertions 96 | Then.onTheAppPage.iShouldSeeTheMessageBox(); 97 | 98 | // Actions 99 | When.onTheAppPage.iCloseTheMessageBox(); 100 | 101 | // Cleanup 102 | Then.iTeardownMyApp(); 103 | }); 104 | 105 | opaTest("Start the App and simulate bad request error: MessageBox should be shown", function (Given, When, Then) { 106 | //Arrangement 107 | Given.iStartMyApp({ 108 | delay : 1000, 109 | errorType : 'serverError' 110 | }); 111 | 112 | // Assertions 113 | Then.onTheAppPage.iShouldSeeTheMessageBox(); 114 | 115 | // Actions 116 | When.onTheAppPage.iCloseTheMessageBox(); 117 | 118 | // Cleanup 119 | Then.iTeardownMyApp(); 120 | }); 121 | 122 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/NavigationJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Master", 6 | "./pages/Browser", 7 | "./pages/Detail" 8 | ], function (opaTest) { 9 | "use strict"; 10 | 11 | QUnit.module("Phone navigation"); 12 | 13 | opaTest("Should see the objects list", function (Given, When, Then) { 14 | // Arrangements 15 | Given.iStartMyApp(); 16 | 17 | // Assertions 18 | Then.onTheMasterPage.iShouldSeeTheList(); 19 | Then.onTheBrowserPage.iShouldSeeAnEmptyHash(); 20 | }); 21 | 22 | opaTest("Should react on hash change", function (Given, When, Then) { 23 | // Actions 24 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1); 25 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 26 | 27 | // Assertions 28 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(); 29 | }); 30 | 31 | opaTest("Detail Page Shows Object Details", function (Given, When, Then) { 32 | // Assertions 33 | Then.onTheDetailPage.iShouldSeeTheObjectLineItemsList(). 34 | and.theLineItemsListShouldHaveTheCorrectNumberOfItems(). 35 | and.theLineItemsHeaderShouldDisplayTheAmountOfEntries(); 36 | }); 37 | 38 | opaTest("Should navigate on press", function (Given, When, Then) { 39 | // Actions 40 | When.onTheDetailPage.iPressTheHeaderActionButton("closeColumn"); 41 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(2). 42 | and.iPressOnTheObjectAtPosition(2); 43 | 44 | // Assertions 45 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(); 46 | 47 | // Cleanup 48 | Then.iTeardownMyApp(); 49 | }); 50 | 51 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/NotFoundJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Master", 6 | "./pages/NotFound", 7 | "./pages/Browser" 8 | ], function (opaTest) { 9 | "use strict"; 10 | 11 | QUnit.module("Desktop not found"); 12 | 13 | opaTest("Should see the resource not found page when navigating to an invalid hash", function (Given, When, Then) { 14 | //Arrangement 15 | Given.iStartMyApp(); 16 | 17 | //Actions 18 | When.onTheBrowserPage.iChangeTheHashToSomethingInvalid(); 19 | 20 | // Assertions 21 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 22 | and.theNotFoundPageShouldSayResourceNotFound(); 23 | 24 | // Cleanup 25 | Then.iTeardownMyApp(); 26 | }); 27 | 28 | opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) { 29 | // Arrangements 30 | Given.iStartMyApp({hash : "somethingThatDoesNotExist"}); 31 | 32 | // Assertions 33 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 34 | and.theNotFoundPageShouldSayResourceNotFound(); 35 | 36 | // Cleanup 37 | Then.iTeardownMyApp(); 38 | }); 39 | 40 | opaTest("Should see the not found master and detail page if an invalid object id has been called", function (Given, When, Then) { 41 | // Arrangements 42 | Given.iStartMyApp({hash : "/BusinessPartnerSet/SomeInvalidObjectId"}); 43 | 44 | // Assertions 45 | Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage(). 46 | and.theNotFoundPageShouldSayObjectNotFound(); 47 | 48 | // Cleanup 49 | Then.iTeardownMyApp(); 50 | }); 51 | 52 | opaTest("Should see the not found text for no search results", function (Given, When, Then) { 53 | // Arrangements 54 | Given.iStartMyApp(); 55 | 56 | //Actions 57 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 58 | 59 | // Assertions 60 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(); 61 | 62 | // Cleanup 63 | Then.iTeardownMyApp(); 64 | }); 65 | 66 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/NotFoundJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/NotFound", 6 | "./pages/Master" 7 | ], function (opaTest) { 8 | "use strict"; 9 | 10 | QUnit.module("Phone not found"); 11 | 12 | opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) { 13 | // Arrangements 14 | Given.iStartMyApp({hash : "somethingThatDoesNotExist"}); 15 | 16 | // Assertions 17 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 18 | and.theNotFoundPageShouldSayResourceNotFound(); 19 | }); 20 | 21 | opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) { 22 | // Actions 23 | When.onTheNotFoundPage.iPressTheBackButton("NotFound"); 24 | 25 | // Assertions 26 | Then.onTheMasterPage.iShouldSeeTheList(); 27 | 28 | // Cleanup 29 | Then.iTeardownMyApp(); 30 | }); 31 | 32 | opaTest("Should see the not found detail page if an invalid object id has been called", function (Given, When, Then) { 33 | // Arrangements 34 | Given.iStartMyApp({hash : "/BusinessPartnerSet/SomeInvalidObjectId"}); 35 | 36 | // Assertions 37 | Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage(). 38 | and.theNotFoundPageShouldSayObjectNotFound(); 39 | }); 40 | 41 | opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) { 42 | // Actions 43 | When.onTheNotFoundPage.iPressTheBackButton("DetailObjectNotFound"); 44 | 45 | // Assertions 46 | Then.onTheMasterPage.iShouldSeeTheList(); 47 | 48 | // Cleanup 49 | Then.iTeardownMyApp(); 50 | }); 51 | 52 | 53 | opaTest("Should see the not found text for no search results", function (Given, When, Then) { 54 | // Arrangements 55 | Given.iStartMyApp(); 56 | 57 | // Actions 58 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 59 | 60 | // Assertions 61 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(); 62 | 63 | // Cleanup 64 | Then.iTeardownMyApp(); 65 | }); 66 | 67 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/PhoneJourneys.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "./arrangements/Startup", 4 | "./NavigationJourneyPhone", 5 | "./NotFoundJourneyPhone", 6 | "./BusyJourneyPhone" 7 | ], function (Opa5, Startup) { 8 | "use strict"; 9 | 10 | Opa5.extendConfig({ 11 | arrangements: new Startup(), 12 | viewNamespace: "ns.BP.view.", 13 | autoWait: true 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /BP/webapp/test/integration/arrangements/Startup.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "ns/BP/localService/mockserver", 4 | "sap/ui/model/odata/v2/ODataModel" 5 | ], function(Opa5, mockserver, ODataModel) { 6 | "use strict"; 7 | 8 | return Opa5.extend("ns.BP.test.integration.arrangements.Startup", { 9 | 10 | /** 11 | * Initializes mock server, then starts the app component 12 | * @param {object} oOptionsParameter An object that contains the configuration for starting up the app 13 | * @param {integer} oOptionsParameter.delay A custom delay to start the app with 14 | * @param {string} [oOptionsParameter.hash] The in-app hash can also be passed separately for better readability in tests 15 | * @param {boolean} [oOptionsParameter.autoWait=true] Automatically wait for pending requests while the application is starting up 16 | */ 17 | iStartMyApp : function (oOptionsParameter) { 18 | var oOptions = oOptionsParameter || {}; 19 | 20 | this._clearSharedData(); 21 | 22 | // start the app with a minimal delay to make tests fast but still async to discover basic timing issues 23 | oOptions.delay = oOptions.delay || 1; 24 | 25 | // configure mock server with the current options 26 | var oMockServerInitialized = mockserver.init(oOptions); 27 | 28 | this.iWaitForPromise(oMockServerInitialized); 29 | // start the app UI component 30 | this.iStartMyUIComponent({ 31 | componentConfig: { 32 | name: "ns.BP", 33 | async: true 34 | }, 35 | hash: oOptions.hash, 36 | autoWait: oOptions.autoWait 37 | }); 38 | }, 39 | _clearSharedData: function () { 40 | // clear shared metadata in ODataModel to allow tests for loading the metadata 41 | ODataModel.mSharedData = { server: {}, service: {}, meta: {} }; 42 | } 43 | }); 44 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/opaTests.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Integration tests for BusinessPartner 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /BP/webapp/test/integration/opaTests.qunit.js: -------------------------------------------------------------------------------- 1 | /* global QUnit */ 2 | 3 | QUnit.config.autostart = false; 4 | 5 | sap.ui.getCore().attachInit(function() { 6 | "use strict"; 7 | 8 | sap.ui.require([ 9 | "ns/BP/test/integration/AllJourneys" 10 | ], function() { 11 | QUnit.start(); 12 | }); 13 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/opaTestsPhone.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Integration tests for BusinessPartner 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /BP/webapp/test/integration/opaTestsPhone.qunit.js: -------------------------------------------------------------------------------- 1 | /* global QUnit */ 2 | 3 | QUnit.config.autostart = false; 4 | 5 | sap.ui.getCore().attachInit(function() { 6 | "use strict"; 7 | 8 | sap.ui.require([ 9 | "ns/BP/test/integration/PhoneJourneys" 10 | ], function() { 11 | QUnit.start(); 12 | }); 13 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/pages/App.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/matchers/PropertyStrictEquals" 4 | ], function(Opa5, PropertyStrictEquals) { 5 | "use strict"; 6 | 7 | var sViewName = "App", 8 | sAppControl = "app"; 9 | 10 | Opa5.createPageObjects({ 11 | onTheAppPage : { 12 | 13 | actions : { 14 | 15 | iCloseTheMessageBox : function () { 16 | return this.waitFor({ 17 | id: "serviceErrorMessageBox", 18 | autoWait: false, 19 | success: function (oMessageBox) { 20 | oMessageBox.destroy(); 21 | Opa5.assert.ok(true, "The MessageBox was closed"); 22 | } 23 | }); 24 | } 25 | }, 26 | 27 | assertions : { 28 | 29 | iShouldSeeTheBusyIndicator : function () { 30 | return this.waitFor({ 31 | id : sAppControl, 32 | viewName : sViewName, 33 | matchers: new PropertyStrictEquals({ 34 | name: "busy", 35 | value: true 36 | }), 37 | autoWait: false, 38 | success : function () { 39 | Opa5.assert.ok(true, "The app is busy"); 40 | }, 41 | errorMessage : "The app is not busy" 42 | }); 43 | }, 44 | 45 | iShouldSeeTheMessageBox : function () { 46 | return this.waitFor({ 47 | searchOpenDialogs: true, 48 | controlType: "sap.m.Dialog", 49 | matchers : new PropertyStrictEquals({ name: "type", value: "Message"}), 50 | success: function () { 51 | Opa5.assert.ok(true, "The correct MessageBox was shown"); 52 | } 53 | }); 54 | }, 55 | 56 | theAppShowsFCLDesign: function (sLayout) { 57 | return this.waitFor({ 58 | id : "layout", 59 | viewName : "App", 60 | matchers : new PropertyStrictEquals({name: "layout", value: sLayout}), 61 | success : function () { 62 | Opa5.assert.ok(true, "the app shows " + sLayout + " layout"); 63 | }, 64 | errorMessage : "The app does not show " + sLayout + " layout" 65 | }); 66 | } 67 | 68 | } 69 | 70 | } 71 | 72 | }); 73 | 74 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/pages/Browser.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5" 3 | ], function(Opa5) { 4 | "use strict"; 5 | 6 | Opa5.createPageObjects({ 7 | onTheBrowserPage : { 8 | 9 | actions : { 10 | 11 | iChangeTheHashToObjectN : function (iObjIndex) { 12 | return this.waitFor(this.createAWaitForAnEntitySet({ 13 | entitySet : "Objects", 14 | success : function (aEntitySet) { 15 | Opa5.getHashChanger().setHash("/BusinessPartnerSet/" + aEntitySet[iObjIndex].BusinessPartnerID); 16 | } 17 | })); 18 | }, 19 | 20 | iChangeTheHashToTheRememberedItem : function () { 21 | return this.waitFor({ 22 | success : function () { 23 | var sObjectId = this.getContext().currentItem.id; 24 | Opa5.getHashChanger().setHash("/BusinessPartnerSet/" + sObjectId); 25 | } 26 | }); 27 | }, 28 | 29 | iChangeTheHashToSomethingInvalid : function () { 30 | return this.waitFor({ 31 | success : function () { 32 | Opa5.getHashChanger().setHash("/somethingInvalid"); 33 | } 34 | }); 35 | } 36 | 37 | }, 38 | 39 | assertions : { 40 | 41 | iShouldSeeTheHashForObjectN : function (iObjIndex) { 42 | return this.waitFor(this.createAWaitForAnEntitySet({ 43 | entitySet : "Objects", 44 | success : function (aEntitySet) { 45 | var oHashChanger = Opa5.getHashChanger(), 46 | sHash = oHashChanger.getHash(); 47 | Opa5.assert.strictEqual(sHash, "BusinessPartnerSet/" + aEntitySet[iObjIndex].BusinessPartnerID, "The Hash is correct"); 48 | } 49 | })); 50 | }, 51 | iShouldSeeTheHashForTheRememberedObject : function () { 52 | return this.waitFor({ 53 | success : function () { 54 | var sObjectId = this.getContext().currentItem.id, 55 | oHashChanger = Opa5.getHashChanger(), 56 | sHash = oHashChanger.getHash(); 57 | Opa5.assert.strictEqual(sHash, "BusinessPartnerSet/" + sObjectId, "The Hash is not correct"); 58 | } 59 | }); 60 | }, 61 | iShouldSeeAnEmptyHash : function () { 62 | return this.waitFor({ 63 | success : function () { 64 | var oHashChanger = Opa5.getHashChanger(), 65 | sHash = oHashChanger.getHash(); 66 | Opa5.assert.strictEqual(sHash, "", "The Hash should be empty"); 67 | }, 68 | errorMessage : "The Hash is not Correct!" 69 | }); 70 | } 71 | 72 | } 73 | 74 | } 75 | 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /BP/webapp/test/integration/pages/Common.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5" 3 | ], function(Opa5) { 4 | "use strict"; 5 | 6 | return Opa5.extend("ns.BP.test.integration.pages.Common", { 7 | 8 | createAWaitForAnEntitySet : function (oOptions) { 9 | return { 10 | success: function () { 11 | var aEntitySet; 12 | 13 | var oMockServerInitialized = this.getMockServer().then(function (oMockServer) { 14 | aEntitySet = oMockServer.getEntitySetData(oOptions.entitySet); 15 | }); 16 | 17 | this.iWaitForPromise(oMockServerInitialized); 18 | return this.waitFor({ 19 | success : function () { 20 | oOptions.success.call(this, aEntitySet); 21 | } 22 | }); 23 | } 24 | }; 25 | }, 26 | 27 | getMockServer : function () { 28 | return new Promise(function (success) { 29 | Opa5.getWindow().sap.ui.require(["ns/BP/localService/mockserver"], function (mockserver) { 30 | success(mockserver.getMockServer()); 31 | }); 32 | }); 33 | } 34 | 35 | }); 36 | 37 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/pages/Detail.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "./Common", 5 | "sap/ui/test/matchers/AggregationLengthEquals", 6 | "sap/ui/test/matchers/AggregationFilled", 7 | "sap/ui/test/matchers/PropertyStrictEquals" 8 | ], function(Opa5, Press, Common, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals) { 9 | "use strict"; 10 | 11 | var sViewName = "Detail"; 12 | 13 | Opa5.createPageObjects({ 14 | onTheDetailPage : { 15 | 16 | baseClass : Common, 17 | 18 | actions : { 19 | 20 | iPressTheHeaderActionButton: function (sId) { 21 | return this.waitFor({ 22 | id : sId, 23 | viewName : sViewName, 24 | actions: new Press(), 25 | errorMessage : "Did not find the button with id" + sId + " on detail page" 26 | }); 27 | } 28 | }, 29 | 30 | assertions : { 31 | 32 | 33 | iShouldSeeNoBusyIndicator : function () { 34 | return this.waitFor({ 35 | id : "detailPage", 36 | viewName : sViewName, 37 | matchers : function (oPage) { 38 | return !oPage.getBusy(); 39 | }, 40 | success : function (oPage) { 41 | // we set the view busy, so we need to query the parent of the app 42 | Opa5.assert.ok(!oPage.getBusy(), "The detail view is not busy"); 43 | }, 44 | errorMessage : "The detail view is busy." 45 | }); 46 | }, 47 | 48 | theObjectPageShowsTheFirstObject : function () { 49 | return this.iShouldBeOnTheObjectNPage(0); 50 | }, 51 | 52 | iShouldBeOnTheObjectNPage : function (iObjIndex) { 53 | return this.waitFor(this.createAWaitForAnEntitySet({ 54 | entitySet : "BusinessPartnerSet", 55 | success : function (aEntitySet) { 56 | var sItemName = aEntitySet[iObjIndex].Name; 57 | 58 | this.waitFor({ 59 | controlType : "sap.m.ObjectHeader", 60 | viewName : sViewName, 61 | matchers : new PropertyStrictEquals({name : "title", value: aEntitySet[iObjIndex].Name}), 62 | success : function () { 63 | Opa5.assert.ok(true, "was on the first object page with the name " + sItemName); 64 | }, 65 | errorMessage : "First object is not shown" 66 | }); 67 | } 68 | })); 69 | }, 70 | 71 | iShouldSeeTheRememberedObject : function () { 72 | return this.waitFor({ 73 | success : function () { 74 | var sBindingPath = this.getContext().currentItem.bindingPath; 75 | this._waitForPageBindingPath(sBindingPath); 76 | } 77 | }); 78 | }, 79 | 80 | _waitForPageBindingPath : function (sBindingPath) { 81 | return this.waitFor({ 82 | id : "detailPage", 83 | viewName : sViewName, 84 | matchers : function (oPage) { 85 | return oPage.getBindingContext() && oPage.getBindingContext().getPath() === sBindingPath; 86 | }, 87 | success : function (oPage) { 88 | Opa5.assert.strictEqual(oPage.getBindingContext().getPath(), sBindingPath, "was on the remembered detail page"); 89 | }, 90 | errorMessage : "Remembered object " + sBindingPath + " is not shown" 91 | }); 92 | }, 93 | 94 | iShouldSeeTheObjectLineItemsList : function () { 95 | return this.waitFor({ 96 | id : "lineItemsList", 97 | viewName : sViewName, 98 | success : function (oList) { 99 | Opa5.assert.ok(oList, "Found the line items list."); 100 | } 101 | }); 102 | }, 103 | 104 | theLineItemsListShouldHaveTheCorrectNumberOfItems : function () { 105 | return this.waitFor(this.createAWaitForAnEntitySet({ 106 | entitySet : "ToProducts", 107 | success : function (aEntitySet) { 108 | 109 | return this.waitFor({ 110 | id : "lineItemsList", 111 | viewName : sViewName, 112 | matchers : new AggregationFilled({name : "items"}), 113 | check: function (oList) { 114 | 115 | var sObjectID = oList.getBindingContext().getProperty("BusinessPartnerID"); 116 | 117 | var iLength = aEntitySet.filter(function (oLineItem) { 118 | return oLineItem.BusinessPartnerID === sObjectID; 119 | }).length; 120 | 121 | return oList.getItems().length === iLength; 122 | }, 123 | success : function () { 124 | Opa5.assert.ok(true, "The list has the correct number of items"); 125 | }, 126 | errorMessage : "The list does not have the correct number of items.\nHint: This test needs suitable mock data in localService directory which can be generated via SAP Web IDE" 127 | }); 128 | } 129 | })); 130 | }, 131 | 132 | theDetailViewShouldContainOnlyFormattedUnitNumbers : function () { 133 | var rTwoDecimalPlaces = /^-?\d+\.\d{2}$/; 134 | return this.waitFor({ 135 | id : "objectHeaderNumber", 136 | viewName : sViewName, 137 | success : function (oNumberControl) { 138 | Opa5.assert.ok(rTwoDecimalPlaces.test(oNumberControl.getNumber()), "Object numbers are properly formatted"); 139 | }, 140 | errorMessage : "Object view has no entries which can be checked for their formatting" 141 | }); 142 | }, 143 | 144 | theLineItemsHeaderShouldDisplayTheAmountOfEntries : function () { 145 | return this.waitFor({ 146 | id : "lineItemsList", 147 | viewName : sViewName, 148 | matchers : new AggregationFilled({name : "items"}), 149 | success : function (oList) { 150 | var iNumberOfItems = oList.getItems().length; 151 | return this.waitFor({ 152 | id : "lineItemsTitle", 153 | viewName : sViewName, 154 | matchers : new PropertyStrictEquals({name: "text", value: " (" + iNumberOfItems + ")"}), 155 | success : function () { 156 | Opa5.assert.ok(true, "The line item list displays " + iNumberOfItems + " items"); 157 | }, 158 | errorMessage : "The line item list does not display " + iNumberOfItems + " items." 159 | }); 160 | } 161 | }); 162 | }, 163 | iShouldSeeHeaderActionButtons: function () { 164 | return this.waitFor({ 165 | id : ["closeColumn", "enterFullScreen"], 166 | viewName : sViewName, 167 | success : function () { 168 | Opa5.assert.ok(true, "The action buttons are visible"); 169 | }, 170 | errorMessage : "The action buttons were not found" 171 | }); 172 | }, 173 | 174 | iShouldSeeTheFullScreenToggleButton : function (sId) { 175 | return this.waitFor({ 176 | id : sId, 177 | viewName : sViewName, 178 | errorMessage : "The toggle button" + sId + "was not found" 179 | }); 180 | } 181 | 182 | } 183 | 184 | } 185 | 186 | }); 187 | 188 | }); 189 | -------------------------------------------------------------------------------- /BP/webapp/test/integration/pages/Master.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "./Common", 5 | "sap/ui/test/actions/EnterText", 6 | "sap/ui/test/matchers/AggregationLengthEquals", 7 | "sap/ui/test/matchers/AggregationFilled", 8 | "sap/ui/test/matchers/PropertyStrictEquals" 9 | ], function(Opa5, Press, Common, EnterText, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals) { 10 | "use strict"; 11 | 12 | var sViewName = "Master", 13 | sSomethingThatCannotBeFound = "*#-Q@@||"; 14 | 15 | Opa5.createPageObjects({ 16 | onTheMasterPage : { 17 | 18 | baseClass : Common, 19 | 20 | actions : { 21 | 22 | iSortTheListOnName : function () { 23 | return this.iChooseASorter("sortButton", "Sort By "); 24 | }, 25 | iSortTheListOnUnitNumber : function () { 26 | return this.iChooseASorter("sortButton", "Sort By "); 27 | }, 28 | 29 | iFilterTheListOnUnitNumber : function () { 30 | return this.iMakeASelection("filterButton", "", "<100 "); 31 | }, 32 | 33 | iGroupTheList : function () { 34 | return this.iChooseASorter("groupButton", " Group"); 35 | }, 36 | 37 | iRemoveListGrouping : function () { 38 | return this.iChooseASorter("groupButton", "None"); 39 | }, 40 | iOpenViewSettingsDialog : function () { 41 | return this.waitFor({ 42 | id : "filterButton", 43 | viewName : sViewName, 44 | actions : new Press(), 45 | errorMessage : "Did not find the 'filter' button." 46 | }); 47 | }, 48 | iPressOKInViewSelectionDialog : function () { 49 | return this.waitFor({ 50 | searchOpenDialogs : true, 51 | controlType : "sap.m.Button", 52 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "text", value : "OK"}), 53 | actions : new Press(), 54 | errorMessage : "Did not find the ViewSettingDialog's 'OK' button." 55 | }); 56 | }, 57 | 58 | iPressResetInViewSelectionDialog : function () { 59 | return this.waitFor({ 60 | searchOpenDialogs : true, 61 | controlType : "sap.m.Button", 62 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "icon", value : "sap-icon://clear-filter"}), 63 | actions : new Press(), 64 | errorMessage : "Did not find the ViewSettingDialog's 'Reset' button." 65 | }); 66 | }, 67 | 68 | iMakeASelection : function (sSelect, sItem, sOption) { 69 | return this.waitFor({ 70 | id : sSelect, 71 | viewName : sViewName, 72 | actions : new Press(), 73 | success : function () { 74 | this.waitFor({ 75 | controlType: "sap.m.StandardListItem", 76 | matchers: new PropertyStrictEquals({name: "title", value: sItem}), 77 | searchOpenDialogs: true, 78 | actions: new Press(), 79 | success: function () { 80 | this.waitFor({ 81 | controlType: "sap.m.StandardListItem", 82 | matchers : new PropertyStrictEquals({name: "title", value: sOption}), 83 | searchOpenDialogs: true, 84 | actions : new Press(), 85 | success: function () { 86 | this.waitFor({ 87 | controlType: "sap.m.Button", 88 | matchers: new PropertyStrictEquals({name: "text", value: "OK"}), 89 | searchOpenDialogs: true, 90 | actions: new Press(), 91 | errorMessage: "The ok button in the dialog was not found and could not be pressed" 92 | }); 93 | }, 94 | errorMessage : "Did not find the" + sOption + "in" + sItem 95 | }); 96 | }, 97 | errorMessage : "Did not find the " + sItem + " element in select" 98 | }); 99 | }, 100 | errorMessage : "Did not find the " + sSelect + " select" 101 | }); 102 | }, 103 | 104 | iRememberTheSelectedItem : function () { 105 | return this.waitFor({ 106 | id : "list", 107 | viewName : sViewName, 108 | matchers : function (oList) { 109 | return oList.getSelectedItem(); 110 | }, 111 | success : function (oListItem) { 112 | this.iRememberTheListItem(oListItem); 113 | }, 114 | errorMessage : "The list does not have a selected item so nothing can be remembered" 115 | }); 116 | }, 117 | 118 | iChooseASorter: function (sSelect, sSort) { 119 | return this.waitFor({ 120 | id : sSelect, 121 | viewName : sViewName, 122 | actions : new Press(), 123 | success : function () { 124 | this.waitFor({ 125 | controlType: "sap.m.StandardListItem", 126 | matchers : new PropertyStrictEquals({name: "title", value: sSort}), 127 | searchOpenDialogs: true, 128 | actions : new Press(), 129 | success : function () { 130 | this.waitFor({ 131 | controlType: "sap.m.Button", 132 | matchers: new PropertyStrictEquals({name: "text", value: "OK"}), 133 | searchOpenDialogs: true, 134 | actions: new Press(), 135 | errorMessage: "The ok button in the dialog was not found and could not be pressed" 136 | }); 137 | }, 138 | errorMessage : "Did not find the" + sSort + " element in select" 139 | }); 140 | }, 141 | errorMessage : "Did not find the " + sSelect + " select" 142 | }); 143 | }, 144 | iRememberTheIdOfListItemAtPosition : function (iPosition) { 145 | return this.waitFor({ 146 | id : "list", 147 | viewName : sViewName, 148 | matchers : function (oList) { 149 | return oList.getItems()[iPosition]; 150 | }, 151 | success : function (oListItem) { 152 | this.iRememberTheListItem(oListItem); 153 | }, 154 | errorMessage : "The list does not have an item at the index " + iPosition 155 | }); 156 | }, 157 | iRememberAnIdOfAnObjectThatsNotInTheList : function () { 158 | return this.waitFor(this.createAWaitForAnEntitySet({ 159 | entitySet : "BusinessPartnerSet", 160 | success : function (aEntityData) { 161 | this.waitFor({ 162 | id : "list", 163 | viewName : sViewName, 164 | matchers : new AggregationFilled({name: "items"}), 165 | success : function (oList) { 166 | var sCurrentId, 167 | aItemsNotInTheList = aEntityData.filter(function (oObject) { 168 | return !oList.getItems().some(function (oListItem) { 169 | return oListItem.getBindingContext().getProperty("BusinessPartnerID") === oObject.BusinessPartnerID; 170 | }); 171 | }); 172 | 173 | if (!aItemsNotInTheList.length) { 174 | // Not enough items all of them are displayed so we take the last one 175 | sCurrentId = aEntityData[aEntityData.length - 1].BusinessPartnerID; 176 | } else { 177 | sCurrentId = aItemsNotInTheList[0].BusinessPartnerID; 178 | } 179 | 180 | var oCurrentItem = this.getContext().currentItem; 181 | // Construct a binding path since the list item is not created yet and we only have the id. 182 | oCurrentItem.bindingPath = "/" + oList.getModel().createKey("BusinessPartnerSet", { 183 | BusinessPartnerID : sCurrentId 184 | }); 185 | oCurrentItem.id = sCurrentId; 186 | }, 187 | errorMessage : "the model does not have a item that is not in the list" 188 | }); 189 | } 190 | })); 191 | }, 192 | iPressOnTheObjectAtPosition : function (iPositon) { 193 | return this.waitFor({ 194 | id : "list", 195 | viewName : sViewName, 196 | matchers : function (oList) { 197 | return oList.getItems()[iPositon]; 198 | }, 199 | actions : new Press(), 200 | errorMessage : "List 'list' in view '" + sViewName + "' does not contain an ObjectListItem at position '" + iPositon + "'" 201 | }); 202 | }, 203 | iSearchForTheFirstObject : function (){ 204 | var sFirstObjectTitle; 205 | return this.waitFor({ 206 | id : "list", 207 | viewName : sViewName, 208 | matchers: new AggregationFilled({name : "items"}), 209 | success : function (oList) { 210 | sFirstObjectTitle = oList.getItems()[0].getTitle(); 211 | return this.iSearchForValue(new EnterText({text: sFirstObjectTitle}), new Press()); 212 | }, 213 | errorMessage : "Did not find list items while trying to search for the first item." 214 | }); 215 | }, 216 | iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh : function () { 217 | return this.iSearchForValue(function (oSearchField) { 218 | oSearchField.setValue(sSomethingThatCannotBeFound); 219 | oSearchField.fireSearch({refreshButtonPressed : true}); 220 | }); 221 | }, 222 | iSearchForValue : function (aActions) { 223 | return this.waitFor({ 224 | id : "searchField", 225 | viewName : sViewName, 226 | actions: aActions, 227 | errorMessage : "Failed to find search field in Master view.'" 228 | }); 229 | }, 230 | 231 | iClearTheSearch : function () { 232 | //can not use 'EnterText' action to enter empty strings (yet) 233 | var fnClearSearchField = function(oSearchField) { 234 | oSearchField.clear(); 235 | }; 236 | return this.iSearchForValue([fnClearSearchField]); 237 | }, 238 | 239 | iSearchForSomethingWithNoResults : function () { 240 | return this.iSearchForValue([new EnterText({text: sSomethingThatCannotBeFound}), new Press()]); 241 | }, 242 | 243 | iRememberTheListItem : function (oListItem) { 244 | var oBindingContext = oListItem.getBindingContext(); 245 | this.getContext().currentItem = { 246 | bindingPath: oBindingContext.getPath(), 247 | id: oBindingContext.getProperty("BusinessPartnerID"), 248 | title: oBindingContext.getProperty("CompanyName") 249 | }; 250 | } 251 | }, 252 | 253 | assertions : { 254 | 255 | iShouldSeeTheBusyIndicator : function () { 256 | return this.waitFor({ 257 | id: "list", 258 | viewName: sViewName, 259 | matchers: new PropertyStrictEquals({ 260 | name: "busy", 261 | value: true 262 | }), 263 | autoWait: false, 264 | success : function () { 265 | Opa5.assert.ok(true, "The master list is busy"); 266 | }, 267 | errorMessage : "The master list is not busy." 268 | }); 269 | }, 270 | 271 | theListShouldContainAGroupHeader : function () { 272 | return this.waitFor({ 273 | controlType : "sap.m.GroupHeaderListItem", 274 | viewName : sViewName, 275 | success : function () { 276 | Opa5.assert.ok(true, "Master list is grouped"); 277 | }, 278 | errorMessage : "Master list is not grouped" 279 | }); 280 | }, 281 | 282 | theListShouldContainOnlyFormattedUnitNumbers : function () { 283 | var rTwoDecimalPlaces = /^-?\d+\.\d{2}$/; 284 | return this.waitFor({ 285 | controlType : "sap.m.ObjectListItem", 286 | viewName : sViewName, 287 | success : function (aNumberControls) { 288 | Opa5.assert.ok(aNumberControls.every(function(oNumberControl){ 289 | return rTwoDecimalPlaces.test(oNumberControl.getNumber()); 290 | }), 291 | "Numbers in ObjectListItems numbers are properly formatted"); 292 | }, 293 | errorMessage : "List has no entries which can be checked for their formatting" 294 | }); 295 | }, 296 | 297 | theListHeaderDisplaysZeroHits : function () { 298 | return this.waitFor({ 299 | viewName : sViewName, 300 | id: "masterPageTitle", 301 | autoWait: false, 302 | matchers: new PropertyStrictEquals({name : "text", value : " (0)"}), 303 | success: function () { 304 | Opa5.assert.ok(true, "The list header displays zero hits"); 305 | }, 306 | errorMessage: "The list header still has items" 307 | }); 308 | }, 309 | 310 | theListHasEntries : function () { 311 | return this.waitFor({ 312 | viewName : sViewName, 313 | id : "list", 314 | matchers : new AggregationFilled({ 315 | name : "items" 316 | }), 317 | success : function () { 318 | Opa5.assert.ok(true, "The list has items"); 319 | }, 320 | errorMessage : "The list had no items" 321 | }); 322 | }, 323 | 324 | theListShouldNotContainGroupHeaders : function () { 325 | function fnIsGroupHeader (oElement) { 326 | return oElement.getMetadata().getName() === "sap.m.GroupHeaderListItem"; 327 | } 328 | 329 | return this.waitFor({ 330 | viewName : sViewName, 331 | id : "list", 332 | matchers : function (oList) { 333 | return !oList.getItems().some(fnIsGroupHeader); 334 | }, 335 | success : function() { 336 | Opa5.assert.ok(true, "Master list does not contain a group header although grouping has been removed."); 337 | }, 338 | errorMessage : "Master list still contains a group header although grouping has been removed." 339 | }); 340 | }, 341 | 342 | theListShouldBeFilteredOnUnitNumber : function () { 343 | return this.theListShouldBeFilteredOnFieldUsingComparator("BusinessPartnerRole", 100); 344 | }, 345 | 346 | 347 | theListShouldBeSortedAscendingOnUnitNumber : function () { 348 | return this.theListShouldBeSortedAscendingOnField("BusinessPartnerRole"); 349 | }, 350 | 351 | theListShouldBeSortedAscendingOnName : function () { 352 | return this.theListShouldBeSortedAscendingOnField("CompanyName"); 353 | }, 354 | 355 | theListShouldBeFilteredOnFieldUsingComparator : function (sField, iComparator) { 356 | function fnCheckFilter(oList){ 357 | var fnIsFiltered = function (oElement) { 358 | if (!oElement.getBindingContext()) { 359 | return false; 360 | } else { 361 | var iValue = oElement.getBindingContext().getProperty(sField); 362 | if (iValue > iComparator) { 363 | return false; 364 | } else { 365 | return true; 366 | } 367 | } 368 | }; 369 | 370 | return oList.getItems().every(fnIsFiltered); 371 | } 372 | 373 | return this.waitFor({ 374 | viewName : sViewName, 375 | id : "list", 376 | matchers : fnCheckFilter, 377 | success : function() { 378 | Opa5.assert.ok(true, "Master list has been filtered correctly for field '" + sField + "'."); 379 | }, 380 | errorMessage : "Master list has not been filtered correctly for field '" + sField + "'." 381 | }); 382 | }, 383 | iShouldSeeTheList : function () { 384 | return this.waitFor({ 385 | id : "list", 386 | viewName : sViewName, 387 | success : function (oList) { 388 | Opa5.assert.ok(oList, "Found the object List"); 389 | }, 390 | errorMessage : "Can't see the master list." 391 | }); 392 | }, 393 | 394 | theListShowsOnlyObjectsWithTheSearchStringInTheirTitle : function () { 395 | this.waitFor({ 396 | id : "list", 397 | viewName : sViewName, 398 | matchers : new AggregationFilled({name : "items"}), 399 | check : function(oList) { 400 | var sTitle = oList.getItems()[0].getTitle(), 401 | bEveryItemContainsTheTitle = oList.getItems().every(function (oItem) { 402 | return oItem.getTitle().indexOf(sTitle) !== -1; 403 | }); 404 | return bEveryItemContainsTheTitle; 405 | }, 406 | success : function (oList) { 407 | Opa5.assert.ok(true, "Every item did contain the title"); 408 | }, 409 | errorMessage : "The list did not have items" 410 | }); 411 | }, 412 | theListShouldHaveAllEntries : function () { 413 | var aAllEntities, 414 | iExpectedNumberOfItems; 415 | // retrieve all BusinessPartnerSet to be able to check for the total amount 416 | this.waitFor(this.createAWaitForAnEntitySet({ 417 | entitySet : "BusinessPartnerSet", 418 | success : function (aEntityData) { 419 | aAllEntities = aEntityData; 420 | } 421 | })); 422 | 423 | return this.waitFor({ 424 | id : "list", 425 | viewName : sViewName, 426 | matchers : function (oList) { 427 | // If there are less items in the list than the growingThreshold, only check for this number. 428 | iExpectedNumberOfItems = Math.min(oList.getGrowingThreshold(), aAllEntities.length); 429 | return new AggregationLengthEquals({name : "items", length : iExpectedNumberOfItems}).isMatching(oList); 430 | }, 431 | success : function (oList) { 432 | Opa5.assert.strictEqual(oList.getItems().length, iExpectedNumberOfItems, "The growing list displays all items"); 433 | }, 434 | errorMessage : "List does not display all entries." 435 | }); 436 | }, 437 | 438 | iShouldSeeTheNoDataTextForNoSearchResults : function () { 439 | return this.waitFor({ 440 | id : "list", 441 | viewName : sViewName, 442 | success : function (oList) { 443 | Opa5.assert.strictEqual(oList.getNoDataText(), oList.getModel("i18n").getProperty("masterListNoDataWithFilterOrSearchText"), "the list should show the no data text for search and filter"); 444 | }, 445 | errorMessage : "list does not show the no data text for search and filter" 446 | }); 447 | }, 448 | 449 | theHeaderShouldDisplayAllEntries : function () { 450 | return this.waitFor({ 451 | id : "list", 452 | viewName : sViewName, 453 | success : function (oList) { 454 | var iExpectedLength = oList.getBinding("items").getLength(); 455 | this.waitFor({ 456 | id : "masterPageTitle", 457 | viewName : sViewName, 458 | matchers : new PropertyStrictEquals({name : "text", value : " (" + iExpectedLength + ")"}), 459 | success : function () { 460 | Opa5.assert.ok(true, "The master page header displays " + iExpectedLength + " items"); 461 | }, 462 | errorMessage : "The master page header does not display " + iExpectedLength + " items." 463 | }); 464 | }, 465 | errorMessage : "Header does not display the number of items in the list" 466 | }); 467 | }, 468 | 469 | theListShouldHaveNoSelection : function () { 470 | return this.waitFor({ 471 | id : "list", 472 | viewName : sViewName, 473 | matchers : function(oList) { 474 | return !oList.getSelectedItem(); 475 | }, 476 | success : function (oList) { 477 | Opa5.assert.strictEqual(oList.getSelectedItems().length, 0, "The list selection is removed"); 478 | }, 479 | errorMessage : "List selection was not removed" 480 | }); 481 | }, 482 | 483 | theRememberedListItemShouldBeSelected : function () { 484 | this.waitFor({ 485 | id : "list", 486 | viewName : sViewName, 487 | matchers : function (oList) { 488 | return oList.getSelectedItem(); 489 | }, 490 | success : function (oSelectedItem) { 491 | Opa5.assert.strictEqual(oSelectedItem.getTitle(), this.getContext().currentItem.title, "The list selection is incorrect.\nHint: If the master list shows integer numbers, use toString function to convert the second parameter to string"); 492 | }, 493 | errorMessage : "The list has no selection" 494 | }); 495 | }, 496 | theListShouldBeSortedAscendingOnField : function (sField) { 497 | function fnCheckSort (oList){ 498 | var oLastValue = null, 499 | fnSortByField = function (oElement) { 500 | if (!oElement.getBindingContext()) { 501 | return false; 502 | } 503 | 504 | var oCurrentValue = oElement.getBindingContext().getProperty(sField); 505 | 506 | if (oCurrentValue === undefined) { 507 | return false; 508 | } 509 | 510 | if (!oLastValue || oCurrentValue >= oLastValue){ 511 | oLastValue = oCurrentValue; 512 | } else { 513 | return false; 514 | } 515 | return true; 516 | }; 517 | 518 | return oList.getItems().every(fnSortByField); 519 | } 520 | 521 | return this.waitFor({ 522 | viewName : sViewName, 523 | id : "list", 524 | matchers : fnCheckSort, 525 | success : function() { 526 | Opa5.assert.ok(true, "Master list has been sorted correctly for field '" + sField + "'."); 527 | }, 528 | errorMessage : "Master list has not been sorted correctly for field '" + sField + "'." 529 | }); 530 | } 531 | } 532 | } 533 | }); 534 | }); -------------------------------------------------------------------------------- /BP/webapp/test/integration/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "sap/ui/test/matchers/PropertyStrictEquals" 5 | ], function(Opa5, Press, PropertyStrictEquals) { 6 | "use strict"; 7 | 8 | var sNotFoundPageId = "page", 9 | sNotFoundView = "NotFound", 10 | sDetailNotFoundView = "DetailObjectNotFound"; 11 | 12 | Opa5.createPageObjects({ 13 | onTheNotFoundPage : { 14 | 15 | actions : { 16 | 17 | iPressTheBackButton : function (sViewName) { 18 | return this.waitFor({ 19 | viewName : sViewName, 20 | controlType : "sap.m.Button", 21 | matchers: new PropertyStrictEquals({name : "type", value : "Back"}), 22 | actions : new Press(), 23 | errorMessage : "Did not find the back button" 24 | }); 25 | } 26 | }, 27 | 28 | assertions : { 29 | 30 | iShouldSeeTheNotFoundGeneralPage : function (sPageId, sPageViewName) { 31 | return this.waitFor({ 32 | controlType : "sap.m.MessagePage", 33 | viewName : sPageViewName, 34 | success : function () { 35 | Opa5.assert.ok(true, "Shows the message page"); 36 | }, 37 | errorMessage : "Did not reach the empty page" 38 | }); 39 | }, 40 | 41 | iShouldSeeTheNotFoundPage : function () { 42 | return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundPageId, sNotFoundView); 43 | }, 44 | 45 | iShouldSeeTheObjectNotFoundPage : function () { 46 | return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundPageId, sDetailNotFoundView); 47 | }, 48 | 49 | theNotFoundPageShouldSayResourceNotFound : function () { 50 | return this.waitFor({ 51 | id : sNotFoundPageId, 52 | viewName : sNotFoundView, 53 | success : function (oPage) { 54 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("notFoundTitle"), "The not found text is shown as title"); 55 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("notFoundText"), "The resource not found text is shown"); 56 | }, 57 | errorMessage : "Did not display the resource not found text" 58 | }); 59 | }, 60 | 61 | theNotFoundPageShouldSayObjectNotFound : function () { 62 | return this.waitFor({ 63 | id : sNotFoundPageId, 64 | viewName : sDetailNotFoundView, 65 | success : function (oPage) { 66 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("detailTitle"), "The object text is shown as title"); 67 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("noObjectFoundText"), "The object not found text is shown"); 68 | }, 69 | errorMessage : "Did not display the object not found text" 70 | }); 71 | } 72 | } 73 | } 74 | }); 75 | }); -------------------------------------------------------------------------------- /BP/webapp/test/mockServer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BusinessPartner 7 | 8 | 19 | 20 | 21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /BP/webapp/test/testsuite.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | QUnit test suite for BusinessPartner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BP/webapp/test/testsuite.qunit.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line sap-no-global-define 2 | window.suite = function () { 3 | "use strict"; 4 | /* eslint-disable new-cap */ 5 | var oSuite = new parent.jsUnitTestSuite(), 6 | sContextPath = location.pathname.substring(0, location.pathname.lastIndexOf("/") + 1); 7 | 8 | oSuite.addTestPage(sContextPath + "unit/unitTests.qunit.html"); 9 | oSuite.addTestPage(sContextPath + "integration/opaTests.qunit.html"); 10 | 11 | return oSuite; 12 | }; -------------------------------------------------------------------------------- /BP/webapp/test/unit/AllTests.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./model/models", 3 | "./model/formatter", 4 | "./controller/ListSelector" 5 | ], function() { 6 | "use strict"; 7 | }); -------------------------------------------------------------------------------- /BP/webapp/test/unit/controller/ListSelector.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "ns/BP/controller/ListSelector" 5 | ], function(ListSelector) { 6 | "use strict"; 7 | 8 | QUnit.module("Initialization", { 9 | beforeEach : function () { 10 | this.oListSelector = new ListSelector(); 11 | }, 12 | afterEach : function () { 13 | this.oListSelector.destroy(); 14 | } 15 | }); 16 | 17 | QUnit.test("Should initialize the List loading promise", function (assert) { 18 | // Arrange 19 | var done = assert.async(), 20 | fnRejectSpy = this.spy(), 21 | fnResolveSpy = this.spy(); 22 | 23 | // Act 24 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 25 | 26 | // Assert 27 | setTimeout(function () { 28 | assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise"); 29 | assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise"); 30 | done(); 31 | }, 0); 32 | }); 33 | 34 | QUnit.module("List loading", { 35 | beforeEach : function () { 36 | this.oListSelector = new ListSelector(); 37 | }, 38 | afterEach : function () { 39 | this.oListSelector.destroy(); 40 | } 41 | }); 42 | 43 | function createListStub (bCreateListItem, sBindingPath) { 44 | var fnGetParameter = function () { 45 | return true; 46 | }, 47 | oDataStub = { 48 | getParameter : fnGetParameter 49 | }, 50 | fnAttachEventOnce = function (sEventName, fnCallback) { 51 | fnCallback(oDataStub); 52 | }, 53 | fnGetBinding = this.stub().returns({ 54 | attachEventOnce : fnAttachEventOnce 55 | }), 56 | fnAttachEvent = function (sEventName, fnCallback, oContext) { 57 | fnCallback.apply(oContext); 58 | }, 59 | oListItemStub = { 60 | getBindingContext : this.stub().returns({ 61 | getPath : this.stub().returns(sBindingPath) 62 | }) 63 | }, 64 | aListItems = []; 65 | 66 | if (bCreateListItem) { 67 | aListItems.push(oListItemStub); 68 | } 69 | 70 | return { 71 | attachEvent : fnAttachEvent, 72 | attachEventOnce : fnAttachEventOnce, 73 | getBinding : fnGetBinding, 74 | getItems : this.stub().returns(aListItems) 75 | }; 76 | } 77 | 78 | QUnit.test("Should resolve the list loading promise, if the list has items", function (assert) { 79 | // Arrange 80 | var done = assert.async(), 81 | fnRejectSpy = this.spy(), 82 | fnResolveSpy = function (sBindingPath) { 83 | // Assert 84 | assert.strictEqual(sBindingPath, sBindingPath, "Did pass the binding path"); 85 | assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise"); 86 | done(); 87 | }; 88 | 89 | // Act 90 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 91 | this.oListSelector.setBoundMasterList(createListStub.call(this, true, "anything")); 92 | }); 93 | 94 | QUnit.test("Should reject the list loading promise, if the list has no items", function (assert) { 95 | // Arrange 96 | var done = assert.async(), 97 | fnResolveSpy = this.spy(), 98 | fnRejectSpy = function () { 99 | // Assert 100 | assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise"); 101 | done(); 102 | }; 103 | 104 | // Act 105 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 106 | this.oListSelector.setBoundMasterList(createListStub.call(this, false)); 107 | }); 108 | 109 | QUnit.module("Selecting item in the list", { 110 | beforeEach : function () { 111 | this.oListSelector = new ListSelector(); 112 | this.oListSelector.oWhenListLoadingIsDone = { 113 | then : function (fnAct) { 114 | this.fnAct = fnAct; 115 | }.bind(this) 116 | }; 117 | }, 118 | afterEach : function () { 119 | this.oListSelector.destroy(); 120 | } 121 | }); 122 | 123 | function createStubbedListItem (sBindingPath) { 124 | return { 125 | getBindingContext : this.stub().returns({ 126 | getPath : this.stub().returns(sBindingPath) 127 | }) 128 | }; 129 | } 130 | 131 | QUnit.test("Should select an Item of the list when it is loaded and the binding contexts match", function (assert) { 132 | // Arrange 133 | var sBindingPath = "anything", 134 | oListItemToSelect = createStubbedListItem.call(this, sBindingPath), 135 | oSelectedListItemStub = createStubbedListItem.call(this, "a different binding path"); 136 | 137 | this.oListSelector._oList = { 138 | getMode : this.stub().returns("SingleSelectMaster"), 139 | getSelectedItem : this.stub().returns(oSelectedListItemStub), 140 | getItems : this.stub().returns([ oSelectedListItemStub, oListItemToSelect, createListStub.call(this, "yet another list binding") ]), 141 | setSelectedItem : function (oItem) { 142 | //Assert 143 | assert.strictEqual(oItem, oListItemToSelect, "Did select the list item with a matching binding context"); 144 | } 145 | }; 146 | 147 | // Act 148 | this.oListSelector.selectAListItem(sBindingPath); 149 | // Resolve list loading 150 | this.fnAct(); 151 | }); 152 | 153 | QUnit.test("Should not select an Item of the list when it is already selected", function (assert) { 154 | // Arrange 155 | var sBindingPath = "anything", 156 | oSelectedListItemStub = createStubbedListItem.call(this, sBindingPath); 157 | 158 | this.oListSelector._oList = { 159 | getMode: this.stub().returns("SingleSelectMaster"), 160 | getSelectedItem : this.stub().returns(oSelectedListItemStub) 161 | }; 162 | 163 | // Act 164 | this.oListSelector.selectAListItem(sBindingPath); 165 | // Resolve list loading 166 | this.fnAct(); 167 | 168 | // Assert 169 | assert.ok(true, "did not fail"); 170 | }); 171 | 172 | QUnit.test("Should not select an item of the list when the list has the selection mode none", function (assert) { 173 | // Arrange 174 | var sBindingPath = "anything"; 175 | 176 | this.oListSelector._oList = { 177 | getMode : this.stub().returns("None") 178 | }; 179 | 180 | // Act 181 | this.oListSelector.selectAListItem(sBindingPath); 182 | // Resolve list loading 183 | this.fnAct(); 184 | 185 | // Assert 186 | assert.ok(true, "did not fail"); 187 | }); 188 | 189 | }); -------------------------------------------------------------------------------- /BP/webapp/test/unit/model/formatter.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/m/Text", 5 | "ns/BP/model/formatter" 6 | ], function (Text, formatter) { 7 | "use strict"; 8 | 9 | QUnit.module("formatter - Currency value"); 10 | 11 | function currencyValueTestCase(assert, sValue, fExpectedNumber) { 12 | // Act 13 | var fCurrency = formatter.currencyValue(sValue); 14 | 15 | // Assert 16 | assert.strictEqual(fCurrency, fExpectedNumber, "The rounding was correct"); 17 | } 18 | 19 | QUnit.test("Should round down a 3 digit number", function (assert) { 20 | currencyValueTestCase.call(this, assert, "3.123", "3.12"); 21 | }); 22 | 23 | QUnit.test("Should round up a 3 digit number", function (assert) { 24 | currencyValueTestCase.call(this, assert, "3.128", "3.13"); 25 | }); 26 | 27 | QUnit.test("Should round a negative number", function (assert) { 28 | currencyValueTestCase.call(this, assert, "-3", "-3.00"); 29 | }); 30 | 31 | QUnit.test("Should round an empty string", function (assert) { 32 | currencyValueTestCase.call(this, assert, "", ""); 33 | }); 34 | 35 | QUnit.test("Should round a zero", function (assert) { 36 | currencyValueTestCase.call(this, assert, "0", "0.00"); 37 | }); 38 | }); -------------------------------------------------------------------------------- /BP/webapp/test/unit/model/models.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "ns/BP/model/models" 5 | ], function (models) { 6 | "use strict"; 7 | 8 | QUnit.module("createDeviceModel", { 9 | afterEach : function () { 10 | this.oDeviceModel.destroy(); 11 | } 12 | }); 13 | 14 | function isPhoneTestCase(assert, bIsPhone) { 15 | // Arrange 16 | this.stub(sap.ui.Device, "system", { phone : bIsPhone }); 17 | 18 | // System under test 19 | this.oDeviceModel = models.createDeviceModel(); 20 | 21 | // Assert 22 | assert.strictEqual(this.oDeviceModel.getData().system.phone, bIsPhone, "IsPhone property is correct"); 23 | } 24 | 25 | QUnit.test("Should initialize a device model for desktop", function (assert) { 26 | isPhoneTestCase.call(this, assert, false); 27 | }); 28 | 29 | QUnit.test("Should initialize a device model for phone", function (assert) { 30 | isPhoneTestCase.call(this, assert, true); 31 | }); 32 | 33 | function isTouchTestCase(assert, bIsTouch) { 34 | // Arrange 35 | this.stub(sap.ui.Device, "support", { touch : bIsTouch }); 36 | 37 | // System under test 38 | this.oDeviceModel = models.createDeviceModel(); 39 | 40 | // Assert 41 | assert.strictEqual(this.oDeviceModel.getData().support.touch, bIsTouch, "IsTouch property is correct"); 42 | } 43 | 44 | QUnit.test("Should initialize a device model for non touch devices", function (assert) { 45 | isTouchTestCase.call(this, assert, false); 46 | }); 47 | 48 | QUnit.test("Should initialize a device model for touch devices", function (assert) { 49 | isTouchTestCase.call(this, assert, true); 50 | }); 51 | 52 | QUnit.test("The binding mode of the device model should be one way", function (assert) { 53 | 54 | // System under test 55 | this.oDeviceModel = models.createDeviceModel(); 56 | 57 | // Assert 58 | assert.strictEqual(this.oDeviceModel.getDefaultBindingMode(), "OneWay", "Binding mode is correct"); 59 | }); 60 | }); -------------------------------------------------------------------------------- /BP/webapp/test/unit/unitTests.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unit tests for BusinessPartner 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /BP/webapp/test/unit/unitTests.qunit.js: -------------------------------------------------------------------------------- 1 | /* global QUnit */ 2 | QUnit.config.autostart = false; 3 | 4 | sap.ui.getCore().attachInit(function () { 5 | "use strict"; 6 | 7 | sap.ui.require([ 8 | "ns/BP/test/unit/AllTests" 9 | ], function () { 10 | QUnit.start(); 11 | }); 12 | }); -------------------------------------------------------------------------------- /BP/webapp/view/App.view.xml: -------------------------------------------------------------------------------- 1 | 8 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /BP/webapp/view/Detail.view.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 15 | </semantic:titleHeading> 16 | <semantic:headerContent> 17 | <ObjectAttribute title="{i18n>priceTitle}"/> 18 | <ObjectNumber 19 | id="objectHeaderNumber" 20 | number="{ 21 | path: 'BusinessPartnerRole' 22 | 23 | }" 24 | unit="{EmailAddress}" 25 | /> 26 | </semantic:headerContent> 27 | <semantic:content> 28 | <Table 29 | id="lineItemsList" 30 | width="auto" 31 | items="{ToProducts}" 32 | updateFinished=".onListUpdateFinished" 33 | noDataText="{i18n>detailLineItemTableNoDataText}" 34 | busyIndicatorDelay="{detailView>/lineItemTableDelay}"> 35 | <headerToolbar> 36 | <Toolbar> 37 | <Title 38 | id="lineItemsTitle" 39 | text="{detailView>/lineItemListTitle}" 40 | titleStyle="H3" 41 | level="H3"/> 42 | </Toolbar> 43 | </headerToolbar> 44 | <columns> 45 | <Column> 46 | <Text text="{i18n>detailLineItemTableIDColumn}"/> 47 | </Column> 48 | <Column 49 | minScreenWidth="Tablet" 50 | demandPopin="true" 51 | hAlign="End"> 52 | <Text text="{i18n>detailLineItemTableUnitNumberColumn}"/> 53 | </Column> 54 | </columns> 55 | <items> 56 | <ColumnListItem> 57 | <cells> 58 | <ObjectIdentifier 59 | title="{Name}" 60 | text="{ProductID}"/> 61 | <ObjectNumber 62 | number="{ 63 | path: 'Price', 64 | formatter: '.formatter.currencyValue' 65 | }" 66 | unit="{CurrencyCode}"/> 67 | </cells> 68 | </ColumnListItem> 69 | </items> 70 | </Table> 71 | </semantic:content> 72 | <!--Semantic ShareMenu Buttons--> 73 | <semantic:sendEmailAction> 74 | <semantic:SendEmailAction 75 | id="shareEmail" 76 | press=".onSendEmailPress"/> 77 | </semantic:sendEmailAction> 78 | 79 | <semantic:closeAction> 80 | <semantic:CloseAction 81 | id="closeColumn" 82 | press=".onCloseDetailPress"/> 83 | </semantic:closeAction> 84 | 85 | <semantic:fullScreenAction> 86 | <semantic:FullScreenAction 87 | id="enterFullScreen" 88 | visible="{= !${device>/system/phone} && !${appView>/actionButtonsInfo/midColumn/fullScreen}}" 89 | press=".toggleFullScreen"/> 90 | </semantic:fullScreenAction> 91 | <semantic:exitFullScreenAction> 92 | <semantic:ExitFullScreenAction 93 | id="exitFullScreen" 94 | visible="{= !${device>/system/phone} && ${appView>/actionButtonsInfo/midColumn/fullScreen}}" 95 | press=".toggleFullScreen"/> 96 | </semantic:exitFullScreenAction> 97 | 98 | </semantic:SemanticPage> 99 | 100 | </mvc:View> -------------------------------------------------------------------------------- /BP/webapp/view/DetailObjectNotFound.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="ns.BP.controller.DetailObjectNotFound" 3 | xmlns="sap.m" 4 | xmlns:mvc="sap.ui.core.mvc"> 5 | 6 | <MessagePage 7 | id="page" 8 | title="{i18n>detailTitle}" 9 | text="{i18n>noObjectFoundText}" 10 | icon="sap-icon://product" 11 | description="" 12 | showNavButton="{= 13 | ${device>/system/phone} || 14 | ${device>/system/tablet} && 15 | ${device>/orientation/portrait} 16 | }" 17 | navButtonPress=".onNavBack"> 18 | </MessagePage> 19 | 20 | </mvc:View> -------------------------------------------------------------------------------- /BP/webapp/view/Master.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="ns.BP.controller.Master" 3 | xmlns="sap.m" 4 | xmlns:semantic="sap.f.semantic" 5 | xmlns:mvc="sap.ui.core.mvc"> 6 | 7 | <semantic:SemanticPage 8 | id="masterPage" 9 | preserveHeaderStateOnScroll="true" 10 | toggleHeaderOnTitleClick="false"> 11 | <semantic:titleHeading> 12 | <Title 13 | id="masterPageTitle" 14 | text="{masterView>/title}" 15 | level="H2"/> 16 | </semantic:titleHeading> 17 | <semantic:content> 18 | <!-- For client side filtering add this to the items attribute: parameters: {operationMode: 'Client'}}" --> 19 | <List 20 | id="list" 21 | width="auto" 22 | class="sapFDynamicPageAlignContent" 23 | items="{ 24 | path: '/BusinessPartnerSet', 25 | sorter: { 26 | path: 'CompanyName', 27 | descending: false 28 | }, 29 | groupHeaderFactory: '.createGroupHeader' 30 | }" 31 | busyIndicatorDelay="{masterView>/delay}" 32 | noDataText="{masterView>/noDataText}" 33 | mode="{= ${device>/system/phone} ? 'None' : 'SingleSelectMaster'}" 34 | growing="true" 35 | growingScrollToLoad="true" 36 | updateFinished=".onUpdateFinished" 37 | selectionChange=".onSelectionChange"> 38 | <infoToolbar> 39 | <Toolbar 40 | active="true" 41 | id="filterBar" 42 | visible="{masterView>/isFilterBarVisible}" 43 | press=".onOpenViewSettings"> 44 | <Title 45 | id="filterBarLabel" 46 | text="{masterView>/filterBarLabel}" 47 | level="H3"/> 48 | </Toolbar> 49 | </infoToolbar> 50 | <headerToolbar> 51 | <OverflowToolbar> 52 | <SearchField 53 | id="searchField" 54 | showRefreshButton="true" 55 | tooltip="{i18n>masterSearchTooltip}" 56 | search=".onSearch" 57 | width="auto"> 58 | <layoutData> 59 | <OverflowToolbarLayoutData 60 | minWidth="150px" 61 | maxWidth="240px" 62 | shrinkable="true" 63 | priority="NeverOverflow"/> 64 | </layoutData> 65 | </SearchField> 66 | <ToolbarSpacer/> 67 | <Button 68 | id="sortButton" 69 | press=".onOpenViewSettings" 70 | icon="sap-icon://sort" 71 | type="Transparent"/> 72 | <Button 73 | id="filterButton" 74 | press=".onOpenViewSettings" 75 | icon="sap-icon://filter" 76 | type="Transparent"/> 77 | <Button 78 | id="groupButton" 79 | press=".onOpenViewSettings" 80 | icon="sap-icon://group-2" 81 | type="Transparent"/> 82 | </OverflowToolbar> 83 | </headerToolbar> 84 | <items> 85 | <ObjectListItem 86 | type="Navigation" 87 | press=".onSelectionChange" 88 | title="{CompanyName}" 89 | number="{ 90 | path: 'BusinessPartnerRole', 91 | formatter: '.formatter.currencyValue' 92 | }" 93 | numberUnit="{EmailAddress}"> 94 | </ObjectListItem> 95 | </items> 96 | </List> 97 | </semantic:content> 98 | </semantic:SemanticPage> 99 | </mvc:View> -------------------------------------------------------------------------------- /BP/webapp/view/NotFound.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="ns.BP.controller.NotFound" 3 | xmlns="sap.m" 4 | xmlns:mvc="sap.ui.core.mvc"> 5 | 6 | <MessagePage 7 | id="page" 8 | title="{i18n>notFoundTitle}" 9 | text="{i18n>notFoundText}" 10 | icon="sap-icon://document" 11 | showNavButton="true" 12 | navButtonPress=".onNavBack"> 13 | </MessagePage> 14 | 15 | </mvc:View> -------------------------------------------------------------------------------- /BP/webapp/view/ViewSettingsDialog.fragment.xml: -------------------------------------------------------------------------------- 1 | <core:FragmentDefinition 2 | xmlns="sap.m" 3 | xmlns:core="sap.ui.core"> 4 | 5 | <ViewSettingsDialog 6 | id="viewSettingsDialog" 7 | confirm=".onConfirmViewSettingsDialog"> 8 | <sortItems> 9 | <ViewSettingsItem 10 | text="{i18n>masterSort1}" 11 | key="CompanyName" 12 | selected="true"/> 13 | <ViewSettingsItem 14 | text="{i18n>masterSort2}" 15 | key="BusinessPartnerRole"/> 16 | </sortItems> 17 | <filterItems> 18 | <ViewSettingsFilterItem 19 | id="filterItems" 20 | text="{i18n>masterFilterName}" 21 | multiSelect="false"> 22 | <items> 23 | <ViewSettingsItem 24 | id="viewFilter1" 25 | text="{i18n>masterFilter1}" 26 | key="Filter1"/> 27 | <ViewSettingsItem 28 | id="viewFilter2" 29 | text="{i18n>masterFilter2}" 30 | key="Filter2"/> 31 | </items> 32 | </ViewSettingsFilterItem> 33 | </filterItems> 34 | <groupItems> 35 | <ViewSettingsItem 36 | text="{i18n>masterGroup1}" 37 | key="BusinessPartnerRole"/> 38 | </groupItems> 39 | </ViewSettingsDialog> 40 | </core:FragmentDefinition> -------------------------------------------------------------------------------- /BP/xs-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcomeFile":"/index.html", 3 | "authenticationMethod":"route", 4 | "logout":{ 5 | "logoutEndpoint":"/do/logout" 6 | }, 7 | "routes":[ 8 | { 9 | "authenticationType":"none", 10 | "csrfProtection":false, 11 | "source":"^/sap/opu/odata/iwbep/GWSAMPLE_BASIC/", 12 | "destination":"ES5" 13 | }, 14 | { 15 | "source":"^(.*)$", 16 | "target":"$1", 17 | "service":"html5-apps-repo-rt", 18 | "authenticationType":"xsuaa", 19 | "scope":"$XSAPPNAME.BPViewer" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 9 | 10 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 11 | 12 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 13 | 14 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 15 | 16 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 17 | 18 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 19 | 20 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 21 | 22 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 23 | 24 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 25 | 26 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 27 | 28 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 29 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 30 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 31 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 32 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 33 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 34 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 35 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 36 | 37 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 38 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 39 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 40 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 41 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 42 | END OF TERMS AND CONDITIONS 43 | 44 | APPENDIX: How to apply the Apache License to your work. 45 | 46 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 47 | 48 | Copyright 2020 SAP SE 49 | 50 | Licensed under the Apache License, Version 2.0 (the "License"); 51 | you may not use this file except in compliance with the License. 52 | You may obtain a copy of the License at 53 | 54 | http://www.apache.org/licenses/LICENSE-2.0 55 | 56 | Unless required by applicable law or agreed to in writing, software 57 | distributed under the License is distributed on an "AS IS" BASIS, 58 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 59 | See the License for the specific language governing permissions and 60 | limitations under the License. -------------------------------------------------------------------------------- /Mission.md: -------------------------------------------------------------------------------- 1 | # Mission: Setup for SAP S/4HANA side-by-side UI Extensions on SAP Business Technology Platform 2 | 3 | The main focus of this mission is to show the full end-to-end setup for a SAP S/4HANA on-premise extension on SAP BTP (Cloud Foundry) this includes the following steps: 4 | * Setup SAP S/4HANA on-premise system 5 | * Setup of SAP BTP account and development environment 6 | * End-to-End connection setup with Principal Propagation (SSO) 7 | * DevOps - using SAP Continuous Integration & Delivery and monitoring 8 | * Integration of the HTML5 application in a central Launchpad 9 | 10 | We will create a simple custom UI application, show the usage of the HTML5 repository and the different options how to expose this application - as a stand-alone or with the different SAP Launchpads environments. 11 | 12 | [Mission in SAP Discovery Center](https://discovery-center.cloud.sap/missiondetail/3239/3325) 13 | 14 | ## Discover 15 | 16 | * [The Mission Story](../../tree/mission/mission/discover/MissionStory.md) 17 | * [Learn the Basics of SAP BTP](../../tree/mission/mission/discover/BTP.md) 18 | * [Learn about SAP S/4HANA](../../tree/mission/mission/discover/S4H.md) 19 | * [Learn about SAP Connectivity Service](../../tree/mission/mission/discover/Connectivity.md) 20 | * [Learn about HTML5 Applications](../../tree/mission/mission/discover/HTML5.md) 21 | * [Learn about SAP Business Application Studio](../../tree/mission/mission/discover/BAS.md) 22 | * [Learn about SAP Cloud Identity Services](../../tree/mission/mission/discover/IAS.md) 23 | * [Learn about DevOps and SAP Continous Integration and Delivery](../../tree/mission/mission/discover/CICD.md) 24 | * [Learn about SAP Launchpad Service and SAP Work Zone](../../tree/mission/mission/discover/Launchpad.md) 25 | * [Learn about Observability on SAP BTP](../../tree/mission/mission/discover/Observability.md) 26 | 27 | These are the step-by-step guidelines for running the mission. It is divided in two workstreams: 28 | 29 | ## Landscape Setup 30 | 31 | The setup of the landscape consists of preparing the API in the SAP S/4HANA on-premise system and exposing the backend oData service using SAP Cloud Connector. There are also step-by-step instructions to setup the trust between SAP Cloud Connector and SAP S/4HANA system. 32 | 33 | * [Setup of SAP S/4HANA system from the SAP Cloud Appliance Library](https://github.com/SAP-samples/cloud-extension-ecc-business-process/blob/mission/mission/cal-setup/CALS4H.md) 34 | * [Setup of S/4HANA on-premise System](../../tree/mission/mission/s4h-setup/README.md) 35 | * [Setup of SAP Cloud Connector & Trust to the SAP S/4HANA System](../../tree/mission/mission/cloud-connector/README.md) 36 | * [Setup of SAP Business Technology Platform Account](../../tree/mission/mission/scp-setup/README.md) 37 | * [End-to-End Connectivity Setup](../../tree/mission/mission/connectivity/README.md) 38 | * [Setup SAP Identity and Authentication Service (optional)](../../tree/mission/mission/custom-idp/README.md) 39 | 40 | 41 | ## Implementation of a simple UI application 42 | 43 | Once we have setup the landscape, we can now develop, test and run a simple UI application. We will show the steps to implement the simple UI application using the SAP BTP managed HTML5 repository. This is a kind of PoC to see if and how the whole landscape setup is working. 44 | 45 | * [Develop a simple UI application](../../tree/mission/mission/create-application/develop/README.md) 46 | * [Test the simple UI application](../../tree/mission/mission/create-application/test/README.md) 47 | * [Build and deploy the application to your SAP BTP Cloud Foundry account](../../tree/mission/mission/create-application/buildDeploy/README.md) 48 | * [Integrate the Continous Integration & Continous Delivery Service](../../tree/mission/mission/ci-cd-service/README.md) 49 | * [Publishing your application to a SAP Launchpad site](../../tree/mission/mission/launchpad/README.md) 50 | 51 | ![Solution diagram](../../tree/mission/mission/images/solution_diagram.png) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mission: Setup for SAP S/4HANA side-by-side UI Extensions on SAP Business Technology Platform 2 | [![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cloud-extension-html5-sample)](https://api.reuse.software/info/github.com/SAP-samples/cloud-extension-html5-sample) 3 | 4 | The main focus of this mission is to show the full end-to-end setup for a SAP S/4HANA on-premise extension on SAP BTP (Cloud Foundry) this includes the following steps: 5 | * Setup SAP S/4HANA on-premise system 6 | * Setup of SAP BTP account and development environment 7 | * End-to-End connection setup with Principal Propagation (SSO) 8 | * DevOps - using SAP Continuous Integration & Delivery and monitoring 9 | * Integration of the HTML5 application in a central Launchpad 10 | 11 | We will create a simple custom UI application, show the usage of the HTML5 repository and the different options how to expose this application - as a stand-alone or with the different SAP Launchpads environments. 12 | 13 | [Mission in SAP Discovery Center](https://discovery-center.cloud.sap/missiondetail/3239/3325) 14 | 15 | 16 | 17 | ## Discover 18 | 19 | * [The Mission Story](../../tree/mission/mission/discover/MissionStory.md) 20 | * [Learn the Basics of SAP BTP](../../tree/mission/mission/discover/BTP.md) 21 | * [Learn about SAP S/4HANA](../../tree/mission/mission/discover/S4H.md) 22 | * [Learn about SAP Connectivity Service](../../tree/mission/mission/discover/Connectivity.md) 23 | * [Learn about HTML5 Applications](../../tree/mission/mission/discover/HTML5.md) 24 | * [Learn about SAP Business Application Studio](../../tree/mission/mission/discover/BAS.md) 25 | * [Learn about SAP Cloud Identity Services](../../tree/mission/mission/discover/IAS.md) 26 | * [Learn about DevOps and SAP Continous Integration and Delivery](../../tree/mission/mission/discover/CICD.md) 27 | * [Learn about SAP Launchpad Service and SAP Work Zone](../../tree/mission/mission/discover/Launchpad.md) 28 | * [Learn about Observability on SAP BTP](../../tree/mission/mission/discover/Observability.md) 29 | 30 | These are the step-by-step guidelines for running the mission. It is divided in two workstreams: 31 | 32 | ## Landscape Setup 33 | 34 | The setup of the landscape consists of preparing the API in the SAP S/4HANA on-premise system and exposing the backend oData service using SAP Cloud Connector. There are also step-by-step instructions to setup the trust between SAP Cloud Connector and SAP S/4HANA system. 35 | 36 | * [Setup of SAP S/4HANA system from the SAP Cloud Appliance Library](https://github.com/SAP-samples/cloud-extension-ecc-business-process/blob/mission/mission/cal-setup/CALS4H.md) 37 | * [Setup of S/4HANA on-premise System](../../tree/mission/mission/s4h-setup/README.md) 38 | * [Setup of SAP Cloud Connector & Trust to the SAP S/4HANA System](../../tree/mission/mission/cloud-connector/README.md) 39 | * [Setup of SAP Business Technology Platform Account](../../tree/mission/mission/scp-setup/README.md) 40 | * [End-to-End Connectivity Setup](../../tree/mission/mission/connectivity/README.md) 41 | * [Setup SAP Identity and Authentication Service (optional)](../../tree/mission/mission/custom-idp/README.md) 42 | 43 | ## Implementation of a custom OData service 44 | 45 | After the setup is done we can start to create UI API for the future application. Standard APIs provided by SAP cannot be used for this purpose. 46 | 47 | * [Motivation](../../tree/mission/mission/rap-service/motivation.md) 48 | * [Explore Business Object Projection](../../tree/mission/mission/rap-service/explore-projection.md) 49 | * [Set up the ABAP Development Tools](../../tree/mission/mission/rap-service/adt-setup.md) 50 | * [Create the Data Definition](../../tree/mission/mission/rap-service/create-data-definitions.md) 51 | * [Create the Metadata Extension](../../tree/mission/mission/rap-service/create-metadata-extension.md) 52 | * [Expose the OData Service](../../tree/mission/mission/rap-service/expose.md) 53 | * [Test the Service](../../tree/mission/mission/rap-service/test.md) 54 | 55 | ## Implementation of a simple UI application 56 | 57 | Once we have setup the landscape, we can now develop, test and run a simple UI application. We will show the steps to implement the simple UI application using the SAP BTP managed HTML5 repository. This is a kind of PoC to see if and how the whole landscape setup is working. 58 | 59 | * [Develop a simple UI application](../../tree/mission/mission/create-application/develop/README.md) 60 | * [Test the simple UI application](../../tree/mission/mission/create-application/test/README.md) 61 | * [Build and deploy the application to your SAP BTP Cloud Foundry account](../../tree/mission/mission/create-application/buildDeploy/README.md) 62 | * [Integrate the Continous Integration & Continous Delivery Service](../../tree/mission/mission/ci-cd-service/README.md) 63 | * [Publishing your application to a SAP Launchpad site](../../tree/mission/mission/launchpad/README.md) 64 | 65 | ![Solution diagram](./doc/img/simpleUI.drawio.png) 66 | 67 | 68 | 69 | 70 | ## Known Issues 71 | 72 | No known issues. 73 | 74 | ## How to Obtain Support 75 | 76 | In case you find a bug, or you need additional support, please [open an issue](https://github.com/SAP-samples/cloud-extension-html5-sample/issues/new) here in GitHub. 77 | 78 | ## License 79 | 80 | Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This project is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSES/Apache-2.0.txt) file. 81 | -------------------------------------------------------------------------------- /RefApp.md: -------------------------------------------------------------------------------- 1 | # Managed HTML5 Application Sample 2 | 3 | 4 | SAP Business Technology Platform enables you to access and run HTML5 Applications in a cloud environment without the need to maintain your own runtime infrastructure. 5 | 6 | HTML5 Applications consist of static content that runs on a browser. You can develop your applications - either in SAP Business Application Studio, or in your own IDE (integrated development environment) - and deploy them to the HTML5 Application Repository. 7 | 8 | Depending on your backend application setup, you either configure the destinations during development, or define them after deploying the application. Finally, to consume the applications, you can create a site in SAP Cloud Portal Service, build the URL, and define custom domains. 9 | 10 | For more information, please refer to the documentation on [SAP Help Portal](https://help.sap.com/viewer/29badeeee3684338b2e870139bdc4d86/Cloud/en-US/c1b9d6facfc942e3bca664ae06387e9b.html). 11 | 12 | ## Build Custom SAP Fiori User Experience 13 | The objective of this reference application is to showcase the ease of building custom frontends for SAP applications – Bring the ease of use of HTML5 Application on SAP Business Technology Platform Neo Environment to the Multi-Cloud environment. 14 | 15 | 16 | ## Prerequisites 17 | 18 | ### Entitlements 19 | 20 | Make sure that you have an account with below entitlements for your sub account 21 | 22 | | Service | Plan | Number of Instances | 23 | |-----------------------------------|------------|:-------------------:| 24 | | Destination | lite | 2 | 25 | | HTML5 Application | app-host | 2 | 26 | 27 | ### Subscriptions 28 | Make sure that the below subscriptions are active for your sub account <br/> 29 | a. Business Application Studio <br/> 30 | b. Portal <br/> 31 | 32 | #### Steps 33 | 1. From the Subaccount Overview page, click on the tab "Subscriptions" 34 | 2. Search for "Portal" 35 | 3. Click on "Portal" 36 | 4. Click on "Subscribe" 37 | 5. Click on your subaccount 38 | 6. Similarly, subscribe to Business Application Studio. 39 | ![Subscription](/doc/img/Subscription.png) 40 | 41 | ### Role Collections 42 | To access Business Application Studio, users will the role "Business_Application_Studio_Developer". 43 | 44 | 1. In the Subaccount Overview page, Expand Security and Click on Trust Configuration 45 | 2. Click on "SAP ID Service" 46 | 3. Enter Email Address and Click on "Show Assignments" 47 | 4. Click on "Add User" to add the user to the SAP ID Service (if user is not already present in the IDP) 48 | 5. Click on "Assign Role Collection" 49 | 6. Select "Business_Application_Studio_Developer" 50 | 7. Click on "Assign Role collection" 51 | ![AssignRoleCollection](/doc/img/AssignRoleCollection.png) 52 | 53 | 54 | ### Destination Setup 55 | A destination to ES5 is to be configured in the subaccount from which you will access the SAP Business Application Studio. 56 | To do this, please follow the steps below: 57 | 58 | 1. Create an Account on the Gateway Demo System using the steps [here](https://developers.sap.com/tutorials/gateway-demo-signup.html) 59 | 2. Create a Destination within the Cloud Foundry Environment using the steps [here](https://developers.sap.com/tutorials/cp-cf-create-destination.html), and set the ES5 destination properties as follows: 60 | Common properties 61 | - Name: ES5 62 | - Type: HTTP 63 | - Description: ES5 64 | - URL: https://sapes5.sapdevcenter.com:443 65 | - Proxy Type: Internet 66 | - Authentication: BasicAuthentication 67 | - User Name: Your ES5 Gateway user 68 | - Authentication: Your ES5 Gateway password 69 | - Additional Properties: 70 | - HTML5.DynamicDestination: true (Type this additional property manually as it is not available in the drop-down list) 71 | - sap-client: 002 72 | - WebIDEEnabled: true 73 | - WebIDESystem: ES5 74 | - WebIDEUsage: odata_abap 75 | ![Destination](/doc/img/Destination.png) 76 | 77 | 78 | ## Build and Deploy of the Application 79 | 80 | 1. Open the Business Application Studio from Subaccount> Subscriptions 81 | 2. Login to the Application using your CF account login credentials 82 | 3. Click on "Create Dev Space" 83 | 4. Enter the name of the space and Choose "SAP Fiori" and select "Launchpad Module" from Additional Extensions 84 | ![BizappDevSpace](/doc/img/BizappDevSpace.png) 85 | 5. Once the devspace is created, open it. 86 | 6. Go to View-> Select "Find Command" 87 | ![FindCommand](/doc/img/FindCommand.png) 88 | 7. Search for "CF Login" 89 | 8. Select for "CF: Login on to Cloud Foundry" 90 | ![CFLoginBizapp](/doc/img/CFLoginBizapp.png) 91 | 9. Enter CF API endpoint 92 | 10. Enter "Email" and "Password" when prompted 93 | 11. Select your org and space to login to Cloud Foundry 94 | 12. Go to View > Find Command 95 | 13. Search for "Clone" and select the command "Git:Clone" 96 | 14. In the pop up, enter the git repository [link](../../) 97 | 15. Once the cloning is complete, you can view the project in 'Explorer' 98 | 16. Go to Terminal > New Terminal 99 | 17. In the terminal window, navigate to the project root folder 100 | 18. Run the command - ```mbt build``` 101 | 19. Once the build is complete, expand the mta_archives folder in the project root folder. 102 | 20. Right click on the cloud-extension-html5-sample-1.0.0.mtar and select " Deploy MTA Archive" 103 | ![BuildAndDeploy](/doc/img/BuildAndDeploy.png) 104 | 21. Add the role collection "BPViewerRC" to your user using the steps mentioned [above](#role-collections). 105 | 22. Now, run the command ```cf html5-list -u -d```. Open the URL corresponding to the application. 106 | 107 | -------------------------------------------------------------------------------- /doc/img/AssignRoleCollection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/AssignRoleCollection.png -------------------------------------------------------------------------------- /doc/img/BizappDevSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/BizappDevSpace.png -------------------------------------------------------------------------------- /doc/img/BuildAndDeploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/BuildAndDeploy.png -------------------------------------------------------------------------------- /doc/img/CFLoginBizapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/CFLoginBizapp.png -------------------------------------------------------------------------------- /doc/img/CICD-Credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/CICD-Credentials.png -------------------------------------------------------------------------------- /doc/img/Destination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/Destination.png -------------------------------------------------------------------------------- /doc/img/FindCommand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/FindCommand.png -------------------------------------------------------------------------------- /doc/img/RunCICD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/RunCICD.png -------------------------------------------------------------------------------- /doc/img/Subscription.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/Subscription.png -------------------------------------------------------------------------------- /doc/img/Webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/Webhook.png -------------------------------------------------------------------------------- /doc/img/gitclone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/gitclone.png -------------------------------------------------------------------------------- /doc/img/images.png: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /doc/img/simpleUI.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/simpleUI.drawio.png -------------------------------------------------------------------------------- /doc/img/solution_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/doc/img/solution_diagram.png -------------------------------------------------------------------------------- /mta.yaml: -------------------------------------------------------------------------------- 1 | _schema-version: "3.2" 2 | ID: cloud-extension-html5 3 | version: 0.0.1 4 | modules: 5 | - name: cloud-extension-html5-destination-content 6 | type: com.sap.application.content 7 | requires: 8 | - name: uaa_cloud-extension-html5 9 | parameters: 10 | service-key: 11 | name: uaa_cloud-extension-html5-key 12 | - name: cloud-extension-html5_html_repo_host 13 | parameters: 14 | service-key: 15 | name: cloud-extension-html5_html_repo_host-key 16 | - name: cloud-extension-html5-destination-service 17 | parameters: 18 | content: 19 | subaccount: 20 | destinations: 21 | - Name: bp_ServiceManaged_cloud_extension_html5_html_repo_host 22 | ServiceInstanceName: cloud-extension-html5-html5-app-host-service 23 | ServiceKeyName: cloud-extension-html5_html_repo_host-key 24 | sap.cloud.service: bp_ServiceManaged 25 | - Authentication: OAuth2UserTokenExchange 26 | Name: bp_ServiceManaged_uaa_cloud_extension_html5 27 | ServiceInstanceName: cloud-extension-html5-xsuaa-service 28 | ServiceKeyName: uaa_cloud-extension-html5-key 29 | sap.cloud.service: bp_ServiceManaged 30 | existing_destinations_policy: update 31 | content-target: true 32 | build-parameters: 33 | no-source: true 34 | - name: cloud-extension-html5_ui_deployer 35 | type: com.sap.application.content 36 | path: . 37 | requires: 38 | - name: cloud-extension-html5_html_repo_host 39 | parameters: 40 | content-target: true 41 | build-parameters: 42 | build-result: resources 43 | requires: 44 | - artifacts: 45 | - BP-content.zip 46 | name: BP 47 | target-path: resources/ 48 | - name: BP 49 | type: html5 50 | path: BP 51 | build-parameters: 52 | builder: custom 53 | commands: 54 | - npm run build 55 | supported-platforms: [] 56 | resources: 57 | - name: cloud-extension-html5-destination-service 58 | type: org.cloudfoundry.managed-service 59 | parameters: 60 | service: destination 61 | service-name: cloud-extension-html5-destination-service 62 | service-plan: lite 63 | - name: cloud-extension-html5_html_repo_host 64 | type: org.cloudfoundry.managed-service 65 | parameters: 66 | service: html5-apps-repo 67 | service-name: cloud-extension-html5-html5-app-host-service 68 | service-plan: app-host 69 | - name: uaa_cloud-extension-html5 70 | type: org.cloudfoundry.managed-service 71 | parameters: 72 | path: ./xs-security.json 73 | service: xsuaa 74 | service-name: cloud-extension-html5-xsuaa-service 75 | service-plan: application 76 | build-parameters: 77 | before-all: 78 | - builder: custom 79 | commands: 80 | - npm install 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud-extension-html5", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@ui5/builder": "^2.0.1", 6 | "@ui5/fs": "^2.0.1", 7 | "@ui5/logger": "^2.0.0", 8 | "@ui5/project": "^2.1.2", 9 | "@ui5/server": "^2.0.2", 10 | "@ui5/cli": "2.2.2" 11 | }, 12 | "devDependencies": { 13 | "@sap/ui5-builder-webide-extension": "1.0.x", 14 | "eslint": "5.16.x", 15 | "@sap/eslint-plugin-ui5-jsdocs": "2.0.x", 16 | "@sapui5/ts-types": "1.71.x", 17 | "bestzip": "2.1.7", 18 | "rimraf": "3.0.2", 19 | "@sap/approuter": "6.7.x", 20 | "@sap/html5-repo-mock": "1.3.x", 21 | "@ui5/builder": "^2.0.1" 22 | }, 23 | "ui5": { 24 | "dependencies": [ 25 | "@sap/ui5-builder-webide-extension" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/BP-content.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP-samples/cloud-extension-html5-sample/44a90aed1420e23ddaefd90c607d1d71937a1442/resources/BP-content.zip -------------------------------------------------------------------------------- /xs-security.json: -------------------------------------------------------------------------------- 1 | { 2 | "xsappname": "cloud-extension-html5", 3 | "tenant-mode": "dedicated", 4 | "description": "Security profile of called application", 5 | "scopes": [ 6 | { 7 | "name": "$XSAPPNAME.BPViewer", 8 | "description": "BusinessPartner Role" 9 | } 10 | ], 11 | "role-templates": [ 12 | { 13 | "name": "BPViewerRole", 14 | "description": "BusinessPartner Role Template", 15 | "scope-references": [ 16 | "$XSAPPNAME.BPViewer" 17 | ] 18 | } 19 | ], 20 | "role-collections": [ 21 | { 22 | "name": "BPViewerRC", 23 | "description": "BusinessPartner Role Collection", 24 | "role-template-references": [ 25 | "$XSAPPNAME.BPViewerRole" 26 | ] 27 | } 28 | ] 29 | } --------------------------------------------------------------------------------