Onur Yaşarlar LinuxV14G2 1 mesiac pred
rodič
commit
a8da8ad175
60 zmenil súbory, kde vykonal 4944 pridanie a 0 odobranie
  1. 175 0
      LICENSE.txt
  2. 14 0
      package.json
  3. 13 0
      ui5.yaml
  4. 71 0
      webapp/Component.js
  5. 41 0
      webapp/controller/App.controller.js
  6. 64 0
      webapp/controller/BaseController.js
  7. 258 0
      webapp/controller/Detail.controller.js
  8. 7 0
      webapp/controller/DetailObjectNotFound.controller.js
  9. 66 0
      webapp/controller/ErrorHandler.js
  10. 105 0
      webapp/controller/ListSelector.js
  11. 360 0
      webapp/controller/Master.controller.js
  12. 16 0
      webapp/controller/NotFound.controller.js
  13. 181 0
      webapp/i18n/i18n.properties
  14. BIN
      webapp/images/Employee.png
  15. 26 0
      webapp/index.html
  16. 219 0
      webapp/localService/metadata.xml
  17. 22 0
      webapp/localService/mockdata/Customer.json
  18. 43 0
      webapp/localService/mockdata/Employee.json
  19. 268 0
      webapp/localService/mockdata/Order_Details.json
  20. 173 0
      webapp/localService/mockdata/Orders.json
  21. 42 0
      webapp/localService/mockdata/Product.json
  22. 112 0
      webapp/localService/mockserver.js
  23. 146 0
      webapp/manifest.json
  24. 102 0
      webapp/model/formatter.js
  25. 16 0
      webapp/model/models.js
  26. 25 0
      webapp/test.html
  27. 16 0
      webapp/test/Test.qunit.html
  28. 15 0
      webapp/test/initMockServer.js
  29. 91 0
      webapp/test/integration/MasterJourney.js
  30. 123 0
      webapp/test/integration/NavigationJourney.js
  31. 47 0
      webapp/test/integration/NavigationJourneyPhone.js
  32. 55 0
      webapp/test/integration/NotFoundJourney.js
  33. 66 0
      webapp/test/integration/NotFoundJourneyPhone.js
  34. 46 0
      webapp/test/integration/arrangements/Startup.js
  35. 21 0
      webapp/test/integration/opaTests.qunit.js
  36. 13 0
      webapp/test/integration/opaTestsNavigation.qunit.js
  37. 13 0
      webapp/test/integration/opaTestsPhone.qunit.js
  38. 62 0
      webapp/test/integration/pages/App.js
  39. 80 0
      webapp/test/integration/pages/Browser.js
  40. 38 0
      webapp/test/integration/pages/Common.js
  41. 202 0
      webapp/test/integration/pages/Detail.js
  42. 435 0
      webapp/test/integration/pages/Master.js
  43. 82 0
      webapp/test/integration/pages/NotFound.js
  44. 27 0
      webapp/test/mockServer.html
  45. 15 0
      webapp/test/testsuite.qunit.html
  46. 39 0
      webapp/test/testsuite.qunit.js
  47. 33 0
      webapp/test/unit/controller/Detail.controller.js
  48. 189 0
      webapp/test/unit/controller/ListSelector.js
  49. 24 0
      webapp/test/unit/helper/FakeI18nModel.js
  50. 109 0
      webapp/test/unit/model/formatter.js
  51. 62 0
      webapp/test/unit/model/models.js
  52. 5 0
      webapp/test/unit/unitTests.qunit.js
  53. 18 0
      webapp/view/App.view.xml
  54. 179 0
      webapp/view/Detail.view.xml
  55. 19 0
      webapp/view/DetailObjectNotFound.view.xml
  56. 140 0
      webapp/view/Master.view.xml
  57. 15 0
      webapp/view/NotFound.view.xml
  58. 33 0
      webapp/view/Processor.view.xml
  59. 30 0
      webapp/view/Shipping.view.xml
  60. 37 0
      webapp/view/ViewSettingsDialog.fragment.xml

+ 175 - 0
LICENSE.txt

@@ -0,0 +1,175 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "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.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "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.
+
+      "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).
+
+      "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.
+
+      "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."
+
+      "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.
+
+   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.
+
+   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.
+
+   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:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (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
+
+      (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.
+
+      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.
+
+   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.
+
+   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.
+
+   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.
+
+   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.
+
+   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.

+ 14 - 0
package.json

@@ -0,0 +1,14 @@
+{
+  "name": "browse-orders",
+  "private": true,
+  "version": "1.0.0",
+  "author": "SAP SE",
+  "description": "UI5 Demo App - Browse Orders",
+  "scripts": {
+    "start": "ui5 serve",
+    "build": "ui5 build --all --clean-dest"
+  },
+  "devDependencies": {
+    "@ui5/cli": "^4"
+  }
+}

+ 13 - 0
ui5.yaml

@@ -0,0 +1,13 @@
+specVersion: "4.0"
+metadata:
+  name: browse-orders
+type: application
+framework:
+  name: OpenUI5
+  version: "1.144.0" #MainVersion#
+  libraries:
+    - name: sap.m
+    - name: sap.f
+    - name: sap.ui.layout
+    - name: sap.ui.core
+    - name: themelib_sap_horizon

+ 71 - 0
webapp/Component.js

@@ -0,0 +1,71 @@
+sap.ui.define([
+	"sap/ui/core/UIComponent",
+	"sap/ui/Device",
+	"./model/models",
+	"./controller/ListSelector",
+	"./controller/ErrorHandler"
+], function (UIComponent, Device, models, ListSelector, ErrorHandler) {
+	"use strict";
+
+	return UIComponent.extend("sap.ui.demo.orderbrowser.Component", {
+
+		metadata : {
+			manifest : "json"
+		},
+
+		/**
+		 * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
+		 * In this method, the device models are set and the router is initialized.
+		 * @public
+		 * @override
+		 */
+		init : function () {
+			this.oListSelector = new ListSelector();
+			this._oErrorHandler = new ErrorHandler(this);
+
+			// set the device model
+			this.setModel(models.createDeviceModel(), "device");
+
+			// call the base component's init function and create the App view
+			UIComponent.prototype.init.apply(this, arguments);
+
+			// create the views based on the url/hash
+			this.getRouter().initialize();
+		},
+
+		/**
+		 * The component is destroyed by UI5 automatically.
+		 * In this method, the ListSelector and ErrorHandler are destroyed.
+		 * @public
+		 * @override
+		 */
+		destroy : function () {
+			this.oListSelector.destroy();
+			this._oErrorHandler.destroy();
+			// call the base component's destroy function
+			UIComponent.prototype.destroy.apply(this, arguments);
+		},
+
+		/**
+		 * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
+		 * design mode class should be set, which influences the size appearance of some controls.
+		 * @public
+		 * @return {string} css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
+		 */
+		getContentDensityClass : function() {
+			if (this._sContentDensityClass === undefined) {
+				// check whether FLP has already set the content density class; do nothing in this case
+				if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
+					this._sContentDensityClass = "";
+				} else if (!Device.support.touch) { // apply "compact" mode if touch is not supported
+					this._sContentDensityClass = "sapUiSizeCompact";
+				} else {
+					// "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
+					this._sContentDensityClass = "sapUiSizeCozy";
+				}
+			}
+			return this._sContentDensityClass;
+		}
+
+	});
+});

+ 41 - 0
webapp/controller/App.controller.js

@@ -0,0 +1,41 @@
+sap.ui.define([
+	"./BaseController",
+	"sap/ui/model/json/JSONModel"
+], function (BaseController, JSONModel) {
+	"use strict";
+
+	return BaseController.extend("sap.ui.demo.orderbrowser.controller.App", {
+
+		onInit : function () {
+			var oViewModel,
+				fnSetAppNotBusy,
+				iOriginalBusyDelay = this.getView().getBusyIndicatorDelay();
+
+			oViewModel = new JSONModel({
+				busy : true,
+				delay : 0,
+				layout : "OneColumn",
+				previousLayout : "",
+				actionButtonsInfo : {
+					midColumn : {
+						fullScreen : false
+					}
+				}
+			});
+			this.setModel(oViewModel, "appView");
+
+			fnSetAppNotBusy = function() {
+				oViewModel.setProperty("/busy", false);
+				oViewModel.setProperty("/delay", iOriginalBusyDelay);
+			};
+
+			// since then() has no "reject"-path attach to the MetadataFailed-Event to disable the busy indicator in case of an error
+			this.getOwnerComponent().getModel().metadataLoaded().then(fnSetAppNotBusy);
+			this.getOwnerComponent().getModel().attachMetadataFailed(fnSetAppNotBusy);
+
+			// apply content density mode to root view
+			this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
+		}
+
+	});
+});

+ 64 - 0
webapp/controller/BaseController.js

@@ -0,0 +1,64 @@
+sap.ui.define([
+	"sap/ui/core/mvc/Controller",
+	"sap/ui/core/routing/History"
+], function (Controller, History) {
+	"use strict";
+
+	return Controller.extend("sap.ui.demo.orderbrowser.controller.BaseController", {
+		/**
+		 * Convenience method for accessing the router in every controller of the application.
+		 * @public
+		 * @returns {sap.ui.core.routing.Router} the router for this component
+		 */
+		getRouter : function () {
+			return this.getOwnerComponent().getRouter();
+		},
+
+		/**
+		 * Convenience method for getting the view model by name in every controller of the application.
+		 * @public
+		 * @param {string} sName the model name
+		 * @returns {sap.ui.model.Model} the model instance
+		 */
+		getModel : function (sName) {
+			return this.getView().getModel(sName);
+		},
+
+		/**
+		 * Convenience method for setting the view model in every controller of the application.
+		 * @public
+		 * @param {sap.ui.model.Model} oModel the model instance
+		 * @param {string} sName the model name
+		 * @returns {sap.ui.core.mvc.View} the view instance
+		 */
+		setModel : function (oModel, sName) {
+			return this.getView().setModel(oModel, sName);
+		},
+
+		/**
+		 * Convenience method for getting the resource bundle.
+		 * @public
+		 * @returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component
+		 */
+		getResourceBundle : function () {
+			return this.getOwnerComponent().getModel("i18n").getResourceBundle();
+		},
+
+		/**
+		 * Event handler for navigating back.
+		 * It there is a history entry we go one step back in the browser history
+		 * If not, it will replace the current entry of the browser history with the master route.
+		 * @public
+		 */
+		onNavBack : function() {
+			var sPreviousHash = History.getInstance().getPreviousHash();
+
+			if (sPreviousHash !== undefined) {
+				history.go(-1);
+			} else {
+				this.getRouter().navTo("master", {}, true);
+			}
+		}
+
+	});
+});

+ 258 - 0
webapp/controller/Detail.controller.js

@@ -0,0 +1,258 @@
+sap.ui.define([
+	"./BaseController",
+	"sap/ui/model/json/JSONModel",
+	"../model/formatter",
+	"sap/m/library"
+], function(BaseController, JSONModel, formatter, mobileLibrary) {
+	"use strict";
+
+	// shortcut for sap.m.URLHelper
+	var URLHelper = mobileLibrary.URLHelper;
+
+	function _calculateOrderTotal (fPreviousTotal, oCurrentContext) {
+		var fItemTotal = oCurrentContext.getObject().Quantity * oCurrentContext.getObject().UnitPrice;
+		return fPreviousTotal + fItemTotal;
+	}
+	return BaseController.extend("sap.ui.demo.orderbrowser.controller.Detail", {
+
+		formatter: formatter,
+
+		/* =========================================================== */
+		/* lifecycle methods                                           */
+		/* =========================================================== */
+
+		onInit : function () {
+			// Model used to manipulate control states. The chosen values make sure,
+			// detail page is busy indication immediately so there is no break in
+			// between the busy indication for loading the view's meta data
+			this._aValidKeys = ["shipping", "processor"];
+			var oViewModel = new JSONModel({
+				busy : false,
+				delay : 0,
+				lineItemListTitle : this.getResourceBundle().getText("detailLineItemTableHeading"),
+				// Set fixed currency on view model (as the OData service does not provide a currency).
+				currency : "EUR",
+				// the sum of all items of this order
+				totalOrderAmount: 0,
+				selectedTab: ""
+			});
+
+			this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this);
+
+			this.setModel(oViewModel, "detailView");
+
+			this.getOwnerComponent().getModel().metadataLoaded().then(this._onMetadataLoaded.bind(this));
+		},
+
+		/* =========================================================== */
+		/* event handlers                                              */
+		/* =========================================================== */
+
+		/**
+		 * Event handler when the share by E-Mail button has been clicked
+		 * @public
+		 */
+		onSendEmailPress : function () {
+			var oViewModel = this.getModel("detailView");
+
+			URLHelper.triggerEmail(
+				null,
+				oViewModel.getProperty("/shareSendEmailSubject"),
+				oViewModel.getProperty("/shareSendEmailMessage")
+			);
+		},
+
+
+		/**
+		 * Updates the item count within the line item table's header
+		 * @param {object} oEvent an event containing the total number of items in the list
+		 * @private
+		 */
+		onListUpdateFinished : function (oEvent) {
+			var sTitle,
+				fOrderTotal = 0,
+				iTotalItems = oEvent.getParameter("total"),
+				oViewModel = this.getModel("detailView"),
+				oItemsBinding = oEvent.getSource().getBinding("items"),
+				aItemsContext;
+
+			// only update the counter if the length is final
+			if (oItemsBinding.isLengthFinal()) {
+				if (iTotalItems) {
+					sTitle = this.getResourceBundle().getText("detailLineItemTableHeadingCount", [iTotalItems]);
+				} else {
+					//Display 'Line Items' instead of 'Line items (0)'
+					sTitle = this.getResourceBundle().getText("detailLineItemTableHeading");
+				}
+				oViewModel.setProperty("/lineItemListTitle", sTitle);
+
+				aItemsContext = oItemsBinding.getContexts();
+				fOrderTotal = aItemsContext.reduce(_calculateOrderTotal, 0);
+				oViewModel.setProperty("/totalOrderAmount", fOrderTotal);
+			}
+
+		},
+
+		/* =========================================================== */
+		/* begin: internal methods                                     */
+		/* =========================================================== */
+
+		/**
+		 * Binds the view to the object path and expands the aggregated line items.
+		 * @function
+		 * @param {sap.ui.base.Event} oEvent pattern match event in route 'object'
+		 * @private
+		 */
+		_onObjectMatched : function (oEvent) {
+			var oArguments = oEvent.getParameter("arguments");
+			this._sObjectId = oArguments.objectId;
+			// Don't show two columns when in full screen mode
+			if (this.getModel("appView").getProperty("/layout") !== "MidColumnFullScreen") {
+				this.getModel("appView").setProperty("/layout", "TwoColumnsMidExpanded");
+			}
+			this.getModel().metadataLoaded().then( function() {
+				var sObjectPath = this.getModel().createKey("Orders", {
+					OrderID :  this._sObjectId
+				});
+				this._bindView("/" + sObjectPath);
+			}.bind(this));
+			var oQuery = oArguments["?query"];
+			if (oQuery && this._aValidKeys.indexOf(oQuery.tab) >= 0){
+				this.getView().getModel("detailView").setProperty("/selectedTab", oQuery.tab);
+				this.getRouter().getTargets().display(oQuery.tab);
+			} else {
+				this.getRouter().navTo("object", {
+					objectId: this._sObjectId,
+					query: {
+						tab: "shipping"
+					}
+				}, true);
+			}
+		},
+
+		/**
+		 * Binds the view to the object path. Makes sure that detail view displays
+		 * a busy indicator while data for the corresponding element binding is loaded.
+		 * @function
+		 * @param {string} sObjectPath path to the object to be bound to the view.
+		 * @private
+		 */
+		_bindView : function (sObjectPath) {
+			// Set busy indicator during view binding
+			var oViewModel = this.getModel("detailView");
+
+			// If the view was not bound yet its not busy, only if the binding requests data it is set to busy again
+			oViewModel.setProperty("/busy", false);
+
+			this.getView().bindElement({
+				path : sObjectPath,
+				parameters: {
+					expand: "Customer,Order_Details/Product,Employee"
+				},
+				events: {
+					change : this._onBindingChange.bind(this),
+					dataRequested : function () {
+						oViewModel.setProperty("/busy", true);
+					},
+					dataReceived: function () {
+						oViewModel.setProperty("/busy", false);
+					}
+				}
+			});
+		},
+
+		_onBindingChange : function () {
+			var oView = this.getView(),
+				oElementBinding = oView.getElementBinding();
+
+			// No data for the binding
+			if (!oElementBinding.getBoundContext()) {
+				this.getRouter().getTargets().display("detailObjectNotFound");
+				// if object could not be found, the selection in the master list
+				// does not make sense anymore.
+				this.getOwnerComponent().oListSelector.clearMasterListSelection();
+				return;
+			}
+
+			var sPath = oElementBinding.getPath(),
+				oResourceBundle = this.getResourceBundle(),
+				oObject = oView.getModel().getObject(sPath),
+				sObjectId = oObject.OrderID,
+				sObjectName = oObject.OrderID,
+				oViewModel = this.getModel("detailView");
+
+			this.getOwnerComponent().oListSelector.selectAListItem(sPath);
+
+			oViewModel.setProperty("/shareSendEmailSubject",
+				oResourceBundle.getText("shareSendEmailObjectSubject", [sObjectId]));
+			oViewModel.setProperty("/shareSendEmailMessage",
+				oResourceBundle.getText("shareSendEmailObjectMessage", [sObjectName, sObjectId, location.href, oObject.ShipName, oObject.EmployeeID, oObject.CustomerID]));
+		},
+
+		_onMetadataLoaded : function () {
+			// Store original busy indicator delay for the detail view
+			var iOriginalViewBusyDelay = this.getView().getBusyIndicatorDelay(),
+				oViewModel = this.getModel("detailView"),
+				oLineItemTable = this.byId("lineItemsList"),
+				iOriginalLineItemTableBusyDelay = oLineItemTable.getBusyIndicatorDelay();
+
+			// Make sure busy indicator is displayed immediately when
+			// detail view is displayed for the first time
+			oViewModel.setProperty("/delay", 0);
+			oViewModel.setProperty("/lineItemTableDelay", 0);
+
+			oLineItemTable.attachEventOnce("updateFinished", function() {
+				// Restore original busy indicator delay for line item table
+				oViewModel.setProperty("/lineItemTableDelay", iOriginalLineItemTableBusyDelay);
+			});
+
+			// Binding the view will set it to not busy - so the view is always busy if it is not bound
+			oViewModel.setProperty("/busy", true);
+			// Restore original busy indicator delay for the detail view
+			oViewModel.setProperty("/delay", iOriginalViewBusyDelay);
+		},
+		onTabSelect : function(oEvent){
+			var sSelectedTab = oEvent.getParameter("selectedKey");
+			this.getRouter().navTo("object", {
+				objectId: this._sObjectId,
+				query: {
+					tab: sSelectedTab
+				}
+			}, true);// true without history
+
+		},
+
+		_onHandleTelephonePress : function (oEvent){
+			var sNumber = oEvent.getSource().getText();
+			URLHelper.triggerTel(sNumber);
+		},
+
+		/**
+		 * Set the full screen mode to false and navigate to master page
+		 */
+		onCloseDetailPress: function () {
+			this.getModel("appView").setProperty("/actionButtonsInfo/midColumn/fullScreen", false);
+			// No item should be selected on master after detail page is closed
+			this.getOwnerComponent().oListSelector.clearMasterListSelection();
+			this.getRouter().navTo("master");
+		},
+
+		/**
+		 * Toggle between full and non full screen mode.
+		 */
+		toggleFullScreen: function () {
+			var bFullScreen = this.getModel("appView").getProperty("/actionButtonsInfo/midColumn/fullScreen");
+			this.getModel("appView").setProperty("/actionButtonsInfo/midColumn/fullScreen", !bFullScreen);
+			if (!bFullScreen) {
+				// store current layout and go full screen
+				this.getModel("appView").setProperty("/previousLayout", this.getModel("appView").getProperty("/layout"));
+				this.getModel("appView").setProperty("/layout", "MidColumnFullScreen");
+			} else {
+				// reset to previous layout
+				this.getModel("appView").setProperty("/layout",  this.getModel("appView").getProperty("/previousLayout"));
+			}
+
+		}
+
+	});
+});

+ 7 - 0
webapp/controller/DetailObjectNotFound.controller.js

@@ -0,0 +1,7 @@
+sap.ui.define([
+	"./BaseController"
+], function (BaseController) {
+	"use strict";
+
+	return BaseController.extend("sap.ui.demo.orderbrowser.controller.DetailObjectNotFound", {});
+});

+ 66 - 0
webapp/controller/ErrorHandler.js

@@ -0,0 +1,66 @@
+sap.ui.define([
+	"sap/ui/base/Object",
+	"sap/m/MessageBox"
+], function (UI5Object, MessageBox) {
+	"use strict";
+
+	return UI5Object.extend("sap.ui.demo.orderbrowser.controller.ErrorHandler", {
+
+		/**
+		 * Handles application errors by automatically attaching to the model events and displaying errors when needed.
+		 * @class
+		 * @param {sap.ui.core.UIComponent} oComponent reference to the app's component
+		 * @public
+		 * @alias sap.ui.demo.orderbrowser.controller.ErrorHandler
+		 */
+		constructor : function (oComponent) {
+			this._oComponent = oComponent;
+			this._oModel = oComponent.getModel();
+			this._bMessageOpen = false;
+
+			this._oModel.attachMetadataFailed(function (oEvent) {
+				var oParams = oEvent.getParameters();
+				this._showServiceError(oParams.response);
+			}, this);
+
+			this._oModel.attachRequestFailed(function (oEvent) {
+				var oParams = oEvent.getParameters();
+				// An entity that was not found in the service is also throwing a 404 error in oData.
+				// We already cover this case with a notFound target so we skip it here.
+				// A request that cannot be sent to the server is a technical error that we have to handle though
+				if (oParams.response.statusCode !== "404" || (oParams.response.statusCode === 404 && oParams.response.responseText.indexOf("Cannot POST") === 0)) {
+					this._showServiceError(oParams.response);
+				}
+			}, this);
+		},
+
+		/**
+		 * Shows a {@link sap.m.MessageBox} when a service call has failed.
+		 * Only the first error message will be display.
+		 * @param {string} sDetails a technical error to be displayed on request
+		 * @private
+		 */
+		_showServiceError : async function (sDetails) {
+			if (this._bMessageOpen) {
+				return;
+			}
+			this._bMessageOpen = true;
+
+			const oResourceBundle = await this._oComponent.getModel("i18n").getResourceBundle();
+			MessageBox.error(
+				oResourceBundle.getText("errorText"),
+				{
+					id : "serviceErrorMessageBox",
+					details : sDetails,
+					styleClass : this._oComponent.getContentDensityClass(),
+					actions : [MessageBox.Action.CLOSE],
+					onClose : function () {
+						this._bMessageOpen = false;
+					}.bind(this)
+				}
+			);
+		}
+
+	});
+
+});

+ 105 - 0
webapp/controller/ListSelector.js

@@ -0,0 +1,105 @@
+sap.ui.define([
+	"sap/ui/base/Object",
+	"sap/base/Log"
+], function (BaseObject, Log) {
+	"use strict";
+
+	return BaseObject.extend("sap.ui.demo.orderbrowser.controller.ListSelector", {
+
+		/**
+		 * 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
+		 * function.
+		 * @class
+		 * @public
+		 * @alias sap.ui.demo.orderbrowser.controller.ListSelector
+		 */
+
+		constructor: function () {
+			this._oWhenListHasBeenSet = new Promise(function (fnResolveListHasBeenSet) {
+				this._fnResolveListHasBeenSet = fnResolveListHasBeenSet;
+			}.bind(this));
+			// This promise needs to be created in the constructor, since it is allowed to
+			// invoke selectItem functions before calling setBoundMasterList
+			this.oWhenListLoadingIsDone = new Promise(function (fnResolve, fnReject) {
+				// Used to wait until the setBound masterList function is invoked
+				this._oWhenListHasBeenSet
+					.then(function (oList) {
+						oList.getBinding("items").attachEventOnce("dataReceived",
+							function () {
+								if (this._oList.getItems().length) {
+									fnResolve({
+										list: oList
+									});
+								} else {
+									// No items in the list
+									fnReject({
+										list: oList
+									});
+								}
+							}.bind(this)
+						);
+					}.bind(this));
+			}.bind(this));
+		},
+
+		/**
+		 * A bound list should be passed in here. Should be done, before the list has received its initial data from the server.
+		 * May only be invoked once per ListSelector instance.
+		 * @param {sap.m.List} oList The list all the select functions will be invoked on.
+		 * @public
+		 */
+		setBoundMasterList: function (oList) {
+			this._oList = oList;
+			this._fnResolveListHasBeenSet(oList);
+		},
+
+		/**
+		 * 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,
+		 * no selection/scrolling will happen
+		 * @param {string} sBindingPath the binding path matching the binding path of a list item
+		 * @public
+		 */
+		selectAListItem: function (sBindingPath) {
+
+			this.oWhenListLoadingIsDone.then(
+				function () {
+					var oList = this._oList,
+						oSelectedItem;
+
+					if (oList.getMode() === "None") {
+						return;
+					}
+
+					oSelectedItem = oList.getSelectedItem();
+
+					// skip update if the current selection is already matching the object path
+					if (oSelectedItem && oSelectedItem.getBindingContext().getPath() === sBindingPath) {
+						return;
+					}
+
+					oList.getItems().some(function (oItem) {
+						if (oItem.getBindingContext() && oItem.getBindingContext().getPath() === sBindingPath) {
+							oList.setSelectedItem(oItem);
+							return true;
+						}
+					});
+				}.bind(this),
+				function () {
+					Log.warning("Could not select the list item with the path" + sBindingPath + " because the list encountered an error or had no items");
+				}
+			);
+		},
+
+		/**
+		 * Removes all selections from master list.
+		 * Does not trigger 'selectionChange' event on master list, though.
+		 * @public
+		 */
+		clearMasterListSelection: function () {
+			//use promise to make sure that 'this._oList' is available
+			this._oWhenListHasBeenSet.then(function () {
+				this._oList.removeSelections(true);
+			}.bind(this));
+		}
+	});
+});

+ 360 - 0
webapp/controller/Master.controller.js

@@ -0,0 +1,360 @@
+sap.ui.define([
+	"./BaseController",
+	"sap/ui/model/json/JSONModel",
+	"sap/ui/model/Filter",
+	"sap/ui/model/FilterOperator",
+	"sap/ui/model/Sorter",
+	"sap/m/GroupHeaderListItem",
+	"sap/ui/Device",
+	"sap/ui/core/Fragment",
+	"../model/formatter",
+	"sap/ui/core/format/DateFormat"
+], function (BaseController, JSONModel, Filter, FilterOperator, Sorter, GroupHeaderListItem, Device, Fragment, formatter, DateFormat) {
+	"use strict";
+
+	return BaseController.extend("sap.ui.demo.orderbrowser.controller.Master", {
+
+		formatter: formatter,
+
+		/* =========================================================== */
+		/* lifecycle methods                                           */
+		/* =========================================================== */
+
+		/**
+		 * Called when the master list controller is instantiated. It sets up the event handling for the master/detail communication and other lifecycle tasks.
+		 * @public
+		 */
+		onInit : function () {
+			// Control state model
+			var oList = this.byId("list"),
+				oViewModel = this._createViewModel(),
+				// Put down master list's original value for busy indicator delay,
+				// so it can be restored later on. Busy handling on the master list is
+				// taken care of by the master list itself.
+				iOriginalBusyDelay = oList.getBusyIndicatorDelay();
+
+			this._oGroupFunctions = {
+				CompanyName: function (oContext) {
+					var sCompanyName = oContext.getProperty("Customer/CompanyName");
+					return {
+						key: sCompanyName,
+						text: sCompanyName
+					};
+				},
+
+				OrderDate: function (oContext) {
+					var oDate = oContext.getProperty("OrderDate"),
+						iYear = oDate.getFullYear(),
+						iMonth = oDate.getMonth() + 1,
+						sMonthName = this._oMonthNameFormat.format(oDate);
+
+					return {
+						key: iYear + "-" + iMonth,
+						text: this.getResourceBundle().getText("masterGroupTitleOrderedInPeriod", [sMonthName, iYear])
+					};
+				}.bind(this),
+
+				ShippedDate: function (oContext) {
+					var oDate = oContext.getProperty("ShippedDate");
+					// Special handling needed because shipping date may be empty (=> not yet shipped).
+					if (oDate != null) {
+						var iYear = oDate.getFullYear(),
+							iMonth = oDate.getMonth() + 1,
+							sMonthName = this._oMonthNameFormat.format(oDate);
+
+						return {
+							key: iYear + "-" + iMonth,
+							text: this.getResourceBundle().getText("masterGroupTitleShippedInPeriod", [sMonthName, iYear])
+						};
+					} else {
+						return {
+							key: 0,
+							text: this.getResourceBundle().getText("masterGroupTitleNotShippedYet")
+						};
+					}
+				}.bind(this)
+			};
+			this._oMonthNameFormat = DateFormat.getInstance({ pattern: "MMMM"});
+
+			this._oList = oList;
+
+			// keeps the filter and search state
+			this._oListFilterState = {
+				aFilter : [],
+				aSearch : []
+			};
+
+			this.setModel(oViewModel, "masterView");
+			// Make sure, busy indication is showing immediately so there is no
+			// break after the busy indication for loading the view's meta data is
+			// ended (see promise 'oWhenMetadataIsLoaded' in AppController)
+			oList.attachEventOnce("updateFinished", function(){
+				// Restore original busy indicator delay for the list
+				oViewModel.setProperty("/delay", iOriginalBusyDelay);
+			});
+
+			this.getView().addEventDelegate({
+				onBeforeFirstShow: function () {
+					this.getOwnerComponent().oListSelector.setBoundMasterList(oList);
+				}.bind(this)
+			});
+
+			this.getRouter().getRoute("master").attachPatternMatched(this._onMasterMatched, this);
+			this.getRouter().attachBypassed(this.onBypassed, this);
+		},
+
+		/* =========================================================== */
+		/* event handlers                                              */
+		/* =========================================================== */
+
+		/**
+		 * After list data is available, this handler method updates the
+		 * master list counter
+		 * @param {sap.ui.base.Event} oEvent the update finished event
+		 * @public
+		 */
+		onUpdateFinished : function (oEvent) {
+			// update the master list object counter after new data is loaded
+			this._updateListItemCount(oEvent.getParameter("total"));
+		},
+
+		/**
+		 * Event handler for the master search field. Applies current
+		 * filter value and triggers a new search. If the search field's
+		 * 'refresh' button has been pressed, no new search is triggered
+		 * and the list binding is refresh instead.
+		 * @param {sap.ui.base.Event} oEvent the search event
+		 * @public
+		 */
+		onSearch : function (oEvent) {
+			if (oEvent.getParameters().refreshButtonPressed) {
+				// Search field's 'refresh' button has been pressed.
+				// This is visible if you select any master list item.
+				// In this case no new search is triggered, we only
+				// refresh the list binding.
+				this.onRefresh();
+				return;
+			}
+
+			var sQuery = oEvent.getParameter("query");
+
+			if (sQuery) {
+				this._oListFilterState.aSearch = [new Filter("CustomerName", FilterOperator.Contains, sQuery)];
+			} else {
+				this._oListFilterState.aSearch = [];
+			}
+			this._applyFilterSearch();
+
+		},
+
+		/**
+		 * Event handler for refresh event. Keeps filter, sort
+		 * and group settings and refreshes the list binding.
+		 * @public
+		 */
+		onRefresh : function () {
+			this._oList.getBinding("items").refresh();
+		},
+
+		/**
+		 * Event handler for the filter, sort and group buttons to open the ViewSettingsDialog.
+		 * @param {sap.ui.base.Event} oEvent the button press event
+		 * @public
+		 */
+		onOpenViewSettings : function (oEvent) {
+			var sDialogTab = "filter";
+			if (oEvent.getSource().isA("sap.m.Button")) {
+				var sButtonId = oEvent.getSource().getId();
+				if (sButtonId.match("sort")) {
+					sDialogTab = "sort";
+				} else if (sButtonId.match("group")) {
+					sDialogTab = "group";
+				}
+			}
+			// load asynchronous XML fragment
+			if (!this._pViewSettingsDialog) {
+				this._pViewSettingsDialog = Fragment.load({
+					id: this.getView().getId(),
+					name: "sap.ui.demo.orderbrowser.view.ViewSettingsDialog",
+					controller: this
+				}).then(function(oDialog){
+					// connect dialog to the root view of this component (models, lifecycle)
+					this.getView().addDependent(oDialog);
+					oDialog.addStyleClass(this.getOwnerComponent().getContentDensityClass());
+					return oDialog;
+				}.bind(this));
+			}
+			this._pViewSettingsDialog.then(function(oDialog) {
+				oDialog.open(sDialogTab);
+			});
+		},
+
+		/**
+		 * Event handler called when ViewSettingsDialog has been confirmed, i.e.
+		 * has been closed with 'OK'. In the case, the currently chosen filters or groupers
+		 * are applied to the master list, which can also mean that they
+		 * are removed from the master list, in case they are
+		 * removed in the ViewSettingsDialog.
+		 * @param {sap.ui.base.Event} oEvent the confirm event
+		 * @public
+		 */
+		onConfirmViewSettingsDialog : function (oEvent) {
+			var aFilterItems = oEvent.getParameter("filterItems"),
+				aFilters = [],
+				aCaptions = [];
+			aFilterItems.forEach(function (oItem) {
+				switch (oItem.getKey()) {
+					case "Shipped":
+						aFilters.push(new Filter("ShippedDate", FilterOperator.NE, null));
+						break;
+					case "NotShipped":
+						aFilters.push(new Filter("ShippedDate", FilterOperator.EQ, null));
+						break;
+					default:
+					break;
+				}
+				aCaptions.push(oItem.getText());
+			});
+			this._oListFilterState.aFilter = aFilters;
+			this._updateFilterBar(aCaptions.join(", "));
+			this._applyFilterSearch();
+			this._applyGrouper(oEvent);
+		},
+
+		/**
+		 * Apply the chosen grouper to the master list
+		 * @param {sap.ui.base.Event} oEvent the confirm event
+		 * @private
+		 */
+		_applyGrouper: function (oEvent) {
+			var mParams = oEvent.getParameters(),
+				sPath,
+				bDescending,
+				aSorters = [];
+			// apply sorter to binding
+			if (mParams.groupItem) {
+				mParams.groupItem.getKey() === "CompanyName" ?
+					sPath = "Customer/" + mParams.groupItem.getKey() : sPath = mParams.groupItem.getKey();
+				bDescending = mParams.groupDescending;
+				var vGroup = this._oGroupFunctions[mParams.groupItem.getKey()];
+				aSorters.push(new Sorter(sPath, bDescending, vGroup));
+			}
+			this._oList.getBinding("items").sort(aSorters);
+		},
+
+		/**
+		 * Event handler for the list selection event
+		 * @param {sap.ui.base.Event} oEvent the list selectionChange event
+		 * @public
+		 */
+		onSelectionChange : function (oEvent) {
+			var oList = oEvent.getSource(),
+				bSelected = oEvent.getParameter("selected");
+
+			// skip navigation when deselecting an item in multi selection mode
+			if (!(oList.getMode() === "MultiSelect" && !bSelected)) {
+				// get the list item, either from the listItem parameter or from the event's source itself (will depend on the device-dependent mode).
+				this._showDetail(oEvent.getParameter("listItem") || oEvent.getSource());
+			}
+		},
+
+		/**
+		 * Event handler for the bypassed event, which is fired when no routing pattern matched.
+		 * If there was an object selected in the master list, that selection is removed.
+		 * @public
+		 */
+		onBypassed : function () {
+			this._oList.removeSelections(true);
+		},
+
+		/**
+		 * Used to create GroupHeaders with non-capitalized caption.
+		 * These headers are inserted into the master list to
+		 * group the master list's items.
+		 * @param {Object} oGroup group whose text is to be displayed
+		 * @public
+		 * @returns {sap.m.GroupHeaderListItem} group header with non-capitalized caption.
+		 */
+		createGroupHeader : function (oGroup) {
+			return new GroupHeaderListItem({
+				title : oGroup.text
+			});
+		},
+
+		/* =========================================================== */
+		/* begin: internal methods                                     */
+		/* =========================================================== */
+
+
+		_createViewModel : function() {
+			return new JSONModel({
+				isFilterBarVisible: false,
+				filterBarLabel: "",
+				delay: 0,
+				titleCount: 0,
+				noDataText: this.getResourceBundle().getText("masterListNoDataText")
+			});
+		},
+
+		_onMasterMatched :  function() {
+			//Set the layout property of the FCL control to 'OneColumn'
+			this.getModel("appView").setProperty("/layout", "OneColumn");
+		},
+
+		/**
+		 * Shows the selected item on the detail page
+		 * On phones a additional history entry is created
+		 * @param {sap.m.ObjectListItem} oItem selected Item
+		 * @private
+		 */
+		_showDetail : function (oItem) {
+			var bReplace = !Device.system.phone;
+			// set the layout property of FCL control to show two columns
+			this.getModel("appView").setProperty("/layout", "TwoColumnsMidExpanded");
+			this.getRouter().navTo("object", {
+				objectId : oItem.getBindingContext().getProperty("OrderID")
+			}, bReplace);
+		},
+
+		/**
+		 * Sets the item count on the master list header
+		 * @param {int} iTotalItems the total number of items in the list
+		 * @private
+		 */
+		_updateListItemCount : function (iTotalItems) {
+			// only update the counter if the length is final
+			if (this._oList.getBinding("items").isLengthFinal()) {
+				this.getModel("masterView").setProperty("/titleCount", iTotalItems);
+			}
+		},
+
+		/**
+		 * Internal helper method to apply both filter and search state together on the list binding
+		 * @private
+		 */
+		_applyFilterSearch : function () {
+			var aFilters = this._oListFilterState.aSearch.concat(this._oListFilterState.aFilter),
+				oViewModel = this.getModel("masterView");
+			this._oList.getBinding("items").filter(aFilters, "Application");
+			// changes the noDataText of the list in case there are no filter results
+			if (aFilters.length !== 0) {
+				oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataWithFilterOrSearchText"));
+			} else if (this._oListFilterState.aSearch.length > 0) {
+				// only reset the no data text to default when no new search was triggered
+				oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataText"));
+			}
+		},
+
+		/**
+		 * Internal helper method that sets the filter bar visibility property and the label's caption to be shown
+		 * @param {string} sFilterBarText the selected filter value
+		 * @private
+		 */
+		_updateFilterBar : function (sFilterBarText) {
+			var oViewModel = this.getModel("masterView");
+			oViewModel.setProperty("/isFilterBarVisible", (this._oListFilterState.aFilter.length > 0));
+			oViewModel.setProperty("/filterBarLabel", this.getResourceBundle().getText("masterFilterBarText", [sFilterBarText]));
+		}
+
+	});
+});

+ 16 - 0
webapp/controller/NotFound.controller.js

@@ -0,0 +1,16 @@
+sap.ui.define([
+	"./BaseController"
+], function (BaseController) {
+	"use strict";
+
+	return BaseController.extend("sap.ui.demo.orderbrowser.controller.NotFound", {
+
+		onInit: function () {
+			this.getRouter().getTarget("notFound").attachDisplay(this._onNotFoundDisplayed, this);
+		},
+
+		_onNotFoundDisplayed : function () {
+			this.getModel("appView").setProperty("/layout", "OneColumn");
+		}
+	});
+});

+ 181 - 0
webapp/i18n/i18n.properties

@@ -0,0 +1,181 @@
+# This is the resource bundle for Browse Orders
+# __ldi.translation.uuid=750eee8c-73f4-4e64-b4a5-bd756e0aab4d
+
+#XTIT: Application name
+appTitle=Browse Orders
+
+#YDES: Application description
+appDescription=Master-Detail demo application for displaying orders
+
+#~~~ Master AND Detail Views ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+#XTIT: View title with placeholder for the number of items
+commonItemTitle=Order {0}
+
+#XFLD: Title for customer name attribute in view headers
+commonCustomerName=Customer
+
+#XFLD: Title for shipping date attribute in view headers
+commonItemShipped=Shipped
+
+#XFLD: Explanation that an order has not been shipped yet
+commonItemNotYetShipped=Not shipped yet
+
+#~~~ Master View ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+#XTIT: Master view title with placeholder for the number of items
+masterTitleCount=Orders ({0})
+
+#XTOL: Tooltip for the search field
+masterSearchTooltip=Enter an order name or a part of it.
+
+#XBLI: text for a list with no data
+masterListNoDataText=No orders are currently available
+
+#XBLI: text for a list with no data with filter or search
+masterListNoDataWithFilterOrSearchText=No matching order found
+
+#XMIT: Group option for master table: Disable grouping
+masterGroupNoGrouping=None
+
+#XMIT: Group option for master table: by customer name
+masterGroupCustomer=Group by Customer
+
+#XMIT: Group option for master table: by order period
+masterGroupOrderPeriod=Group by Order Period
+
+#XGRP: Group title in grouped table for orders ordered in a period of time, consisting on month (parameter 0) and year (parameter 1)
+masterGroupTitleOrderedInPeriod=Ordered in {0} {1}
+
+#XMIT: Group option for master table: by shipped period
+masterGroupShippedPeriod=Group by Shipped Period
+
+#XGRP: Group title in grouped table for orders shipped in a period of time, consisting on month (parameter 0) and year (parameter 1)
+masterGroupTitleShippedInPeriod=Shipped in {0} {1}
+
+#XGRP: Special group title in grouped table for orders shipped in a period of time, for orders with pending shipment
+masterGroupTitleNotShippedYet=Not Shipped Yet
+
+#XGRP: Filter option for master table: Only orders with shipments
+masterFilterShipped=Only Shipped Orders
+
+#XGRP: Filter option for master table: Only orders with pending shipments
+masterFilterNotShipped=Only Orders without Shipment
+
+#XTXT: Text for the view settings filter item
+filterName=Orders
+
+#YMSG: Filter text that is displayed above the master list
+masterFilterBarText=Filtered by {0}
+
+#~~~ Detail View ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+#XFLD: Title for order date attribute in header of detail view
+detailOrderDate=Ordered
+
+#XTOL: Icon Tab Bar Info
+detailIconTabBarItems=Items
+
+#XTOL: Icon Tab Bar Attachments
+detailIconTabBarShipping=Shipping Info
+
+#XTOL: Icon Tab Bar Attachments
+detailIconTabBarAttachments=Attachments
+
+#XTOL: Icon Tab Bar Processor
+detailIconTabBarProcessor=Processor
+
+#XTIT: Form Title on the Tab Shipping Address Info
+detailShippingAddressTitle=Shipping Address
+
+#XTIT: Form Title on the Tab Processor Info
+detailProcessorTitle=Processor Information
+
+#XTIT: Details Title on the Tab Processor
+detailsProcessorTitle=Details
+
+#XTIT: Photo Title  on the Tab Processor
+photoProcessorTitle=Picture
+
+#XFLD: Label for Address
+detailName=Name
+
+#XFLD: Label for Address
+detailShippingStreet=Street
+
+#XFLD: Label for Address
+detailShippingZIPCodeCity=ZIP Code / City
+
+#XFLD: Label for Address
+detailShippingRegion=Region
+
+#XFLD: Label for Address
+detailShippingCountry=Country
+
+#XFLD: Label for Employee ID
+detailProcessorEmployeeID=Employee ID
+
+#XFLD: Label for Job Title
+detailProcessorJobTitle=Job Title
+
+#XFLD: Label for Phone
+detailProcessorPhone=Phone
+
+#XBLI: Text for the Order_Details table with no data
+detailLineItemTableNoDataText=No line items
+
+#XTIT: Title of the Order_Details table
+detailLineItemTableHeading=Line Items
+
+#XTIT: Title of the Order_Details table
+detailLineItemTableHeadingCount=Line Items ({0})
+
+#XGRP: Title for the ProductID column in the Order_Details table
+detailLineItemTableIDColumn=Product
+
+#XGRP: Title for the Quantity column in the Order_Details table
+detailLineItemTableUnitQuantityColumn=Quantity
+
+#XGRP: Title for the Price column in the Order_Details table
+detailLineItemTableUnitPriceColumn=Unit Price
+
+#XGRP: Title for the Total column in the Order_Details table
+detailLineItemTableTotalColumn=Total
+
+#XTIT: Send E-Mail subject
+shareSendEmailObjectSubject=Order {0}
+
+#YMSG: Send E-Mail message
+shareSendEmailObjectMessage=Please take a look at order {0} (id: {1})\r\n{2} \n\nMore Information on this order:\n- Customer Name: {3}\n- Customer ID: {5}\n- Processor ID: {4}
+
+#XTIT: Label text for price
+priceText=Price
+
+#XTIT: Title for the Order Details
+detailTitle=Order Details
+
+#~~~ Not Found View ~~~~~~~~~~~~~~~~~~~~~~~
+
+#XTIT: Not found view title
+notFoundTitle=Not Found
+
+#YMSG: The Orders not found text is displayed when there is no Orders with this id
+noObjectFoundText=This order is not available
+
+#YMSG: The not found text is displayed when there was an error loading the resource (404 error)
+notFoundText=The requested resource was not found
+
+#~~~ Not Available View ~~~~~~~~~~~~~~~~~~~~~~~
+
+#XTIT: Master view title
+notAvailableViewTitle=Orders
+
+#~~~ Error Handling ~~~~~~~~~~~~~~~~~~~~~~~
+
+#YMSG: Error dialog description
+errorText=Sorry, a technical error occurred! Please try again later.
+
+#~~~  Data Binding Content ~~~~~~~~~~~~~~~~~~~~~~~
+formatterDeliveryTooLate=Too late
+formatterDeliveryInTime=In time
+formatterDeliveryUrgent=Urgent

BIN
webapp/images/Employee.png


+ 26 - 0
webapp/index.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+	<title>Browse Orders</title>
+
+	<!-- Bootstrapping UI5 -->
+	<script id="sap-ui-bootstrap"
+		src="../../../../../../resources/sap-ui-core.js"
+		data-sap-ui-theme="sap_horizon"
+		data-sap-ui-resourceroots='{
+			"sap.ui.demo.orderbrowser": "./"
+		}'
+		data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
+		data-sap-ui-compatVersion="edge"
+		data-sap-ui-async="true"
+		data-sap-ui-frameOptions="trusted">
+	</script>
+</head>
+
+<!-- UI Content -->
+<body class="sapUiBody">
+	<div data-sap-ui-component data-name="sap.ui.demo.orderbrowser" data-id="container" data-settings='{"id" : "orderbrowser"}'></div>
+</body>
+</html>

+ 219 - 0
webapp/localService/metadata.xml

@@ -0,0 +1,219 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<edmx:Edmx Version="1.0"
+        xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
+	<edmx:DataServices
+		xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:DataServiceVersion="1.0">
+		<Schema Namespace="NorthwindModel"
+				xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
+				xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
+			<EntityType Name="Customer">
+				<Key>
+					<PropertyRef Name="CustomerID"/>
+				</Key>
+				<Property Name="CustomerID" Type="Edm.String" Nullable="false" MaxLength="5" Unicode="true"
+						FixedLength="true"/>
+				<Property Name="CompanyName" Type="Edm.String" Nullable="false" MaxLength="40" Unicode="true"
+						FixedLength="false"/>
+			</EntityType>
+			<EntityType Name="Employee">
+				<Key>
+					<PropertyRef Name="EmployeeID"/>
+				</Key>
+				<Property Name="EmployeeID" Type="Edm.Int32" Nullable="false" p8:StoreGeneratedPattern="Identity"
+						xmlns:p8="http://schemas.microsoft.com/ado/2009/02/edm/annotation"/>
+				<Property Name="LastName" Type="Edm.String" Nullable="false" MaxLength="20" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="FirstName" Type="Edm.String" Nullable="false" MaxLength="10" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="Title" Type="Edm.String" Nullable="true" MaxLength="30" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="HomePhone" Type="Edm.String" Nullable="true" MaxLength="24" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="Photo" Type="Edm.Binary" Nullable="true" MaxLength="Max" FixedLength="false"/>
+			</EntityType>
+			<EntityType Name="Order_Detail">
+				<Key>
+					<PropertyRef Name="OrderID"/>
+					<PropertyRef Name="ProductID"/>
+				</Key>
+				<Property Name="OrderID" Type="Edm.Int32" Nullable="false"/>
+				<Property Name="ProductID" Type="Edm.Int32" Nullable="false"/>
+				<Property Name="UnitPrice" Type="Edm.Decimal" Nullable="false" Precision="19" Scale="4"/>
+				<Property Name="Quantity" Type="Edm.Int16" Nullable="false"/>
+				<Property Name="Discount" Type="Edm.Single" Nullable="false"/>
+				<NavigationProperty Name="Order" Relationship="NorthwindModel.FK_Order_Details_Orders"
+									FromRole="Order_Details" ToRole="Orders"/>
+				<NavigationProperty Name="Product" Relationship="NorthwindModel.FK_Order_Details_Products"
+									FromRole="Order_Details" ToRole="Products"/>
+			</EntityType>
+			<EntityType Name="Order">
+				<Key>
+					<PropertyRef Name="OrderID"/>
+				</Key>
+				<Property Name="OrderID" Type="Edm.Int32" Nullable="false" p8:StoreGeneratedPattern="Identity"
+						xmlns:p8="http://schemas.microsoft.com/ado/2009/02/edm/annotation"/>
+				<Property Name="CustomerID" Type="Edm.String" Nullable="true" MaxLength="5" Unicode="true"
+						FixedLength="true"/>
+				<Property Name="CustomerName" Type="Edm.String" Nullable="true" MaxLength="40" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="EmployeeID" Type="Edm.Int32" Nullable="true"/>
+				<Property Name="OrderDate" Type="Edm.DateTime" Nullable="true"/>
+				<Property Name="RequiredDate" Type="Edm.DateTime" Nullable="true"/>
+				<Property Name="ShippedDate" Type="Edm.DateTime" Nullable="true"/>
+				<Property Name="ShipVia" Type="Edm.Int32" Nullable="true"/>
+				<Property Name="Freight" Type="Edm.Decimal" Nullable="true" Precision="19" Scale="4"/>
+				<Property Name="ShipName" Type="Edm.String" Nullable="true" MaxLength="40" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="ShipAddress" Type="Edm.String" Nullable="true" MaxLength="60" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="ShipCity" Type="Edm.String" Nullable="true" MaxLength="15" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="ShipRegion" Type="Edm.String" Nullable="true" MaxLength="15" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="ShipPostalCode" Type="Edm.String" Nullable="true" MaxLength="10" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="ShipCountry" Type="Edm.String" Nullable="true" MaxLength="15" Unicode="true"
+						FixedLength="false"/>
+				<NavigationProperty Name="Customer" Relationship="NorthwindModel.FK_Orders_Customers" FromRole="Orders"
+									ToRole="Customers"/>
+				<NavigationProperty Name="Employee" Relationship="NorthwindModel.FK_Orders_Employees" FromRole="Orders"
+									ToRole="Employees"/>
+				<NavigationProperty Name="Order_Details" Relationship="NorthwindModel.FK_Order_Details_Orders"
+									FromRole="Orders" ToRole="Order_Details"/>
+				<NavigationProperty Name="Shipper" Relationship="NorthwindModel.FK_Orders_Shippers" FromRole="Orders"
+									ToRole="Shippers"/>
+			</EntityType>
+			<EntityType Name="Product">
+				<Key>
+					<PropertyRef Name="ProductID"/>
+				</Key>
+				<Property Name="ProductID" Type="Edm.Int32" Nullable="false" p8:StoreGeneratedPattern="Identity"
+						xmlns:p8="http://schemas.microsoft.com/ado/2009/02/edm/annotation"/>
+				<Property Name="ProductName" Type="Edm.String" Nullable="false" MaxLength="40" Unicode="true"
+						FixedLength="false"/>
+			</EntityType>
+			<EntityType Name="Shipper">
+				<Key>
+					<PropertyRef Name="ShipperID"/>
+				</Key>
+				<Property Name="ShipperID" Type="Edm.Int32" Nullable="false" p8:StoreGeneratedPattern="Identity"
+						xmlns:p8="http://schemas.microsoft.com/ado/2009/02/edm/annotation"/>
+				<Property Name="CompanyName" Type="Edm.String" Nullable="false" MaxLength="40" Unicode="true"
+						FixedLength="false"/>
+				<Property Name="Phone" Type="Edm.String" Nullable="true" MaxLength="24" Unicode="true"
+						FixedLength="false"/>
+				<NavigationProperty Name="Orders" Relationship="NorthwindModel.FK_Orders_Shippers" FromRole="Shippers"
+									ToRole="Orders"/>
+			</EntityType>
+			<Association Name="FK_Orders_Customers">
+				<End Role="Customers" Type="NorthwindModel.Customer" Multiplicity="0..1"/>
+				<End Role="Orders" Type="NorthwindModel.Order" Multiplicity="*"/>
+				<ReferentialConstraint>
+					<Principal Role="Customers">
+						<PropertyRef Name="CustomerID"/>
+					</Principal>
+					<Dependent Role="Orders">
+						<PropertyRef Name="CustomerID"/>
+					</Dependent>
+				</ReferentialConstraint>
+			</Association>
+			<Association Name="FK_Employees_Employees">
+				<End Role="Employees" Type="NorthwindModel.Employee" Multiplicity="0..1"/>
+				<End Role="Employees1" Type="NorthwindModel.Employee" Multiplicity="*"/>
+				<ReferentialConstraint>
+					<Principal Role="Employees">
+						<PropertyRef Name="EmployeeID"/>
+					</Principal>
+					<Dependent Role="Employees1">
+						<PropertyRef Name="ReportsTo"/>
+					</Dependent>
+				</ReferentialConstraint>
+			</Association>
+			<Association Name="FK_Orders_Employees">
+				<End Role="Employees" Type="NorthwindModel.Employee" Multiplicity="0..1"/>
+				<End Role="Orders" Type="NorthwindModel.Order" Multiplicity="*"/>
+				<ReferentialConstraint>
+					<Principal Role="Employees">
+						<PropertyRef Name="EmployeeID"/>
+					</Principal>
+					<Dependent Role="Orders">
+						<PropertyRef Name="EmployeeID"/>
+					</Dependent>
+				</ReferentialConstraint>
+			</Association>
+			<Association Name="FK_Order_Details_Orders">
+				<End Role="Orders" Type="NorthwindModel.Order" Multiplicity="1"/>
+				<End Role="Order_Details" Type="NorthwindModel.Order_Detail" Multiplicity="*"/>
+				<ReferentialConstraint>
+					<Principal Role="Orders">
+						<PropertyRef Name="OrderID"/>
+					</Principal>
+					<Dependent Role="Order_Details">
+						<PropertyRef Name="OrderID"/>
+					</Dependent>
+				</ReferentialConstraint>
+			</Association>
+			<Association Name="FK_Order_Details_Products">
+				<End Role="Products" Type="NorthwindModel.Product" Multiplicity="1"/>
+				<End Role="Order_Details" Type="NorthwindModel.Order_Detail" Multiplicity="*"/>
+				<ReferentialConstraint>
+					<Principal Role="Products">
+						<PropertyRef Name="ProductID"/>
+					</Principal>
+					<Dependent Role="Order_Details">
+						<PropertyRef Name="ProductID"/>
+					</Dependent>
+				</ReferentialConstraint>
+			</Association>
+			<Association Name="FK_Orders_Shippers">
+				<End Role="Shippers" Type="NorthwindModel.Shipper" Multiplicity="0..1"/>
+				<End Role="Orders" Type="NorthwindModel.Order" Multiplicity="*"/>
+				<ReferentialConstraint>
+					<Principal Role="Shippers">
+						<PropertyRef Name="ShipperID"/>
+					</Principal>
+					<Dependent Role="Orders">
+						<PropertyRef Name="ShipVia"/>
+					</Dependent>
+				</ReferentialConstraint>
+			</Association>
+		</Schema>
+		<Schema Namespace="ODataWeb.Northwind.Model"
+				xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
+				xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
+				xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
+			<EntityContainer Name="NorthwindEntities" p7:LazyLoadingEnabled="true" m:IsDefaultEntityContainer="true"
+							 xmlns:p7="http://schemas.microsoft.com/ado/2009/02/edm/annotation">
+				<EntitySet Name="Customers" EntityType="NorthwindModel.Customer"/>
+				<EntitySet Name="Employees" EntityType="NorthwindModel.Employee"/>
+				<EntitySet Name="Order_Details" EntityType="NorthwindModel.Order_Detail"/>
+				<EntitySet Name="Orders" EntityType="NorthwindModel.Order"/>
+				<EntitySet Name="Products" EntityType="NorthwindModel.Product"/>
+				<AssociationSet Name="FK_Products_Categories" Association="NorthwindModel.FK_Products_Categories">
+					<End Role="Categories" EntitySet="Categories"/>
+					<End Role="Products" EntitySet="Products"/>
+				</AssociationSet>
+				<AssociationSet Name="FK_Orders_Customers" Association="NorthwindModel.FK_Orders_Customers">
+					<End Role="Customers" EntitySet="Customers"/>
+					<End Role="Orders" EntitySet="Orders"/>
+				</AssociationSet>
+				<AssociationSet Name="FK_Employees_Employees" Association="NorthwindModel.FK_Employees_Employees">
+					<End Role="Employees" EntitySet="Employees"/>
+					<End Role="Employees1" EntitySet="Employees"/>
+				</AssociationSet>
+				<AssociationSet Name="FK_Orders_Employees" Association="NorthwindModel.FK_Orders_Employees">
+					<End Role="Employees" EntitySet="Employees"/>
+					<End Role="Orders" EntitySet="Orders"/>
+				</AssociationSet>
+				<AssociationSet Name="FK_Order_Details_Orders" Association="NorthwindModel.FK_Order_Details_Orders">
+					<End Role="Orders" EntitySet="Orders"/>
+					<End Role="Order_Details" EntitySet="Order_Details"/>
+				</AssociationSet>
+				<AssociationSet Name="FK_Order_Details_Products" Association="NorthwindModel.FK_Order_Details_Products">
+					<End Role="Products" EntitySet="Products"/>
+					<End Role="Order_Details" EntitySet="Order_Details"/>
+				</AssociationSet>
+			</EntityContainer>
+		</Schema>
+	</edmx:DataServices>
+</edmx:Edmx>

+ 22 - 0
webapp/localService/mockdata/Customer.json

@@ -0,0 +1,22 @@
+[
+  {
+    "CustomerID": "TORTU",
+    "CompanyName": "Tortuga Restaurante"
+  },
+  {
+    "CustomerID": "ALFKI",
+    "CompanyName": "Alfreds Futterkiste"
+  },
+  {
+    "CustomerID": "AROUT",
+    "CompanyName": "Around the Horn"
+  },
+  {
+    "CustomerID": "BERGS",
+    "CompanyName": "Berglunds snabbköp"
+  },
+  {
+    "CustomerID": "BOTTM",
+    "CompanyName": "Bottom-Dollar Markets"
+  }
+]

+ 43 - 0
webapp/localService/mockdata/Employee.json

@@ -0,0 +1,43 @@
+[
+  {
+    "EmployeeID": 7424,
+    "FirstName": "Jack",
+    "LastName": "Smith",
+    "Title": "Developer",
+    "HomePhone": "01781163487",
+    "Photo": ""
+    },
+  {
+    "EmployeeID": 7827,
+    "FirstName": "Loura",
+    "LastName": "Hajjar",
+    "Title": "Developer",
+    "HomePhone": "01781237845",
+    "Photo": ""
+  },
+  {
+    "EmployeeID": 7829,
+    "FirstName": "Steven",
+    "LastName": "Buchanan",
+    "Title": "Manager",
+    "HomePhone": "01787650439",
+    "Photo": ""
+
+  },
+  {
+    "EmployeeID": 7830,
+    "FirstName": "Andrew",
+    "LastName": "Fuller",
+    "Title": "Designer",
+    "HomePhone": "01781234598",
+    "Photo": ""
+  },
+  {
+    "EmployeeID": 7840,
+    "FirstName": "Anne",
+    "LastName": "Dodsworth",
+    "Title": "Developer",
+    "HomePhone": "01796577660",
+    "Photo": ""
+  }
+]

+ 268 - 0
webapp/localService/mockdata/Order_Details.json

@@ -0,0 +1,268 @@
+[
+	{
+	"OrderID": 7918,
+	"ProductID": 1412,
+	"UnitPrice": 65.89,
+	"Quantity": 899,
+	"Discount": 2.16
+	},
+	{
+	"OrderID": 7918,
+	"ProductID": 5672,
+	"UnitPrice": 14.77,
+	"Quantity": 2367,
+	"Discount": 10.48
+	},
+	{
+	"OrderID": 7918,
+	"ProductID": 5046,
+	"UnitPrice": 61.69,
+	"Quantity": 867,
+	"Discount": 3.22
+	},
+	{
+	"OrderID": 7918,
+	"ProductID": 9505,
+	"UnitPrice": 3.07,
+	"Quantity": 1060,
+	"Discount": 2.60
+	},
+	{
+	"OrderID": 2686,
+	"ProductID": 5267,
+	"UnitPrice": 24.34,
+	"Quantity": 7783,
+	"Discount": 1.37
+	},
+	{
+	"OrderID": 2686,
+	"ProductID": 1114,
+	"UnitPrice": 20.50,
+	"Quantity": 523,
+	"Discount": 1.05
+	},
+	{
+	"OrderID": 2686,
+	"ProductID": 5046,
+	"UnitPrice": 61.69,
+	"Quantity": 336,
+	"Discount": 5.10
+	},
+	{
+	"OrderID": 6858,
+	"ProductID": 1114,
+	"UnitPrice": 20.50,
+	"Quantity": 2960,
+	"Discount": 3.07
+	},
+	{
+	"OrderID": 6858,
+	"ProductID": 4663,
+	"UnitPrice": 9.31,
+	"Quantity": 9491,
+	"Discount": 1.77
+	},
+	{
+	"OrderID": 6858,
+	"ProductID": 5672,
+	"UnitPrice": 14.77,
+	"Quantity": 547,
+	"Discount": 3.90
+	},
+	{
+	"OrderID": 6858,
+	"ProductID": 4008,
+	"UnitPrice": 47.1,
+	"Quantity": 5780,
+	"Discount": 2.69
+	},
+	{
+	"OrderID": 7311,
+	"ProductID": 5046,
+	"UnitPrice": 61.69,
+	"Quantity": 6636,
+	"Discount": 5.23
+	},
+	{
+	"OrderID": 7311,
+	"ProductID": 1412,
+	"UnitPrice": 65.89,
+	"Quantity": 3436,
+	"Discount": 5.32
+	},
+	{
+	"OrderID": 7311,
+	"ProductID": 5672,
+	"UnitPrice": 14.77,
+	"Quantity": 8076,
+	"Discount": 8.23
+	},
+	{
+	"OrderID": 7991,
+	"ProductID": 4663,
+	"UnitPrice": 9.31,
+	"Quantity": 9491,
+	"Discount": 1.77
+	},
+	{
+	"OrderID": 7991,
+	"ProductID": 5672,
+	"UnitPrice": 14.77,
+	"Quantity": 547,
+	"Discount": 3.90
+	},
+	{
+	"OrderID": 7991,
+	"ProductID": 4008,
+	"UnitPrice": 47.1,
+	"Quantity": 5780,
+	"Discount": 2.69
+	},
+	{
+	"OrderID": 7991,
+	"ProductID": 9505,
+	"UnitPrice": 3.07,
+	"Quantity": 3239,
+	"Discount": 8.02
+	},
+	{
+	"OrderID": 7991,
+	"ProductID": 8486,
+	"UnitPrice": 6.31,
+	"Quantity": 5039,
+	"Discount": 10.02
+	},
+	{
+	"OrderID": 6189,
+	"ProductID": 5672,
+	"UnitPrice": 14.77,
+	"Quantity": 552,
+	"Discount": 3.00
+	},
+	{
+	"OrderID": 6189,
+	"ProductID": 4663,
+	"UnitPrice": 9.31,
+	"Quantity": 1052,
+	"Discount": 5.45
+	},
+	{
+	"OrderID": 6189,
+	"ProductID": 5267,
+	"UnitPrice": 24.34,
+	"Quantity": 852,
+	"Discount": 1.87
+	},
+	{
+	"OrderID": 6189,
+	"ProductID": 1412,
+	"UnitPrice": 65.89,
+	"Quantity": 6752,
+	"Discount": 8.34
+	},
+	{
+	"OrderID": 828,
+	"ProductID": 8486,
+	"UnitPrice": 6.31,
+	"Quantity": 1204,
+	"Discount": 9.39
+	},
+	{
+	"OrderID": 828,
+	"ProductID": 1114,
+	"UnitPrice": 20.50,
+	"Quantity": 2960,
+	"Discount": 3.07
+	},
+	{
+	"OrderID": 828,
+	"ProductID": 4663,
+	"UnitPrice": 9.31,
+	"Quantity": 9491,
+	"Discount": 1.77
+	},
+	{
+	"OrderID": 828,
+	"ProductID": 5672,
+	"UnitPrice": 14.77,
+	"Quantity": 547,
+	"Discount": 3.90
+	},
+	{
+	"OrderID": 828,
+	"ProductID": 4008,
+	"UnitPrice": 47.1,
+	"Quantity": 5780,
+	"Discount": 2.69
+	},
+	{
+	"OrderID": 3115,
+	"ProductID": 1114,
+	"UnitPrice": 20.50,
+	"Quantity": 1530,
+	"Discount": 1.21
+	},
+	{
+	"OrderID": 3115,
+	"ProductID": 5079,
+	"UnitPrice": 62.4,
+	"Quantity": 2370,
+	"Discount": 5.78
+	},
+	{
+	"OrderID": 3115,
+	"ProductID": 8486,
+	"UnitPrice": 6.31,
+	"Quantity": 2567,
+	"Discount": 3.87
+	},
+	{
+	"OrderID": 3115,
+	"ProductID": 4663,
+	"UnitPrice": 9.31,
+	"Quantity": 1809,
+	"Discount": 2.98
+	},
+	{
+	"OrderID": 3115,
+	"ProductID": 4008,
+	"UnitPrice": 47.1,
+	"Quantity": 1310,
+	"Discount": 3.21
+	},
+	{
+	"OrderID": 7375,
+	"ProductID": 4008,
+	"UnitPrice": 47.1,
+	"Quantity": 5780,
+	"Discount": 2.69
+	},
+	{
+	"OrderID": 7375,
+	"ProductID": 9505,
+	"UnitPrice": 3.07,
+	"Quantity": 3239,
+	"Discount": 8.02
+	},
+	{
+	"OrderID": 7375,
+	"ProductID": 8486,
+	"UnitPrice": 6.31,
+	"Quantity": 5039,
+	"Discount": 10.02
+	},
+	{
+	"OrderID": 6368,
+	"ProductID": 9505,
+	"UnitPrice": 3.07,
+	"Quantity": 3239,
+	"Discount": 8.02
+	},
+	{
+	"OrderID": 6368,
+	"ProductID": 8486,
+	"UnitPrice": 6.31,
+	"Quantity": 5039,
+	"Discount": 10.02
+	}
+]

+ 173 - 0
webapp/localService/mockdata/Orders.json

@@ -0,0 +1,173 @@
+[
+
+	{
+	"OrderID": 7918,
+	"CustomerID": "TORTU",
+	"CustomerName": "Tortuga Restaurante",
+	"EmployeeID": 7424,
+	"OrderDate": "/Date(1474149600000)/",
+	"RequiredDate": "/Date(1475186400000)/",
+	"ShippedDate": "/Date(1474840800000)/",
+	"ShipVia": 6030,
+	"Freight": 7948.67,
+	"ShipName": "ExcellentParcel",
+	"ShipAddress": "Tottenham Court Road",
+	"ShipCity": "London",
+	"ShipRegion": "Greater London",
+	"ShipPostalCode": "N170AA",
+	"ShipCountry": "United Kingdom"
+  },
+  {
+	"OrderID": 7311,
+	"CustomerID": "ALFKI",
+	"CustomerName": "Alfreds Futterkiste",
+	"EmployeeID": 7827,
+	"OrderDate": "/Date(1479682800000)/",
+	"RequiredDate": "/Date(1481151600000)/",
+	"ShippedDate": "/Date(1480028400000)/",
+	"ShipVia": 3416,
+	"Freight": 7624.42,
+	"ShipName": "ExcellentParcel",
+	"ShipAddress": "Tottenham Court Road",
+	"ShipCity": "London",
+	"ShipRegion": "Greater London",
+	"ShipPostalCode": "N170AA",
+	"ShipCountry": "United Kingdom"
+  },
+  {
+	"OrderID": 7375,
+	"CustomerID": "AROUT",
+	"CustomerName": "Around the Horn",
+	"EmployeeID": 7424,
+	"OrderDate": "/Date(1475704800000)/",
+	"RequiredDate": "/Date(1479337200000)/",
+	"ShippedDate": "/Date(1476396000000)/",
+	"ShipVia": 7532,
+	"Freight": 4019.56,
+	"ShipName": "ExcellentParcel",
+	"ShipAddress": "Tottenham Court Road",
+	"ShipCity": "London",
+	"ShipRegion": "Greater London",
+	"ShipPostalCode": "N170AA",
+	"ShipCountry": "United Kingdom"
+  },
+  {
+	"OrderID": 6189,
+	"CustomerID": "BERGS",
+	"CustomerName": "Berglunds snabbköp",
+	"EmployeeID": 7829,
+	"OrderDate": "/Date(1480287600000)/",
+	"RequiredDate": "/Date(1482274800000)/",
+	"ShippedDate": "/Date(1480460400000)/",
+	"ShipVia": 1923,
+	"Freight": 5422.26,
+	"ShipName": "1A Paket- und Lieferservice",
+	"ShipAddress": "Bismarckstraße 5",
+	"ShipCity": "Berlin",
+	"ShipRegion": "Berlin",
+	"ShipPostalCode": "10179",
+	"ShipCountry": "Deutschland"
+  },
+  {
+	"OrderID": 3115,
+	"CustomerID": "BERGS",
+	  "CustomerName": "",
+	"EmployeeID": 7830,
+	"OrderDate": "/Date(1480806000000)/",
+	"RequiredDate": "/Date(1480978800000)/",
+	"ShippedDate": "/Date(1482447600000)/",
+	"ShipVia": 3059,
+	"Freight": 9130.44,
+	"ShipName": "1A Paket- und Lieferservice",
+	"ShipAddress": "Bismarckstraße 5",
+	"ShipCity": "Berlin",
+	"ShipRegion": "Berlin",
+	"ShipPostalCode": "10179",
+	"ShipCountry": "Deutschland"
+  },
+  {
+	"OrderID": 2686,
+	"CustomerID": "BOTTM",
+	"CustomerName": "Bottom-Dollar Markets",
+	"EmployeeID": 7840,
+	"OrderDate": "/Date(1477519200000)/",
+	"RequiredDate": "/Date(1478646000000)/",
+	"ShippedDate": "/Date(1477954800000)/",
+	"ShipVia": 4728,
+	"Freight": 8237.06,
+	"ShipName": "1A Paket- und Lieferservice",
+	"ShipAddress": "Bismarckstraße 5",
+	"ShipCity": "Berlin",
+	"ShipRegion": "Berlin",
+	"ShipPostalCode": "10179",
+	"ShipCountry": "Deutschland"
+  },
+  {
+	"OrderID": 6858,
+	"CustomerID": "TORTU",
+	  "CustomerName": "Tortuga Restaurante",
+	"EmployeeID": 7840,
+	"OrderDate": "/Date(1478991600000)/",
+	"RequiredDate": "/Date(1480460400000)/",
+	"ShippedDate": "/Date(1479078000000)/",
+	"ShipVia": 6103,
+	"Freight": 4097.09,
+	"ShipName": "ExcellentParcel",
+	"ShipAddress": "Tottenham Court Road",
+	"ShipCity": "London",
+	"ShipRegion": "Greater London",
+	"ShipPostalCode": "N170AA",
+	"ShipCountry": "United Kingdom"
+  },
+  {
+	"OrderID": 6368,
+	"CustomerID": "ALFKI",
+	"CustomerName": "Alfreds Futterkiste",
+	"EmployeeID": 7424,
+	"OrderDate": "/Date(1478991600000)/",
+	"RequiredDate": "/Date(1480460400000)/",
+	"ShippedDate": "/Date(1479510000000)/",
+	"ShipVia": 9412,
+	"Freight": 1858.37,
+	"ShipName": "ExcellentParcel",
+	"ShipAddress": "Tottenham Court Road",
+	"ShipCity": "London",
+	"ShipRegion": "Greater London",
+	"ShipPostalCode": "N170AA",
+	"ShipCountry": "United Kingdom"
+  },
+  {
+	"OrderID": 828,
+	"CustomerID": "AROUT",
+	"CustomerName": "Around the Horn",
+	"EmployeeID": 7829,
+	"OrderDate": "/Date(1479682800000)/",
+	"RequiredDate": "/Date(1481151600000)/",
+	"ShippedDate": "/Date(1480028400000)/",
+	"ShipVia": 508,
+	"Freight": 2347.15,
+	"ShipName": "ShipEx",
+	"ShipAddress": "5th Avenue 610",
+	"ShipCity": "New York",
+	"ShipRegion": "New Jersey",
+	"ShipPostalCode": "10020",
+	"ShipCountry": "United Stated of America"
+  },
+  {
+	"OrderID": 7991,
+	"CustomerID": "BERGS",
+	"CustomerName": "Berglunds snabbköp",
+	"EmployeeID": 7830,
+	"OrderDate": "/Date(1479682800000)/",
+	"RequiredDate": "/Date(1481151600000)/",
+	"ShippedDate": "/Date(1480028400000)/",
+	"ShipVia": 6419,
+	"Freight": 6554.71,
+	"ShipName": "ShipEx",
+	"ShipAddress": "5th Avenue 610",
+	"ShipCity": "New York",
+	"ShipRegion": "New Jersey",
+	"ShipPostalCode": "10020",
+	"ShipCountry": "United Stated of America"
+  }
+]

+ 42 - 0
webapp/localService/mockdata/Product.json

@@ -0,0 +1,42 @@
+[
+	{
+		"ProductID": 1412,
+		"ProductName": "Aniseed Syrup"
+	},
+	{
+		"ProductID": 5267,
+		"ProductName": "Uncle Bob's Organic Dried Pears"
+	},
+	{
+		"ProductID": 5046,
+		"ProductName": "Northwoods Cranberry Sauce"
+	},
+	{
+		"ProductID": 1114,
+		"ProductName": "Grandma's Boysenberry Spread"
+	},
+	{
+		"ProductID": 5079,
+		"ProductName": "Chef Anton's Cajun Seasoning"
+	},
+	{
+		"ProductID": 4008,
+		"ProductName": "Sir Rodney's Marmalade"
+	},
+	{
+		"ProductID": 5672,
+		"ProductName": "Tunnbröd"
+	},
+	{
+		"ProductID": 8486,
+		"ProductName": "Mascarpone Fabioli"
+	},
+	{
+		"ProductID": 9505,
+		"ProductName": "Camembert Pierrot"
+	},
+	{
+		"ProductID": 4663,
+		"ProductName": "Louisiana Hot Spiced Okra"
+	}
+]

+ 112 - 0
webapp/localService/mockserver.js

@@ -0,0 +1,112 @@
+sap.ui.define([
+	"sap/ui/core/util/MockServer",
+	"sap/ui/model/json/JSONModel",
+	"sap/base/Log"
+], function (MockServer, JSONModel, Log) {
+	"use strict";
+
+	var oMockServer,
+		_sAppPath = "sap/ui/demo/orderbrowser/",
+		_sJsonFilesPath = _sAppPath + "localService/mockdata";
+
+	var oMockServerInterface = {
+
+		/**
+		 * Initializes the mock server asynchronously.
+		 * You can configure the delay with the URL parameter "serverDelay".
+		 * The local mock data in this folder is returned instead of the real data for testing.
+		 * @protected
+		 * @returns{Promise} a promise that is resolved when the mock server has been started
+		 */
+		init : function (oOptionsParameter) {
+			var oOptions = oOptionsParameter || {};
+
+			return new Promise(function(fnResolve) {
+				var sManifestUrl = sap.ui.require.toUrl(_sAppPath + "manifest.json"),
+					oManifestModel = new JSONModel(sManifestUrl);
+
+				oManifestModel.attachRequestCompleted(function ()  {
+					var oUriParameters = new URLSearchParams(window.location.search),
+						// parse manifest for local metadata URI
+						sJsonFilesUrl = sap.ui.require.toUrl(_sJsonFilesPath),
+						oMainDataSource = oManifestModel.getProperty("/sap.app/dataSources/mainService"),
+						sMetadataUrl = sap.ui.require.toUrl(_sAppPath + oMainDataSource.settings.localUri),
+						// ensure there is a trailing slash
+						sMockServerUrl = /.*\/$/.test(oMainDataSource.uri) ? oMainDataSource.uri : oMainDataSource.uri + "/";
+
+					// create a mock server instance or stop the existing one to reinitialize
+					if (!oMockServer) {
+						oMockServer = new MockServer({
+							rootUri: sMockServerUrl
+						});
+					} else {
+						oMockServer.stop();
+					}
+
+					// configure mock server with the given options or a default delay of 0.5s
+					MockServer.config({
+						autoRespond : true,
+						autoRespondAfter : (oOptions.delay || oUriParameters.get("serverDelay") || 500)
+					});
+
+					// simulate all requests using mock data
+					oMockServer.simulate(sMetadataUrl, {
+						sMockdataBaseUrl : sJsonFilesUrl,
+						bGenerateMissingMockData : true
+					});
+
+					var aRequests = oMockServer.getRequests();
+
+					// compose an error response for requesti
+					var fnResponse = function (iErrCode, sMessage, aRequest) {
+						aRequest.response = function(oXhr){
+							oXhr.respond(iErrCode, {"Content-Type": "text/plain;charset=utf-8"}, sMessage);
+						};
+					};
+
+					// simulate metadata errors
+					if (oOptions.metadataError || oUriParameters.get("metadataError")) {
+						aRequests.forEach(function (aEntry) {
+							if (aEntry.path.toString().indexOf("$metadata") > -1) {
+								fnResponse(500, "metadata Error", aEntry);
+							}
+						});
+					}
+
+					// simulate request errors
+					var sErrorParam = oOptions.errorType || oUriParameters.get("errorType"),
+						iErrorCode = sErrorParam === "badRequest" ? 400 : 500;
+					if (sErrorParam) {
+						aRequests.forEach(function (aEntry) {
+							fnResponse(iErrorCode, sErrorParam, aEntry);
+						});
+					}
+
+					// custom mock behaviour may be added here
+
+					// set requests and start the server
+					oMockServer.setRequests(aRequests);
+					oMockServer.start();
+
+					Log.info("Running the app with mock data");
+					fnResolve();
+				});
+
+				oManifestModel.attachRequestFailed(function () {
+					Log.error("Failed to load application manifest");
+					fnResolve();
+				});
+			});
+		},
+
+		/**
+		 * @public returns the mockserver of the app, should be used in integration tests
+		 * @returns {sap.ui.core.util.MockServer} the mockserver instance
+		 */
+		getMockServer : function () {
+			return oMockServer;
+		}
+	};
+
+	return oMockServerInterface;
+});

+ 146 - 0
webapp/manifest.json

@@ -0,0 +1,146 @@
+{
+	"_version": "1.21.0",
+	"sap.app": {
+		"id": "sap.ui.demo.orderbrowser",
+		"type": "application",
+		"resources": "resources.json",
+		"i18n": {
+			"bundleUrl": "i18n/i18n.properties",
+			"supportedLocales": [
+				""
+			],
+			"fallbackLocale": ""
+		},
+		"title": "{{appTitle}}",
+		"description": "{{appDescription}}",
+		"applicationVersion": {
+			"version": "1.0.0"
+		},
+		"dataSources": {
+			"mainService": {
+				"uri": "/here/goes/your/serviceUrl/",
+				"type": "OData",
+				"settings": {
+					"odataVersion": "2.0",
+					"localUri": "localService/metadata.xml"
+				}
+			}
+		}
+	},
+	"sap.ui": {
+		"technology": "UI5",
+		"icons": {
+			"icon": "sap-icon://detail-view",
+			"favIcon": "",
+			"phone": "",
+			"phone@2": "",
+			"tablet": "",
+			"tablet@2": ""
+		},
+		"deviceTypes": {
+			"desktop": true,
+			"tablet": true,
+			"phone": true
+		}
+	},
+	"sap.ui5": {
+		"rootView": {
+			"viewName": "sap.ui.demo.orderbrowser.view.App",
+			"type": "XML",
+			"async": true,
+			"id": "app"
+		},
+		"dependencies": {
+			"minUI5Version": "1.98.0",
+			"libs": {
+				"sap.f": {},
+				"sap.m": {},
+				"sap.ui.core": {}
+			}
+		},
+		"contentDensities": {
+			"compact": true,
+			"cozy": true
+		},
+		"models": {
+			"i18n": {
+				"type": "sap.ui.model.resource.ResourceModel",
+				"settings": {
+					"bundleName": "sap.ui.demo.orderbrowser.i18n.i18n",
+					"supportedLocales": [
+						""
+					],
+					"fallbackLocale": ""
+				}
+			},
+			"": {
+				"dataSource": "mainService",
+				"preload": true
+			}
+		},
+		"routing": {
+			"config": {
+				"routerClass": "sap.f.routing.Router",
+				"type": "View",
+				"viewType": "XML",
+				"path": "sap.ui.demo.orderbrowser.view",
+				"controlId": "layout",
+				"controlAggregation": "beginColumnPages",
+				"bypassed": {
+					"target": "notFound"
+				},
+				"async": true
+			},
+			"routes": [
+				{
+					"pattern": "",
+					"name": "master",
+					"target": "master"
+				},
+				{
+					"pattern": "Orders/{objectId}/:?query:",
+					"name": "object",
+					"target": [
+						"master",
+						"object"
+					]
+				}
+			],
+			"targets": {
+				"master": {
+					"name": "Master",
+					"level": 1,
+					"id": "master"
+				},
+				"object": {
+					"name": "Detail",
+					"id": "detail",
+					"level": 1,
+					"controlAggregation": "midColumnPages"
+				},
+				"detailObjectNotFound": {
+					"name": "DetailObjectNotFound",
+					"id": "detailObjectNotFound",
+					"controlAggregation": "midColumnPages"
+				},
+				"notFound": {
+					"name": "NotFound",
+					"id": "notFound"
+				},
+				"shipping": {
+					"name": "Shipping",
+					"parent": "object",
+					"controlId": "iconTabFilterShipping",
+					"controlAggregation": "content"
+				},
+				"processor": {
+					"name": "Processor",
+					"parent": "object",
+					"controlId": "iconTabFilterProcessor",
+					"controlAggregation": "content"
+				}
+			}
+		},
+		"flexBundle": false
+	}
+}

+ 102 - 0
webapp/model/formatter.js

@@ -0,0 +1,102 @@
+sap.ui.define([
+	"sap/ui/model/type/Currency"
+], function (Currency) {
+	"use strict";
+
+	return {
+
+		/**
+		 * Rounds the currency value to 2 digits
+		 *
+		 * @public
+		 * @param {string} sValue value to be formatted
+		 * @returns {string} formatted currency value with 2 digits
+		 */
+		currencyValue: function (sValue) {
+			if (!sValue) {
+				return "";
+			}
+			return parseFloat(sValue).toFixed(2);
+		},
+
+		/**
+		 * Rounds the currency value to 2 digits
+		 *
+		 * @public
+		 * @param {number} iQuantity product quantity
+		 * @param {number} fPrice product price
+		 * @param {string} sCurrencyCode currency code for the price
+		 * @returns {string} formatted currency value with 2 digits
+		 */
+		calculateItemTotal: function (iQuantity, fPrice, sCurrencyCode) {
+			var oCurrency = new Currency({showMeasure: false});
+			var fTotal = iQuantity * fPrice;
+			return oCurrency.formatValue([fTotal.toFixed(2), sCurrencyCode], "string");
+		},
+
+		/**
+		 * Converts a binary string into an image format suitable for the src attribute
+		 *
+		 * @public
+		 * @param {string} vData a binary string representing the image data
+		 * @returns {string} formatted string with image metadata based on the input or a default image when the input is empty
+		 */
+		handleBinaryContent: function(vData){
+			if (vData) {
+				var sMetaData1 = 'data:image/jpeg;base64,';
+				var sMetaData2 = vData.substr(104); // stripping the first 104 bytes from the binary data when using base64 encoding.
+				return sMetaData1 + sMetaData2;
+			} else {
+				return "../images/Employee.png";
+			}
+		},
+
+		/**
+		 * Provides a text to indicate the delivery status based on shipped and required dates
+		 *
+		 * @public
+		 * @param {object} oRequiredDate required date of the order
+		 * @param {object} oShippedDate shipped date of the order
+		 * @returns {string} delivery status text from the resource bundle
+		 */
+		deliveryText: function (oRequiredDate, oShippedDate) {
+			var oResourceBundle = this.getModel("i18n").getResourceBundle();
+
+			if (oShippedDate === null) {
+				return "None";
+			}
+
+			// delivery is urgent (takes more than 7 days)
+			if (oRequiredDate - oShippedDate > 0 && oRequiredDate - oShippedDate <= 432000000) {
+				return oResourceBundle.getText("formatterDeliveryUrgent");
+			} else if (oRequiredDate < oShippedDate) { //d elivery is too late
+				return oResourceBundle.getText("formatterDeliveryTooLate");
+			} else { // delivery is in time
+				return oResourceBundle.getText("formatterDeliveryInTime");
+			}
+		},
+
+		/**
+		 * Provides a semantic state to indicate the delivery status based on shipped and required dates
+		 *
+		 * @public
+		 * @param {object} oRequiredDate required date of the order
+		 * @param {object} oShippedDate shipped date of the order
+		 * @returns {string} semantic state of the order
+		 */
+		deliveryState: function (oRequiredDate, oShippedDate) {
+			if (oShippedDate === null) {
+				return "None";
+			}
+
+			// delivery is urgent (takes more than 7 days)
+			if (oRequiredDate - oShippedDate > 0 && oRequiredDate - oShippedDate <= 432000000) {
+				return "Warning";
+			} else if (oRequiredDate < oShippedDate) { // delivery is too late
+				return "Error";
+			} else { // delivery is in time
+				return "Success";
+			}
+		}
+	};
+});

+ 16 - 0
webapp/model/models.js

@@ -0,0 +1,16 @@
+sap.ui.define([
+		"sap/ui/model/json/JSONModel",
+		"sap/ui/Device"
+	], function (JSONModel, Device) {
+		"use strict";
+
+		return {
+			createDeviceModel : function () {
+				var oModel = new JSONModel(Device);
+				oModel.setDefaultBindingMode("OneWay");
+				return oModel;
+			}
+		};
+
+	}
+);

+ 25 - 0
webapp/test.html

@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Testing Overview</title>
+	<!-- try to load the basic UI5 styles -->
+	<link rel="stylesheet" type="text/css" href="../../../../../../resources/sap/ui/core/themes/sap_horizon/library.css">
+</head>
+<body class="sapUiBody sapUiMediumMargin sapUiForceWidthAuto">
+	<h1>Testing Overview</h1>
+	<p>This is an overview page of various ways to test the generated app during development.<br/>Choose one of the access points below to launch the app as a standalone application.</p>
+
+	<ul>
+		<li><a href="index.html">index.html</a> - start the app (with real service)</li>
+		<li><a href="test/mockServer.html">test/mockServer.html</a> - start the app (with local mock data)</li>
+		<li><a href="test/testsuite.qunit.html">test/testsuite.qunit.html</a> - run all tests in test suite</li>
+		<li><a href="test/unit/unitTests.qunit.html">test/unit/unitTests.qunit.html</a> - run all unit tests</li>
+		<li><a href="test/integration/opaTests.qunit.html">test/integration/opaTests.qunit.html</a> - run integration tests (separated in 2 test pages to run fast (in less than 30s)</li>
+		<li><a href="test/integration/opaTestsNavigation.qunit.html">test/integration/opaTestsNavigation.qunit.html</a> - run integration tests (separated in 2 test pages to run fast (in less than 30s)</li>
+		<li>
+			<b>run this in phone emulation mode!</b>
+			<a href="test/integration/opaTestsPhone.qunit.html">test/integration/opaTestsPhone.qunit.html</a> - run all integration tests for phone
+		</li>
+	</ul>
+</body>
+</html>

+ 16 - 0
webapp/test/Test.qunit.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="utf-8">
+	<script
+		src="../../../../../../../resources/sap/ui/test/starter/runTest.js"
+		data-sap-ui-resource-roots='{
+			"test-resources.sap.ui.demo.orderbrowser": "./"
+		}'
+	></script>
+</head>
+<body class="sapUiBody">
+	<div id="qunit"></div>
+	<div id="qunit-fixture"></div>
+</body>
+</html>

+ 15 - 0
webapp/test/initMockServer.js

@@ -0,0 +1,15 @@
+sap.ui.define([
+	"sap/ui/demo/orderbrowser/localService/mockserver"
+], function (mockserver) {
+	"use strict";
+
+	var aMockservers = [];
+
+	// initialize the mock server
+	aMockservers.push(mockserver.init());
+
+	Promise.all(aMockservers).then(function () {
+		// initialize the embedded component on the HTML page
+		sap.ui.require(["sap/ui/core/ComponentSupport"]);
+	});
+});

+ 91 - 0
webapp/test/integration/MasterJourney.js

@@ -0,0 +1,91 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/test/opaQunit",
+	"./pages/Master"
+], function (opaTest) {
+	"use strict";
+
+	QUnit.module("Master List");
+
+	opaTest("Should see the master list with all entries", function (Given, When, Then) {
+		// Arrangements
+		Given.iStartMyApp();
+
+		// Assertions
+		Then.onTheMasterPage.iShouldSeeTheList().
+			and.theListShouldHaveAllEntries().
+			and.theHeaderShouldDisplayAllEntries();
+	});
+
+	opaTest("Search for the First object should deliver results that contain the firstObject in the name", function (Given, When, Then) {
+		var sSearch = "B";
+		//Actions
+		When.onTheMasterPage.iSearchFor(sSearch);
+
+		// Assertions
+		Then.onTheMasterPage.theListShowsOnlyObjectsContaining(sSearch);
+	});
+
+	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) {
+		//Actions
+		When.onTheMasterPage.iSearchForNotFound()
+			.and.iClearTheSearch();
+
+		// Assertions
+		Then.onTheMasterPage.theListHasEntries();
+	});
+
+	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) {
+		//Actions
+		When.onTheMasterPage.iSearchForNotFound();
+
+		// Assertions
+		Then.onTheMasterPage.iShouldSeeTheNoDataText().
+			and.theListHeaderDisplaysZeroHits();
+	});
+
+	opaTest("Should display items again if the searchfield is emptied", function (Given, When, Then) {
+		//Actions
+		When.onTheMasterPage.iClearTheSearch();
+
+		// Assertions
+		Then.onTheMasterPage.theListShouldHaveAllEntries();
+	});
+
+	opaTest("MasterList Filtering on Shipped Orders", function(Given, When, Then) {
+		// Action
+		When.onTheMasterPage.iFilterTheListOn("masterFilterShipped");
+
+		// Assertion
+		Then.onTheMasterPage.theListShouldBeFilteredOnShippedOrders();
+	});
+
+	opaTest("MasterList remove filter should display all items", function(Given, When, Then) {
+		// Action
+		When.onTheMasterPage.iResetFilters();
+
+		// Assertion
+		Then.onTheMasterPage.theListShouldHaveAllEntries();
+	});
+
+	opaTest("MasterList grouping created group headers", function(Given, When, Then) {
+		// Action
+		When.onTheMasterPage.iGroupTheList();
+
+		// Assertion
+		Then.onTheMasterPage.theListShouldContainAGroupHeader();
+	});
+
+	opaTest("Remove grouping from MasterList delivers initial list", function(Given, When, Then) {
+		// Action
+		When.onTheMasterPage.iResetGrouping();
+
+		// Assertion
+		Then.onTheMasterPage.theListShouldNotContainGroupHeaders().
+			and.theListShouldHaveAllEntries();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+});

+ 123 - 0
webapp/test/integration/NavigationJourney.js

@@ -0,0 +1,123 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/test/opaQunit",
+	"./pages/Master",
+	"./pages/Detail",
+	"./pages/Browser",
+	"./pages/App"
+], function (opaTest) {
+	"use strict";
+
+	QUnit.module("Desktop navigation");
+
+	opaTest("Should navigate on press", function (Given, When, Then) {
+		// Arrangements
+		Given.iStartMyApp({
+			hash: "Orders/7991/"
+		});
+
+		//Actions
+		When.onTheMasterPage.iRememberTheSelectedItem();
+
+		// Assertions
+		Then.onTheDetailPage.iShouldSeeTheRememberedObject().
+			and.iShouldSeeHeaderActionButtons();
+		Then.onTheBrowserPage.iShouldSeeTheHashForTheRememberedObject();
+	});
+
+	opaTest("Should press full screen toggle button: The app shows one column", function (Given, When, Then) {
+		// Actions
+		When.onTheDetailPage.iPressTheHeaderActionButton("enterFullScreen");
+
+		// Assertions
+		Then.onTheAppPage.theAppShowsFCLDesign("MidColumnFullScreen");
+		Then.onTheDetailPage.iShouldSeeTheFullScreenToggleButton("exitFullScreen");
+	});
+
+	opaTest("Should press full screen toggle button: The app shows two columns", function (Given, When, Then) {
+		// Actions
+		When.onTheDetailPage.iPressTheHeaderActionButton("exitFullScreen");
+
+		// Assertions
+		Then.onTheAppPage.theAppShowsFCLDesign("TwoColumnsMidExpanded");
+		Then.onTheDetailPage.iShouldSeeTheFullScreenToggleButton("enterFullScreen");
+	});
+
+	opaTest("Should react on hash change", function (Given, When, Then) {
+		// Actions
+		When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(2);
+		When.onTheBrowserPage.iChangeTheHashToTheRememberedItem();
+
+		// Assertions
+		Then.onTheDetailPage.iShouldSeeTheRememberedObject();
+		Then.onTheMasterPage.theRememberedListItemShouldBeSelected();
+	});
+
+	opaTest("Should navigate on press", function (Given, When, Then) {
+		// Actions
+		When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1).
+			and.iPressOnTheObjectAtPosition(1);
+
+		// Assertions
+		Then.onTheDetailPage.iShouldSeeTheRememberedObject().
+			and.iShouldSeeTheObjectLineItemsList().
+			and.theLineItemsListShouldHaveTheCorrectNumberOfItems().
+			and.theLineItemsHeaderShouldDisplayTheAmountOfEntries().
+			and.theLineItemsTableShouldContainOnlyFormattedUnitNumbers();
+	});
+
+	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) {
+		//Actions
+		When.onTheMasterPage.iRememberAnIdOfAnObjectThatsNotInTheList();
+		When.onTheBrowserPage.iChangeTheHashToTheRememberedItem();
+
+		// Assertions
+		Then.onTheDetailPage.iShouldSeeTheRememberedObject();
+	});
+
+	opaTest("Should press close column button: The app shows one columns", function (Given, When, Then) {
+		// Actions
+		When.onTheDetailPage.iPressTheHeaderActionButton("closeColumn");
+
+		// Assertions
+		Then.onTheAppPage.theAppShowsFCLDesign("OneColumn");
+		Then.onTheMasterPage.theListShouldHaveNoSelection();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+	opaTest("Start the App and simulate metadata error: MessageBox should be shown", function (Given, When, Then) {
+		//Arrangement
+		Given.iStartMyApp({
+			delay: 7000,
+			metadataError: true
+		});
+
+		// Assertions
+		Then.onTheAppPage.iShouldSeeTheMessageBox();
+
+		// Actions
+		When.onTheAppPage.iCloseTheMessageBox();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+	opaTest("Start the App and simulate bad request error: MessageBox should be shown", function (Given, When, Then) {
+		//Arrangement
+		Given.iStartMyApp({
+			delay: 7000,
+			errorType: 'serverError'
+		});
+		// Assertions
+		Then.onTheAppPage.iShouldSeeTheMessageBox();
+
+		// Actions
+		When.onTheAppPage.iCloseTheMessageBox();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+});

+ 47 - 0
webapp/test/integration/NavigationJourneyPhone.js

@@ -0,0 +1,47 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/test/opaQunit",
+	"./pages/Master",
+	"./pages/Browser",
+	"./pages/Detail"
+], function (opaTest) {
+	"use strict";
+
+	QUnit.module("Phone navigation");
+
+	opaTest("Should see the objects list", function (Given, When, Then) {
+		// Arrangements
+		Given.iStartMyApp();
+
+		// Assertions
+		Then.onTheMasterPage.iShouldSeeTheList();
+		Then.onTheBrowserPage.iShouldSeeAnEmptyHash();
+	});
+
+	opaTest("Should react on hash change", function (Given, When, Then) {
+		// Actions
+		When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(3);
+		When.onTheBrowserPage.iChangeTheHashToTheRememberedItem();
+
+		// Assertions
+		Then.onTheDetailPage.iShouldSeeTheRememberedObject().
+			and.iShouldSeeTheObjectLineItemsList().
+			and.theLineItemsListShouldHaveTheCorrectNumberOfItems().
+			and.theLineItemsHeaderShouldDisplayTheAmountOfEntries();
+	});
+
+	opaTest("Should navigate on press", function (Given, When, Then) {
+		// Actions
+		When.onTheDetailPage.iPressTheHeaderActionButton("closeColumn");
+		When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(2).
+			and.iPressOnTheObjectAtPosition(2);
+
+		// Assertions
+		Then.onTheDetailPage.iShouldSeeTheRememberedObject();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+});

+ 55 - 0
webapp/test/integration/NotFoundJourney.js

@@ -0,0 +1,55 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/test/opaQunit",
+	"./pages/Master",
+	"./pages/NotFound",
+	"./pages/Browser"
+], function (opaTest) {
+	"use strict";
+
+	QUnit.module("Desktop not found");
+
+	opaTest("Should see the resource not found page when navigating to an invalid hash", function (Given, When, Then) {
+		//Arrangement
+		Given.iStartMyApp();
+
+		//Actions
+		When.onTheMasterPage.iWaitUntilTheListIsLoaded();
+		When.onTheBrowserPage.iChangeTheHashToSomethingInvalid();
+
+		// Assertions
+		Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage().
+			and.theNotFoundPageShouldSayResourceNotFound();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+	opaTest("Should see the not found master and detail page if an invalid object id has been called", function (Given, When, Then) {
+		// Arrangements
+		Given.iStartMyApp({ hash: "/Orders/SomeInvalidObjectId" });
+
+		// Assertions
+		Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage().
+			and.theNotFoundPageShouldSayObjectNotFound();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+	opaTest("Should see the not found text for no search results", function (Given, When, Then) {
+		// Arrangements
+		Given.iStartMyApp();
+
+		//Actions
+		When.onTheMasterPage.iSearchForNotFound();
+
+		// Assertions
+		Then.onTheMasterPage.iShouldSeeTheNoDataText();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+});

+ 66 - 0
webapp/test/integration/NotFoundJourneyPhone.js

@@ -0,0 +1,66 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/test/opaQunit",
+	"./pages/NotFound",
+	"./pages/Master"
+], function (opaTest) {
+	"use strict";
+
+	QUnit.module("Phone not found");
+
+	opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) {
+		// Arrangements
+		Given.iStartMyApp({hash: "somethingThatDoesNotExist"});
+
+		// Assertions
+		Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage().
+			and.theNotFoundPageShouldSayResourceNotFound();
+	});
+
+	opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) {
+		// Actions
+		When.onTheNotFoundPage.iPressTheBackButton("NotFound");
+
+		// Assertions
+		Then.onTheMasterPage.iShouldSeeTheList();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+	opaTest("Should see the not found detail page if an invalid object id has been called", function (Given, When, Then) {
+		// Arrangements
+		Given.iStartMyApp({ hash: "/Orders/SomeInvalidObjectId" });
+
+		// Assertions
+		Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage().
+			and.theNotFoundPageShouldSayObjectNotFound();
+	});
+
+	opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) {
+		// Actions
+		When.onTheNotFoundPage.iPressTheBackButton("DetailObjectNotFound");
+
+		// Assertions
+		Then.onTheMasterPage.iShouldSeeTheList();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+	opaTest("Should see the not found text for no search results", function (Given, When, Then) {
+		// Arrangements
+		Given.iStartMyApp();
+
+		// Actions
+		When.onTheMasterPage.iSearchForNotFound();
+
+		// Assertions
+		Then.onTheMasterPage.iShouldSeeTheNoDataText();
+
+		// Cleanup
+		Then.iTeardownMyApp();
+	});
+
+});

+ 46 - 0
webapp/test/integration/arrangements/Startup.js

@@ -0,0 +1,46 @@
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"sap/ui/demo/orderbrowser/localService/mockserver",
+	"sap/ui/model/odata/v2/ODataModel"
+], function (Opa5, mockserver, ODataModel) {
+	"use strict";
+
+	return Opa5.extend("sap.ui.demo.orderbrowser.test.integration.arrangements.Startup", {
+
+		/**
+		 * Initializes mock server, then starts the app component
+		 * @param {object} oOptionsParameter An object that contains the configuration for starting up the app
+		 * @param {int} oOptionsParameter.delay A custom delay to start the app with
+		 * @param {string} [oOptionsParameter.hash] The in-app hash can also be passed separately for better readability in tests
+		 * @param {boolean} [oOptionsParameter.autoWait=true] Automatically wait for pending requests while the application is starting up
+		 */
+		iStartMyApp: function (oOptionsParameter) {
+			var oOptions = oOptionsParameter || {};
+
+			this._clearSharedData();
+
+			// start the app with a minimal delay to make tests fast but still async to discover basic timing issues
+			oOptions.delay = oOptions.delay || 1;
+
+			// configure mock server with the current options
+			this.iWaitForPromise(mockserver.init(oOptions));
+			// start the app UI component
+			this.iStartMyUIComponent({
+				componentConfig: {
+					name: "sap.ui.demo.orderbrowser",
+					manifest: true
+				},
+				hash: oOptions.hash,
+				autoWait: oOptions.autoWait
+			});
+		},
+		_clearSharedData: function () {
+			// clear shared metadata in ODataModel to allow tests for loading the metadata
+			ODataModel.mSharedData = {
+				server: {},
+				service: {},
+				meta: {}
+			};
+		}
+	});
+});

+ 21 - 0
webapp/test/integration/opaTests.qunit.js

@@ -0,0 +1,21 @@
+// We cannot provide stable mock data out of the template.
+// If you introduce mock data, by adding .json files in your webapp/localService/mockdata folder you have to provide the following minimum data:
+// * At least 3 Objects in the list
+// * All 3 Objects have at least one LineItems
+
+// NavigationJourney is separated so that each test page runs fast enough (<30s)
+
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"./arrangements/Startup",
+	"./MasterJourney",
+	"./NotFoundJourney"
+], function (Opa5, Startup) {
+	"use strict";
+
+	Opa5.extendConfig({
+		arrangements: new Startup(),
+		viewNamespace: "sap.ui.demo.orderbrowser.view.",
+		autoWait: true
+	});
+});

+ 13 - 0
webapp/test/integration/opaTestsNavigation.qunit.js

@@ -0,0 +1,13 @@
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"sap/ui/demo/orderbrowser/test/integration/arrangements/Startup",
+	"sap/ui/demo/orderbrowser/test/integration/NavigationJourney"
+], function (Opa5, Startup) {
+	"use strict";
+
+	Opa5.extendConfig({
+		arrangements: new Startup(),
+		viewNamespace: "sap.ui.demo.orderbrowser.view.",
+		autoWait: true
+	});
+});

+ 13 - 0
webapp/test/integration/opaTestsPhone.qunit.js

@@ -0,0 +1,13 @@
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"./arrangements/Startup",
+	"./NavigationJourneyPhone",
+	"./NotFoundJourneyPhone"
+], function (Opa5, Startup) {
+	"use strict";
+
+	Opa5.extendConfig({
+		arrangements: new Startup(),
+		viewNamespace: "sap.ui.demo.orderbrowser.view."
+	});
+});

+ 62 - 0
webapp/test/integration/pages/App.js

@@ -0,0 +1,62 @@
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"sap/ui/test/matchers/Properties"
+], function (Opa5, Properties) {
+	"use strict";
+
+	// var sViewName = "App";
+
+	Opa5.createPageObjects({
+		onTheAppPage: {
+			viewName: "App",
+
+			actions: {
+				// TODO - destroy() ?
+				iCloseTheMessageBox: function () {
+					return this.waitFor({
+						id: "serviceErrorMessageBox",
+						searchOpenDialogs: true,
+						success: function (oMessageBox) {
+							oMessageBox.destroy();
+							Opa5.assert.ok(true, "The MessageBox was closed");
+						}
+					});
+				}
+			},
+
+			assertions: {
+
+				iShouldSeeTheMessageBox: function () {
+					return this.waitFor({
+						searchOpenDialogs: true,
+						controlType: "sap.m.Dialog",
+						matchers: new Properties({
+							type: "Message"
+						}),
+						success: function () {
+							Opa5.assert.ok(true, "The correct MessageBox was shown");
+						}
+					});
+				},
+
+				theAppShowsFCLDesign: function (sLayout) {
+					return this.waitFor({
+						id: "layout",
+						// viewName: sViewName,
+						matchers: new Properties({
+							layout: sLayout
+						}),
+						success: function () {
+							Opa5.assert.ok(true, "The app shows " + sLayout + " layout");
+						},
+						errorMessage: "The app doesn't show " + sLayout + " layout"
+					});
+				}
+
+			}
+
+		}
+
+	});
+
+});

+ 80 - 0
webapp/test/integration/pages/Browser.js

@@ -0,0 +1,80 @@
+sap.ui.define([
+	"sap/ui/test/Opa5"
+], function (Opa5) {
+	"use strict";
+
+	Opa5.createPageObjects({
+		onTheBrowserPage: {
+
+			actions: {
+
+				iChangeTheHashToObjectN: function (iObjIndex) {
+					return this.waitFor({
+						success: function () {
+							var aEntitySet = this.getEntitySet("Objects");
+							Opa5.getHashChanger().setHash("/Orders/" + aEntitySet[iObjIndex].OrderID);
+						}
+					});
+				},
+
+				iChangeTheHashToTheRememberedItem: function () {
+					return this.waitFor({
+						success: function () {
+							var sObjectId = this.getContext().currentItem.id;
+							Opa5.getHashChanger().setHash("/Orders/" + sObjectId);
+						}
+					});
+				},
+
+				iChangeTheHashToSomethingInvalid: function () {
+					return this.waitFor({
+						success: function () {
+							Opa5.getHashChanger().setHash("/somethingInvalid");
+						}
+					});
+				}
+
+			},
+
+			assertions: {
+
+				iShouldSeeTheHashForObjectN: function (iObjIndex) {
+					return this.waitFor({
+						success: function () {
+							var aEntitySet = this.getEntitySet("Objects");
+							var oHashChanger = Opa5.getHashChanger();
+							var sHash = oHashChanger.getHash();
+							Opa5.assert.strictEqual(sHash, "Orders/" + aEntitySet[iObjIndex].OrderID, "The Hash is not correct");
+						}
+					});
+				},
+
+				iShouldSeeTheHashForTheRememberedObject: function () {
+					return this.waitFor({
+						success: function () {
+							var sObjectId = this.getContext().currentItem.id;
+							var	oHashChanger = Opa5.getHashChanger();
+							var	sHash = oHashChanger.getHash();
+							Opa5.assert.strictEqual(sHash, "Orders/" + sObjectId + "/?tab=shipping", "The Hash is not correct");
+						}
+					});
+				},
+
+				iShouldSeeAnEmptyHash: function () {
+					return this.waitFor({
+						success: function () {
+							var oHashChanger = Opa5.getHashChanger();
+							var sHash = oHashChanger.getHash();
+							Opa5.assert.strictEqual(sHash, "", "The Hash should be empty");
+						},
+						errorMessage: "The Hash is not Correct!"
+					});
+				}
+
+			}
+
+		}
+
+	});
+
+});

+ 38 - 0
webapp/test/integration/pages/Common.js

@@ -0,0 +1,38 @@
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"sap/ui/demo/orderbrowser/localService/mockserver",
+	"sap/base/strings/capitalize",
+	"sap/ui/core/Lib"
+], function (Opa5, mockserver, capitalize, Library) {
+	"use strict";
+
+	return Opa5.extend("sap.ui.demo.orderbrowser.test.integration.pages.Common", {
+
+		getEntitySet: function  (sEntitySet) {
+			return mockserver.getMockServer().getEntitySetData(sEntitySet);
+		},
+		I18NTextExtended: function(oControl, sResourceId, sPropertyName, sLibrary, aParams){
+			var oModel, oResourceBundle, sText;
+			var fnProperty = oControl["get" + capitalize(sPropertyName, 0)];
+
+			// check property
+			if (!fnProperty) {
+				return false;
+			}
+
+			var sPropertyValue = fnProperty.call(oControl);
+
+			if (sLibrary) {
+				oResourceBundle = Library.getResourceBundleFor(sLibrary);
+			} else {
+				oModel = oControl.getModel("i18n");
+				oResourceBundle = oModel.getResourceBundle();
+			}
+
+			sText = oResourceBundle.getText(sResourceId, aParams);
+
+			return sText === sPropertyValue;
+		}
+	});
+
+});

+ 202 - 0
webapp/test/integration/pages/Detail.js

@@ -0,0 +1,202 @@
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"sap/ui/test/actions/Press",
+	"./Common",
+	"sap/ui/test/matchers/AggregationFilled",
+	"sap/ui/test/matchers/Properties"
+], function (Opa5, Press, Common, AggregationFilled, Properties) {
+	"use strict";
+
+	Opa5.createPageObjects({
+		onTheDetailPage: {
+			baseClass: Common,
+			viewName: "Detail",
+
+			actions: {
+
+				iPressProcessorTab: function () {
+					return this.waitFor({
+						id: "iconTabFilterProcessor",
+						actions: new Press(),
+						errorMessage: "Did not find the processor tab on detail page"
+					});
+				},
+
+				iPressTheHeaderActionButton: function (sId) {
+					return this.waitFor({
+						id: sId,
+						actions: new Press(),
+						errorMessage: "Did not find the button with id " + sId + " on detail page"
+					});
+				}
+
+			},
+
+			assertions: {
+
+				theObjectPageShowsTheFirstObject: function () {
+					return this.iShouldBeOnTheObjectNPage(0);
+				},
+
+				iShouldBeOnTheObjectNPage: function (iObjIndex) {
+					return this.waitFor(this.getEntitySet({
+						entitySet: "Orders",
+						success: function (aEntitySet) {
+							var sItemName = aEntitySet[iObjIndex].Name;
+
+							this.waitFor({
+								controlType: "sap.m.ObjectHeader",
+								matchers: new Properties({
+									title: aEntitySet[iObjIndex].Name
+								}),
+								success: function () {
+									Opa5.assert.ok(true, "was on the first object page with the name " + sItemName);
+								},
+								errorMessage: "First object is not shown"
+							});
+						}
+					}));
+				},
+// TODO matcher??
+				iShouldSeeTheRememberedObject: function () {
+					return this.waitFor({
+						success: function () {
+							var sBindingPath = this.getContext().currentItem.bindingPath;
+							this._waitForPageBindingPath(sBindingPath);
+						}
+					});
+				},
+
+				_waitForPageBindingPath: function (sBindingPath) {
+					return this.waitFor({
+						id: "page",
+						matchers: function (oPage) {
+							return oPage.getBindingContext() && oPage.getBindingContext().getPath() === sBindingPath;
+						},
+						success: function (oPage) {
+							Opa5.assert.strictEqual(oPage.getBindingContext().getPath(), sBindingPath, "was on the remembered detail page");
+						},
+						errorMessage: "Remembered object " + sBindingPath + " is not shown"
+					});
+				},
+
+				iShouldSeeTheObjectLineItemsList: function () {
+					return this.waitFor({
+						id: "lineItemsList",
+						success: function (oList) {
+							Opa5.assert.ok(oList, "Found the line items list.");
+						}
+					});
+				},
+
+				theLineItemsListShouldHaveTheCorrectNumberOfItems: function () {
+					return this.waitFor(this.getEntitySet({
+						entitySet: "Order_Details",
+						success: function (aEntitySet) {
+
+							return this.waitFor({
+								id: "lineItemsList",
+								matchers: new AggregationFilled({
+									name: "items"
+								}),
+								// TODO matcher
+								check: function (oList) {
+
+									var sObjectID = oList.getBindingContext().getProperty("OrderID");
+
+									var iLength = aEntitySet.filter(function (oLineItem) {
+										return oLineItem.OrderID === sObjectID;
+									}).length;
+
+									return oList.getItems().length === iLength;
+								},
+								success: function () {
+									Opa5.assert.ok(true, "The list has the correct number of items");
+								},
+								errorMessage: "The list does not have the correct number of items."
+							});
+						}
+					}));
+				},
+
+				theLineItemsTableShouldContainOnlyFormattedUnitNumbers: function () {
+					var rTwoDecimalPlaces =  /^-?[\d,]+\.\d{2}$/;
+					return this.waitFor({
+						controlType: "sap.m.ObjectNumber",
+						success: function (aNumberControls) {
+							Opa5.assert.ok(aNumberControls.every(function(oNumberControl){
+									return rTwoDecimalPlaces.test(oNumberControl.getNumber());
+								}),
+								"Object numbers are properly formatted");
+						},
+						errorMessage: "LineItems Table has no entries which can be checked for their formatting"
+					});
+				},
+// TODO repetitive?
+				theLineItemsHeaderShouldDisplayTheAmountOfEntries: function () {
+					return this.waitFor({
+						id: "lineItemsList",
+						matchers: new AggregationFilled({
+							name: "items"
+						}),
+						success: function (oList) {
+							var iNumberOfItems = oList.getItems().length;
+							return this.waitFor({
+								id: "lineItemsHeader",
+								matchers: function(oControl){
+									return this.I18NTextExtended(oControl, "detailLineItemTableHeadingCount", "text", null, [iNumberOfItems]);
+								}.bind(this),
+								success: function () {
+									Opa5.assert.ok(true, "The line item list displays " + iNumberOfItems + " items");
+								},
+								errorMessage: "The line item list does not display " + iNumberOfItems + " items."
+							});
+						}
+					});
+				},
+
+				iShouldSeeTheShippingInfo: function () {
+					return this.waitFor({
+						id: "SimpleFormShipAddress",
+						viewName: "Shipping",
+						success: function () {
+							Opa5.assert.ok("The shipping tab is rendered");
+						},
+						errorMessage: "Did not find shipping info"
+					});
+				},
+
+				iShouldSeeTheProcessorInfo: function () {
+					return this.waitFor({
+						id: "SimpleFormProcessorInfo",
+						viewName: "Processor",
+						success: function () {
+							Opa5.assert.ok("The processor tab is rendered");
+						},
+						errorMessage: "Did not find processor info"
+					});
+				},
+
+				iShouldSeeHeaderActionButtons: function () {
+					return this.waitFor({
+						id: ["closeColumn", "enterFullScreen"],
+						success: function () {
+							Opa5.assert.ok(true, "The action buttons are visible");
+						},
+						errorMessage: "The action buttons were not found"
+					});
+				},
+
+				iShouldSeeTheFullScreenToggleButton: function (sId) {
+					return this.waitFor({
+						id: sId,
+						errorMessage: "The toggle button" + sId + "was not found"
+					});
+				}
+			}
+
+		}
+
+	});
+
+});

+ 435 - 0
webapp/test/integration/pages/Master.js

@@ -0,0 +1,435 @@
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"sap/ui/test/actions/Press",
+	"./Common",
+	"sap/ui/test/actions/EnterText",
+	"sap/ui/test/matchers/AggregationLengthEquals",
+	"sap/ui/test/matchers/AggregationFilled"
+], function(Opa5, Press, Common, EnterText, AggregationLengthEquals, AggregationFilled) {
+	"use strict";
+
+	Opa5.createPageObjects({
+		onTheMasterPage: {
+			baseClass: Common,
+			viewName: "Master",
+
+			actions: {
+
+				iWaitUntilTheListIsLoaded: function () {
+					return this.waitFor({
+						id: "list",
+						matchers: new AggregationFilled({
+							name: "items"
+						}),
+						errorMessage: "The master list has not been loaded"
+					});
+				},
+
+				iRememberTheSelectedItem: function () {
+					return this.waitFor({
+						id: "list",
+						matchers: function (oList) {
+							return oList.getSelectedItem();
+						},
+						success: function (oListItem) {
+							this.iRememberTheListItem(oListItem);
+						},
+						errorMessage: "The list does not have a selected item so nothing can be remembered"
+					});
+				},
+
+				iRememberTheIdOfListItemAtPosition: function (iPosition) {
+					return this.waitFor({
+						id: "list",
+						matchers: function (oList) {
+							return oList.getItems()[iPosition];
+						},
+						success: function (oListItem) {
+							this.iRememberTheListItem(oListItem);
+						},
+						errorMessage: "The list does not have an item at the index " + iPosition
+					});
+				},
+
+				iRememberAnIdOfAnObjectThatsNotInTheList: function () {
+					var aEntityData = this.getEntitySet("Orders");
+					return this.waitFor({
+						id: "list",
+						matchers: new AggregationFilled({
+							name: "items"
+						}),
+						success: function (oList) {
+							var sCurrentId,
+								aItemsNotInTheList = aEntityData.filter(function (oObject) {
+									return !oList.getItems().some(function (oListItem) {
+										return oListItem.getBindingContext().getProperty("OrderID") === oObject.OrderID;
+									});
+								});
+
+							if (!aItemsNotInTheList.length) {
+								// Not enough items all of them are displayed so we take the last one
+								sCurrentId = aEntityData[aEntityData.length - 1].OrderID;
+							} else {
+								sCurrentId = aItemsNotInTheList[0].OrderID;
+							}
+
+							var oCurrentItem = this.getContext().currentItem;
+							// Construct a binding path since the list item is not created yet and we only have the id.
+							oCurrentItem.bindingPath = "/" + oList.getModel().createKey("Orders", {
+								OrderID: sCurrentId
+							});
+							oCurrentItem.id = sCurrentId;
+						},
+						errorMessage: "the model does not have a item that is not in the list"
+					});
+				},
+
+				iPressOnTheObjectAtPosition: function (iPositon) {
+					return this.waitFor({
+						id: "list",
+						matchers: function (oList) {
+							return oList.getItems()[iPositon];
+						},
+						actions: new Press(),
+						errorMessage: "List 'list' in view 'Master' does not contain an ObjectListItem at position '" + iPositon + "'"
+					});
+				},
+
+				iSearchFor: function (sSearch){
+					return this.waitFor({
+						id: "searchField",
+						actions: [
+							new EnterText({
+								text: sSearch
+							}),
+							new Press()
+						],
+						errorMessage: "Can't search for " + sSearch
+					});
+				},
+
+				iSearchForNotFound: function () {
+					return this.iSearchFor("*#-Q@@||");
+				},
+
+				iClearTheSearch: function () {
+					return this.waitFor({
+						id: "searchField",
+						actions: new Press({
+							idSuffix: "reset"
+						}),
+						errorMessage: "Failed to clear the search in master list"
+					});
+				},
+
+				iRememberTheListItem: function (oListItem) {
+					var oBindingContext = oListItem.getBindingContext();
+					this.getContext().currentItem = {
+						bindingPath: oBindingContext.getPath(),
+						id: oBindingContext.getProperty("OrderID"),
+						title: oBindingContext.getProperty("OrderID")
+					};
+				},
+
+				iFilterTheListOn: function (sOption) {
+					return this.waitFor({
+						id: "filterButton",
+						actions: new Press(),
+						success: function () {
+							this.waitFor({
+								searchOpenDialogs: true,
+								controlType: "sap.m.StandardListItem",
+								matchers: function(oControl){
+									return this.I18NTextExtended(oControl, "filterName", "title");
+								}.bind(this),
+								actions: new Press(),
+								success: function () {
+									this.waitFor({
+										searchOpenDialogs: true,
+										controlType: "sap.m.StandardListItem",
+										matchers: function(oControl){
+											return this.I18NTextExtended(oControl, sOption, "title");
+										}.bind(this),
+										actions: new Press(),
+										success: function () {
+											this.waitFor({
+												searchOpenDialogs: true,
+												controlType: "sap.m.Button",
+												matchers: function(oControl){
+													return this.I18NTextExtended(oControl, "VIEWSETTINGS_ACCEPT", "text", "sap.m");
+												}.bind(this),
+												actions: new Press(),
+												errorMessage: "The OK button in the filter dialog could not be pressed"
+											});
+										},
+										errorMessage: "Did not find the " +  sOption + " option in Orders"
+									});
+								},
+								errorMessage: "Did not find the Orders in filter dialog"
+							});
+						},
+						errorMessage: "Did not find the filter button"
+					});
+				},
+
+				iResetFilters: function () {
+					return this.waitFor({
+						id: "filterButton",
+						actions: new Press(),
+						success: function () {
+							return this.waitFor({
+								searchOpenDialogs: true,
+								controlType: "sap.m.Button",
+								matchers: function(oControl){
+									return this.I18NTextExtended(oControl, "VIEWSETTINGS_RESET", "text", "sap.m");
+								}.bind(this),
+								actions: new Press(),
+								success: function () {
+									return this.waitFor({
+										searchOpenDialogs: true,
+										controlType: "sap.m.Button",
+										matchers: function(oControl){
+											return this.I18NTextExtended(oControl, "VIEWSETTINGS_ACCEPT", "text", "sap.m");
+										}.bind(this),
+										actions: new Press(),
+										errorMessage: "Did not find the ViewSettingDialog's 'OK' button."
+									});
+								},
+								errorMessage: "Did not find the ViewSettingDialog's 'Reset' button."
+							});
+						},
+						errorMessage: "Did not find the 'filter' button."
+					});
+				},
+
+				iGroupTheList: function () {
+					return this.iChooseGrouping("masterGroupCustomer");
+				},
+
+				iResetGrouping: function () {
+					return this.iChooseGrouping("VIEWSETTINGS_NONE_ITEM", "sap.m");
+				},
+
+				iChooseGrouping: function (sResourceId, sLibrary) {
+					return this.waitFor({
+						id: "groupButton",
+						actions: new Press(),
+						success: function () {
+							this.waitFor({
+								controlType: "sap.m.StandardListItem",
+								matchers: function(oControl){
+									return this.I18NTextExtended(oControl, sResourceId, "title", sLibrary);
+								}.bind(this),
+								searchOpenDialogs: true,
+								actions: new Press(),
+								success: function () {
+									this.waitFor({
+										controlType: "sap.m.Button",
+										matchers: function(oControl){
+											return this.I18NTextExtended(oControl, "VIEWSETTINGS_ACCEPT", "text", "sap.m");
+										}.bind(this),
+										searchOpenDialogs: true,
+										actions: new Press(),
+										errorMessage: "The ok button in the grouping dialog could not be pressed"
+									});
+								},
+								errorMessage: "Did not find the " +  sResourceId + " element in grouping dialog"
+							});
+						},
+						errorMessage: "Did not find the group button"
+					});
+				}
+			},
+
+			assertions: {
+
+				theListHeaderDisplaysZeroHits: function () {
+					return this.theHeaderShouldDisplayOrders(0);
+				},
+
+				theListHasEntries: function () {
+					return this.waitFor({
+						id: "list",
+						matchers: new AggregationFilled({
+							name: "items"
+						}),
+						success: function () {
+							Opa5.assert.ok(true, "The master list has items");
+						},
+						errorMessage: "The maste list has no items"
+					});
+				},
+
+				iShouldSeeTheList: function () {
+					return this.waitFor({
+						id: "list",
+						success: function (oList) {
+							Opa5.assert.ok(oList, "Found the master List");
+						},
+						errorMessage: "Can't find the master list."
+					});
+				},
+
+				theListShowsOnlyObjectsContaining: function (sSearch) {
+					this.waitFor({
+						id: "list",
+						matchers: new AggregationFilled({
+							name: "items"
+						}),
+						check: function(oList) {
+							var bEveryItemContainsTheTitle = oList.getItems().every(function (oItem) {
+									return oItem.getAttributes()[0].getText().indexOf(sSearch) !== -1;
+								});
+							return bEveryItemContainsTheTitle;
+						},
+						success: function () {
+							Opa5.assert.ok(true, "Every item in the master list contains the text " + sSearch);
+						},
+						errorMessage: "Not all items in the master list contain the text " + sSearch
+					});
+				},
+
+				theListShouldHaveAllEntries: function () {
+					var	iExpectedNumberOfItems;
+					return this.waitFor({
+						id: "list",
+						success: function (oList) {
+							var aAllEntities = this.getEntitySet("Orders");
+							iExpectedNumberOfItems = Math.min(oList.getGrowingThreshold(), aAllEntities.length);
+							return this.waitFor({
+								id: "list",
+								matchers: new AggregationLengthEquals({
+									name: "items",
+									length: iExpectedNumberOfItems
+								}),
+								success: function () {
+									Opa5.assert.ok(true, "The master list displays all items up to the growing threshold");
+								},
+								errorMessage: "The master list does not display all items up to the growing threshold"
+							});
+						}.bind(this)
+					});
+				},
+
+				iShouldSeeTheNoDataText: function () {
+					return this.waitFor({
+						id: "list",
+						success: function (oList) {
+							Opa5.assert.strictEqual(oList.getNoData().getTitle(), oList.getModel("i18n").getProperty("masterListNoDataWithFilterOrSearchText"), "the list should show the no data text for search and filter");
+							Opa5.assert.ok(!!oList.getNoData().getDomRef(), "the NoData text is visible");
+						},
+						errorMessage: "list does not show the no data text for search and filter"
+					});
+				},
+
+				theHeaderShouldDisplayAllEntries: function () {
+					return this.waitFor({
+						id: "list",
+						success: function (oList) {
+							var iExpectedLength = oList.getBinding("items").getLength();
+							return this.theHeaderShouldDisplayOrders(iExpectedLength);
+						},
+						errorMessage: "Can't find the master list"
+					});
+				},
+
+				theHeaderShouldDisplayOrders: function (iOrders) {
+					return this.waitFor({
+						id: "masterHeaderTitle",
+						matchers: function(oControl){
+							return this.I18NTextExtended(oControl, "masterTitleCount", "text", null, [iOrders]);
+						}.bind(this),
+						success: function () {
+							Opa5.assert.ok(true, "The master page header displays " + iOrders + " orders");
+						},
+						errorMessage: "The  master page header does not display " + iOrders + " orders."
+					});
+				},
+
+				theListShouldHaveNoSelection: function () {
+					return this.waitFor({
+						id: "list",
+						matchers: function(oList) {
+							return !oList.getSelectedItem();
+						},
+						success: function (oList) {
+							Opa5.assert.strictEqual(oList.getSelectedItems().length, 0, "The list selection is removed");
+						},
+						errorMessage: "List selection was not removed"
+					});
+				},
+
+				theRememberedListItemShouldBeSelected: function () {
+					this.waitFor({
+						id: "list",
+						matchers: function (oList) {
+							return oList.getSelectedItem();
+						},
+						success: function (oSelectedItem) {
+							Opa5.assert.strictEqual(oSelectedItem.getTitle(), "Order " + this.getContext().currentItem.title, "The list selection is incorrect");
+						},
+						errorMessage: "The list has no selection"
+					});
+				},
+
+				theListShouldBeFilteredOnShippedOrders: function () {
+					function fnCheckFilter (oList){
+						var fnIsFiltered = function (oElement) {
+							if (!oElement.getBindingContext()) {
+								return false;
+							} else {
+								var sDate = oElement.getBindingContext().getProperty("ShippedDate");
+								if (!sDate) {
+									return false;
+								} else {
+									return true;
+								}
+							}
+						};
+
+						return oList.getItems().every(fnIsFiltered);
+					}
+
+					return this.waitFor({
+						id: "list",
+						matchers: fnCheckFilter,
+						success: function() {
+							Opa5.assert.ok(true, "Master list has been filtered correctly");
+						},
+						errorMessage: "Master list has not been filtered correctly"
+					});
+				},
+
+				theListShouldContainAGroupHeader: function () {
+					return this.waitFor({
+						controlType: "sap.m.GroupHeaderListItem",
+						success: function () {
+							Opa5.assert.ok(true, "Master list is grouped");
+						},
+						errorMessage: "Master list is not grouped"
+					});
+				},
+
+				theListShouldNotContainGroupHeaders: function () {
+					function fnIsGroupHeader (oElement) {
+						return oElement.getMetadata().getName() === "sap.m.GroupHeaderListItem";
+					}
+
+					return this.waitFor({
+						id: "list",
+						matchers: function (oList) {
+							return !oList.getItems().some(fnIsGroupHeader);
+						},
+						success: function() {
+							Opa5.assert.ok(true, "Master list does not contain a group header");
+						},
+						errorMessage: "Master list still contains a group header although grouping has been removed."
+					});
+				}
+			}
+
+		}
+
+	});
+
+});

+ 82 - 0
webapp/test/integration/pages/NotFound.js

@@ -0,0 +1,82 @@
+sap.ui.define([
+	"sap/ui/test/Opa5",
+	"sap/ui/test/actions/Press",
+	"sap/ui/test/matchers/Properties"
+], function (Opa5, Press, Properties) {
+	"use strict";
+
+	var sNotFoundPageId = "page",
+		sNotFoundView = "NotFound",
+		sDetailNotFoundView = "DetailObjectNotFound";
+
+	Opa5.createPageObjects({
+		onTheNotFoundPage: {
+
+			actions: {
+
+				iPressTheBackButton: function (sViewName) {
+					return this.waitFor({
+						viewName: sViewName,
+						controlType: "sap.m.Button",
+						matchers: new Properties({
+							type: "Back"
+						}),
+						actions: new Press(),
+						errorMessage: "Did not find the back button"
+					});
+				}
+
+			},
+
+			assertions: {
+
+				iShouldSeeTheNotFoundGeneralPage: function (sPageViewName) {
+					return this.waitFor({
+						controlType: "sap.m.IllustratedMessage",
+						viewName: sPageViewName,
+						success: function () {
+							Opa5.assert.ok(true, "Shows the message page");
+						},
+						errorMessage: "Did not reach the empty page"
+					});
+				},
+
+				iShouldSeeTheNotFoundPage: function () {
+					return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundView);
+				},
+
+				iShouldSeeTheObjectNotFoundPage: function () {
+					return this.iShouldSeeTheNotFoundGeneralPage(sDetailNotFoundView);
+				},
+
+				theNotFoundPageShouldSayResourceNotFound: function () {
+					return this.waitFor({
+						id: sNotFoundPageId,
+						viewName: sNotFoundView,
+						success: function (oPage) {
+							Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("notFoundTitle"), "The not found text is shown as title");
+							Opa5.assert.strictEqual(oPage.getContent()[0].getTitle(), oPage.getModel("i18n").getProperty("notFoundText"), "The resource not found text is shown");
+						},
+						errorMessage: "Did not display the resource not found text"
+					});
+				},
+
+				theNotFoundPageShouldSayObjectNotFound: function () {
+					return this.waitFor({
+						id: sNotFoundPageId,
+						viewName: sDetailNotFoundView,
+						success: function (oPage) {
+							Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("detailTitle"), "The object text is shown as title");
+							Opa5.assert.strictEqual(oPage.getContent()[0].getTitle(), oPage.getModel("i18n").getProperty("noObjectFoundText"), "The object not found text is shown");
+						},
+						errorMessage: "Did not display the object not found text"
+					});
+				}
+
+			}
+
+		}
+
+	});
+
+});

+ 27 - 0
webapp/test/mockServer.html

@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+	<title>Browse Orders</title>
+
+	<!-- Bootstrapping UI5 -->
+	<script id="sap-ui-bootstrap"
+			src="../../../../../../../resources/sap-ui-core.js"
+			data-sap-ui-theme="sap_horizon"
+			data-sap-ui-resourceroots='{
+				"sap.ui.demo.orderbrowser": "../"
+			}'
+			data-sap-ui-oninit="module:sap/ui/demo/orderbrowser/test/initMockServer"
+			data-sap-ui-compatVersion="edge"
+			data-sap-ui-async="true"
+			data-sap-ui-frameOptions="trusted">
+	</script>
+
+</head>
+
+<!-- UI Content -->
+<body class="sapUiBody">
+	<div data-sap-ui-component data-name="sap.ui.demo.orderbrowser" data-id="container" data-settings='{"id" : "orderbrowser"}'></div>
+</body>
+</html>

+ 15 - 0
webapp/test/testsuite.qunit.html

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="utf-8">
+	<script
+		src="../../../../../../../resources/sap/ui/test/starter/createSuite.js"
+		data-sap-ui-testsuite="test-resources/sap/ui/demo/orderbrowser/testsuite.qunit"
+		data-sap-ui-resource-roots='{
+			"test-resources.sap.ui.demo.orderbrowser": "./"
+		}'
+	></script>
+</head>
+<body>
+</body>
+</html>

+ 39 - 0
webapp/test/testsuite.qunit.js

@@ -0,0 +1,39 @@
+sap.ui.define(() => {
+	"use strict";
+
+	return {
+		name: "Test suite for Order Browser",
+		defaults: {
+			page: "ui5://test-resources/sap/ui/demo/orderbrowser/Test.qunit.html?testsuite={suite}&test={name}",
+			qunit: {
+				version: 2
+			},
+			sinon: {
+				version: 1
+			},
+			ui5: {
+				theme: "sap_horizon"
+			},
+			loader: {
+				paths: {
+					"sap/ui/demo/orderbrowser": "../"
+				}
+			}
+		},
+		tests: {
+			"unit/unitTests": {
+				title: "Unit tests for Browse Orders"
+			},
+			"integration/opaTestsNavigation": {
+				title: "Integration tests for Browse Orders Navigation scenario"
+			},
+			"integration/opaTests": {
+				title: "Integration tests for Browse Orders"
+			},
+			"integration/opaTestsPhone": {
+				title: "Integration tests for Browse Orders on phone",
+				skip: true
+			}
+		}
+	};
+});

+ 33 - 0
webapp/test/unit/controller/Detail.controller.js

@@ -0,0 +1,33 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/demo/orderbrowser/controller/Detail.controller",
+	"sap/m/library"
+], function(Detail, mobileLibrary) {
+	"use strict";
+
+	QUnit.module("DetailController" ,{
+		beforeEach: function (){
+			this.Detail = new Detail();
+			this.oEvent = {
+				getSource: function () {
+					return {
+						getText: this.stub().returns("12345")
+					};
+				}
+			};
+		},
+		afterEach: function () {
+			this.Detail.destroy();
+		}
+	});
+
+	QUnit.test("Should trigger the telephone helper in the _onHandleTelephonePress event", function (assert) {
+		var oStub = this.stub(mobileLibrary.URLHelper, "triggerTel");
+
+		this.Detail._onHandleTelephonePress(this.oEvent);
+
+		assert.ok(oStub.calledWith("12345"), "The function \"sap.m.URLHelper.triggerTel\" was called with the telephone number");
+		assert.strictEqual(oStub.callCount, 1, "the telephone action has been triggered once");
+	});
+});

+ 189 - 0
webapp/test/unit/controller/ListSelector.js

@@ -0,0 +1,189 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/demo/orderbrowser/controller/ListSelector"
+], function(ListSelector) {
+	"use strict";
+
+	QUnit.module("Initialization", {
+		beforeEach : function () {
+			this.oListSelector = new ListSelector();
+		},
+		afterEach : function () {
+			this.oListSelector.destroy();
+		}
+	});
+
+	QUnit.test("Should initialize the List loading promise", function (assert) {
+		// Arrange
+		var done = assert.async(),
+			fnRejectSpy = this.spy(),
+			fnResolveSpy = this.spy();
+
+		// Act
+		this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy);
+
+		// Assert
+		setTimeout(function () {
+			assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise");
+			assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise");
+			done();
+		}, 0);
+	});
+
+	QUnit.module("List loading", {
+		beforeEach : function () {
+			this.oListSelector = new ListSelector();
+		},
+		afterEach : function () {
+			this.oListSelector.destroy();
+		}
+	});
+
+	function createListStub (bCreateListItem, sBindingPath) {
+		var fnGetParameter = function () {
+				return true;
+			},
+			oDataStub = {
+				getParameter : fnGetParameter
+			},
+			fnAttachEventOnce = function (sEventName, fnCallback) {
+				fnCallback(oDataStub);
+			},
+			fnGetBinding = this.stub().returns({
+				attachEventOnce : fnAttachEventOnce
+			}),
+			fnAttachEvent = function (sEventName, fnCallback, oContext) {
+				fnCallback.apply(oContext);
+			},
+			oListItemStub = {
+				getBindingContext : this.stub().returns({
+					getPath : this.stub().returns(sBindingPath)
+				})
+			},
+			aListItems = [];
+
+		if (bCreateListItem) {
+			aListItems.push(oListItemStub);
+		}
+
+		return {
+			attachEvent : fnAttachEvent,
+			attachEventOnce : fnAttachEventOnce,
+			getBinding : fnGetBinding,
+			getItems : this.stub().returns(aListItems)
+		};
+	}
+
+	QUnit.test("Should resolve the list loading promise, if the list has items", function (assert) {
+		// Arrange
+		var done = assert.async(),
+			fnRejectSpy = this.spy(),
+			fnResolveSpy = function (sBindingPath) {
+				// Assert
+				assert.strictEqual(sBindingPath, sBindingPath, "Did pass the binding path");
+				assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise");
+				done();
+			};
+
+		// Act
+		this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy);
+		this.oListSelector.setBoundMasterList(createListStub.call(this, true, "anything"));
+	});
+
+	QUnit.test("Should reject the list loading promise, if the list has no items", function (assert) {
+		// Arrange
+		var done = assert.async(),
+			fnResolveSpy = this.spy(),
+			fnRejectSpy = function () {
+				// Assert
+				assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise");
+				done();
+			};
+
+		// Act
+		this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy);
+		this.oListSelector.setBoundMasterList(createListStub.call(this, false));
+	});
+
+	QUnit.module("Selecting item in the list", {
+		beforeEach : function () {
+			this.oListSelector = new ListSelector();
+			this.oListSelector.oWhenListLoadingIsDone = {
+				then : function (fnAct) {
+					this.fnAct = fnAct;
+				}.bind(this)
+			};
+		},
+		afterEach : function () {
+			this.oListSelector.destroy();
+		}
+	});
+
+	function createStubbedListItem (sBindingPath) {
+		return {
+			getBindingContext : this.stub().returns({
+				getPath : this.stub().returns(sBindingPath)
+			})
+		};
+	}
+
+	QUnit.test("Should select an Item of the list when it is loaded and the binding contexts match", function (assert) {
+		// Arrange
+		var sBindingPath = "anything",
+			oListItemToSelect = createStubbedListItem.call(this, sBindingPath),
+			oSelectedListItemStub = createStubbedListItem.call(this, "a different binding path");
+
+		this.oListSelector._oList = {
+			getMode : this.stub().returns("SingleSelectMaster"),
+			getSelectedItem : this.stub().returns(oSelectedListItemStub),
+			getItems : this.stub().returns([ oSelectedListItemStub, oListItemToSelect, createListStub.call(this, "yet another list binding") ]),
+			setSelectedItem : function (oItem) {
+				//Assert
+				assert.strictEqual(oItem, oListItemToSelect, "Did select the list item with a matching binding context");
+			}
+		};
+
+		// Act
+		this.oListSelector.selectAListItem(sBindingPath);
+		// Resolve list loading
+		this.fnAct();
+	});
+
+	QUnit.test("Should not select an Item of the list when it is already selected", function (assert) {
+		// Arrange
+		var sBindingPath = "anything",
+			oSelectedListItemStub = createStubbedListItem.call(this, sBindingPath);
+
+		this.oListSelector._oList = {
+			getMode:  this.stub().returns("SingleSelectMaster"),
+			getSelectedItem : this.stub().returns(oSelectedListItemStub)
+		};
+
+		// Act
+		this.oListSelector.selectAListItem(sBindingPath);
+		// Resolve list loading
+		this.fnAct();
+
+		// Assert
+		assert.ok(true, "did not fail");
+	});
+
+	QUnit.test("Should not select an item of the list when the list has the selection mode none", function (assert) {
+		// Arrange
+		var sBindingPath = "anything";
+
+		this.oListSelector._oList = {
+			getMode : this.stub().returns("None")
+		};
+
+		// Act
+		this.oListSelector.selectAListItem(sBindingPath);
+		// Resolve list loading
+		this.fnAct();
+
+		// Assert
+		assert.ok(true, "did not fail");
+	});
+
+});

+ 24 - 0
webapp/test/unit/helper/FakeI18nModel.js

@@ -0,0 +1,24 @@
+sap.ui.define([
+		"sap/ui/model/Model"
+	], function (Model) {
+		"use strict";
+
+		return Model.extend("test.unit.helper.FakeI18nModel", {
+
+			constructor : function (mTexts) {
+				Model.call(this);
+				this.mTexts = mTexts || {};
+			},
+
+			getResourceBundle : function () {
+				return {
+					getText : function (sTextName) {
+						return this.mTexts[sTextName];
+					}.bind(this)
+				};
+			}
+
+		});
+
+	}
+);

+ 109 - 0
webapp/test/unit/model/formatter.js

@@ -0,0 +1,109 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/demo/orderbrowser/model/formatter",
+	"../helper/FakeI18nModel"
+], function (formatter, FakeI18n) {
+	"use strict";
+
+	QUnit.module("formatter - Currency value");
+
+	function currencyValueTestCase(assert, sValue, fExpectedNumber) {
+		// Act
+		var fCurrency = formatter.currencyValue(sValue);
+
+		// Assert
+		assert.strictEqual(fCurrency, fExpectedNumber, "The rounding was correct");
+	}
+
+	QUnit.test("Should round down a 3 digit number", function (assert) {
+		currencyValueTestCase.call(this, assert, "3.123", "3.12");
+	});
+
+	QUnit.test("Should round up a 3 digit number", function (assert) {
+		currencyValueTestCase.call(this, assert, "3.128", "3.13");
+	});
+
+	QUnit.test("Should round a negative number", function (assert) {
+		currencyValueTestCase.call(this, assert, "-3", "-3.00");
+	});
+
+	QUnit.test("Should round an empty string", function (assert) {
+		currencyValueTestCase.call(this, assert, "", "");
+	});
+
+	QUnit.test("Should round a zero", function (assert) {
+		currencyValueTestCase.call(this, assert, "0", "0.00");
+	});
+
+	QUnit.module("formatter - Binary Content");
+
+	QUnit.test("The type metadata is prepended  to the image string when binary date is passed to the formatter", function (assert) {
+		var sResult = formatter.handleBinaryContent("binaryData");
+		assert.strictEqual(sResult, "data:image/jpeg;base64,", "The image is formatted correctly");
+	});
+
+	QUnit.test("Calling the formatter with no picture content returns the default picture URL", function (assert) {
+		var sResult = formatter.handleBinaryContent("");
+		assert.strictEqual(sResult, "../images/Employee.png", "The image is formatted correctly");
+	});
+
+	QUnit.module("formatter - Delivery text");
+
+	function deliveryTextTestCase(assert, oRequiredDate, oShippedDate, fExpectedText) {
+
+		//Act
+		var oControllerStub = {
+			getModel: function () {
+				return new FakeI18n({
+					"formatterDeliveryUrgent": 1,
+					"formatterDeliveryInTime": 2,
+					"formatterDeliveryTooLate": 3
+				});
+			}
+		};
+		var fnStubbedFormatter = formatter.deliveryText.bind(oControllerStub);
+		var fText = fnStubbedFormatter(oRequiredDate, oShippedDate);
+
+		//Assert
+		assert.strictEqual(fText, fExpectedText, "Correct text was assigned");
+}
+
+	QUnit.test("Should provide the delivery status 'None' for orders with no shipped date", function (assert) {
+		deliveryTextTestCase.call(this, assert, "1128522175000", null, "None");
+	});
+
+	QUnit.test("Should provide the delivery status 'Urgent' for orders with shipped date > required date", function (assert) {
+		deliveryTextTestCase.call(this, assert, "1206800575000", "1206368675000", 1);
+	});
+
+	QUnit.test("Should provide the delivery status text 'In time' for orders with shipped date > required date", function (assert) {
+		deliveryTextTestCase.call(this, assert, "1129818175000", "1128522175000", 2);
+	});
+
+	QUnit.test("Should provide the delivery status text 'Too late' for orders with shipped date > required date", function (assert) {
+		deliveryTextTestCase.call(this, assert, "1243952575000", "1389972175000", 3);
+	});
+
+	QUnit.module("formatter - Delivery state");
+
+	function deliveryStateTestCase(assert, oRequiredDate, oShippedDate, fExpectedState) {
+		//Act
+		var fState = formatter.deliveryState(oRequiredDate, oShippedDate);
+
+		//Assert
+		assert.strictEqual(fState, fExpectedState, "The formatter returned the correct state");
+	}
+
+	QUnit.test("Should return \"Warning\" state for orders with no shipped date", function (assert) {
+		deliveryStateTestCase.call(this, assert, "1206800575000", "1206368675000", "Warning");
+	});
+
+	QUnit.test("Should return \"Success\" status for orders with shipped date > required date", function (assert) {
+		deliveryStateTestCase.call(this, assert, "1129818175000", "1128522175000", "Success");
+	});
+
+	QUnit.test("Should return \"Error\" state for orders with shipped date > required date", function (assert) {
+		deliveryStateTestCase.call(this, assert, "1243952575000", "1389972175000", "Error");
+	});
+});

+ 62 - 0
webapp/test/unit/model/models.js

@@ -0,0 +1,62 @@
+/*global QUnit*/
+
+sap.ui.define([
+	"sap/ui/demo/orderbrowser/model/models",
+	"sap/ui/Device"
+], function (models, Device) {
+	"use strict";
+
+	QUnit.module("createDeviceModel", {
+		afterEach : function () {
+			this.oDeviceModel.destroy();
+		}
+	});
+
+	function isPhoneTestCase(assert, bIsPhone) {
+		// Arrange
+		this.stub(Device, "system", { phone : bIsPhone });
+
+		// System under test
+		this.oDeviceModel = models.createDeviceModel();
+
+		// Assert
+		assert.strictEqual(this.oDeviceModel.getData().system.phone, bIsPhone, "IsPhone property is correct");
+	}
+
+	QUnit.test("Should initialize a device model for desktop", function (assert) {
+		isPhoneTestCase.call(this, assert, false);
+	});
+
+	QUnit.test("Should initialize a device model for phone", function (assert) {
+		isPhoneTestCase.call(this, assert, true);
+	});
+
+	function isTouchTestCase(assert, bIsTouch) {
+		// Arrange
+		this.stub(Device, "support", { touch : bIsTouch });
+
+		// System under test
+		this.oDeviceModel = models.createDeviceModel();
+
+		// Assert
+		assert.strictEqual(this.oDeviceModel.getData().support.touch, bIsTouch, "IsTouch property is correct");
+	}
+
+	QUnit.test("Should initialize a device model for non touch devices", function (assert) {
+		isTouchTestCase.call(this, assert, false);
+	});
+
+	QUnit.test("Should initialize a device model for touch devices", function (assert) {
+		isTouchTestCase.call(this, assert, true);
+	});
+
+	QUnit.test("The binding mode of the device model should be one way", function (assert) {
+
+		// System under test
+		this.oDeviceModel = models.createDeviceModel();
+
+		// Assert
+		assert.strictEqual(this.oDeviceModel.getDefaultBindingMode(), "OneWay", "Binding mode is correct");
+	});
+
+});

+ 5 - 0
webapp/test/unit/unitTests.qunit.js

@@ -0,0 +1,5 @@
+sap.ui.define([
+	"./model/models",
+	"./model/formatter",
+	"./controller/ListSelector"
+]);

+ 18 - 0
webapp/view/App.view.xml

@@ -0,0 +1,18 @@
+<mvc:View
+	controllerName="sap.ui.demo.orderbrowser.controller.App"
+	displayBlock="true"
+	height="100%"
+	xmlns="sap.m"
+	xmlns:f="sap.f"
+	xmlns:mvc="sap.ui.core.mvc">
+	<App
+		id="app"
+		busy="{appView>/busy}"
+		busyIndicatorDelay="{appView>/delay}">
+		<f:FlexibleColumnLayout
+			id="layout"
+			layout="{appView>/layout}"
+			backgroundDesign="Translucent">
+		</f:FlexibleColumnLayout>
+	</App>
+</mvc:View>

+ 179 - 0
webapp/view/Detail.view.xml

@@ -0,0 +1,179 @@
+<mvc:View
+	controllerName="sap.ui.demo.orderbrowser.controller.Detail"
+	xmlns="sap.m"
+	xmlns:semantic="sap.f.semantic"
+	xmlns:mvc="sap.ui.core.mvc"
+	xmlns:core="sap.ui.core"
+	xmlns:l="sap.ui.layout">
+	<semantic:SemanticPage
+		id="page"
+		busy="{detailView>/busy}"
+		busyIndicatorDelay="{detailView>/delay}"
+		core:require="{
+			formatMessage: 'sap/base/strings/formatMessage',
+			DateType: 'sap/ui/model/type/Date'
+		}">
+		<semantic:titleHeading>
+			<Title
+				text="{
+					parts: [
+						'i18n>commonItemTitle',
+						'OrderID'
+					],
+					formatter: 'formatMessage'
+				}"/>
+		</semantic:titleHeading>
+		<semantic:headerContent>
+			<l:HorizontalLayout>
+				<l:VerticalLayout class="sapUiMediumMarginEnd">
+					<ObjectAttribute
+						title="{i18n>commonCustomerName}"
+						text="{Customer/CompanyName}"/>
+					<ObjectAttribute
+						title="{i18n>detailOrderDate}"
+						text="{
+							path: 'OrderDate',
+							type: 'DateType',
+							formatOptions: { style: 'medium' }
+						}"/>
+					<ObjectAttribute
+						title="{i18n>commonItemShipped}"
+						text="{= ${ShippedDate} ?
+								${
+									path: 'ShippedDate',
+									type: 'DateType',
+									formatOptions: { style: 'medium' }
+								}
+							:${i18n>commonItemNotYetShipped} }"/>
+				</l:VerticalLayout>
+				<l:VerticalLayout>
+					<Label text="{i18n>priceText}"/>
+					<ObjectNumber
+						number="{
+							parts: [
+								{path:'detailView>/totalOrderAmount'},
+								{path:'detailView>/currency'}
+							],
+							type: 'sap.ui.model.type.Currency',
+							formatOptions: {
+								showMeasure: false
+							}
+						}"
+						unit="{detailView>/currency}"/>
+				</l:VerticalLayout>
+			</l:HorizontalLayout>
+		</semantic:headerContent>
+		<semantic:content>
+			<l:VerticalLayout>
+				<IconTabBar
+					id="iconTabBar"
+					headerBackgroundDesign="Transparent"
+					select=".onTabSelect"
+					selectedKey="{detailView>/selectedTab}">
+					<items>
+						<IconTabFilter
+							id="iconTabFilterShipping"
+							icon="sap-icon://shipping-status"
+							tooltip="{i18n>detailIconTabBarShipping}"
+							key="shipping">
+						</IconTabFilter>
+						<IconTabFilter
+							id="iconTabFilterProcessor"
+							icon="sap-icon://employee"
+							tooltip="{i18n>detailIconTabBarProcessor}"
+							key="processor">
+						</IconTabFilter>
+					</items>
+				</IconTabBar>
+				<Table
+					id="lineItemsList"
+					class="sapUiSmallMarginTop"
+					width="auto"
+					items="{Order_Details}"
+					updateFinished=".onListUpdateFinished"
+					noDataText="{i18n>detailLineItemTableNoDataText}"
+					busyIndicatorDelay="{detailView>/lineItemTableDelay}">
+					<headerToolbar>
+						<Toolbar id="lineItemsToolbar">
+							<Title
+								id="lineItemsHeader"
+								text="{detailView>/lineItemListTitle}"/>
+						</Toolbar>
+					</headerToolbar>
+					<columns>
+						<Column>
+							<Text text="{i18n>detailLineItemTableIDColumn}"/>
+						</Column>
+						<Column
+							minScreenWidth="Tablet"
+							demandPopin="true"
+							hAlign="End">
+							<Text text="{i18n>detailLineItemTableUnitPriceColumn}"/>
+						</Column>
+						<Column
+							minScreenWidth="Tablet"
+							demandPopin="true"
+							hAlign="End">
+							<Text text="{i18n>detailLineItemTableUnitQuantityColumn}"/>
+						</Column>
+						<Column
+							minScreenWidth="Tablet"
+							demandPopin="true"
+							hAlign="End">
+							<Text text="{i18n>detailLineItemTableTotalColumn}"/>
+						</Column>
+					</columns>
+					<items>
+						<ColumnListItem>
+							<cells>
+								<ObjectIdentifier
+									title="{Product/ProductName}"
+									text="{ProductID}"/>
+								<ObjectNumber
+									number="{
+										path: 'UnitPrice',
+										formatter: '.formatter.currencyValue'
+									}"
+									unit="{detailView>/currency}"/>
+								<ObjectAttribute
+									text="{Quantity}"/>
+								<ObjectNumber
+									number="{
+										parts:[
+											{path:'Quantity'},
+											{path:'UnitPrice'},
+											{path:'detailView>/currency'}
+										],
+										formatter: '.formatter.calculateItemTotal'
+									}"
+									unit="{detailView>/currency}"/>
+							</cells>
+						</ColumnListItem>
+					</items>
+				</Table>
+			</l:VerticalLayout>
+		</semantic:content>
+		<semantic:sendEmailAction>
+			<semantic:SendEmailAction
+				id="shareEmail"
+				press=".onSendEmailPress"/>
+		</semantic:sendEmailAction>
+		<semantic:closeAction>
+			<semantic:CloseAction
+				id="closeColumn"
+				press=".onCloseDetailPress"/>
+		</semantic:closeAction>
+		<semantic:fullScreenAction>
+			<semantic:FullScreenAction
+				id="enterFullScreen"
+				visible="{= !${device>/system/phone} &amp;&amp; !${appView>/actionButtonsInfo/midColumn/fullScreen}}"
+				press="toggleFullScreen"/>
+		</semantic:fullScreenAction>
+		<semantic:exitFullScreenAction>
+			<semantic:ExitFullScreenAction
+				id="exitFullScreen"
+				visible="{= !${device>/system/phone} &amp;&amp; ${appView>/actionButtonsInfo/midColumn/fullScreen}}"
+				press="toggleFullScreen"/>
+		</semantic:exitFullScreenAction>
+	</semantic:SemanticPage>
+</mvc:View>

+ 19 - 0
webapp/view/DetailObjectNotFound.view.xml

@@ -0,0 +1,19 @@
+<mvc:View
+	controllerName="sap.ui.demo.orderbrowser.controller.DetailObjectNotFound"
+	xmlns="sap.m"
+	xmlns:mvc="sap.ui.core.mvc">
+	<Page
+		id="page"
+		title="{i18n>detailTitle}"
+		showNavButton="{=
+			${device>/system/phone} ||
+			${device>/system/tablet} &amp;&amp;
+			${device>/orientation/portrait}
+		}"
+		navButtonPress=".onNavBack">
+		<IllustratedMessage
+			title="{i18n>noObjectFoundText}"
+			illustrationType="sapIllus-BeforeSearch"
+			enableDefaultTitleAndDescription="false"/>
+	</Page>
+</mvc:View>

+ 140 - 0
webapp/view/Master.view.xml

@@ -0,0 +1,140 @@
+<mvc:View
+	controllerName="sap.ui.demo.orderbrowser.controller.Master"
+	xmlns="sap.m"
+	xmlns:mvc="sap.ui.core.mvc"
+	xmlns:core="sap.ui.core"
+	xmlns:semantic="sap.f.semantic">
+	<semantic:SemanticPage id="page"
+		core:require="{
+			formatMessage: 'sap/base/strings/formatMessage',
+			DateType: 'sap/ui/model/type/Date'
+		}">
+		<semantic:titleHeading>
+			<Title
+				id="masterHeaderTitle"
+				text="{
+					parts: [
+						'i18n>masterTitleCount',
+						'masterView>/titleCount'
+					],
+					formatter: 'formatMessage'
+			}"/>
+		</semantic:titleHeading>
+		<semantic:content>
+			<!-- For client side filtering add this to the items attribute: parameters: {operationMode: 'Client'}}" -->
+			<List
+				id="list"
+				width="auto"
+				class="sapFDynamicPageAlignContent"
+				items="{
+					path: '/Orders',
+					parameters: {expand: 'Customer'},
+					sorter: {
+						path: 'OrderID',
+						descending: true
+					},
+					groupHeaderFactory: '.createGroupHeader'
+				}"
+				busyIndicatorDelay="{masterView>/delay}"
+				mode="{= ${device>/system/phone} ? 'None' : 'SingleSelectMaster'}"
+				growing="true"
+				growingScrollToLoad="true"
+				updateFinished=".onUpdateFinished"
+				selectionChange=".onSelectionChange">
+				<noData>
+					<IllustratedMessage
+						title="{masterView>/noDataText}"
+						illustrationType="sapIllus-NoFilterResults"
+						enableDefaultTitleAndDescription="false"/>
+				</noData>
+				<infoToolbar>
+					<Toolbar
+						active="true"
+						id="filterBar"
+						visible="{masterView>/isFilterBarVisible}"
+						press=".onOpenViewSettings">
+						<Title
+							id="filterBarLabel"
+							text="{masterView>/filterBarLabel}" />
+					</Toolbar>
+				</infoToolbar>
+				<headerToolbar>
+					<OverflowToolbar>
+						<SearchField
+							id="searchField"
+							showRefreshButton="true"
+							tooltip="{i18n>masterSearchTooltip}"
+							width="100%"
+							search=".onSearch">
+							<layoutData>
+								<OverflowToolbarLayoutData
+									minWidth="150px"
+									maxWidth="240px"
+									shrinkable="true"
+									priority="NeverOverflow"/>
+							</layoutData>
+						</SearchField>
+						<ToolbarSpacer/>
+						<Button
+							id="filterButton"
+							press=".onOpenViewSettings"
+							icon="sap-icon://filter"
+							type="Transparent"/>
+						<Button
+							id="groupButton"
+							press=".onOpenViewSettings"
+							icon="sap-icon://group-2"
+							type="Transparent"/>
+					</OverflowToolbar>
+				</headerToolbar>
+				<items>
+					<ObjectListItem
+						type="{= ${device>/system/phone} ? 'Active' : 'Inactive'}"
+						press=".onSelectionChange"
+						title="{
+							parts: [
+								'i18n>commonItemTitle',
+								'OrderID'
+							],
+							formatter: 'formatMessage'
+						}"
+						number="{
+							path: 'OrderDate',
+							type: 'DateType',
+							formatOptions: { style: 'short' }
+						}">
+						<firstStatus>
+							<ObjectStatus
+								state="{
+									parts: [
+										{path: 'RequiredDate'},
+										{path: 'ShippedDate'},
+										{path: 'OrderID'}
+									],
+									formatter:'.formatter.deliveryState'
+								}"
+								text="{
+									parts: [
+										{path: 'RequiredDate'},
+										{path: 'ShippedDate'},
+										{path: 'OrderID'}
+									],
+									formatter:'.formatter.deliveryText'
+								}"/>
+
+						</firstStatus>
+						<attributes>
+							<ObjectAttribute id="companyName" text="{Customer/CompanyName}" />
+							<ObjectAttribute title="{i18n>commonItemShipped}"
+								text="{= ${ShippedDate}
+									? ${ path: 'ShippedDate',
+										 type: 'DateType',
+										 formatOptions: { style: 'medium' } }
+									: ${i18n>commonItemNotYetShipped} }" />
+						</attributes>
+					</ObjectListItem>
+				</items>
+			</List>
+		</semantic:content>
+	</semantic:SemanticPage>
+</mvc:View>

+ 15 - 0
webapp/view/NotFound.view.xml

@@ -0,0 +1,15 @@
+<mvc:View
+	controllerName="sap.ui.demo.orderbrowser.controller.NotFound"
+	xmlns="sap.m"
+	xmlns:mvc="sap.ui.core.mvc">
+	<Page
+		id="page"
+		title="{i18n>notFoundTitle}"
+		showNavButton="true"
+		navButtonPress=".onNavBack">
+		<IllustratedMessage
+			title="{i18n>notFoundText}"
+			illustrationType="sapIllus-PageNotFound"
+			enableDefaultTitleAndDescription="false"/>
+	</Page>
+</mvc:View>

+ 33 - 0
webapp/view/Processor.view.xml

@@ -0,0 +1,33 @@
+<mvc:View
+	controllerName="sap.ui.demo.orderbrowser.controller.Detail"
+	xmlns="sap.m"
+	xmlns:mvc="sap.ui.core.mvc"
+	xmlns:f="sap.ui.layout.form"
+	xmlns:core="sap.ui.core">
+	<VBox>
+		<f:SimpleForm id="SimpleFormProcessorInfo"
+			editable="false"
+			layout="ResponsiveGridLayout"
+			title="{i18n>detailProcessorTitle}"
+			labelSpanL="3"
+			labelSpanM="3"
+			emptySpanL="4"
+			emptySpanM="4"
+			columnsL="2"
+			columnsM="2">
+			<f:content>
+				<core:Title text="{i18n>detailsProcessorTitle}"/>
+				<Label text="{i18n>detailName}" />
+				<Text text="{Employee/FirstName} {Employee/LastName}" />
+				<Label text="{i18n>detailProcessorEmployeeID}" />
+				<Text text="{Employee/EmployeeID}" />
+				<Label text="{i18n>detailProcessorJobTitle}" />
+				<Text text="{Employee/Title}" />
+				<Label text="{i18n>detailProcessorPhone}" />
+				<Link text="{Employee/HomePhone}" press="_onHandleTelephonePress" />
+				<core:Title text="{i18n>photoProcessorTitle}" />
+				<Image src="{path:'Employee/Photo', formatter: '.formatter.handleBinaryContent'}" width="50%" height="50%" />
+			</f:content>
+		</f:SimpleForm>
+	</VBox>
+</mvc:View>

+ 30 - 0
webapp/view/Shipping.view.xml

@@ -0,0 +1,30 @@
+<mvc:View
+	xmlns="sap.m"
+	xmlns:mvc="sap.ui.core.mvc"
+	xmlns:f="sap.ui.layout.form">
+	<VBox>
+		<f:SimpleForm id="SimpleFormShipAddress"
+			editable="false"
+			layout="ResponsiveGridLayout"
+			title="{i18n>detailShippingAddressTitle}"
+			labelSpanL="3"
+			labelSpanM="3"
+			emptySpanL="4"
+			emptySpanM="4"
+			columnsL="1"
+			columnsM="1" >
+			<f:content>
+				<Label text="{i18n>detailName}" />
+				<Text text="{ShipName}" />
+				<Label text="{i18n>detailShippingStreet}" />
+				<Text text="{ShipAddress}" />
+				<Label text="{i18n>detailShippingZIPCodeCity}" />
+				<Text text="{ShipPostalCode} {ShipCity}" />
+				<Label text="{i18n>detailShippingRegion}" />
+				<Text text="{ShipRegion}" />
+				<Label text="{i18n>detailShippingCountry}" />
+				<Text text="{ShipCountry}" />
+			</f:content>
+		</f:SimpleForm>
+	</VBox>
+</mvc:View>

+ 37 - 0
webapp/view/ViewSettingsDialog.fragment.xml

@@ -0,0 +1,37 @@
+<core:FragmentDefinition
+	xmlns="sap.m"
+	xmlns:core="sap.ui.core">
+	<ViewSettingsDialog
+		id="viewSettingsDialog"
+		confirm=".onConfirmViewSettingsDialog">
+		<filterItems>
+			<ViewSettingsFilterItem
+				id="filterItems"
+				text="{i18n>filterName}"
+				key="Orders"
+				multiSelect="false">
+				<items>
+					<ViewSettingsItem
+						id="viewFilter1"
+						text="{i18n>masterFilterShipped}"
+						key="Shipped"/>
+					<ViewSettingsItem
+						id="viewFilter2"
+						text="{i18n>masterFilterNotShipped}"
+						key="NotShipped"/>
+				</items>
+			</ViewSettingsFilterItem>
+		</filterItems>
+		<groupItems>
+			<ViewSettingsItem
+				text="{i18n>masterGroupCustomer}"
+				key="CompanyName"/>
+			<ViewSettingsItem
+				text="{i18n>masterGroupOrderPeriod}"
+				key="OrderDate"/>
+			<ViewSettingsItem
+				text="{i18n>masterGroupShippedPeriod}"
+				key="ShippedDate"/>
+		</groupItems>
+	</ViewSettingsDialog>
+</core:FragmentDefinition>