├── .gitignore ├── bs-config.json ├── .editorconfig ├── .gitattributes ├── src ├── view │ ├── App.view.xml │ ├── NotFound.view.xml │ ├── DetailObjectNotFound.view.xml │ ├── DetailNoObjectsAvailable.view.xml │ ├── ViewSettingsDialog.fragment.xml │ ├── Detail.view.xml │ └── Master.view.xml ├── test │ ├── unit │ │ ├── allTests.js │ │ ├── model │ │ │ ├── formatter.js │ │ │ ├── GroupSortState.js │ │ │ ├── models.js │ │ │ └── grouper.js │ │ ├── unitTests.qunit.html │ │ └── controller │ │ │ ├── App.controller.js │ │ │ └── ListSelector.js │ ├── testsuite.qunit.html │ ├── integration │ │ ├── opaTests.qunit.html │ │ ├── opaTestsPhone.qunit.html │ │ ├── BusyJourneyPhone.js │ │ ├── BusyJourney.js │ │ ├── PhoneJourneys.js │ │ ├── AllJourneys.js │ │ ├── NavigationJourneyPhone.js │ │ ├── NotFoundJourneyPhone.js │ │ ├── NotFoundJourney.js │ │ ├── pages │ │ │ ├── App.js │ │ │ ├── Browser.js │ │ │ ├── Common.js │ │ │ ├── NotFound.js │ │ │ ├── Detail.js │ │ │ └── Master.js │ │ ├── NavigationJourney.js │ │ └── MasterJourney.js │ └── mockServer.html ├── model │ ├── models.ts │ ├── formatter.ts │ ├── grouper.ts │ └── GroupSortState.ts ├── test.html ├── controller │ ├── App.controller.ts │ ├── ErrorHandler.ts │ ├── BaseController.ts │ ├── ListSelector.ts │ ├── Detail.controller.ts │ └── Master.controller.js ├── index.html ├── Component.ts ├── localService │ ├── mockserver.ts │ ├── mockdata │ │ ├── Objects.json │ │ └── LineItems.json │ └── metadata.xml ├── i18n │ └── i18n.properties └── manifest.json ├── .vscode └── settings.json ├── package.json ├── tsconfig.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /resources 3 | *.js 4 | *.js.map 5 | -------------------------------------------------------------------------------- /bs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8000, 3 | "server": { 4 | "baseDir": "src", 5 | "routes": { 6 | "/node_modules": "node_modules", 7 | "/resources": "resources" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [{src}/**.{html,xml,css,json,js,ts,properties}] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | tab_width = 4 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.html text eol=lf 4 | *.xml text eol=lf 5 | *.css text eol=lf 6 | *.json text eol=lf 7 | *.js text eol=lf 8 | *.ts text eol=lf 9 | *.properties text eol=lf 10 | 11 | *.png binary 12 | *.ico binary 13 | -------------------------------------------------------------------------------- /src/view/App.view.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/test/unit/allTests.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "test/unit/model/models", 3 | "test/unit/model/formatter", 4 | "test/unit/controller/App.controller", 5 | "test/unit/controller/ListSelector", 6 | "test/unit/model/grouper", 7 | "test/unit/model/GroupSortState" 8 | ], function() { 9 | "use strict"; 10 | }); -------------------------------------------------------------------------------- /src/model/models.ts: -------------------------------------------------------------------------------- 1 | import JSONModel from "sap/ui/model/json/JSONModel"; 2 | import Device from "sap/ui/Device"; 3 | 4 | export default { 5 | createDeviceModel(): JSONModel { 6 | //TODO|ui5ts: generate constructors 7 | var oModel = new JSONModel(Device); 8 | oModel.setDefaultBindingMode(sap.ui.model.BindingMode.OneWay); 9 | return oModel; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "editor.insertSpaces": true, 4 | "editor.detectIndentation": false, 5 | "files.exclude": { 6 | "**/.git": true, 7 | "**/*.js.map": true, 8 | "**/*.js": { 9 | "when": "$(basename).ts" 10 | }, 11 | "node_modules": true, 12 | "resources": true 13 | }, 14 | "ui5ts.manifestlocation": "src/manifest.json" 15 | } -------------------------------------------------------------------------------- /src/model/formatter.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * Rounds the currency value to 2 digits 4 | * 5 | * @public 6 | * @param {string} sValue value to be formatted 7 | * @returns {string} formatted currency value with 2 digits 8 | */ 9 | currencyValue : function (sValue?: string): string { 10 | if (!sValue) { 11 | return ""; 12 | } 13 | 14 | return parseFloat(sValue).toFixed(2); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/view/NotFound.view.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/view/DetailObjectNotFound.view.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/view/DetailNoObjectsAvailable.view.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-typescript-example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "concurrently \"npm run build:watch\" \"npm run serve\"", 6 | "serve": "lite-server -c=bs-config.json", 7 | "build": "tsc -p tsconfig.json", 8 | "build:watch": "tsc -p tsconfig.json -w", 9 | "lint": "tslint ./src/**/*.ts -t verbose" 10 | }, 11 | "dependencies": { 12 | "ui5ts": "^0.3.0" 13 | }, 14 | "devDependencies": { 15 | "@types/es6-shim": "^0.31.35", 16 | "@types/jquery": "^2.0.47", 17 | "concurrently": "^3.2.0", 18 | "lite-server": "^2.3.0", 19 | "tslint": "^3.15.1", 20 | "typescript": "^2.5.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/testsuite.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | QUnit TestSuite for Master-Detail 5 | 6 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "amd", 5 | "experimentalDecorators": true, 6 | "alwaysStrict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": true, 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "sourceMap": true, 12 | "baseUrl": "./", 13 | "paths": { 14 | "typescript/example/ui5app/*": [ "./src/*" ], 15 | "sap/*": [ "./node_modules/ui5ts/exports/sap/*" ] 16 | } 17 | }, 18 | "files": [ 19 | "node_modules/ui5ts/ui5ts.d.ts", 20 | "node_modules/ui5ts/ui5-types/1.48/sap.d.ts", 21 | "node_modules/ui5ts/ui5-types/1.48/jQuery.d.ts" 22 | ], 23 | "include": [ 24 | "src/**/*", 25 | "node_modules/@types" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "resources", 30 | "**/*.spec.ts" 31 | ] 32 | } -------------------------------------------------------------------------------- /src/test/integration/opaTests.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Opa tests for Master-Detail 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /src/test/integration/opaTestsPhone.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Opa tests for Master-Detail 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /src/view/ViewSettingsDialog.fragment.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 15 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/test/integration/BusyJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit" 5 | ], function (opaTest) { 6 | "use strict"; 7 | 8 | QUnit.module("Phone busy indication"); 9 | 10 | opaTest("Should see a global busy indication while loading the metadata", function (Given, When, Then) { 11 | // Arrangements 12 | Given.iStartTheAppWithDelay("", 5000); 13 | 14 | //Actions 15 | When.onTheAppPage.iLookAtTheScreen(); 16 | 17 | // Assertions 18 | Then.onTheAppPage.iShouldSeeTheBusyIndicator(). 19 | and.iTeardownMyAppFrame(); 20 | }); 21 | 22 | opaTest("Should see a busy indication on the master after loading the metadata", function (Given, When, Then) { 23 | // Arrangements 24 | Given.iStartTheAppWithDelay("", 2000); 25 | 26 | //Actions 27 | When.onTheAppPage.iWaitUntilTheBusyIndicatorIsGone(); 28 | 29 | // Assertions 30 | Then.onTheMasterPage.iShouldSeeTheBusyIndicator(). 31 | and.iTeardownMyAppFrame(); 32 | }); 33 | 34 | }); -------------------------------------------------------------------------------- /src/test/integration/BusyJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit" 5 | ], function (opaTest) { 6 | "use strict"; 7 | 8 | QUnit.module("Desktop busy indication"); 9 | 10 | opaTest("Should see a global busy indication while loading the metadata", function (Given, When, Then) { 11 | // Arrangements 12 | Given.iStartTheAppWithDelay("", 5000); 13 | 14 | // Actions 15 | When.onTheAppPage.iLookAtTheScreen(); 16 | 17 | // Assertions 18 | Then.onTheAppPage.iShouldSeeTheBusyIndicator(). 19 | and.iTeardownMyAppFrame(); 20 | }); 21 | 22 | opaTest("Should see a busy indication on the master and detail after loading the metadata", function (Given, When, Then) { 23 | // Arrangements 24 | Given.iStartTheAppWithDelay("", 2000); 25 | 26 | // Actions 27 | When.onTheAppPage.iWaitUntilTheBusyIndicatorIsGone(); 28 | 29 | // Assertions 30 | Then.onTheMasterPage.iShouldSeeTheBusyIndicator(). 31 | and.theListHeaderDisplaysZeroHits(); 32 | Then.onTheDetailPage.iShouldSeeTheBusyIndicator(). 33 | and.iTeardownMyAppFrame(); 34 | }); 35 | 36 | }); -------------------------------------------------------------------------------- /src/test/integration/PhoneJourneys.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | jQuery.sap.require("sap.ui.qunit.qunit-css"); 4 | jQuery.sap.require("sap.ui.thirdparty.qunit"); 5 | jQuery.sap.require("sap.ui.qunit.qunit-junit"); 6 | QUnit.config.autostart = false; 7 | 8 | sap.ui.require([ 9 | "sap/ui/test/Opa5", 10 | "typescript/example/ui5app/test/integration/pages/Common", 11 | "sap/ui/test/opaQunit", 12 | "typescript/example/ui5app/test/integration/pages/App", 13 | "typescript/example/ui5app/test/integration/pages/Browser", 14 | "typescript/example/ui5app/test/integration/pages/Master", 15 | "typescript/example/ui5app/test/integration/pages/Detail", 16 | "typescript/example/ui5app/test/integration/pages/NotFound" 17 | ], function (Opa5, Common) { 18 | "use strict"; 19 | Opa5.extendConfig({ 20 | arrangements: new Common(), 21 | viewNamespace: "typescript.example.ui5app.view." 22 | }); 23 | 24 | sap.ui.require([ 25 | "typescript/example/ui5app/test/integration/NavigationJourneyPhone", 26 | "typescript/example/ui5app/test/integration/NotFoundJourneyPhone", 27 | "typescript/example/ui5app/test/integration/BusyJourneyPhone" 28 | ], function () { 29 | QUnit.start(); 30 | }); 31 | }); -------------------------------------------------------------------------------- /src/test/unit/model/formatter.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/m/Text", 5 | "typescript/example/ui5app/model/formatter" 6 | ], function (Text, formatter) { 7 | "use strict"; 8 | 9 | QUnit.module("formatter - Currency value"); 10 | 11 | function currencyValueTestCase(assert, sValue, fExpectedNumber) { 12 | // Act 13 | var fCurrency = formatter.currencyValue(sValue); 14 | 15 | // Assert 16 | assert.strictEqual(fCurrency, fExpectedNumber, "The rounding was correct"); 17 | } 18 | 19 | QUnit.test("Should round down a 3 digit number", function (assert) { 20 | currencyValueTestCase.call(this, assert, "3.123", "3.12"); 21 | }); 22 | 23 | QUnit.test("Should round up a 3 digit number", function (assert) { 24 | currencyValueTestCase.call(this, assert, "3.128", "3.13"); 25 | }); 26 | 27 | QUnit.test("Should round a negative number", function (assert) { 28 | currencyValueTestCase.call(this, assert, "-3", "-3.00"); 29 | }); 30 | 31 | QUnit.test("Should round an empty string", function (assert) { 32 | currencyValueTestCase.call(this, assert, "", ""); 33 | }); 34 | 35 | QUnit.test("Should round a zero", function (assert) { 36 | currencyValueTestCase.call(this, assert, "0", "0.00"); 37 | }); 38 | 39 | }); -------------------------------------------------------------------------------- /src/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Testing Overview 5 | 6 | 7 | 8 | 9 |

Testing Overview

10 |

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

11 | 12 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/unit/unitTests.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unit tests for Master-Detail 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 35 | 36 | 37 | 38 |
39 |
40 | 41 | -------------------------------------------------------------------------------- /src/controller/App.controller.ts: -------------------------------------------------------------------------------- 1 | import BaseController from "typescript/example/ui5app/controller/BaseController"; 2 | import JSONModel from "sap/ui/model/json/JSONModel"; 3 | 4 | @UI5("typescript.example.ui5app.controller.App") 5 | export default class App extends BaseController { 6 | 7 | public onInit(): void { 8 | var oViewModel: JSONModel, 9 | oComponent = this.getOwnerComponent(), 10 | oModel = oComponent.getModel(), 11 | fnSetAppNotBusy: () => void, 12 | oListSelector = oComponent.oListSelector, 13 | iOriginalBusyDelay = this.getView().getBusyIndicatorDelay(); 14 | 15 | oViewModel = new JSONModel({ 16 | busy : true, 17 | delay : 0 18 | }); 19 | this.setModel(oViewModel, "appView"); 20 | 21 | fnSetAppNotBusy = () => { 22 | oViewModel.setProperty("/busy", false); 23 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 24 | }; 25 | 26 | oModel.metadataLoaded() 27 | .then(fnSetAppNotBusy); 28 | 29 | // Makes sure that master view is hidden in split app 30 | // after a new list entry has been selected. 31 | oListSelector.attachListSelectionChange(() => { 32 | this.byId("idAppControl").hideMaster(); 33 | }, this); 34 | 35 | // apply content density mode to root view 36 | this.getView().addStyleClass(oComponent.getContentDensityClass()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/integration/AllJourneys.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | jQuery.sap.require("sap.ui.qunit.qunit-css"); 4 | jQuery.sap.require("sap.ui.thirdparty.qunit"); 5 | jQuery.sap.require("sap.ui.qunit.qunit-junit"); 6 | QUnit.config.autostart = false; 7 | 8 | // We cannot provide stable mock data out of the template. 9 | // If you introduce mock data, by adding .json files in your webapp/localService/mockdata folder you have to provide the following minimum data: 10 | // * At least 3 Objects in the list 11 | // * All 3 Objects have at least one LineItems 12 | 13 | sap.ui.require([ 14 | "sap/ui/test/Opa5", 15 | "typescript/example/ui5app/test/integration/pages/Common", 16 | "sap/ui/test/opaQunit", 17 | "typescript/example/ui5app/test/integration/pages/App", 18 | "typescript/example/ui5app/test/integration/pages/Browser", 19 | "typescript/example/ui5app/test/integration/pages/Master", 20 | "typescript/example/ui5app/test/integration/pages/Detail", 21 | "typescript/example/ui5app/test/integration/pages/NotFound" 22 | ], function (Opa5, Common) { 23 | "use strict"; 24 | Opa5.extendConfig({ 25 | arrangements: new Common(), 26 | viewNamespace: "typescript.example.ui5app.view." 27 | }); 28 | 29 | sap.ui.require([ 30 | "typescript/example/ui5app/test/integration/MasterJourney", 31 | "typescript/example/ui5app/test/integration/NavigationJourney", 32 | "typescript/example/ui5app/test/integration/NotFoundJourney", 33 | "typescript/example/ui5app/test/integration/BusyJourney" 34 | ], function () { 35 | QUnit.start(); 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/model/grouper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Use this file to implement your custom grouping functions 3 | * The predefined functions are simple examples and might be replaced by your more complex implementations 4 | * to be called with .bind() and handed over to a sap.ui.model.Sorter 5 | * return value for all your functions is an object with key-text pairs 6 | * the oContext parameter is not under your control! 7 | */ 8 | 9 | export default { 10 | 11 | /** 12 | * Groups the items by a price in two groups: Lesser equal than 20 and greater than 20 13 | * This grouping function needs the resource bundle so we pass it as a dependency 14 | * @param {sap.ui.model.resource.ResourceModel} oResourceBundle the resource bundle of your i18n model 15 | * @returns {Function} the grouper function you can pass to your sorter 16 | */ 17 | groupUnitNumber(oResourceBundle: sap.ui.model.resource.ResourceModel): (oContext: jQuery.sap.util.ResourceBundle) => { key: string, text: string } { 18 | return (oContext: jQuery.sap.util.ResourceBundle) => { 19 | var iPrice = oContext.getProperty("UnitNumber"), 20 | sKey, 21 | sText; 22 | 23 | if (iPrice <= 20) { 24 | sKey = "LE20"; 25 | sText = oResourceBundle.getText("masterGroup1Header1"); 26 | } else { 27 | sKey = "GT20"; 28 | sText = oResourceBundle.getText("masterGroup1Header2"); 29 | } 30 | 31 | return { 32 | key: sKey, 33 | text: sText 34 | }; 35 | }; 36 | } 37 | 38 | }; -------------------------------------------------------------------------------- /src/test/integration/NavigationJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit" 5 | ], function (opaTest) { 6 | "use strict"; 7 | 8 | QUnit.module("Phone navigation"); 9 | 10 | opaTest("Should see the objects list", function (Given, When, Then) { 11 | // Arrangements 12 | Given.iStartTheApp(); 13 | 14 | //Actions 15 | When.onTheMasterPage.iLookAtTheScreen(); 16 | 17 | // Assertions 18 | Then.onTheMasterPage.iShouldSeeTheList(); 19 | Then.onTheBrowserPage.iShouldSeeAnEmptyHash(); 20 | }); 21 | 22 | opaTest("Should react on hashchange", function (Given, When, Then) { 23 | // Actions 24 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(3); 25 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 26 | 27 | // Assertions 28 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(); 29 | }); 30 | 31 | opaTest("Detail Page Shows Object Details", function (Given, When, Then) { 32 | // Actions 33 | When.onTheDetailPage.iLookAtTheScreen(); 34 | 35 | // Assertions 36 | Then.onTheDetailPage.iShouldSeeTheObjectLineItemsList(). 37 | and.theLineItemsListShouldHaveTheCorrectNumberOfItems(). 38 | and.theLineItemsHeaderShouldDisplayTheAmountOfEntries(); 39 | }); 40 | 41 | opaTest("Should navigate on press", function (Given, When, Then) { 42 | // Actions 43 | When.onTheDetailPage.iPressTheBackButton(); 44 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(2). 45 | and.iPressOnTheObjectAtPosition(2); 46 | 47 | // Assertions 48 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(). 49 | and.iTeardownMyAppFrame(); 50 | }); 51 | 52 | }); -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Master-Detail 8 | 9 | 10 | 19 | 20 | 21 | 22 | 23 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /src/test/mockServer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Master-Detail 8 | 9 | 10 | 19 | 20 | 21 | 22 | 23 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/test/unit/model/GroupSortState.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "typescript/example/ui5app/model/GroupSortState", 5 | "sap/ui/model/json/JSONModel" 6 | ], function (GroupSortState, JSONModel) { 7 | "use strict"; 8 | 9 | QUnit.module("GroupSortState - grouping and sorting", { 10 | beforeEach: function () { 11 | this.oModel = new JSONModel({}); 12 | // System under test 13 | this.oGroupSortState = new GroupSortState(this.oModel, function() {}); 14 | } 15 | }); 16 | 17 | QUnit.test("Should always return a sorter when sorting", function (assert) { 18 | // Act + Assert 19 | assert.strictEqual(this.oGroupSortState.sort("UnitNumber").length, 1, "The sorting by UnitNumber returned a sorter"); 20 | assert.strictEqual(this.oGroupSortState.sort("Name").length, 1, "The sorting by Name returned a sorter"); 21 | }); 22 | 23 | QUnit.test("Should return a grouper when grouping", function (assert) { 24 | // Act + Assert 25 | assert.strictEqual(this.oGroupSortState.group("UnitNumber").length, 1, "The group by UnitNumber returned a sorter"); 26 | assert.strictEqual(this.oGroupSortState.group("None").length, 0, "The sorting by None returned no sorter"); 27 | }); 28 | 29 | 30 | QUnit.test("Should set the sorting to UnitNumber if the user groupes by UnitNumber", function (assert) { 31 | // Act + Assert 32 | this.oGroupSortState.group("UnitNumber"); 33 | assert.strictEqual(this.oModel.getProperty("/sortBy"), "UnitNumber", "The sorting is the same as the grouping"); 34 | }); 35 | 36 | QUnit.test("Should set the grouping to None if the user sorts by Name and there was a grouping before", function (assert) { 37 | // Arrange 38 | this.oModel.setProperty("/groupBy", "UnitNumber"); 39 | 40 | this.oGroupSortState.sort("Name"); 41 | 42 | // Assert 43 | assert.strictEqual(this.oModel.getProperty("/groupBy"), "None", "The grouping got reset"); 44 | }); 45 | }); -------------------------------------------------------------------------------- /src/test/unit/controller/App.controller.js: -------------------------------------------------------------------------------- 1 | /*global QUnit,sinon*/ 2 | 3 | sap.ui.define([ 4 | "typescript/example/ui5app/controller/App.controller", 5 | "sap/m/SplitApp", 6 | "sap/ui/core/Control", 7 | "sap/ui/model/json/JSONModel", 8 | "sap/ui/thirdparty/sinon", 9 | "sap/ui/thirdparty/sinon-qunit" 10 | ], function(AppController, SplitApp, Control, JSONModel) { 11 | "use strict"; 12 | 13 | QUnit.module("AppController - Hide master"); 14 | 15 | QUnit.test("Should hide the master of a SplitApp when selection in the list changes", function (assert) { 16 | // Arrange 17 | var fnOnSelectionChange, 18 | oViewStub = new Control(), 19 | oODataModelStub = new JSONModel(), 20 | oComponentStub = new Control(), 21 | oSplitApp = new SplitApp(), 22 | fnHideMasterSpy = sinon.spy(oSplitApp,"hideMaster"); 23 | 24 | oComponentStub.oListSelector = { 25 | attachListSelectionChange : function (fnFunctionToCall, oListener) { 26 | fnOnSelectionChange = fnFunctionToCall.bind(oListener); 27 | } 28 | }; 29 | oComponentStub.getContentDensityClass = jQuery.noop; 30 | 31 | oODataModelStub.metadataLoaded = function () { 32 | return { 33 | then : jQuery.noop 34 | }; 35 | }; 36 | oComponentStub.setModel(oODataModelStub); 37 | 38 | // System under Test 39 | var oAppController = new AppController(); 40 | 41 | this.stub(oAppController, "byId").withArgs("idAppControl").returns(oSplitApp); 42 | this.stub(oAppController, "getView").returns(oViewStub); 43 | this.stub(oAppController, "getOwnerComponent").returns(oComponentStub); 44 | 45 | // Act 46 | oAppController.onInit(); 47 | assert.ok(fnOnSelectionChange, "Did register to the change event of the ListSelector"); 48 | // Simulate the event of the list 49 | fnOnSelectionChange(); 50 | 51 | // Assert 52 | assert.strictEqual(fnHideMasterSpy.callCount, 1, "Did hide the master"); 53 | }); 54 | 55 | }); -------------------------------------------------------------------------------- /src/test/unit/model/models.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "typescript/example/ui5app/model/models", 5 | "sap/ui/thirdparty/sinon", 6 | "sap/ui/thirdparty/sinon-qunit" 7 | ], function (models) { 8 | "use strict"; 9 | 10 | QUnit.module("createDeviceModel", { 11 | afterEach : function () { 12 | this.oDeviceModel.destroy(); 13 | } 14 | }); 15 | 16 | function isPhoneTestCase(assert, bIsPhone) { 17 | // Arrange 18 | this.stub(sap.ui.Device, "system", { phone : bIsPhone }); 19 | 20 | // System under test 21 | this.oDeviceModel = models.createDeviceModel(); 22 | 23 | // Assert 24 | assert.strictEqual(this.oDeviceModel.getData().system.phone, bIsPhone, "IsPhone property is correct"); 25 | } 26 | 27 | QUnit.test("Should initialize a device model for desktop", function (assert) { 28 | isPhoneTestCase.call(this, assert, false); 29 | }); 30 | 31 | QUnit.test("Should initialize a device model for phone", function (assert) { 32 | isPhoneTestCase.call(this, assert, true); 33 | }); 34 | 35 | function isTouchTestCase(assert, bIsTouch) { 36 | // Arrange 37 | this.stub(sap.ui.Device, "support", { touch : bIsTouch }); 38 | 39 | // System under test 40 | this.oDeviceModel = models.createDeviceModel(); 41 | 42 | // Assert 43 | assert.strictEqual(this.oDeviceModel.getData().support.touch, bIsTouch, "IsTouch property is correct"); 44 | } 45 | 46 | QUnit.test("Should initialize a device model for non touch devices", function (assert) { 47 | isTouchTestCase.call(this, assert, false); 48 | }); 49 | 50 | QUnit.test("Should initialize a device model for touch devices", function (assert) { 51 | isTouchTestCase.call(this, assert, true); 52 | }); 53 | 54 | QUnit.test("The binding mode of the device model should be one way", function (assert) { 55 | 56 | // System under test 57 | this.oDeviceModel = models.createDeviceModel(); 58 | 59 | // Assert 60 | assert.strictEqual(this.oDeviceModel.getDefaultBindingMode(), "OneWay", "Binding mode is correct"); 61 | }); 62 | 63 | }); -------------------------------------------------------------------------------- /src/test/integration/NotFoundJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit" 5 | ], function (opaTest) { 6 | "use strict"; 7 | 8 | QUnit.module("Phone not found"); 9 | 10 | opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) { 11 | // Arrangements 12 | Given.iStartTheApp({ hash : "somethingThatDoesNotExist" }); 13 | 14 | // Actions 15 | When.onTheNotFoundPage.iLookAtTheScreen(); 16 | 17 | // Assertions 18 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 19 | and.theNotFoundPageShouldSayResourceNotFound(); 20 | }); 21 | 22 | opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) { 23 | // Actions 24 | When.onTheNotFoundPage.iPressTheBackButton("NotFound"); 25 | 26 | // Assertions 27 | Then.onTheMasterPage.iShouldSeeTheList(). 28 | and.iTeardownMyAppFrame(); 29 | }); 30 | 31 | opaTest("Should see the not found master and detail page if an invalid object id has been called", function (Given, When, Then) { 32 | // Arrangements 33 | Given.iStartTheApp({ hash : "/Objects/SomeInvalidObjectId" }); 34 | 35 | // Actions 36 | When.onTheNotFoundPage.iLookAtTheScreen(); 37 | 38 | // Assertions 39 | Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage(). 40 | and.theNotFoundPageShouldSayObjectNotFound(); 41 | }); 42 | 43 | opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) { 44 | // Actions 45 | When.onTheNotFoundPage.iPressTheBackButton("DetailObjectNotFound"); 46 | 47 | // Assertions 48 | Then.onTheMasterPage.iShouldSeeTheList(). 49 | and.iTeardownMyAppFrame(); 50 | }); 51 | 52 | opaTest("Should see the not found text for no search results", function (Given, When, Then) { 53 | // Arrangements 54 | Given.iStartTheApp(); 55 | 56 | // Actions 57 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 58 | 59 | // Assertions 60 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(). 61 | and.iTeardownMyAppFrame(); 62 | }); 63 | 64 | }); -------------------------------------------------------------------------------- /src/test/unit/model/grouper.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "typescript/example/ui5app/model/grouper", 5 | "sap/ui/model/resource/ResourceModel", 6 | "jquery.sap.global" 7 | ], function (Grouper, ResourceModel, $) { 8 | "use strict"; 9 | 10 | function createResourceModel () { 11 | return new ResourceModel({ 12 | bundleUrl : [$.sap.getModulePath("typescript.example.ui5app"), "i18n/i18n.properties"].join("/") 13 | }); 14 | } 15 | 16 | QUnit.module("Sorter - Grouping functions", { 17 | beforeEach : function () { 18 | this._oResourceModel = createResourceModel(); 19 | }, 20 | afterEach : function () { 21 | this._oResourceModel.destroy(); 22 | } 23 | }); 24 | 25 | function createContextObject(vValue) { 26 | return { 27 | getProperty : function () { 28 | return vValue; 29 | } 30 | }; 31 | } 32 | 33 | QUnit.test("Should group a price lesser equal 20", function (assert) { 34 | // Arrange 35 | var oContextObject = createContextObject(17.2), 36 | oGrouperReturn; 37 | 38 | // System under test 39 | var fnGroup = Grouper.groupUnitNumber(this._oResourceModel.getResourceBundle()); 40 | 41 | // Assert 42 | oGrouperReturn = fnGroup(oContextObject); 43 | assert.strictEqual(oGrouperReturn.key, "LE20", "The key is as expected for a low value"); 44 | assert.strictEqual(oGrouperReturn.text, this._oResourceModel.getResourceBundle().getText("masterGroup1Header1"), "The group header is as expected for a low value"); 45 | }); 46 | 47 | QUnit.test("Should group the price", function (assert) { 48 | // Arrange 49 | var oContextObject = createContextObject(55.5), 50 | oGrouperReturn; 51 | 52 | // System under test 53 | var fnGroup = Grouper.groupUnitNumber(this._oResourceModel.getResourceBundle()); 54 | 55 | // Assert 56 | oGrouperReturn = fnGroup(oContextObject); 57 | assert.strictEqual(oGrouperReturn.key, "GT20", "The key is as expected for a high value"); 58 | assert.strictEqual(oGrouperReturn.text, this._oResourceModel.getResourceBundle().getText("masterGroup1Header2"), "The group header is as expected for a high value"); 59 | }); 60 | 61 | }); -------------------------------------------------------------------------------- /src/test/integration/NotFoundJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit" 5 | ], function (opaTest) { 6 | "use strict"; 7 | 8 | QUnit.module("Desktop not found"); 9 | 10 | opaTest("Should see the resource not found page and no selection in the master list when navigating to an invalid hash", function (Given, When, Then) { 11 | //Arrangement 12 | Given.iStartTheApp(); 13 | 14 | //Actions 15 | When.onTheMasterPage.iWaitUntilTheListIsLoaded() 16 | .and.iWaitUntilTheFirstItemIsSelected(); 17 | When.onTheBrowserPage.iChangeTheHashToSomethingInvalid(); 18 | 19 | // Assertions 20 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 21 | and.theNotFoundPageShouldSayResourceNotFound(); 22 | Then.onTheMasterPage.theListShouldHaveNoSelection(). 23 | and.iTeardownMyAppFrame(); 24 | }); 25 | 26 | opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) { 27 | // Arrangements 28 | Given.iStartTheApp({ hash : "somethingThatDoesNotExist" }); 29 | 30 | // Actions 31 | When.onTheNotFoundPage.iLookAtTheScreen(); 32 | 33 | // Assertions 34 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 35 | and.theNotFoundPageShouldSayResourceNotFound(). 36 | and.iTeardownMyAppFrame(); 37 | }); 38 | 39 | opaTest("Should see the not found master and detail page if an invalid object id has been called", function (Given, When, Then) { 40 | // Arrangements 41 | Given.iStartTheApp({ hash : "/Objects/SomeInvalidObjectId" }); 42 | 43 | //Actions 44 | When.onTheNotFoundPage.iLookAtTheScreen(); 45 | 46 | // Assertions 47 | Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage(). 48 | and.theNotFoundPageShouldSayObjectNotFound(). 49 | and.iTeardownMyAppFrame(); 50 | }); 51 | 52 | opaTest("Should see the not found text for no search results", function (Given, When, Then) { 53 | // Arrangements 54 | Given.iStartTheApp(); 55 | 56 | //Actions 57 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 58 | 59 | // Assertions 60 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(). 61 | and.iTeardownMyAppFrame(); 62 | }); 63 | 64 | }); -------------------------------------------------------------------------------- /src/test/integration/pages/App.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "typescript/example/ui5app/test/integration/pages/Common", 4 | "sap/ui/test/matchers/PropertyStrictEquals" 5 | ], function(Opa5, Common, PropertyStrictEquals) { 6 | "use strict"; 7 | 8 | var sViewName = "App", 9 | sAppControl = "idAppControl"; 10 | 11 | Opa5.createPageObjects({ 12 | onTheAppPage : { 13 | baseClass : Common, 14 | 15 | actions : { 16 | 17 | iWaitUntilTheBusyIndicatorIsGone : function () { 18 | return this.waitFor({ 19 | id : sAppControl, 20 | viewName : sViewName, 21 | // inline-matcher directly as function 22 | matchers : function(oRootView) { 23 | // we set the view busy, so we need to query the parent of the app 24 | return oRootView.getParent().getBusy() === false; 25 | }, 26 | errorMessage : "The app is still busy." 27 | }); 28 | } 29 | 30 | }, 31 | 32 | assertions : { 33 | 34 | iShouldSeeTheBusyIndicator : function () { 35 | return this.waitFor({ 36 | id : sAppControl, 37 | viewName : sViewName, 38 | success : function (oRootView) { 39 | // we set the view busy, so we need to query the parent of the app 40 | Opa5.assert.ok(oRootView.getParent().getBusy(), "The app is busy"); 41 | }, 42 | errorMessage : "The app is not busy." 43 | }); 44 | }, 45 | 46 | iShouldSeeTheMessageBox : function () { 47 | return this.waitFor({ 48 | searchOpenDialogs: true, 49 | controlType: "sap.m.Dialog", 50 | matchers : new PropertyStrictEquals({ name: "type", value: "Message"}), 51 | success: function () { 52 | Opa5.assert.ok(true, "The correct MessageBox was shown"); 53 | } 54 | }); 55 | } 56 | 57 | } 58 | 59 | } 60 | 61 | }); 62 | 63 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui5-typescript-example 2 | A Master-Detail demo app (the same available in SAPUI5/OpenUI5 SDK) using TypeScript with npm ui5ts package. 3 | 4 | ## How to run 5 | 6 | ``` 7 | git clone https://github.com/lmcarreiro/ui5-typescript-example.git 8 | cd ui5-typescript-example 9 | npm install 10 | npm start 11 | ``` 12 | 13 | ## UI5 TypeScript example class 14 | 15 | ```typescript 16 | import UIComponent from "sap/ui/core/UIComponent"; 17 | import models from "typescript/example/ui5app/model/models"; 18 | 19 | @UI5("typescript.example.ui5app.Component") 20 | export default class Component extends UIComponent 21 | { 22 | public static metadata: any = { 23 | manifest : "json" 24 | }; 25 | 26 | public init(): void { 27 | // set the device model 28 | this.setModel(models.createDeviceModel(), "device"); 29 | // call the base component's init function and create the App view 30 | super.init(); 31 | // create the views based on the url/hash 32 | this.getRouter().initialize(); 33 | } 34 | } 35 | ``` 36 | 37 | ## Progress 38 | 39 | I've published an incomplete work, not all classes of this example app was converted to typescript yet. But it is running fine, without 40 | error, and the core idea of working ui5 with typescript is the npm ui5ts package, that already 41 | works. 42 | 43 | I hope that the classes in this example that is already converted to typescript are enough for you to understand how to do it, and start 44 | using typescript in your own ui5 projects. 45 | 46 | * [ ] /controller 47 | * [x] ~~App.controller.js~~ -> App.controller.ts 48 | * [x] ~~BaseController.js~~ -> BaseController.ts 49 | * [x] ~~Detail.controller.js~~ -> Detail.controller.ts 50 | * [x] ~~ErrorHandler.js~~ -> ErrorHandler.ts 51 | * [x] ~~ListSelector.js~~ -> ListSelector.ts 52 | * [ ] Master.controller.js 53 | * [x] /localService 54 | * [x] ~~mockserver.js~~ -> mockserver.ts 55 | * [x] /model 56 | * [x] ~~formatter.js~~ -> formatter.ts 57 | * [x] ~~grouper.js~~ -> grouper.ts 58 | * [x] ~~GroupSortState.js~~ -> GroupSortState.ts 59 | * [x] ~~models.js~~ -> models.ts 60 | * [ ] /test 61 | * [x] ~~Component.js~~ -> Component.ts 62 | 63 | 64 | ## References 65 | 66 | - ui5ts npm package: https://www.npmjs.com/package/ui5ts 67 | - ui5ts github repository: https://github.com/lmcarreiro/ui5ts 68 | - OpenUI5 Master-Detail template: https://openui5.hana.ondemand.com/#/topic/8ed9339f3a99418e82a02f0fb4b5d6b9 69 | - OpenUI5 Master-Detail template repository: https://github.com/SAP/openui5-masterdetail-app 70 | -------------------------------------------------------------------------------- /src/model/GroupSortState.ts: -------------------------------------------------------------------------------- 1 | import BaseObject from "sap/ui/base/Object"; 2 | import Sorter from "sap/ui/model/Sorter"; 3 | 4 | @UI5("typescript.example.ui5app.model.GroupSortState") 5 | export default class GroupSortState extends BaseObject 6 | { 7 | private _oViewModel: sap.ui.model.json.JSONModel; 8 | private _fnGroupFunction: Function; 9 | 10 | /** 11 | * Creates sorters and groupers for the master list. 12 | * Since grouping also means sorting, this class modifies the viewmodel. 13 | * If a user groups by a field, and there is a corresponding sort option, the option will be chosen. 14 | * If a user ungroups, the sorting will be reset to the default sorting. 15 | * @class 16 | * @public 17 | * @param {sap.ui.model.json.JSONModel} oViewModel the model of the current view 18 | * @param {function} fnGroupFunction the grouping function to be applied 19 | * @alias typescript.example.ui5app.model.GroupSortState 20 | */ 21 | constructor(oViewModel: sap.ui.model.json.JSONModel, fnGroupFunction: Function) { 22 | super(); 23 | this._oViewModel = oViewModel; 24 | this._fnGroupFunction = fnGroupFunction; 25 | } 26 | 27 | /** 28 | * Sorts by Name, or by UnitNumber 29 | * 30 | * @param {string} sKey - the key of the field used for grouping 31 | * @returns {sap.ui.model.Sorter[]} an array of sorters 32 | */ 33 | sort(sKey: string): sap.ui.model.Sorter[] { 34 | var sGroupedBy = this._oViewModel.getProperty("/groupBy"); 35 | 36 | if (sGroupedBy !== "None") { 37 | // If the list is grouped, remove the grouping since the user wants to sort by something different 38 | // Grouping only works if the list is primary sorted by the grouping - the first sorten contains a grouper function 39 | this._oViewModel.setProperty("/groupBy", "None"); 40 | } 41 | 42 | return [new Sorter(sKey, false)]; 43 | } 44 | 45 | /** 46 | * Groups by UnitNumber, or resets the grouping for the key "None" 47 | * 48 | * @param {string} sKey - the key of the field used for grouping 49 | * @returns {sap.ui.model.Sorter[]} an array of sorters 50 | */ 51 | group(sKey: string): sap.ui.model.Sorter[] { 52 | var aSorters = []; 53 | 54 | if (sKey === "UnitNumber") { 55 | // Grouping means sorting so we set the select to the same Entity used for grouping 56 | this._oViewModel.setProperty("/sortBy", "UnitNumber"); 57 | 58 | aSorters.push( 59 | new Sorter("UnitNumber", false, 60 | this._fnGroupFunction.bind(this)) 61 | ); 62 | } else if (sKey === "None") { 63 | // select the default sorting again 64 | this._oViewModel.setProperty("/sortBy", "Name"); 65 | } 66 | 67 | return aSorters; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/controller/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import UI5Object from "sap/ui/base/Object"; 2 | import MessageBox from "sap/m/MessageBox"; 3 | import MyUIComponent from "typescript/example/ui5app/Component"; 4 | 5 | @UI5("typescript.example.ui5app.controller.ErrorHandler") 6 | export default class ErrorHandler extends UI5Object 7 | { 8 | private _oResourceModel: sap.ui.model.resource.ResourceModel; 9 | private _oResourceBundle: typeof jQuery.sap.util.ResourceBundle; 10 | private _oComponent: MyUIComponent; 11 | private _oModel: sap.ui.model.odata.v2.ODataModel; 12 | private _bMessageOpen: boolean; 13 | private _sErrorText: string; 14 | 15 | /** 16 | * Handles application errors by automatically attaching to the model events and displaying errors when needed. 17 | * @class 18 | * @param {sap.ui.core.UIComponent} oComponent reference to the app's component 19 | * @public 20 | * @alias typescript.example.ui5app.controller.ErrorHandler 21 | */ 22 | public constructor(oComponent: MyUIComponent) { 23 | super(); 24 | this._oResourceModel = oComponent.getModel("i18n"); 25 | //TODO|ui5ts: check how to convert T|Promise into T 26 | this._oResourceBundle = this._oResourceModel.getResourceBundle(); 27 | this._oComponent = oComponent; 28 | this._oModel = oComponent.getModel(); 29 | this._bMessageOpen = false; 30 | this._sErrorText = this._oResourceBundle.getText("errorText"); 31 | 32 | this._oModel.attachMetadataFailed((oEvent: sap.ui.base.Event) => { 33 | var oParams = oEvent.getParameters(); 34 | this._showServiceError(oParams.response); 35 | }, this); 36 | 37 | this._oModel.attachRequestFailed((oEvent: sap.ui.base.Event) => { 38 | var oParams = oEvent.getParameters(); 39 | // An entity that was not found in the service is also throwing a 404 error in oData. 40 | // We already cover this case with a notFound target so we skip it here. 41 | // A request that cannot be sent to the server is a technical error that we have to handle though 42 | if (oParams.response.statusCode !== "404" || (oParams.response.statusCode === 404 && oParams.response.responseText.indexOf("Cannot POST") === 0)) { 43 | this._showServiceError(oParams.response); 44 | } 45 | }, this); 46 | } 47 | 48 | /** 49 | * Shows a {@link sap.m.MessageBox} when a service call has failed. 50 | * Only the first error message will be display. 51 | * @param {string} sDetails a technical error to be displayed on request 52 | * @private 53 | */ 54 | private _showServiceError(sDetails: string): void { 55 | if (this._bMessageOpen) { 56 | return; 57 | } 58 | this._bMessageOpen = true; 59 | MessageBox.error( 60 | this._sErrorText, 61 | { 62 | id : "serviceErrorMessageBox", 63 | details : sDetails, 64 | styleClass : this._oComponent.getContentDensityClass(), 65 | actions : [MessageBox.Action.CLOSE], 66 | onClose : () => { 67 | this._bMessageOpen = false; 68 | } 69 | } 70 | ); 71 | } 72 | } -------------------------------------------------------------------------------- /src/test/integration/NavigationJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit" 5 | ], function (opaTest) { 6 | "use strict"; 7 | 8 | QUnit.module("Desktop navigation"); 9 | 10 | opaTest("Should start the app with empty hash: the hash should reflect the selection of the first item in the list", function (Given, When, Then) { 11 | // Arrangements 12 | Given.iStartTheApp(); 13 | 14 | //Actions 15 | When.onTheMasterPage.iRememberTheSelectedItem(); 16 | 17 | // Assertions 18 | Then.onTheMasterPage.theFirstItemShouldBeSelected(); 19 | Then.onTheDetailPage.iShouldSeeTheRememberedObject().and.iShouldSeeNoBusyIndicator(); 20 | Then.onTheBrowserPage.iShouldSeeTheHashForTheRememberedObject(); 21 | }); 22 | 23 | opaTest("Should react on hashchange", function (Given, When, Then) { 24 | // Actions 25 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(2); 26 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 27 | 28 | // Assertions 29 | Then.onTheDetailPage.iShouldSeeTheRememberedObject().and.iShouldSeeNoBusyIndicator(); 30 | Then.onTheMasterPage.theRememberedListItemShouldBeSelected(); 31 | }); 32 | 33 | 34 | opaTest("Should navigate on press", function (Given, When, Then) { 35 | // Actions 36 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1). 37 | and.iPressOnTheObjectAtPosition(1); 38 | 39 | // Assertions 40 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(); 41 | }); 42 | 43 | opaTest("Detail Page Shows Object Details", function (Given, When, Then) { 44 | // Actions 45 | When.onTheDetailPage.iLookAtTheScreen(); 46 | 47 | // Assertions 48 | Then.onTheDetailPage.iShouldSeeTheObjectLineItemsList(). 49 | and.theDetailViewShouldContainOnlyFormattedUnitNumbers(). 50 | and.theLineItemsListShouldHaveTheCorrectNumberOfItems(). 51 | and.theLineItemsHeaderShouldDisplayTheAmountOfEntries(). 52 | and.theLineItemsTableShouldContainOnlyFormattedUnitNumbers(); 53 | 54 | }); 55 | 56 | 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) { 57 | //Actions 58 | When.onTheMasterPage.iRememberAnIdOfAnObjectThatsNotInTheList(); 59 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 60 | 61 | // Assertions 62 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(). 63 | and.iTeardownMyAppFrame(); 64 | }); 65 | 66 | opaTest("Start the App and simulate metadata error: MessageBox should be shown", function (Given, When, Then) { 67 | //Arrangement 68 | Given.iStartMyAppOnADesktopToTestErrorHandler("metadataError=true"); 69 | 70 | // Assertions 71 | Then.onTheAppPage.iShouldSeeTheMessageBox(). 72 | and.iTeardownMyAppFrame(); 73 | }); 74 | 75 | opaTest("Start the App and simulate bad request error: MessageBox should be shown", function (Given, When, Then) { 76 | //Arrangement 77 | Given.iStartMyAppOnADesktopToTestErrorHandler("errorType=serverError"); 78 | 79 | // Assertions 80 | Then.onTheAppPage.iShouldSeeTheMessageBox(). 81 | and.iTeardownMyAppFrame(); 82 | }); 83 | 84 | }); -------------------------------------------------------------------------------- /src/test/integration/pages/Browser.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "typescript/example/ui5app/test/integration/pages/Common" 4 | ], function(Opa5, Common) { 5 | "use strict"; 6 | 7 | Opa5.createPageObjects({ 8 | onTheBrowserPage : { 9 | baseClass : Common, 10 | 11 | actions : { 12 | 13 | iChangeTheHashToObjectN : function (iObjIndex) { 14 | return this.waitFor(this.createAWaitForAnEntitySet({ 15 | entitySet : "Objects", 16 | success : function (aEntitySet) { 17 | Opa5.getHashChanger().setHash("/Objects/" + aEntitySet[iObjIndex].ObjectID); 18 | } 19 | })); 20 | }, 21 | 22 | iChangeTheHashToTheRememberedItem : function () { 23 | return this.waitFor({ 24 | success : function () { 25 | var sObjectId = this.getContext().currentItem.id; 26 | Opa5.getHashChanger().setHash("/Objects/" + sObjectId); 27 | } 28 | }); 29 | }, 30 | 31 | iChangeTheHashToSomethingInvalid : function () { 32 | return this.waitFor({ 33 | success : function () { 34 | Opa5.getHashChanger().setHash("/somethingInvalid"); 35 | } 36 | }); 37 | } 38 | 39 | }, 40 | 41 | assertions : { 42 | 43 | iShouldSeeTheHashForObjectN : function (iObjIndex) { 44 | return this.waitFor(this.createAWaitForAnEntitySet({ 45 | entitySet : "Objects", 46 | success : function (aEntitySet) { 47 | var oHashChanger = Opa5.getHashChanger(), 48 | sHash = oHashChanger.getHash(); 49 | Opa5.assert.strictEqual(sHash, "Objects/" + aEntitySet[iObjIndex].ObjectID, "The Hash is not correct"); 50 | } 51 | })); 52 | }, 53 | 54 | iShouldSeeTheHashForTheRememberedObject : function () { 55 | return this.waitFor({ 56 | success : function () { 57 | var sObjectId = this.getContext().currentItem.id, 58 | oHashChanger = Opa5.getHashChanger(), 59 | sHash = oHashChanger.getHash(); 60 | Opa5.assert.strictEqual(sHash, "Objects/" + sObjectId, "The Hash is not correct"); 61 | } 62 | }); 63 | }, 64 | 65 | iShouldSeeAnEmptyHash : function () { 66 | return this.waitFor({ 67 | success : function () { 68 | var oHashChanger = Opa5.getHashChanger(), 69 | sHash = oHashChanger.getHash(); 70 | Opa5.assert.strictEqual(sHash, "", "The Hash should be empty"); 71 | }, 72 | errorMessage : "The Hash is not Correct!" 73 | }); 74 | } 75 | 76 | } 77 | 78 | } 79 | 80 | }); 81 | 82 | }); -------------------------------------------------------------------------------- /src/Component.ts: -------------------------------------------------------------------------------- 1 | import UIComponent from "sap/ui/core/UIComponent"; 2 | import Device from "sap/ui/Device"; 3 | import models from "typescript/example/ui5app/model/models"; 4 | import ListSelector from "typescript/example/ui5app/controller/ListSelector"; 5 | import ErrorHandler from "typescript/example/ui5app/controller/ErrorHandler"; 6 | 7 | @UI5("typescript.example.ui5app.Component") 8 | export default class Component extends UIComponent 9 | { 10 | public static metadata: any = { 11 | manifest : "json" 12 | }; 13 | 14 | public oListSelector: ListSelector; 15 | private _oErrorHandler: ErrorHandler; 16 | private _sContentDensityClass: string; 17 | 18 | /** 19 | * The component is initialized by UI5 automatically during the startup of the app and calls the init method once. 20 | * In this method, the device models are set and the router is initialized. 21 | * @public 22 | * @override 23 | */ 24 | public init(): void { 25 | this.oListSelector = new ListSelector(); 26 | this._oErrorHandler = new ErrorHandler(this); 27 | 28 | // set the device model 29 | this.setModel(models.createDeviceModel(), "device"); 30 | 31 | // call the base component's init function and create the App view 32 | super.init(); 33 | 34 | // create the views based on the url/hash 35 | this.getRouter().initialize(); 36 | } 37 | 38 | /** 39 | * The component is destroyed by UI5 automatically. 40 | * In this method, the ListSelector and ErrorHandler are destroyed. 41 | * @public 42 | * @override 43 | */ 44 | public destroy(bSuppressInvalidate: boolean): void { 45 | this.oListSelector.destroy(); 46 | this._oErrorHandler.destroy(); 47 | 48 | // call the base component's destroy function 49 | super.destroy(bSuppressInvalidate); 50 | } 51 | 52 | /** 53 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy 54 | * design mode class should be set, which influences the size appearance of some controls. 55 | * @public 56 | * @return {string} css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set 57 | */ 58 | public getContentDensityClass(): string { 59 | if (this._sContentDensityClass === undefined) { 60 | // check whether FLP has already set the content density class; do nothing in this case 61 | if (jQuery(document.body).hasClass("sapUiSizeCozy") || jQuery(document.body).hasClass("sapUiSizeCompact")) { 62 | this._sContentDensityClass = ""; 63 | } else if (!Device.support.touch) { // apply "compact" mode if touch is not supported 64 | this._sContentDensityClass = "sapUiSizeCompact"; 65 | } else { 66 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table 67 | this._sContentDensityClass = "sapUiSizeCozy"; 68 | } 69 | } 70 | return this._sContentDensityClass; 71 | } 72 | 73 | /** 74 | * Convenience method for getting the model. 75 | * @public 76 | * @override 77 | * @returns the model of the component 78 | */ 79 | public getModel(sName?: string): T { 80 | return super.getModel(sName); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/integration/pages/Common.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5" 3 | ], function(Opa5) { 4 | "use strict"; 5 | 6 | function getFrameUrl (sHash, sUrlParameters) { 7 | var sUrl = jQuery.sap.getResourcePath("typescript/example/ui5app/app", ".html"); 8 | sHash = sHash || ""; 9 | sUrlParameters = sUrlParameters ? "?" + sUrlParameters : ""; 10 | 11 | if (sHash) { 12 | sHash = "#" + (sHash.indexOf("/") === 0 ? sHash.substring(1) : sHash); 13 | } else { 14 | sHash = ""; 15 | } 16 | 17 | return sUrl + sUrlParameters + sHash; 18 | } 19 | 20 | return Opa5.extend("typescript.example.ui5app.test.integration.pages.Common", { 21 | 22 | iStartTheApp : function (oOptions) { 23 | oOptions = oOptions || {}; 24 | // Start the app with a minimal delay to make tests run fast but still async to discover basic timing issues 25 | this.iStartMyAppInAFrame(getFrameUrl(oOptions.hash, "serverDelay=50")); 26 | }, 27 | 28 | iStartTheAppWithDelay : function (sHash, iDelay) { 29 | this.iStartMyAppInAFrame(getFrameUrl(sHash, "serverDelay=" + iDelay)); 30 | }, 31 | 32 | iLookAtTheScreen : function () { 33 | return this; 34 | }, 35 | 36 | iStartMyAppOnADesktopToTestErrorHandler : function (sParam) { 37 | this.iStartMyAppInAFrame(getFrameUrl("", sParam)); 38 | }, 39 | 40 | createAWaitForAnEntitySet : function (oOptions) { 41 | return { 42 | success: function () { 43 | var bMockServerAvailable = false, 44 | aEntitySet; 45 | 46 | this.getMockServer().then(function (oMockServer) { 47 | aEntitySet = oMockServer.getEntitySetData(oOptions.entitySet); 48 | bMockServerAvailable = true; 49 | }); 50 | 51 | return this.waitFor({ 52 | check: function () { 53 | return bMockServerAvailable; 54 | }, 55 | success : function () { 56 | oOptions.success.call(this, aEntitySet); 57 | } 58 | }); 59 | } 60 | }; 61 | }, 62 | 63 | getMockServer : function () { 64 | return new Promise(function (success) { 65 | Opa5.getWindow().sap.ui.require(["typescript/example/ui5app/localService/mockserver"], function (mockserver) { 66 | success(mockserver.getMockServer()); 67 | }); 68 | }); 69 | }, 70 | 71 | theUnitNumbersShouldHaveTwoDecimals : function (sControlType, sViewName, sSuccessMsg, sErrMsg) { 72 | var rTwoDecimalPlaces = /^-?\d+\.\d{2}$/; 73 | 74 | return this.waitFor({ 75 | controlType : sControlType, 76 | viewName : sViewName, 77 | success : function (aNumberControls) { 78 | Opa5.assert.ok(aNumberControls.every(function(oNumberControl){ 79 | return rTwoDecimalPlaces.test(oNumberControl.getNumber()); 80 | }), 81 | sSuccessMsg); 82 | }, 83 | errorMessage : sErrMsg 84 | }); 85 | } 86 | 87 | }); 88 | 89 | }); -------------------------------------------------------------------------------- /src/localService/mockserver.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "sap/ui/core/util/MockServer"; 2 | 3 | var oMockServer: MockServer, 4 | _sAppModulePath = "typescript/example/ui5app/", 5 | _sJsonFilesModulePath = _sAppModulePath + "localService/mockdata"; 6 | 7 | export default { 8 | /** 9 | * Initializes the mock server. 10 | * You can configure the delay with the URL parameter "serverDelay". 11 | * The local mock data in this folder is returned instead of the real data for testing. 12 | * @public 13 | */ 14 | init(): void { 15 | //TODO|openui5: getUriParameters parameter must be optional 16 | var oUriParameters = jQuery.sap.getUriParameters(undefined), 17 | //TODO|openui5: getModulePath 2nd parameter must be optional 18 | sJsonFilesUrl = jQuery.sap.getModulePath(_sJsonFilesModulePath, undefined), 19 | sManifestUrl = jQuery.sap.getModulePath(_sAppModulePath + "manifest", ".json"), 20 | sEntity = "Objects", 21 | sErrorParam = oUriParameters.get("errorType"), 22 | iErrorCode = sErrorParam === "badRequest" ? 400 : 500, 23 | oManifest = (jQuery.sap.syncGetJSON(sManifestUrl, undefined)).data, 24 | oMainDataSource = oManifest["sap.app"].dataSources.mainService, 25 | sMetadataUrl = jQuery.sap.getModulePath(_sAppModulePath + oMainDataSource.settings.localUri.replace(".xml", ""), ".xml"), 26 | // ensure there is a trailing slash 27 | sMockServerUrl = /.*\/$/.test(oMainDataSource.uri) ? oMainDataSource.uri : oMainDataSource.uri + "/"; 28 | 29 | //TODO|ui5ts: generate constructors (with overloads when there is an optional parameter followed by a required one) 30 | oMockServer = new MockServer({ 31 | rootUri : sMockServerUrl 32 | }); 33 | 34 | // configure mock server with a delay of 1s 35 | MockServer.config({ 36 | autoRespond : true, 37 | autoRespondAfter : (oUriParameters.get("serverDelay") || 1000) 38 | }); 39 | 40 | oMockServer.simulate(sMetadataUrl, { 41 | sMockdataBaseUrl : sJsonFilesUrl, 42 | bGenerateMissingMockData : true 43 | }); 44 | 45 | var aRequests = oMockServer.getRequests(), 46 | fnResponse = function (iErrCode: number, sMessage: string, aRequest: any) { 47 | aRequest.response = function(oXhr: any){ 48 | oXhr.respond(iErrCode, {"Content-Type": "text/plain;charset=utf-8"}, sMessage); 49 | }; 50 | }; 51 | 52 | // handling the metadata error test 53 | if (oUriParameters.get("metadataError")) { 54 | aRequests.forEach( function ( aEntry ) { 55 | if (aEntry.path.toString().indexOf("$metadata") > -1) { 56 | fnResponse(500, "metadata Error", aEntry); 57 | } 58 | }); 59 | } 60 | 61 | // Handling request errors 62 | if (sErrorParam) { 63 | aRequests.forEach( function ( aEntry ) { 64 | if (aEntry.path.toString().indexOf(sEntity) > -1) { 65 | fnResponse(iErrorCode, sErrorParam, aEntry); 66 | } 67 | }); 68 | } 69 | oMockServer.start(); 70 | 71 | jQuery.sap.log.info("Running the app with mock data"); 72 | }, 73 | 74 | /** 75 | * @public returns the mockserver of the app, should be used in integration tests 76 | * @returns {sap.ui.core.util.MockServer} the mockserver instance 77 | */ 78 | getMockServer(): MockServer { 79 | return oMockServer; 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/controller/BaseController.ts: -------------------------------------------------------------------------------- 1 | /*global history */ 2 | import Controller from "sap/ui/core/mvc/Controller"; 3 | import History from "sap/ui/core/routing/History"; 4 | import MyUIComponent from "typescript/example/ui5app/Component"; 5 | 6 | @UI5("typescript.example.ui5app.controller.BaseController") 7 | export default class BaseController extends Controller 8 | { 9 | /** 10 | * Convenience method for accessing the router in every controller of the application. 11 | * @public 12 | * @returns {sap.ui.core.routing.Router} the router for this component 13 | */ 14 | public getRouter(): sap.ui.core.routing.Router { 15 | return (this.getOwnerComponent()).getRouter(); 16 | } 17 | 18 | /** 19 | * Convenience method for getting the view model by name in every controller of the application. 20 | * @public 21 | * @param {string} sName the model name 22 | * @returns {sap.ui.model.Model} the model instance 23 | */ 24 | public getModel(sName?: string): T { 25 | return this.getView().getModel(sName); 26 | } 27 | 28 | /** 29 | * Convenience method for setting the view model in every controller of the application. 30 | * @public 31 | * @param {sap.ui.model.Model} oModel the model instance 32 | * @param {string} sName the model name 33 | * @returns {sap.ui.core.mvc.View} the view instance 34 | */ 35 | public setModel(oModel: sap.ui.model.Model, sName?: string|undefined): sap.ui.core.mvc.View { 36 | let view = this.getView(); 37 | view.setModel(oModel, sName); 38 | return view; 39 | } 40 | 41 | /** 42 | * Convenience method for getting the resource bundle. 43 | * @public 44 | * @returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component 45 | */ 46 | public getResourceBundle(): typeof jQuery.sap.util.ResourceBundle { 47 | let resourceModel = this.getOwnerComponent().getModel("i18n"); 48 | //TODO: decide what to do when a method return T|Promise 49 | return resourceModel.getResourceBundle(); 50 | } 51 | 52 | /** 53 | * Convenience method for getting the typed owner component. 54 | * @public 55 | * @override 56 | * @returns {typescript.example.ui5app.Component} the owner component 57 | */ 58 | public getOwnerComponent(): MyUIComponent { 59 | return super.getOwnerComponent(); 60 | } 61 | 62 | /** 63 | * Convenience method for getting an typed element by Id. 64 | * @public 65 | * @override 66 | * @returns the element 67 | */ 68 | public byId(sId: string): T { 69 | return super.byId(sId); 70 | } 71 | 72 | /** 73 | * Event handler for navigating back. 74 | * It there is a history entry we go one step back in the browser history 75 | * If not, it will replace the current entry of the browser history with the master route. 76 | * @public 77 | */ 78 | public onNavBack(): void { 79 | var sPreviousHash = History.getInstance().getPreviousHash(); 80 | 81 | //TODO|ui5ts: History's getPreviousHash() method should return string|undefined instead of just string, like is said on the docs: 82 | // "gets the previous hash in the history - if the last direction was Unknown or there was no navigation yet, undefined will be returned" 83 | 84 | if (sPreviousHash !== undefined) { 85 | history.go(-1); 86 | } else { 87 | this.getRouter().navTo("master", {}, true); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/integration/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "sap/ui/test/matchers/PropertyStrictEquals", 5 | "typescript/example/ui5app/test/integration/pages/Common" 6 | ], function(Opa5, Press, PropertyStrictEquals, Common) { 7 | "use strict"; 8 | 9 | var sNotFoundPageId = "page", 10 | sNotFoundView = "NotFound", 11 | sDetailNotFoundView = "DetailObjectNotFound"; 12 | 13 | Opa5.createPageObjects({ 14 | onTheNotFoundPage : { 15 | baseClass : Common, 16 | 17 | actions : { 18 | 19 | iPressTheBackButton : function (sViewName) { 20 | return this.waitFor({ 21 | viewName : sViewName, 22 | controlType : "sap.m.Button", 23 | matchers: new PropertyStrictEquals({name : "type", value : "Back"}), 24 | actions : new Press(), 25 | errorMessage : "Did not find the back button" 26 | }); 27 | } 28 | 29 | }, 30 | 31 | assertions : { 32 | 33 | iShouldSeeTheNotFoundGeneralPage : function (sPageId, sPageViewName) { 34 | return this.waitFor({ 35 | controlType : "sap.m.MessagePage", 36 | viewName : sPageViewName, 37 | success : function () { 38 | Opa5.assert.ok(true, "Shows the message page"); 39 | }, 40 | errorMessage : "Did not reach the empty page" 41 | }); 42 | }, 43 | 44 | iShouldSeeTheNotFoundPage : function () { 45 | return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundPageId, sNotFoundView); 46 | }, 47 | 48 | iShouldSeeTheObjectNotFoundPage : function () { 49 | return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundPageId, sDetailNotFoundView); 50 | }, 51 | 52 | theNotFoundPageShouldSayResourceNotFound : function () { 53 | return this.waitFor({ 54 | id : sNotFoundPageId, 55 | viewName : sNotFoundView, 56 | success : function (oPage) { 57 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("notFoundTitle"), "The not found text is shown as title"); 58 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("notFoundText"), "The resource not found text is shown"); 59 | }, 60 | errorMessage : "Did not display the resource not found text" 61 | }); 62 | }, 63 | 64 | theNotFoundPageShouldSayObjectNotFound : function () { 65 | return this.waitFor({ 66 | id : sNotFoundPageId, 67 | viewName : sDetailNotFoundView, 68 | success : function (oPage) { 69 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("detailTitle"), "The object text is shown as title"); 70 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("noObjectFoundText"), "The object not found text is shown"); 71 | }, 72 | errorMessage : "Did not display the object not found text" 73 | }); 74 | } 75 | 76 | } 77 | 78 | } 79 | 80 | }); 81 | 82 | }); -------------------------------------------------------------------------------- /src/view/Detail.view.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | 24 | 25 | 28 | 29 | 33 | 34 | 38 | 39 | 40 | 41 | 49 | 50 | 51 | 54 | </Toolbar> 55 | </headerToolbar> 56 | <columns> 57 | <Column> 58 | <Text text="{i18n>detailLineItemTableIDColumn}"/> 59 | </Column> 60 | <Column 61 | minScreenWidth="Tablet" 62 | demandPopin="true" 63 | hAlign="End"> 64 | <Text text="{i18n>detailLineItemTableUnitNumberColumn}"/> 65 | </Column> 66 | </columns> 67 | <items> 68 | <ColumnListItem> 69 | <cells> 70 | <ObjectIdentifier 71 | title="{Name}" 72 | text="{LineItemID}"/> 73 | <ObjectNumber 74 | number="{ 75 | path: 'UnitNumber', 76 | formatter: '.formatter.currencyValue' 77 | }" 78 | unit="{UnitOfMeasure}"/> 79 | </cells> 80 | </ColumnListItem> 81 | </items> 82 | </Table> 83 | </semantic:content> 84 | 85 | <semantic:sendEmailAction> 86 | <semantic:SendEmailAction 87 | id="shareEmail" 88 | press="onShareEmailPress"/> 89 | </semantic:sendEmailAction> 90 | 91 | </semantic:DetailPage> 92 | 93 | </mvc:View> -------------------------------------------------------------------------------- /src/i18n/i18n.properties: -------------------------------------------------------------------------------- 1 | # This is the resource bundle for Master-Detail 2 | 3 | #XTIT: Application name 4 | appTitle=Master-Detail 5 | 6 | #YDES: Application description 7 | appDescription=Best-practice starting point for a master-detail app (standalone) 8 | 9 | #~~~ Master View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | #XTIT: Master view title with placeholder for the number of items 12 | masterTitleCount=<Objects> ({0}) 13 | 14 | #XTOL: Tooltip for the search field 15 | masterSearchTooltip=Enter an <Objects> name or a part of it. 16 | 17 | #XBLI: text for a list with no data 18 | masterListNoDataText=No <ObjectsPlural> are currently available 19 | 20 | #XBLI: text for a list with no data with filter or search 21 | masterListNoDataWithFilterOrSearchText=No matching <ObjectsPlural> found 22 | 23 | #XSEL: Option to sort the master list by Name 24 | masterSort1=Sort By <Name> 25 | 26 | #XSEL: Option to sort the master list by UnitNumber 27 | masterSort2=Sort By <UnitNumber> 28 | 29 | #XSEL: Option to filter the master list by UnitNumber 30 | masterFilterName=<UnitNumber> 31 | 32 | #XSEL: Option to not filter the master list 33 | masterFilterNone=none 34 | 35 | #XSEL: Option to filter the master list by UnitOfMeasure if the value is less than 100 36 | masterFilter1=<100 <UnitOfMeasure> 37 | 38 | #XSEL: Option to filter the master list by UnitOfMeasure if the value is greater than 100 39 | masterFilter2=>100 <UnitOfMeasure> 40 | 41 | #YMSG: Filter text that is displayed above the master list 42 | masterFilterBarText=Filtered by {0} 43 | 44 | #XSEL: Option to not group the master list 45 | masterGroupNone=(Not grouped) 46 | 47 | #XSEL: Option to group the master list by UnitNumber 48 | masterGroup1=<UnitNumber> Group 49 | 50 | #XGRP: Group header UnitNumber 51 | masterGroup1Header1=<UnitNumber> 20 or less 52 | 53 | #XGRP: Group header UnitNumber 54 | masterGroup1Header2=<UnitNumber> higher than 20 55 | 56 | #~~~ Detail View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | 58 | #XTIT: Detail view title 59 | detailTitle=<Objects> 60 | 61 | #XTOL: Icon Tab Bar Info 62 | detailIconTabBarInfo=Info 63 | 64 | #XTOL: Icon Tab Bar Attachments 65 | detailIconTabBarAttachments=Attachments 66 | 67 | #XBLI: Text for the LineItems table with no data 68 | detailLineItemTableNoDataText=No <LineItemsPlural> 69 | 70 | #XTIT: Title of the LineItems table 71 | detailLineItemTableHeading=<LineItemsPlural> 72 | 73 | #XTIT: Title of the LineItems table 74 | detailLineItemTableHeadingCount=<LineItemsPlural> ({0}) 75 | 76 | #XGRP: Title for the Name column in the LineItems table 77 | detailLineItemTableIDColumn=<FirstColumnName> 78 | 79 | #XGRP: Title for the UnitNumber column in the LineItems table 80 | detailLineItemTableUnitNumberColumn=<LastColumnName> 81 | 82 | #XTIT: Send E-Mail subject 83 | shareSendEmailObjectSubject=<Email subject including object identifier PLEASE REPLACE ACCORDING TO YOUR USE CASE> {0} 84 | 85 | #YMSG: Send E-Mail message 86 | shareSendEmailObjectMessage=<Email body PLEASE REPLACE ACCORDING TO YOUR USE CASE> {0} (id: {1})\r\n{2} 87 | 88 | #~~~ Not Found View ~~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | #XTIT: Not found view title 91 | notFoundTitle=Not Found 92 | 93 | #YMSG: The Objects not found text is displayed when there is no Objects with this id 94 | noObjectFoundText=This <Objects> is not available 95 | 96 | #YMSG: The Objects not available text is displayed when there is no data when starting the app 97 | noObjectsAvailableText=No <ObjectsPlural> are currently available 98 | 99 | #YMSG: The not found text is displayed when there was an error loading the resource (404 error) 100 | notFoundText=The requested resource was not found 101 | 102 | #~~~ Not Available View ~~~~~~~~~~~~~~~~~~~~~~~ 103 | 104 | #XTIT: Master view title 105 | notAvailableViewTitle=<Objects> 106 | 107 | #~~~ Error Handling ~~~~~~~~~~~~~~~~~~~~~~~ 108 | 109 | #YMSG: Error dialog description 110 | errorText=Sorry, a technical error occurred! Please try again later. -------------------------------------------------------------------------------- /src/view/Master.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="typescript.example.ui5app.controller.Master" 3 | xmlns:mvc="sap.ui.core.mvc" 4 | xmlns:core="sap.ui.core" 5 | xmlns="sap.m" 6 | xmlns:semantic="sap.m.semantic"> 7 | 8 | <semantic:MasterPage 9 | id="page" 10 | title="{masterView>/title}" 11 | navButtonPress="onNavBack" 12 | showNavButton="true"> 13 | <semantic:subHeader> 14 | <Bar id="headerBar"> 15 | <contentMiddle> 16 | <SearchField 17 | id="searchField" 18 | showRefreshButton="{= !${device>/support/touch} }" 19 | tooltip="{i18n>masterSearchTooltip}" 20 | width="100%" 21 | search="onSearch"> 22 | </SearchField> 23 | </contentMiddle> 24 | </Bar> 25 | </semantic:subHeader> 26 | 27 | <semantic:content> 28 | <PullToRefresh 29 | id="pullToRefresh" 30 | visible="{device>/support/touch}" 31 | refresh="onRefresh" /> 32 | <!-- For client side filtering add this to the items attribute: parameters: {operationMode: 'Client'}}" --> 33 | <List 34 | id="list" 35 | items="{ 36 | path: '/Objects', 37 | sorter: { 38 | path: 'Name', 39 | descending: false 40 | }, 41 | groupHeaderFactory: '.createGroupHeader' 42 | }" 43 | busyIndicatorDelay="{masterView>/delay}" 44 | noDataText="{masterView>/noDataText}" 45 | mode="{= ${device>/system/phone} ? 'None' : 'SingleSelectMaster'}" 46 | growing="true" 47 | growingScrollToLoad="true" 48 | updateFinished="onUpdateFinished" 49 | selectionChange="onSelectionChange"> 50 | <infoToolbar> 51 | <Toolbar 52 | active="true" 53 | id="filterBar" 54 | visible="{masterView>/isFilterBarVisible}" 55 | press="onOpenViewSettings"> 56 | <Title 57 | id="filterBarLabel" 58 | text="{masterView>/filterBarLabel}" /> 59 | </Toolbar> 60 | </infoToolbar> 61 | <items> 62 | <ObjectListItem 63 | type="{= ${device>/system/phone} ? 'Active' : 'Inactive'}" 64 | press="onSelectionChange" 65 | title="{Name}" 66 | number="{ 67 | path: 'UnitNumber', 68 | formatter: '.formatter.currencyValue' 69 | }" 70 | numberUnit="{UnitOfMeasure}"> 71 | </ObjectListItem> 72 | </items> 73 | </List> 74 | </semantic:content> 75 | 76 | <semantic:sort> 77 | <semantic:SortSelect 78 | id="sort" 79 | selectedKey="{masterView>/sortBy}" 80 | change="onSort"> 81 | <core:Item 82 | id="masterSort1" 83 | key="Name" 84 | text="{i18n>masterSort1}"/> 85 | <core:Item 86 | id="masterSort2" 87 | key="UnitNumber" 88 | text="{i18n>masterSort2}"/> 89 | </semantic:SortSelect> 90 | </semantic:sort> 91 | 92 | <semantic:filter> 93 | <semantic:FilterAction 94 | id="filter" 95 | press="onOpenViewSettings" /> 96 | </semantic:filter> 97 | 98 | <semantic:group> 99 | <semantic:GroupSelect 100 | id="group" 101 | selectedKey="{masterView>/groupBy}" 102 | change="onGroup"> 103 | <core:Item 104 | id="masterGroupNone" 105 | key="None" 106 | text="{i18n>masterGroupNone}"/> 107 | <core:Item 108 | id="masterGroup1" 109 | key="UnitNumber" 110 | text="{i18n>masterGroup1}"/> 111 | </semantic:GroupSelect> 112 | </semantic:group> 113 | 114 | </semantic:MasterPage> 115 | 116 | </mvc:View> -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "_version": "1.4.0", 3 | 4 | "sap.app": { 5 | "id": "typescript.example.ui5app", 6 | "type": "application", 7 | "i18n": "i18n/i18n.properties", 8 | "title": "{{appTitle}}", 9 | "description": "{{appDescription}}", 10 | 11 | "applicationVersion": { 12 | "version": "1.0.0" 13 | }, 14 | "dataSources": { 15 | "mainService": { 16 | "uri": "/here/goes/your/serviceUrl/", 17 | "type": "OData", 18 | "settings": { 19 | "odataVersion": "2.0", 20 | "localUri": "localService/metadata.xml" 21 | } 22 | } 23 | } 24 | }, 25 | 26 | "sap.ui": { 27 | "technology": "UI5", 28 | "icons": { 29 | "icon": "sap-icon://detail-view", 30 | "favIcon": "", 31 | "phone": "", 32 | "phone@2": "", 33 | "tablet": "", 34 | "tablet@2": "" 35 | }, 36 | "deviceTypes": { 37 | "desktop": true, 38 | "tablet": true, 39 | "phone": true 40 | }, 41 | "supportedThemes": [ 42 | "sap_hcb", 43 | "sap_belize" 44 | ] 45 | }, 46 | 47 | "sap.ui5": { 48 | "rootView": { 49 | "viewName": "typescript.example.ui5app.view.App", 50 | "type": "XML", 51 | "id": "app" 52 | }, 53 | 54 | "dependencies": { 55 | "minUI5Version": "1.42.0", 56 | "libs": { 57 | "sap.ui.core": { 58 | "minVersion": "1.42.0" 59 | }, 60 | "sap.m": { 61 | "minVersion": "1.42.0" 62 | } 63 | } 64 | }, 65 | 66 | "contentDensities": { 67 | "compact": true, 68 | "cozy": true 69 | }, 70 | 71 | "models": { 72 | "i18n": { 73 | "type": "sap.ui.model.resource.ResourceModel", 74 | "settings": { 75 | "bundleName": "typescript.example.ui5app.i18n.i18n" 76 | } 77 | }, 78 | "": { 79 | "dataSource": "mainService", 80 | "preload": true 81 | } 82 | }, 83 | 84 | "routing": { 85 | "config": { 86 | "routerClass": "sap.m.routing.Router", 87 | "viewType": "XML", 88 | "viewPath": "typescript.example.ui5app.view", 89 | "controlId": "idAppControl", 90 | "controlAggregation": "detailPages", 91 | "bypassed": { 92 | "target": [ 93 | "master", 94 | "notFound" 95 | ] 96 | }, 97 | "async": true 98 | }, 99 | 100 | "routes": [ 101 | { 102 | "pattern": "", 103 | "name": "master", 104 | "target": [ 105 | "object", 106 | "master" 107 | ] 108 | }, 109 | { 110 | "pattern": "Objects/{objectId}", 111 | "name": "object", 112 | "target": [ 113 | "master", 114 | "object" 115 | ] 116 | } 117 | ], 118 | 119 | "targets": { 120 | "master": { 121 | "viewName": "Master", 122 | "viewLevel": 1, 123 | "viewId": "master", 124 | "controlAggregation": "masterPages" 125 | }, 126 | "object": { 127 | "viewName": "Detail", 128 | "viewId": "detail", 129 | "viewLevel": 2 130 | }, 131 | "detailObjectNotFound": { 132 | "viewName": "DetailObjectNotFound", 133 | "viewId": "detailObjectNotFound" 134 | }, 135 | "detailNoObjectsAvailable": { 136 | "viewName": "DetailNoObjectsAvailable", 137 | "viewId": "detailNoObjectsAvailable" 138 | }, 139 | "notFound": { 140 | "viewName": "NotFound", 141 | "viewId": "notFound" 142 | } 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /src/localService/mockdata/Objects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ObjectID" : "ObjectID_1", 4 | "Name" : "Object 1", 5 | "Attribute1" : "Attribute A", 6 | "Attribute2" : "Attribute B", 7 | "UnitOfMeasure" : "UoM", 8 | "UnitNumber" : 81 9 | }, { 10 | "ObjectID" : "ObjectID_2", 11 | "Name" : "Object 2", 12 | "Attribute1" : "Attribute C", 13 | "Attribute2" : "Attribute D", 14 | "UnitOfMeasure" : "UoM", 15 | "UnitNumber" : 42 16 | }, { 17 | "ObjectID" : "ObjectID_3", 18 | "Name" : "Object 3", 19 | "Attribute1" : "Attribute E", 20 | "Attribute2" : "Attribute F", 21 | "UnitOfMeasure" : "UoM", 22 | "UnitNumber" : 27 23 | }, { 24 | "ObjectID" : "ObjectID_4", 25 | "Name" : "Object 4", 26 | "Attribute1" : "Attribute G", 27 | "Attribute2" : "Attribute H", 28 | "UnitOfMeasure" : "UoM", 29 | "UnitNumber" : 13 30 | }, { 31 | "ObjectID" : "ObjectID_5", 32 | "Name" : "Object 5", 33 | "Attribute1" : "Attribute I", 34 | "Attribute2" : "Attribute J", 35 | "UnitOfMeasure" : "UoM", 36 | "UnitNumber" : 121 37 | }, { 38 | "ObjectID" : "ObjectID_6", 39 | "Name" : "Object 6", 40 | "Attribute1" : "Attribute K", 41 | "Attribute2" : "Attribute L", 42 | "UnitOfMeasure" : "UoM", 43 | "UnitNumber" : 481 44 | }, { 45 | "ObjectID" : "ObjectID_7", 46 | "Name" : "Object 7", 47 | "Attribute1" : "Attribute M", 48 | "Attribute2" : "Attribute N", 49 | "UnitOfMeasure" : "UoM", 50 | "UnitNumber" : 63 51 | }, { 52 | "ObjectID" : "ObjectID_8", 53 | "Name" : "Object 8", 54 | "Attribute1" : "Attribute O", 55 | "Attribute2" : "Attribute P", 56 | "UnitOfMeasure" : "UoM", 57 | "UnitNumber" : 28 58 | }, { 59 | "ObjectID" : "ObjectID_9", 60 | "Name" : "Object 9", 61 | "Attribute1" : "Attribute Q", 62 | "Attribute2" : "Attribute R", 63 | "UnitOfMeasure" : "UoM", 64 | "UnitNumber" : 25 65 | }, { 66 | "ObjectID" : "ObjectID_10", 67 | "Name" : "Object 10", 68 | "Attribute1" : "Attribute S", 69 | "Attribute2" : "Attribute T", 70 | "UnitOfMeasure" : "UoM", 71 | "UnitNumber" : 106 72 | }, { 73 | "ObjectID" : "ObjectID_11", 74 | "Name" : "Object 11", 75 | "Attribute1" : "Attribute U", 76 | "Attribute2" : "Attribute V", 77 | "UnitOfMeasure" : "UoM", 78 | "UnitNumber" : 61 79 | }, { 80 | "ObjectID" : "ObjectID_12", 81 | "Name" : "Object 12", 82 | "Attribute1" : "Attribute W", 83 | "Attribute2" : "Attribute X", 84 | "UnitOfMeasure" : "UoM", 85 | "UnitNumber" : 3006 86 | }, { 87 | "ObjectID" : "ObjectID_13", 88 | "Name" : "Object 13", 89 | "Attribute1" : "Attribute Y", 90 | "Attribute2" : "Attribute Z", 91 | "UnitOfMeasure" : "UoM", 92 | "UnitNumber" : 256 93 | }, { 94 | "ObjectID" : "ObjectID_14", 95 | "Name" : "Object 14", 96 | "Attribute1" : "Attribute AA", 97 | "Attribute2" : "Attribute AB", 98 | "UnitOfMeasure" : "UoM", 99 | "UnitNumber" : 234 100 | }, { 101 | "ObjectID" : "ObjectID_15", 102 | "Name" : "Object 15", 103 | "Attribute1" : "Attribute AC", 104 | "Attribute2" : "Attribute AD", 105 | "UnitOfMeasure" : "UoM", 106 | "UnitNumber" : 34 107 | }, { 108 | "ObjectID" : "ObjectID_16", 109 | "Name" : "Object 16", 110 | "Attribute1" : "Attribute AE", 111 | "Attribute2" : "Attribute AF", 112 | "UnitOfMeasure" : "UoM", 113 | "UnitNumber" : -60 114 | }, { 115 | "ObjectID" : "ObjectID_17", 116 | "Name" : "Object 17", 117 | "Attribute1" : "Attribute AG", 118 | "Attribute2" : "Attribute AH", 119 | "UnitOfMeasure" : "UoM", 120 | "UnitNumber" : 3006 121 | }, { 122 | "ObjectID" : "ObjectID_18", 123 | "Name" : "Object 18", 124 | "Attribute1" : "Attribute AI", 125 | "Attribute2" : "Attribute AJ", 126 | "UnitOfMeasure" : "UoM", 127 | "UnitNumber" : 4587 128 | }, { 129 | "ObjectID" : "ObjectID_19", 130 | "Name" : "Object 19", 131 | "Attribute1" : "Attribute AK", 132 | "Attribute2" : "Attribute AL", 133 | "UnitOfMeasure" : "UoM", 134 | "UnitNumber" : 560 135 | }, { 136 | "ObjectID" : "ObjectID_20", 137 | "Name" : "Object 20", 138 | "Attribute1" : "Attribute AM", 139 | "Attribute2" : "Attribute AN", 140 | "UnitOfMeasure" : "UoM", 141 | "UnitNumber" : 4503 142 | }, { 143 | "ObjectID" : "ObjectID_21", 144 | "Name" : "Object 21", 145 | "Attribute1" : "Attribute AO", 146 | "Attribute2" : "Attribute AP", 147 | "UnitOfMeasure" : "UoM", 148 | "UnitNumber" : 403 149 | } 150 | ] -------------------------------------------------------------------------------- /src/test/integration/MasterJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit" 5 | ], function (opaTest) { 6 | "use strict"; 7 | 8 | QUnit.module("Master List"); 9 | 10 | opaTest("Should see the master list with all entries", function (Given, When, Then) { 11 | // Arrangements 12 | Given.iStartTheApp(); 13 | 14 | //Actions 15 | When.onTheMasterPage.iLookAtTheScreen(); 16 | 17 | // Assertions 18 | Then.onTheMasterPage.iShouldSeeTheList(). 19 | and.theListShouldHaveAllEntries(). 20 | and.theHeaderShouldDisplayAllEntries(). 21 | and.theListShouldContainOnlyFormattedUnitNumbers(); 22 | }); 23 | 24 | opaTest("Search for the First object should deliver results that contain the firstObject in the name", function (Given, When, Then) { 25 | //Actions 26 | When.onTheMasterPage.iSearchForTheFirstObject(); 27 | 28 | // Assertions 29 | Then.onTheMasterPage.theListShowsOnlyObjectsWithTheSearchStringInTheirTitle(); 30 | }); 31 | 32 | 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) { 33 | //Actions 34 | When.onTheMasterPage.iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh(); 35 | 36 | // Assertions 37 | Then.onTheMasterPage.theListHasEntries(); 38 | }); 39 | 40 | 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) { 41 | //Actions 42 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 43 | 44 | // Assertions 45 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(). 46 | and.theListHeaderDisplaysZeroHits(); 47 | }); 48 | 49 | opaTest("Should display items again if the searchfield is emptied", function (Given, When, Then) { 50 | //Actions 51 | When.onTheMasterPage.iClearTheSearch(); 52 | 53 | // Assertions 54 | Then.onTheMasterPage.theListShouldHaveAllEntries(); 55 | }); 56 | 57 | opaTest("MasterList Sorting on UnitNumber", function(Given, When, Then) { 58 | // Actions 59 | When.onTheMasterPage.iSortTheListOnUnitNumber(); 60 | 61 | // Assertions 62 | Then.onTheMasterPage.theListShouldBeSortedAscendingOnUnitNumber(); 63 | }); 64 | 65 | opaTest("MasterList Sorting on Name", function(Given, When, Then) { 66 | // Actions 67 | When.onTheMasterPage.iSortTheListOnName(); 68 | 69 | // Assertions 70 | Then.onTheMasterPage.theListShouldBeSortedAscendingOnName(); 71 | }); 72 | 73 | opaTest("MasterList Filtering on UnitNumber less than 100", function(Given, When, Then) { 74 | 75 | // Action 76 | When.onTheMasterPage.iOpenViewSettingsDialog(). 77 | and.iSelectListItemInViewSettingsDialog("<UnitNumber>"). 78 | and.iSelectListItemInViewSettingsDialog("<100 <UnitOfMeasure>"). 79 | and.iPressOKInViewSelectionDialog(); 80 | 81 | // Assertion 82 | Then.onTheMasterPage.theMasterListShouldBeFilteredOnUnitNumberValueLessThanTheGroupBoundary(); 83 | }); 84 | 85 | opaTest("MasterList Filtering on UnitNumber more than 100", function(Given, When, Then) { 86 | // Action 87 | When.onTheMasterPage.iOpenViewSettingsDialog(). 88 | and.iSelectListItemInViewSettingsDialog(">100 <UnitOfMeasure>"). 89 | and.iPressOKInViewSelectionDialog(); 90 | 91 | // Assertion 92 | Then.onTheMasterPage.theMasterListShouldBeFilteredOnUnitNumberValueMoreThanTheGroupBoundary(); 93 | }); 94 | 95 | opaTest("MasterList remove filter should display all items", function(Given, When, Then) { 96 | // Action 97 | When.onTheMasterPage.iOpenViewSettingsDialog(). 98 | and.iPressResetInViewSelectionDialog(). 99 | and.iPressOKInViewSelectionDialog(); 100 | 101 | // Assertion 102 | Then.onTheMasterPage.theListShouldHaveAllEntries(); 103 | }); 104 | 105 | opaTest("MasterList grouping created group headers", function(Given, When, Then) { 106 | // Action 107 | When.onTheMasterPage.iGroupTheList(); 108 | 109 | // Assertion 110 | Then.onTheMasterPage.theListShouldContainAGroupHeader(); 111 | }); 112 | 113 | opaTest("Remove grouping from MasterList delivers initial list", function(Given, When, Then) { 114 | // Action 115 | When.onTheMasterPage.iRemoveListGrouping(); 116 | 117 | // Assertion 118 | Then.onTheMasterPage.theListShouldNotContainGroupHeaders(). 119 | and.theListShouldHaveAllEntries(); 120 | }); 121 | 122 | opaTest("Grouping the master list and filtering it by the object identifier should deliver the initial list", function(Given, When, Then) { 123 | // Action 124 | When.onTheMasterPage.iGroupTheList(). 125 | and.iSortTheListOnName(); 126 | 127 | // Assertion 128 | Then.onTheMasterPage.theListShouldNotContainGroupHeaders(). 129 | and.iTeardownMyAppFrame(); 130 | }); 131 | 132 | }); -------------------------------------------------------------------------------- /src/localService/metadata.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <edmx:Edmx Version="1.0" 3 | xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx"> 4 | <edmx:DataServices m:DataServiceVersion="2.0" 5 | xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"> 6 | <Schema Namespace="Master_Detail" xml:lang="en" 7 | xmlns="http://schemas.microsoft.com/ado/2008/09/edm"> 8 | 9 | <EntityType Name="Object" sap:content-version="1" 10 | xmlns:sap="http://www.sap.com/Protocols/SAPData"> 11 | <Key> 12 | <PropertyRef Name="ObjectID" /> 13 | </Key> 14 | <Property Name="ObjectID" Type="Edm.String" Nullable="false" 15 | MaxLength="40" sap:label="Object ID" sap:creatable="false" 16 | sap:updatable="false" /> 17 | <Property Name="Name" Type="Edm.String" Nullable="false" 18 | MaxLength="255" sap:label="Name" sap:creatable="false" 19 | sap:updatable="false" /> 20 | <Property Name="Attribute1" Type="Edm.String" Nullable="false" 21 | MaxLength="40" sap:label="Attribute1" sap:creatable="false" 22 | sap:updatable="false" sap:sortable="false" /> 23 | <Property Name="Attribute2" Type="Edm.String" Nullable="false" 24 | MaxLength="40" sap:label="Attribute2" sap:creatable="false" 25 | sap:updatable="false" sap:sortable="false" /> 26 | <Property Name="UnitOfMeasure" Type="Edm.String" Nullable="false" 27 | MaxLength="3" sap:label="Unit of Measure" sap:creatable="false" 28 | sap:updatable="false" sap:sortable="false" sap:filterable="false" /> 29 | <Property Name="UnitNumber" Type="Edm.Decimal" Nullable="false" 30 | Precision="23" Scale="4" sap:label="Unit Number" sap:creatable="false" 31 | sap:updatable="false" sap:filterable="false" /> 32 | <NavigationProperty Name="LineItems" Relationship="Master_Detail.FK_Object_LineItems" FromRole="Objects" ToRole="LineItems" /> 33 | </EntityType> 34 | 35 | <EntityType Name="LineItem" sap:content-version="1" 36 | xmlns:sap="http://www.sap.com/Protocols/SAPData"> 37 | <Key> 38 | <PropertyRef Name="LineItemID" /> 39 | </Key> 40 | <Property Name="LineItemID" Type="Edm.String" Nullable="false" 41 | MaxLength="40" sap:creatable="false" sap:updatable="false" 42 | sap:sortable="false" sap:filterable="false" /> 43 | <Property Name="ObjectID" Type="Edm.String" Nullable="false" 44 | MaxLength="40" sap:label="Object ID" sap:creatable="false" 45 | sap:updatable="false" /> 46 | <Property Name="Name" Type="Edm.String" Nullable="false" 47 | sap:creatable="false" sap:updatable="false" sap:sortable="false" 48 | sap:filterable="false" /> 49 | <Property Name="Attribute" Type="Edm.String" Nullable="false" 50 | MaxLength="40" sap:creatable="false" sap:updatable="false" 51 | sap:sortable="false" sap:filterable="false" /> 52 | <Property Name="UnitOfMeasure" Type="Edm.String" Nullable="false" 53 | MaxLength="3" sap:label="Unit of Measure" sap:creatable="false" 54 | sap:updatable="false" sap:sortable="false" sap:filterable="false" /> 55 | <Property Name="UnitNumber" Type="Edm.Decimal" Nullable="false" 56 | Precision="23" Scale="4" sap:label="Unit Number" sap:creatable="false" 57 | sap:updatable="false" sap:filterable="false" /> 58 | <NavigationProperty Name="Objects" Relationship="Master_Detail.FK_Object_LineItems" FromRole="LineItems" ToRole="Objects" /> 59 | </EntityType> 60 | 61 | <Association Name="FK_Object_LineItems"> 62 | <End Role="LineItems" Type="Master_Detail.LineItem" Multiplicity="*" /> 63 | <End Role="Objects" Type="Master_Detail.Object" Multiplicity="1" /> 64 | <ReferentialConstraint> 65 | <Principal Role="Objects"> 66 | <PropertyRef Name="ObjectID" /> 67 | </Principal> 68 | <Dependent Role="LineItems"> 69 | <PropertyRef Name="ObjectID" /> 70 | </Dependent> 71 | </ReferentialConstraint> 72 | </Association> 73 | 74 | <EntityContainer Name="Master_Detail_ENTITIES" 75 | m:IsDefaultEntityContainer="true"> 76 | <EntitySet Name="Objects" EntityType="Master_Detail.Object" 77 | sap:creatable="false" sap:updatable="false" sap:deletable="false" 78 | sap:pageable="false" sap:content-version="1" 79 | xmlns:sap="http://www.sap.com/Protocols/SAPData" /> 80 | <EntitySet Name="LineItems" 81 | EntityType="Master_Detail.LineItem" 82 | sap:creatable="false" sap:updatable="false" sap:deletable="false" 83 | sap:pageable="false" sap:content-version="1" 84 | xmlns:sap="http://www.sap.com/Protocols/SAPData" /> 85 | <AssociationSet Name="Master_Detail.FK_Object_LineItems" Association="Master_Detail.FK_Object_LineItems"> 86 | <End Role="LineItems" EntitySet="LineItems" /> 87 | <End Role="Objects" EntitySet="Objects" /> 88 | </AssociationSet> 89 | </EntityContainer> 90 | </Schema> 91 | </edmx:DataServices> 92 | </edmx:Edmx> -------------------------------------------------------------------------------- /src/test/unit/controller/ListSelector.js: -------------------------------------------------------------------------------- 1 | /*global QUnit,sinon*/ 2 | 3 | sap.ui.define([ 4 | "typescript/example/ui5app/controller/ListSelector", 5 | "sap/ui/thirdparty/sinon", 6 | "sap/ui/thirdparty/sinon-qunit" 7 | ], function(ListSelector) { 8 | "use strict"; 9 | 10 | QUnit.module("Initialization", { 11 | beforeEach : function () { 12 | sinon.config.useFakeTimers = false; 13 | this.oListSelector = new ListSelector(); 14 | }, 15 | afterEach : function () { 16 | this.oListSelector.destroy(); 17 | } 18 | }); 19 | 20 | QUnit.test("Should initialize the List loading promise", function (assert) { 21 | // Arrange 22 | var done = assert.async(), 23 | fnRejectSpy = this.spy(), 24 | fnResolveSpy = this.spy(); 25 | 26 | // Act 27 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 28 | 29 | // Assert 30 | setTimeout(function () { 31 | assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise"); 32 | assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise"); 33 | done(); 34 | }, 0); 35 | }); 36 | 37 | QUnit.module("List loading", { 38 | beforeEach : function () { 39 | sinon.config.useFakeTimers = false; 40 | this.oListSelector = new ListSelector(); 41 | }, 42 | afterEach : function () { 43 | this.oListSelector.destroy(); 44 | } 45 | }); 46 | 47 | function createListStub (bCreateListItem, sBindingPath) { 48 | var fnGetParameter = function () { 49 | return true; 50 | }, 51 | oDataStub = { 52 | getParameter : fnGetParameter 53 | }, 54 | fnAttachEventOnce = function (sEventName, oData, fnCallback) { 55 | fnCallback(oDataStub); 56 | }, 57 | fnGetBinding = this.stub().returns({ 58 | attachEventOnce : fnAttachEventOnce 59 | }), 60 | fnAttachEvent = function (sEventName, fnCallback, oContext) { 61 | fnCallback.apply(oContext); 62 | }, 63 | oListItemStub = { 64 | getBindingContext : this.stub().returns({ 65 | getPath : this.stub().returns(sBindingPath) 66 | }) 67 | }, 68 | aListItems = []; 69 | 70 | if (bCreateListItem) { 71 | aListItems.push(oListItemStub); 72 | } 73 | 74 | return { 75 | attachEvent : fnAttachEvent, 76 | attachEventOnce : fnAttachEventOnce, 77 | getBinding : fnGetBinding, 78 | getItems : this.stub().returns(aListItems) 79 | }; 80 | } 81 | 82 | QUnit.test("Should resolve the list loading promise, if the list has items", function (assert) { 83 | // Arrange 84 | var done = assert.async(), 85 | fnRejectSpy = this.spy(), 86 | fnResolveSpy = function (sBindingPath) { 87 | // Assert 88 | assert.strictEqual(sBindingPath, sBindingPath, "Did pass the binding path"); 89 | assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise"); 90 | done(); 91 | }; 92 | 93 | // Act 94 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 95 | this.oListSelector.setBoundMasterList(createListStub.call(this, true, "anything")); 96 | }); 97 | 98 | QUnit.test("Should reject the list loading promise, if the list has no items", function (assert) { 99 | // Arrange 100 | var done = assert.async(), 101 | fnResolveSpy = this.spy(), 102 | fnRejectSpy = function () { 103 | // Assert 104 | assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise"); 105 | done(); 106 | }; 107 | 108 | // Act 109 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 110 | this.oListSelector.setBoundMasterList(createListStub.call(this, false)); 111 | }); 112 | 113 | QUnit.module("Selecting item in the list", { 114 | beforeEach : function () { 115 | sinon.config.useFakeTimers = false; 116 | this.oListSelector = new ListSelector(); 117 | this.oListSelector.oWhenListLoadingIsDone = { 118 | then : function (fnAct) { 119 | this.fnAct = fnAct; 120 | }.bind(this) 121 | }; 122 | }, 123 | afterEach : function () { 124 | this.oListSelector.destroy(); 125 | } 126 | }); 127 | 128 | function createStubbedListItem (sBindingPath) { 129 | return { 130 | getBindingContext : this.stub().returns({ 131 | getPath : this.stub().returns(sBindingPath) 132 | }) 133 | }; 134 | } 135 | 136 | QUnit.test("Should select an Item of the list when it is loaded and the binding contexts match", function (assert) { 137 | // Arrange 138 | var sBindingPath = "anything", 139 | oListItemToSelect = createStubbedListItem.call(this, sBindingPath), 140 | oSelectedListItemStub = createStubbedListItem.call(this, "a different binding path"); 141 | 142 | this.oListSelector._oList = { 143 | getMode : this.stub().returns("SingleSelectMaster"), 144 | getSelectedItem : this.stub().returns(oSelectedListItemStub), 145 | getItems : this.stub().returns([ oSelectedListItemStub, oListItemToSelect, createListStub.call(this, "yet another list binding") ]), 146 | setSelectedItem : function (oItem) { 147 | //Assert 148 | assert.strictEqual(oItem, oListItemToSelect, "Did select the list item with a matching binding context"); 149 | } 150 | }; 151 | 152 | // Act 153 | this.oListSelector.selectAListItem(sBindingPath); 154 | // Resolve list loading 155 | this.fnAct(); 156 | }); 157 | 158 | QUnit.test("Should not select an Item of the list when it is already selected", function (assert) { 159 | // Arrange 160 | var sBindingPath = "anything", 161 | oSelectedListItemStub = createStubbedListItem.call(this, sBindingPath); 162 | 163 | this.oListSelector._oList = { 164 | getMode: this.stub().returns("SingleSelectMaster"), 165 | getSelectedItem : this.stub().returns(oSelectedListItemStub) 166 | }; 167 | 168 | // Act 169 | this.oListSelector.selectAListItem(sBindingPath); 170 | // Resolve list loading 171 | this.fnAct(); 172 | 173 | // Assert 174 | assert.ok(true, "did not fail"); 175 | }); 176 | 177 | QUnit.test("Should not select an item of the list when the list has the selection mode none", function (assert) { 178 | // Arrange 179 | var sBindingPath = "anything"; 180 | 181 | this.oListSelector._oList = { 182 | getMode : this.stub().returns("None") 183 | }; 184 | 185 | // Act 186 | this.oListSelector.selectAListItem(sBindingPath); 187 | // Resolve list loading 188 | this.fnAct(); 189 | 190 | // Assert 191 | assert.ok(true, "did not fail"); 192 | }); 193 | 194 | }); -------------------------------------------------------------------------------- /src/controller/ListSelector.ts: -------------------------------------------------------------------------------- 1 | import BaseObject from "sap/ui/base/Object"; 2 | 3 | @UI5("typescript.example.ui5app.model.ListSelector") 4 | export default class ListSelector extends BaseObject 5 | { 6 | private _oList: sap.m.List 7 | private _fnResolveListHasBeenSet: (oList: sap.m.List) => void; 8 | private _oWhenListHasBeenSet: Promise<sap.m.List>; 9 | private oWhenListLoadingIsDone: Promise<{ list: sap.m.List, firstListitem: sap.m.ListItemBase }>; 10 | 11 | /** 12 | * 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 13 | * function. 14 | * @class 15 | * @public 16 | * @alias typescript.example.ui5app.model.ListSelector 17 | */ 18 | constructor() { 19 | super(); 20 | this._oWhenListHasBeenSet = new Promise((fnResolveListHasBeenSet: (oList: sap.m.List) => void) => { 21 | this._fnResolveListHasBeenSet = fnResolveListHasBeenSet; 22 | }); 23 | // This promise needs to be created in the constructor, since it is allowed to 24 | // invoke selectItem functions before calling setBoundMasterList 25 | this.oWhenListLoadingIsDone = new Promise((fnResolve, fnReject) => { 26 | // Used to wait until the setBound masterList function is invoked 27 | this._oWhenListHasBeenSet 28 | .then((oList: sap.m.List) => { 29 | oList.getBinding("items").attachEventOnce("dataReceived", 30 | (oEvent: sap.ui.base.Event) => { 31 | if (!oEvent.getParameter("data")) { 32 | fnReject({ 33 | list : oList, 34 | error : true 35 | }); 36 | } 37 | var oFirstListItem = oList.getItems()[0]; 38 | if (oFirstListItem) { 39 | // Have to make sure that first list Item is selected 40 | // and a select event is triggered. Like that, the corresponding 41 | // detail page is loaded automatically 42 | fnResolve({ 43 | list : oList, 44 | firstListitem : oFirstListItem 45 | }); 46 | } else { 47 | // No items in the list 48 | fnReject({ 49 | list : oList, 50 | error : false 51 | }); 52 | } 53 | } 54 | ); 55 | }); 56 | }); 57 | } 58 | 59 | /** 60 | * A bound list should be passed in here. Should be done, before the list has received its initial data from the server. 61 | * May only be invoked once per ListSelector instance. 62 | * @param {sap.m.List} oList The list all the select functions will be invoked on. 63 | * @public 64 | */ 65 | public setBoundMasterList(oList: sap.m.List): void { 66 | this._oList = oList; 67 | this._fnResolveListHasBeenSet(oList); 68 | } 69 | 70 | 71 | /** 72 | * 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, 73 | * no selection/scrolling will happen 74 | * @param {string} sBindingPath the binding path matching the binding path of a list item 75 | * @public 76 | */ 77 | public selectAListItem(sBindingPath: string): void { 78 | 79 | this.oWhenListLoadingIsDone.then( 80 | () => { 81 | var oList = this._oList, 82 | oSelectedItem: sap.m.ListItemBase; 83 | 84 | if (oList.getMode() === sap.m.ListMode.None) { 85 | return; 86 | } 87 | 88 | oSelectedItem = oList.getSelectedItem(); 89 | 90 | //TODO|openui5: getPath's argument must be optional 91 | // skip update if the current selection is already matching the object path 92 | if (oSelectedItem && oSelectedItem.getBindingContext().getPath("") === sBindingPath) { 93 | return; 94 | } 95 | 96 | oList.getItems().some((oItem: sap.m.ListItemBase) => { 97 | //TODO|openui5: getPath's argument must be optional 98 | if (oItem.getBindingContext() && oItem.getBindingContext().getPath("") === sBindingPath) { 99 | oList.setSelectedItem(oItem, true); 100 | return true; 101 | } 102 | return false; 103 | }); 104 | }, 105 | () => jQuery.sap.log.warning("Could not select the list item with the path" + sBindingPath + " because the list encountered an error or had no items") 106 | ); 107 | } 108 | 109 | 110 | /* =========================================================== */ 111 | /* Convenience Functions for List Selection Change Event */ 112 | /* =========================================================== */ 113 | 114 | /** 115 | * Attaches a listener and listener function to the ListSelector's bound master list. By using 116 | * a promise, the listener is added, even if the list is not available when 'attachListSelectionChange' 117 | * is called. 118 | * @param {function} fnFunction the function to be executed when the list fires a selection change event 119 | * @param {function} oListener the listener object 120 | * @return {typescript.example.ui5app.model.ListSelector} the list selector object for method chaining 121 | * @public 122 | */ 123 | public attachListSelectionChange(fnFunction: Function, oListener: any): ListSelector { 124 | this._oWhenListHasBeenSet.then(() => { 125 | this._oList.attachSelectionChange(fnFunction, oListener); 126 | }); 127 | return this; 128 | } 129 | 130 | /** 131 | * Detaches a listener and listener function from the ListSelector's bound master list. By using 132 | * a promise, the listener is removed, even if the list is not available when 'detachListSelectionChange' 133 | * is called. 134 | * @param {function} fnFunction the function to be executed when the list fires a selection change event 135 | * @param {function} oListener the listener object 136 | * @return {typescript.example.ui5app.model.ListSelector} the list selector object for method chaining 137 | * @public 138 | */ 139 | public detachListSelectionChange(fnFunction: Function, oListener: any): ListSelector { 140 | this._oWhenListHasBeenSet.then(() => { 141 | this._oList.detachSelectionChange(fnFunction, oListener); 142 | }); 143 | return this; 144 | } 145 | 146 | /** 147 | * Removes all selections from master list. 148 | * Does not trigger 'selectionChange' event on master list, though. 149 | * @public 150 | */ 151 | public clearMasterListSelection(): void { 152 | //use promise to make sure that 'this._oList' is available 153 | this._oWhenListHasBeenSet.then(() => { 154 | this._oList.removeSelections(true); 155 | }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/controller/Detail.controller.ts: -------------------------------------------------------------------------------- 1 | import BaseController from "typescript/example/ui5app/controller/BaseController"; 2 | import JSONModel from "sap/ui/model/json/JSONModel"; 3 | import formatter from "typescript/example/ui5app/model/formatter"; 4 | 5 | @UI5("typescript.example.ui5app.controller.Detail") 6 | export default class Detail extends BaseController 7 | { 8 | public formatter = formatter; 9 | 10 | /* =========================================================== */ 11 | /* lifecycle methods */ 12 | /* =========================================================== */ 13 | 14 | onInit() { 15 | // Model used to manipulate control states. The chosen values make sure, 16 | // detail page is busy indication immediately so there is no break in 17 | // between the busy indication for loading the view's meta data 18 | var oViewModel = new JSONModel({ 19 | busy : false, 20 | delay : 0, 21 | lineItemListTitle : this.getResourceBundle().getText("detailLineItemTableHeading") 22 | }); 23 | 24 | this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this); 25 | 26 | this.setModel(oViewModel, "detailView"); 27 | 28 | this.getOwnerComponent().getModel<sap.ui.model.odata.v2.ODataModel>().metadataLoaded().then(this._onMetadataLoaded.bind(this)); 29 | } 30 | 31 | /* =========================================================== */ 32 | /* event handlers */ 33 | /* =========================================================== */ 34 | 35 | /** 36 | * Event handler when the share by E-Mail button has been clicked 37 | * @public 38 | */ 39 | onShareEmailPress() { 40 | var oViewModel = this.getModel<sap.ui.model.json.JSONModel>("detailView"); 41 | 42 | sap.m.URLHelper.triggerEmail( 43 | <string><any>null, 44 | oViewModel.getProperty("/shareSendEmailSubject"), 45 | oViewModel.getProperty("/shareSendEmailMessage") 46 | ); 47 | } 48 | 49 | 50 | /** 51 | * Updates the item count within the line item table's header 52 | * @param {object} oEvent an event containing the total number of items in the list 53 | * @private 54 | */ 55 | onListUpdateFinished(oEvent: sap.ui.base.Event) { 56 | var sTitle, 57 | iTotalItems = oEvent.getParameter("total"), 58 | oViewModel = this.getModel<sap.ui.model.json.JSONModel>("detailView"); 59 | 60 | // only update the counter if the length is final 61 | if ((<sap.ui.model.odata.ODataListBinding>this.byId<sap.m.Table>("lineItemsList").getBinding("items")).isLengthFinal()) { 62 | if (iTotalItems) { 63 | sTitle = this.getResourceBundle().getText("detailLineItemTableHeadingCount", [iTotalItems]); 64 | } else { 65 | //Display 'Line Items' instead of 'Line items (0)' 66 | sTitle = this.getResourceBundle().getText("detailLineItemTableHeading"); 67 | } 68 | oViewModel.setProperty("/lineItemListTitle", sTitle); 69 | } 70 | } 71 | 72 | /* =========================================================== */ 73 | /* begin: internal methods */ 74 | /* =========================================================== */ 75 | 76 | /** 77 | * Binds the view to the object path and expands the aggregated line items. 78 | * @function 79 | * @param {sap.ui.base.Event} oEvent pattern match event in route 'object' 80 | * @private 81 | */ 82 | private _onObjectMatched(oEvent: sap.ui.base.Event): void { 83 | var sObjectId = oEvent.getParameter("arguments").objectId; 84 | this.getModel<sap.ui.model.odata.v2.ODataModel>().metadataLoaded().then(() => { 85 | var sObjectPath = this.getModel<sap.ui.model.odata.v2.ODataModel>().createKey("Objects", { 86 | ObjectID : sObjectId 87 | }); 88 | this._bindView("/" + sObjectPath); 89 | }); 90 | } 91 | 92 | /** 93 | * Binds the view to the object path. Makes sure that detail view displays 94 | * a busy indicator while data for the corresponding element binding is loaded. 95 | * @function 96 | * @param {string} sObjectPath path to the object to be bound to the view. 97 | * @private 98 | */ 99 | private _bindView(sObjectPath: string): void { 100 | // Set busy indicator during view binding 101 | var oViewModel = this.getModel<sap.ui.model.json.JSONModel>("detailView"); 102 | 103 | // If the view was not bound yet its not busy, only if the binding requests data it is set to busy again 104 | oViewModel.setProperty("/busy", false); 105 | 106 | this.getView().bindElement({ 107 | path : sObjectPath, 108 | events: { 109 | change : this._onBindingChange.bind(this), 110 | dataRequested : function () { 111 | oViewModel.setProperty("/busy", true); 112 | }, 113 | dataReceived: function () { 114 | oViewModel.setProperty("/busy", false); 115 | } 116 | } 117 | }); 118 | } 119 | 120 | private _onBindingChange(): void { 121 | var oView = this.getView(), 122 | oElementBinding = <sap.ui.model.odata.v2.ODataContextBinding>oView.getElementBinding(<string><any>undefined); 123 | 124 | // No data for the binding 125 | //TODO|openui5: method getBoundContext() doesn't exists on the docs, but it do exists on the ui5 library code. 126 | if (!(<any>oElementBinding).getBoundContext()) { 127 | this.getRouter().getTargets().display("detailObjectNotFound"); 128 | // if object could not be found, the selection in the master list 129 | // does not make sense anymore. 130 | this.getOwnerComponent().oListSelector.clearMasterListSelection(); 131 | return; 132 | } 133 | 134 | //TODO|openui5: method getPath() doesn't exists on the docs, but it do exists on the ui5 library code. 135 | var sPath: string = (<any>oElementBinding).getPath(), 136 | oResourceBundle = this.getResourceBundle(), 137 | oObject = (<sap.ui.model.odata.v2.ODataModel>oView.getModel(undefined)).getObject(sPath), 138 | sObjectId = oObject.ObjectID, 139 | sObjectName = oObject.Name, 140 | oViewModel = this.getModel<sap.ui.model.json.JSONModel>("detailView"); 141 | 142 | this.getOwnerComponent().oListSelector.selectAListItem(sPath); 143 | 144 | oViewModel.setProperty("/shareSendEmailSubject", 145 | oResourceBundle.getText("shareSendEmailObjectSubject", [sObjectId])); 146 | oViewModel.setProperty("/shareSendEmailMessage", 147 | oResourceBundle.getText("shareSendEmailObjectMessage", [sObjectName, sObjectId, location.href])); 148 | } 149 | 150 | private _onMetadataLoaded(): void { 151 | // Store original busy indicator delay for the detail view 152 | var iOriginalViewBusyDelay = this.getView().getBusyIndicatorDelay(), 153 | oViewModel = this.getModel<sap.ui.model.json.JSONModel>("detailView"), 154 | oLineItemTable = this.byId<sap.m.Table>("lineItemsList"), 155 | iOriginalLineItemTableBusyDelay = oLineItemTable.getBusyIndicatorDelay(); 156 | 157 | // Make sure busy indicator is displayed immediately when 158 | // detail view is displayed for the first time 159 | oViewModel.setProperty("/delay", 0); 160 | oViewModel.setProperty("/lineItemTableDelay", 0); 161 | 162 | oLineItemTable.attachEventOnce("updateFinished", undefined, () => { 163 | // Restore original busy indicator delay for line item table 164 | oViewModel.setProperty("/lineItemTableDelay", iOriginalLineItemTableBusyDelay); 165 | }); 166 | 167 | // Binding the view will set it to not busy - so the view is always busy if it is not bound 168 | oViewModel.setProperty("/busy", true); 169 | // Restore original busy indicator delay for the detail view 170 | oViewModel.setProperty("/delay", iOriginalViewBusyDelay); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/test/integration/pages/Detail.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "typescript/example/ui5app/test/integration/pages/Common", 5 | "sap/ui/test/matchers/AggregationLengthEquals", 6 | "sap/ui/test/matchers/AggregationFilled", 7 | "sap/ui/test/matchers/PropertyStrictEquals" 8 | ], function(Opa5, Press, Common, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals) { 9 | "use strict"; 10 | 11 | var sViewName = "Detail"; 12 | 13 | Opa5.createPageObjects({ 14 | onTheDetailPage : { 15 | baseClass : Common, 16 | 17 | actions : { 18 | 19 | iPressTheBackButton : function () { 20 | return this.waitFor({ 21 | id : "page", 22 | viewName : sViewName, 23 | actions: new Press(), 24 | errorMessage : "Did not find the nav button on detail page" 25 | }); 26 | } 27 | 28 | }, 29 | 30 | assertions : { 31 | 32 | iShouldSeeTheBusyIndicator : function () { 33 | return this.waitFor({ 34 | id : "page", 35 | viewName : sViewName, 36 | success : function (oPage) { 37 | // we set the view busy, so we need to query the parent of the app 38 | Opa5.assert.ok(oPage.getBusy(), "The detail view is busy"); 39 | }, 40 | errorMessage : "The detail view is not busy." 41 | }); 42 | }, 43 | 44 | iShouldSeeNoBusyIndicator : function () { 45 | return this.waitFor({ 46 | id : "page", 47 | viewName : sViewName, 48 | matchers : function (oPage) { 49 | return !oPage.getBusy(); 50 | }, 51 | success : function (oPage) { 52 | // we set the view busy, so we need to query the parent of the app 53 | Opa5.assert.ok(!oPage.getBusy(), "The detail view is not busy"); 54 | }, 55 | errorMessage : "The detail view is busy." 56 | }); 57 | }, 58 | 59 | theObjectPageShowsTheFirstObject : function () { 60 | return this.iShouldBeOnTheObjectNPage(0); 61 | }, 62 | 63 | iShouldBeOnTheObjectNPage : function (iObjIndex) { 64 | return this.waitFor(this.createAWaitForAnEntitySet({ 65 | entitySet : "Objects", 66 | success : function (aEntitySet) { 67 | var sItemName = aEntitySet[iObjIndex].Name; 68 | 69 | this.waitFor({ 70 | controlType : "sap.m.ObjectHeader", 71 | viewName : sViewName, 72 | matchers : new PropertyStrictEquals({name : "title", value: aEntitySet[iObjIndex].Name}), 73 | success : function () { 74 | Opa5.assert.ok(true, "was on the first object page with the name " + sItemName); 75 | }, 76 | errorMessage : "First object is not shown" 77 | }); 78 | } 79 | })); 80 | }, 81 | 82 | iShouldSeeTheRememberedObject : function () { 83 | return this.waitFor({ 84 | success : function () { 85 | var sBindingPath = this.getContext().currentItem.bindingPath; 86 | this._waitForPageBindingPath(sBindingPath); 87 | } 88 | }); 89 | }, 90 | 91 | _waitForPageBindingPath : function (sBindingPath) { 92 | return this.waitFor({ 93 | id : "page", 94 | viewName : sViewName, 95 | matchers : function (oPage) { 96 | return oPage.getBindingContext() && oPage.getBindingContext().getPath() === sBindingPath; 97 | }, 98 | success : function (oPage) { 99 | Opa5.assert.strictEqual(oPage.getBindingContext().getPath(), sBindingPath, "was on the remembered detail page"); 100 | }, 101 | errorMessage : "Remembered object " + sBindingPath + " is not shown" 102 | }); 103 | }, 104 | 105 | iShouldSeeTheObjectLineItemsList : function () { 106 | return this.waitFor({ 107 | id : "lineItemsList", 108 | viewName : sViewName, 109 | success : function (oList) { 110 | Opa5.assert.ok(oList, "Found the line items list."); 111 | } 112 | }); 113 | }, 114 | 115 | theLineItemsListShouldHaveTheCorrectNumberOfItems : function () { 116 | return this.waitFor(this.createAWaitForAnEntitySet({ 117 | entitySet : "LineItems", 118 | success : function (aEntitySet) { 119 | 120 | return this.waitFor({ 121 | id : "lineItemsList", 122 | viewName : sViewName, 123 | matchers : new AggregationFilled({name : "items"}), 124 | check: function (oList) { 125 | 126 | var sObjectID = oList.getBindingContext().getProperty("ObjectID"); 127 | 128 | var iLength = aEntitySet.filter(function (oLineItem) { 129 | return oLineItem.ObjectID === sObjectID; 130 | }).length; 131 | 132 | return oList.getItems().length === iLength; 133 | }, 134 | success : function () { 135 | Opa5.assert.ok(true, "The list has the correct number of items"); 136 | }, 137 | errorMessage : "The list does not have the correct number of items." 138 | }); 139 | } 140 | })); 141 | }, 142 | 143 | theDetailViewShouldContainOnlyFormattedUnitNumbers : function () { 144 | return this.theUnitNumbersShouldHaveTwoDecimals("sap.m.ObjectHeader", 145 | sViewName, 146 | "Object header are properly formatted", 147 | "Object view has no entries which can be checked for their formatting"); 148 | }, 149 | 150 | theLineItemsTableShouldContainOnlyFormattedUnitNumbers : function () { 151 | return this.theUnitNumbersShouldHaveTwoDecimals("sap.m.ObjectNumber", 152 | sViewName, 153 | "Object numbers are properly formatted", 154 | "LineItmes Table has no entries which can be checked for their formatting"); 155 | }, 156 | 157 | theLineItemsHeaderShouldDisplayTheAmountOfEntries : function () { 158 | return this.waitFor({ 159 | id : "lineItemsList", 160 | viewName : sViewName, 161 | matchers : new AggregationFilled({name : "items"}), 162 | success : function (oList) { 163 | var iNumberOfItems = oList.getItems().length; 164 | return this.waitFor({ 165 | id : "lineItemsHeader", 166 | viewName : sViewName, 167 | matchers : new PropertyStrictEquals({name: "text", value: "<LineItemsPlural> (" + iNumberOfItems + ")"}), 168 | success : function () { 169 | Opa5.assert.ok(true, "The line item list displays " + iNumberOfItems + " items"); 170 | }, 171 | errorMessage : "The line item list does not display " + iNumberOfItems + " items." 172 | }); 173 | } 174 | }); 175 | }, 176 | 177 | iShouldSeeTheShareEmailButton : function () { 178 | return this.waitFor({ 179 | id : "shareEmail", 180 | viewName : sViewName, 181 | success : function () { 182 | Opa5.assert.ok(true, "The E-Mail button is visible"); 183 | }, 184 | errorMessage : "The E-Mail button was not found" 185 | }); 186 | } 187 | } 188 | 189 | } 190 | 191 | }); 192 | 193 | }); -------------------------------------------------------------------------------- /src/localService/mockdata/LineItems.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "LineItemID" : "LineItemID_1", 4 | "ObjectID": "ObjectID_1", 5 | "Name" : "Line Item 1", 6 | "Attribute" : "Attribute A", 7 | "UnitOfMeasure" : "UoM", 8 | "UnitNumber" : 23 9 | }, { 10 | "LineItemID" : "LineItemID_2", 11 | "ObjectID": "ObjectID_1", 12 | "Name" : "Line Item 2", 13 | "Attribute" : "Attribute B", 14 | "UnitOfMeasure" : "UoM", 15 | "UnitNumber" : 42 16 | }, { 17 | "LineItemID" : "LineItemID_3", 18 | "ObjectID": "ObjectID_1", 19 | "Name" : "Line Item 3", 20 | "Attribute" : "Attribute C", 21 | "UnitOfMeasure" : "UoM", 22 | "UnitNumber" : 27 23 | }, { 24 | "LineItemID" : "LineItemID_4", 25 | "ObjectID": "ObjectID_1", 26 | "Name" : "Line Item 4", 27 | "Attribute" : "Attribute D", 28 | "UnitOfMeasure" : "UoM", 29 | "UnitNumber" : 76 30 | }, { 31 | "LineItemID" : "LineItemID_5", 32 | "ObjectID": "ObjectID_2", 33 | "Name" : "Line Item 5", 34 | "Attribute" : "Attribute E", 35 | "UnitOfMeasure" : "UoM", 36 | "UnitNumber" : 568 37 | }, { 38 | "LineItemID" : "LineItemID_6", 39 | "ObjectID": "ObjectID_3", 40 | "Name" : "Line Item 6", 41 | "Attribute" : "Attribute F", 42 | "UnitOfMeasure" : "UoM", 43 | "UnitNumber" : 453 44 | }, { 45 | "LineItemID" : "LineItemID_7", 46 | "ObjectID": "ObjectID_3", 47 | "Name" : "Line Item 7", 48 | "Attribute" : "Attribute G", 49 | "UnitOfMeasure" : "UoM", 50 | "UnitNumber" : 736 51 | }, { 52 | "LineItemID" : "LineItemID_8", 53 | "ObjectID": "ObjectID_3", 54 | "Name" : "Line Item 8", 55 | "Attribute" : "Attribute H", 56 | "UnitOfMeasure" : "UoM", 57 | "UnitNumber" : 123 58 | }, { 59 | "LineItemID" : "LineItemID_9", 60 | "ObjectID": "ObjectID_4", 61 | "Name" : "Line Item 9", 62 | "Attribute" : "Attribute I", 63 | "UnitOfMeasure" : "UoM", 64 | "UnitNumber" : 13 65 | }, { 66 | "LineItemID" : "LineItemID_10", 67 | "ObjectID": "ObjectID_4", 68 | "Name" : "Line Item 10", 69 | "Attribute" : "Attribute J", 70 | "UnitOfMeasure" : "UoM", 71 | "UnitNumber" : 23 72 | }, { 73 | "LineItemID" : "LineItemID_11", 74 | "ObjectID": "ObjectID_5", 75 | "Name" : "Line Item 11", 76 | "Attribute" : "Attribute K", 77 | "UnitOfMeasure" : "UoM", 78 | "UnitNumber" : 23 79 | }, { 80 | "LineItemID" : "LineItemID_12", 81 | "ObjectID": "ObjectID_5", 82 | "Name" : "Line Item 12", 83 | "Attribute" : "Attribute L", 84 | "UnitOfMeasure" : "UoM", 85 | "UnitNumber" : 123 86 | }, { 87 | "LineItemID" : "LineItemID_13", 88 | "ObjectID": "ObjectID_5", 89 | "Name" : "Line Item 13", 90 | "Attribute" : "Attribute M", 91 | "UnitOfMeasure" : "UoM", 92 | "UnitNumber" : 43 93 | }, { 94 | "LineItemID" : "LineItemID_14", 95 | "ObjectID": "ObjectID_6", 96 | "Name" : "Line Item 14", 97 | "Attribute" : "Attribute N", 98 | "UnitOfMeasure" : "UoM", 99 | "UnitNumber" : 37 100 | }, { 101 | "LineItemID" : "LineItemID_15", 102 | "ObjectID": "ObjectID_6", 103 | "Name" : "Line Item 15", 104 | "Attribute" : "Attribute O", 105 | "UnitOfMeasure" : "UoM", 106 | "UnitNumber" : 599 107 | }, { 108 | "LineItemID" : "LineItemID_16", 109 | "ObjectID": "ObjectID_6", 110 | "Name" : "Line Item 16", 111 | "Attribute" : "Attribute P", 112 | "UnitOfMeasure" : "UoM", 113 | "UnitNumber" : 7 114 | }, { 115 | "LineItemID" : "LineItemID_17", 116 | "ObjectID": "ObjectID_7", 117 | "Name" : "Line Item 17", 118 | "Attribute" : "Attribute Q", 119 | "UnitOfMeasure" : "UoM", 120 | "UnitNumber" : 31 121 | }, { 122 | "LineItemID" : "LineItemID_18", 123 | "ObjectID": "ObjectID_7", 124 | "Name" : "Line Item 18", 125 | "Attribute" : "Attribute R", 126 | "UnitOfMeasure" : "UoM", 127 | "UnitNumber" : 82 128 | }, { 129 | "LineItemID" : "LineItemID_19", 130 | "ObjectID": "ObjectID_7", 131 | "Name" : "Line Item 19", 132 | "Attribute" : "Attribute S", 133 | "UnitOfMeasure" : "UoM", 134 | "UnitNumber" : 19 135 | }, { 136 | "LineItemID" : "LineItemID_20", 137 | "ObjectID": "ObjectID_8", 138 | "Name" : "Line Item 20", 139 | "Attribute" : "Attribute T", 140 | "UnitOfMeasure" : "UoM", 141 | "UnitNumber" : 429 142 | }, { 143 | "LineItemID" : "LineItemID_21", 144 | "ObjectID": "ObjectID_8", 145 | "Name" : "Line Item 21", 146 | "Attribute" : "Attribute U", 147 | "UnitOfMeasure" : "UoM", 148 | "UnitNumber" : 43 149 | }, { 150 | "LineItemID" : "LineItemID_22", 151 | "ObjectID": "ObjectID_9", 152 | "Name" : "Line Item 22", 153 | "Attribute" : "Attribute V", 154 | "UnitOfMeasure" : "UoM", 155 | "UnitNumber" : 237 156 | }, { 157 | "LineItemID" : "LineItemID_23", 158 | "ObjectID": "ObjectID_9", 159 | "Name" : "Line Item 23", 160 | "Attribute" : "Attribute W", 161 | "UnitOfMeasure" : "UoM", 162 | "UnitNumber" : 723 163 | }, { 164 | "LineItemID" : "LineItemID_24", 165 | "ObjectID": "ObjectID_9", 166 | "Name" : "Line Item 24", 167 | "Attribute" : "Attribute X", 168 | "UnitOfMeasure" : "UoM", 169 | "UnitNumber" : 427 170 | }, { 171 | "LineItemID" : "LineItemID_25", 172 | "ObjectID": "ObjectID_9", 173 | "Name" : "Line Item 25", 174 | "Attribute" : "Attribute Y", 175 | "UnitOfMeasure" : "UoM", 176 | "UnitNumber" : 13 177 | }, { 178 | "LineItemID" : "LineItemID_26", 179 | "ObjectID": "ObjectID_10", 180 | "Name" : "Line Item 26", 181 | "Attribute" : "Attribute Z", 182 | "UnitOfMeasure" : "UoM", 183 | "UnitNumber" : 178 184 | }, { 185 | "LineItemID" : "LineItemID_27", 186 | "ObjectID": "ObjectID_11", 187 | "Name" : "Line Item 227", 188 | "Attribute" : "Attribute AA", 189 | "UnitOfMeasure" : "UoM", 190 | "UnitNumber" : 457 191 | }, { 192 | "LineItemID" : "LineItemID_28", 193 | "ObjectID": "ObjectID_11", 194 | "Name" : "Line Item 28", 195 | "Attribute" : "Attribute AB", 196 | "UnitOfMeasure" : "UoM", 197 | "UnitNumber" : 395 198 | }, { 199 | "LineItemID" : "LineItemID_29", 200 | "ObjectID": "ObjectID_12", 201 | "Name" : "Line Item 29", 202 | "Attribute" : "Attribute AC", 203 | "UnitOfMeasure" : "UoM", 204 | "UnitNumber" : 1379 205 | }, { 206 | "LineItemID" : "LineItemID_30", 207 | "ObjectID": "ObjectID_13", 208 | "Name" : "Line Item 30", 209 | "Attribute" : "Attribute AD", 210 | "UnitOfMeasure" : "UoM", 211 | "UnitNumber" : 406 212 | }, { 213 | "LineItemID" : "LineItemID_31", 214 | "ObjectID": "ObjectID_13", 215 | "Name" : "Line Item 31", 216 | "Attribute" : "Attribute AD", 217 | "UnitOfMeasure" : "UoM", 218 | "UnitNumber" : 504 219 | }, { 220 | "LineItemID" : "LineItemID_32", 221 | "ObjectID": "ObjectID_13", 222 | "Name" : "Line Item 32", 223 | "Attribute" : "Attribute AF", 224 | "UnitOfMeasure" : "UoM", 225 | "UnitNumber" : 483 226 | }, { 227 | "LineItemID" : "LineItemID_33", 228 | "ObjectID": "ObjectID_14", 229 | "Name" : "Line Item 33", 230 | "Attribute" : "Attribute AG", 231 | "UnitOfMeasure" : "UoM", 232 | "UnitNumber" : 2005 233 | }, { 234 | "LineItemID" : "LineItemID_34", 235 | "ObjectID": "ObjectID_15", 236 | "Name" : "Line Item 34", 237 | "Attribute" : "Attribute AH", 238 | "UnitOfMeasure" : "UoM", 239 | "UnitNumber" : 5 240 | }, { 241 | "LineItemID" : "LineItemID_35", 242 | "ObjectID": "ObjectID_16", 243 | "Name" : "Line Item 35", 244 | "Attribute" : "Attribute AI", 245 | "UnitOfMeasure" : "UoM", 246 | "UnitNumber" : 462 247 | }, { 248 | "LineItemID" : "LineItemID_36", 249 | "ObjectID": "ObjectID_16", 250 | "Name" : "Line Item 36", 251 | "Attribute" : "Attribute AJ", 252 | "UnitOfMeasure" : "UoM", 253 | "UnitNumber" : 237 254 | }, { 255 | "LineItemID" : "LineItemID_37", 256 | "ObjectID": "ObjectID_17", 257 | "Name" : "Line Item 37", 258 | "Attribute" : "Attribute AK", 259 | "UnitOfMeasure" : "UoM", 260 | "UnitNumber" : 4023 261 | }, { 262 | "LineItemID" : "LineItemID_38", 263 | "ObjectID": "ObjectID_17", 264 | "Name" : "Line Item 38", 265 | "Attribute" : "Attribute AL", 266 | "UnitOfMeasure" : "UoM", 267 | "UnitNumber" : 42 268 | }, { 269 | "LineItemID" : "LineItemID_39", 270 | "ObjectID": "ObjectID_18", 271 | "Name" : "Line Item 39", 272 | "Attribute" : "Attribute AM", 273 | "UnitOfMeasure" : "UoM", 274 | "UnitNumber" : 52 275 | }, { 276 | "LineItemID" : "LineItemID_40", 277 | "ObjectID": "ObjectID_20", 278 | "Name" : "Line Item 40", 279 | "Attribute" : "Attribute AN", 280 | "UnitOfMeasure" : "UoM", 281 | "UnitNumber" : 3 282 | } 283 | ] -------------------------------------------------------------------------------- /src/controller/Master.controller.js: -------------------------------------------------------------------------------- 1 | /*global history */ 2 | sap.ui.define([ 3 | "typescript/example/ui5app/controller/BaseController", 4 | "sap/ui/model/json/JSONModel", 5 | "sap/ui/model/Filter", 6 | "sap/ui/model/FilterOperator", 7 | "sap/m/GroupHeaderListItem", 8 | "sap/ui/Device", 9 | "typescript/example/ui5app/model/formatter", 10 | "typescript/example/ui5app/model/grouper", 11 | "typescript/example/ui5app/model/GroupSortState" 12 | ], function (BaseController, JSONModel, Filter, FilterOperator, GroupHeaderListItem, Device, formatter, grouper, GroupSortState) { 13 | "use strict"; 14 | 15 | return BaseController.extend("typescript.example.ui5app.controller.Master", { 16 | 17 | formatter: formatter, 18 | 19 | /* =========================================================== */ 20 | /* lifecycle methods */ 21 | /* =========================================================== */ 22 | 23 | /** 24 | * Called when the master list controller is instantiated. It sets up the event handling for the master/detail communication and other lifecycle tasks. 25 | * @public 26 | */ 27 | onInit : function () { 28 | // Control state model 29 | var oList = this.byId("list"), 30 | oViewModel = this._createViewModel(), 31 | // Put down master list's original value for busy indicator delay, 32 | // so it can be restored later on. Busy handling on the master list is 33 | // taken care of by the master list itself. 34 | iOriginalBusyDelay = oList.getBusyIndicatorDelay(); 35 | 36 | this._oGroupSortState = new GroupSortState(oViewModel, grouper.groupUnitNumber(this.getResourceBundle())); 37 | 38 | this._oList = oList; 39 | // keeps the filter and search state 40 | this._oListFilterState = { 41 | aFilter : [], 42 | aSearch : [] 43 | }; 44 | 45 | this.setModel(oViewModel, "masterView"); 46 | // Make sure, busy indication is showing immediately so there is no 47 | // break after the busy indication for loading the view's meta data is 48 | // ended (see promise 'oWhenMetadataIsLoaded' in AppController) 49 | oList.attachEventOnce("updateFinished", undefined, function(){ 50 | // Restore original busy indicator delay for the list 51 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 52 | }); 53 | 54 | this.getView().addEventDelegate({ 55 | onBeforeFirstShow: function () { 56 | this.getOwnerComponent().oListSelector.setBoundMasterList(oList); 57 | }.bind(this) 58 | }); 59 | 60 | this.getRouter().getRoute("master").attachPatternMatched(this._onMasterMatched, this); 61 | this.getRouter().attachBypassed(this.onBypassed, this); 62 | }, 63 | 64 | /* =========================================================== */ 65 | /* event handlers */ 66 | /* =========================================================== */ 67 | 68 | /** 69 | * After list data is available, this handler method updates the 70 | * master list counter and hides the pull to refresh control, if 71 | * necessary. 72 | * @param {sap.ui.base.Event} oEvent the update finished event 73 | * @public 74 | */ 75 | onUpdateFinished : function (oEvent) { 76 | // update the master list object counter after new data is loaded 77 | this._updateListItemCount(oEvent.getParameter("total")); 78 | // hide pull to refresh if necessary 79 | this.byId("pullToRefresh").hide(); 80 | }, 81 | 82 | /** 83 | * Event handler for the master search field. Applies current 84 | * filter value and triggers a new search. If the search field's 85 | * 'refresh' button has been pressed, no new search is triggered 86 | * and the list binding is refresh instead. 87 | * @param {sap.ui.base.Event} oEvent the search event 88 | * @public 89 | */ 90 | onSearch : function (oEvent) { 91 | if (oEvent.getParameters().refreshButtonPressed) { 92 | // Search field's 'refresh' button has been pressed. 93 | // This is visible if you select any master list item. 94 | // In this case no new search is triggered, we only 95 | // refresh the list binding. 96 | this.onRefresh(); 97 | return; 98 | } 99 | 100 | var sQuery = oEvent.getParameter("query"); 101 | 102 | if (sQuery) { 103 | this._oListFilterState.aSearch = [new Filter("Name", FilterOperator.Contains, sQuery)]; 104 | } else { 105 | this._oListFilterState.aSearch = []; 106 | } 107 | this._applyFilterSearch(); 108 | 109 | }, 110 | 111 | /** 112 | * Event handler for refresh event. Keeps filter, sort 113 | * and group settings and refreshes the list binding. 114 | * @public 115 | */ 116 | onRefresh : function () { 117 | this._oList.getBinding("items").refresh(); 118 | }, 119 | 120 | /** 121 | * Event handler for the sorter selection. 122 | * @param {sap.ui.base.Event} oEvent the select event 123 | * @public 124 | */ 125 | onSort : function (oEvent) { 126 | var sKey = oEvent.getSource().getSelectedItem().getKey(), 127 | aSorters = this._oGroupSortState.sort(sKey); 128 | 129 | this._applyGroupSort(aSorters); 130 | }, 131 | 132 | /** 133 | * Event handler for the grouper selection. 134 | * @param {sap.ui.base.Event} oEvent the search field event 135 | * @public 136 | */ 137 | onGroup : function (oEvent) { 138 | var sKey = oEvent.getSource().getSelectedItem().getKey(), 139 | aSorters = this._oGroupSortState.group(sKey); 140 | 141 | this._applyGroupSort(aSorters); 142 | }, 143 | 144 | /** 145 | * Event handler for the filter button to open the ViewSettingsDialog. 146 | * which is used to add or remove filters to the master list. This 147 | * handler method is also called when the filter bar is pressed, 148 | * which is added to the beginning of the master list when a filter is applied. 149 | * @public 150 | */ 151 | onOpenViewSettings : function () { 152 | if (!this._oViewSettingsDialog) { 153 | this._oViewSettingsDialog = sap.ui.xmlfragment("typescript.example.ui5app.view.ViewSettingsDialog", this); 154 | this.getView().addDependent(this._oViewSettingsDialog); 155 | // forward compact/cozy style into Dialog 156 | this._oViewSettingsDialog.addStyleClass(this.getOwnerComponent().getContentDensityClass()); 157 | } 158 | this._oViewSettingsDialog.open(); 159 | }, 160 | 161 | /** 162 | * Event handler called when ViewSettingsDialog has been confirmed, i.e. 163 | * has been closed with 'OK'. In the case, the currently chosen filters 164 | * are applied to the master list, which can also mean that the currently 165 | * applied filters are removed from the master list, in case the filter 166 | * settings are removed in the ViewSettingsDialog. 167 | * @param {sap.ui.base.Event} oEvent the confirm event 168 | * @public 169 | */ 170 | onConfirmViewSettingsDialog : function (oEvent) { 171 | var aFilterItems = oEvent.getParameters().filterItems, 172 | aFilters = [], 173 | aCaptions = []; 174 | 175 | // update filter state: 176 | // combine the filter array and the filter string 177 | aFilterItems.forEach(function (oItem) { 178 | switch (oItem.getKey()) { 179 | case "Filter1" : 180 | aFilters.push(new Filter("UnitNumber", FilterOperator.LE, 100)); 181 | break; 182 | case "Filter2" : 183 | aFilters.push(new Filter("UnitNumber", FilterOperator.GT, 100)); 184 | break; 185 | default : 186 | break; 187 | } 188 | aCaptions.push(oItem.getText()); 189 | }); 190 | 191 | this._oListFilterState.aFilter = aFilters; 192 | this._updateFilterBar(aCaptions.join(", ")); 193 | this._applyFilterSearch(); 194 | }, 195 | 196 | /** 197 | * Event handler for the list selection event 198 | * @param {sap.ui.base.Event} oEvent the list selectionChange event 199 | * @public 200 | */ 201 | onSelectionChange : function (oEvent) { 202 | // get the list item, either from the listItem parameter or from the event's source itself (will depend on the device-dependent mode). 203 | this._showDetail(oEvent.getParameter("listItem") || oEvent.getSource()); 204 | }, 205 | 206 | /** 207 | * Event handler for the bypassed event, which is fired when no routing pattern matched. 208 | * If there was an object selected in the master list, that selection is removed. 209 | * @public 210 | */ 211 | onBypassed : function () { 212 | this._oList.removeSelections(true); 213 | }, 214 | 215 | /** 216 | * Used to create GroupHeaders with non-capitalized caption. 217 | * These headers are inserted into the master list to 218 | * group the master list's items. 219 | * @param {Object} oGroup group whose text is to be displayed 220 | * @public 221 | * @returns {sap.m.GroupHeaderListItem} group header with non-capitalized caption. 222 | */ 223 | createGroupHeader : function (oGroup) { 224 | return new GroupHeaderListItem({ 225 | title : oGroup.text, 226 | upperCase : false 227 | }); 228 | }, 229 | 230 | /** 231 | * Event handler for navigating back. 232 | * We navigate back in the browser historz 233 | * @public 234 | */ 235 | onNavBack : function() { 236 | history.go(-1); 237 | }, 238 | 239 | /* =========================================================== */ 240 | /* begin: internal methods */ 241 | /* =========================================================== */ 242 | 243 | 244 | _createViewModel : function() { 245 | return new JSONModel({ 246 | isFilterBarVisible: false, 247 | filterBarLabel: "", 248 | delay: 0, 249 | title: this.getResourceBundle().getText("masterTitleCount", [0]), 250 | noDataText: this.getResourceBundle().getText("masterListNoDataText"), 251 | sortBy: "Name", 252 | groupBy: "None" 253 | }); 254 | }, 255 | 256 | /** 257 | * If the master route was hit (empty hash) we have to set 258 | * the hash to to the first item in the list as soon as the 259 | * listLoading is done and the first item in the list is known 260 | * @private 261 | */ 262 | _onMasterMatched : function() { 263 | this.getOwnerComponent().oListSelector.oWhenListLoadingIsDone.then( 264 | function (mParams) { 265 | if (mParams.list.getMode() === "None") { 266 | return; 267 | } 268 | var sObjectId = mParams.firstListitem.getBindingContext().getProperty("ObjectID"); 269 | this.getRouter().navTo("object", {objectId : sObjectId}, true); 270 | }.bind(this), 271 | function (mParams) { 272 | if (mParams.error) { 273 | return; 274 | } 275 | this.getRouter().getTargets().display("detailNoObjectsAvailable"); 276 | }.bind(this) 277 | ); 278 | }, 279 | 280 | /** 281 | * Shows the selected item on the detail page 282 | * On phones a additional history entry is created 283 | * @param {sap.m.ObjectListItem} oItem selected Item 284 | * @private 285 | */ 286 | _showDetail : function (oItem) { 287 | var bReplace = !Device.system.phone; 288 | this.getRouter().navTo("object", { 289 | objectId : oItem.getBindingContext().getProperty("ObjectID") 290 | }, bReplace); 291 | }, 292 | 293 | /** 294 | * Sets the item count on the master list header 295 | * @param {integer} iTotalItems the total number of items in the list 296 | * @private 297 | */ 298 | _updateListItemCount : function (iTotalItems) { 299 | var sTitle; 300 | // only update the counter if the length is final 301 | if (this._oList.getBinding("items").isLengthFinal()) { 302 | sTitle = this.getResourceBundle().getText("masterTitleCount", [iTotalItems]); 303 | this.getModel("masterView").setProperty("/title", sTitle); 304 | } 305 | }, 306 | 307 | /** 308 | * Internal helper method to apply both filter and search state together on the list binding 309 | * @private 310 | */ 311 | _applyFilterSearch : function () { 312 | var aFilters = this._oListFilterState.aSearch.concat(this._oListFilterState.aFilter), 313 | oViewModel = this.getModel("masterView"); 314 | this._oList.getBinding("items").filter(aFilters, "Application"); 315 | // changes the noDataText of the list in case there are no filter results 316 | if (aFilters.length !== 0) { 317 | oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataWithFilterOrSearchText")); 318 | } else if (this._oListFilterState.aSearch.length > 0) { 319 | // only reset the no data text to default when no new search was triggered 320 | oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataText")); 321 | } 322 | }, 323 | 324 | /** 325 | * Internal helper method to apply both group and sort state together on the list binding 326 | * @param {sap.ui.model.Sorter[]} aSorters an array of sorters 327 | * @private 328 | */ 329 | _applyGroupSort : function (aSorters) { 330 | this._oList.getBinding("items").sort(aSorters); 331 | }, 332 | 333 | /** 334 | * Internal helper method that sets the filter bar visibility property and the label's caption to be shown 335 | * @param {string} sFilterBarText the selected filter value 336 | * @private 337 | */ 338 | _updateFilterBar : function (sFilterBarText) { 339 | var oViewModel = this.getModel("masterView"); 340 | oViewModel.setProperty("/isFilterBarVisible", (this._oListFilterState.aFilter.length > 0)); 341 | oViewModel.setProperty("/filterBarLabel", this.getResourceBundle().getText("masterFilterBarText", [sFilterBarText])); 342 | } 343 | 344 | }); 345 | 346 | } 347 | ); -------------------------------------------------------------------------------- /src/test/integration/pages/Master.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "sap/ui/test/actions/EnterText", 5 | "typescript/example/ui5app/test/integration/pages/Common", 6 | "sap/ui/test/matchers/AggregationLengthEquals", 7 | "sap/ui/test/matchers/AggregationFilled", 8 | "sap/ui/test/matchers/PropertyStrictEquals" 9 | ], function(Opa5, Press, EnterText, Common, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals) { 10 | "use strict"; 11 | 12 | var sViewName = "Master", 13 | sSomethingThatCannotBeFound = "*#-Q@@||", 14 | iGroupingBoundary = 100; 15 | 16 | Opa5.createPageObjects({ 17 | onTheMasterPage : { 18 | baseClass : Common, 19 | 20 | actions : { 21 | 22 | iWaitUntilTheListIsLoaded : function () { 23 | return this.waitFor({ 24 | id : "list", 25 | viewName : sViewName, 26 | matchers : new AggregationFilled({name : "items"}), 27 | errorMessage : "The master list has not been loaded" 28 | }); 29 | }, 30 | 31 | iWaitUntilTheFirstItemIsSelected : function () { 32 | return this.waitFor({ 33 | id : "list", 34 | viewName : sViewName, 35 | matchers : function(oList) { 36 | // wait until the list has a selected item 37 | var oSelectedItem = oList.getSelectedItem(); 38 | return oSelectedItem && oList.getItems().indexOf(oSelectedItem) === 0; 39 | }, 40 | errorMessage : "The first item of the master list is not selected" 41 | }); 42 | }, 43 | 44 | iSortTheListOnName : function () { 45 | return this.iPressItemInSelectInFooter("sort-select", "masterSort1"); 46 | }, 47 | 48 | iSortTheListOnUnitNumber : function () { 49 | return this.iPressItemInSelectInFooter("sort-select", "masterSort2"); 50 | }, 51 | 52 | iRemoveFilterFromTheList : function () { 53 | return this.iPressItemInSelectInFooter("filter-select", "masterFilterNone"); 54 | }, 55 | 56 | iFilterTheListLessThan100UoM : function () { 57 | return this.iPressItemInSelectInFooter("filter-select", "masterFilter1"); 58 | }, 59 | 60 | iFilterTheListMoreThan100UoM : function () { 61 | return this.iPressItemInSelectInFooter("filter-select", "masterFilter2"); 62 | }, 63 | 64 | iGroupTheList : function () { 65 | return this.iPressItemInSelectInFooter("group-select", "masterGroup1"); 66 | }, 67 | 68 | iRemoveListGrouping : function () { 69 | return this.iPressItemInSelectInFooter("group-select", "masterGroupNone"); 70 | }, 71 | 72 | iOpenViewSettingsDialog : function () { 73 | return this.waitFor({ 74 | id : "filter-button", 75 | viewName : sViewName, 76 | check : function () { 77 | var oViewSettingsDialog = Opa5.getWindow().sap.ui.getCore().byId("viewSettingsDialog"); 78 | // check if the dialog is still open - wait until it is closed 79 | // view settings dialog has no is open function and no open close events so checking the domref is the only option here 80 | // if there is no view settings dialog yet, there is no need to wait 81 | return !oViewSettingsDialog || oViewSettingsDialog.$().length === 0; 82 | }, 83 | actions : new Press(), 84 | errorMessage : "Did not find the 'filter' button." 85 | }); 86 | }, 87 | 88 | iSelectListItemInViewSettingsDialog : function (sListItemTitle) { 89 | return this.waitFor({ 90 | searchOpenDialogs : true, 91 | controlType : "sap.m.StandardListItem", 92 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "title", value : sListItemTitle}), 93 | actions: new Press(), 94 | errorMessage : "Did not find list item with title " + sListItemTitle + " in View Settings Dialog." 95 | }); 96 | }, 97 | 98 | iPressOKInViewSelectionDialog : function () { 99 | return this.waitFor({ 100 | searchOpenDialogs : true, 101 | controlType : "sap.m.Button", 102 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "text", value : "OK"}), 103 | actions : new Press(), 104 | errorMessage : "Did not find the ViewSettingDialog's 'OK' button." 105 | }); 106 | }, 107 | 108 | iPressResetInViewSelectionDialog : function () { 109 | return this.waitFor({ 110 | searchOpenDialogs : true, 111 | controlType : "sap.m.Button", 112 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "icon", value : "sap-icon://refresh"}), 113 | actions : new Press(), 114 | errorMessage : "Did not find the ViewSettingDialog's 'Reset' button." 115 | }); 116 | }, 117 | 118 | iPressItemInSelectInFooter : function (sSelect, sItem) { 119 | return this.waitFor({ 120 | id : sSelect, 121 | viewName : sViewName, 122 | success : function (oSelect) { 123 | oSelect.open(); 124 | this.waitFor({ 125 | id : sItem, 126 | viewName : sViewName, 127 | success : function(oElem){ 128 | oElem.$().trigger("tap"); 129 | }, 130 | errorMessage : "Did not find the " + sItem + " element in select" 131 | }); 132 | }.bind(this), 133 | errorMessage : "Did not find the " + sSelect + " select" 134 | }); 135 | }, 136 | 137 | iRememberTheSelectedItem : function () { 138 | return this.waitFor({ 139 | id : "list", 140 | viewName : sViewName, 141 | matchers : function (oList) { 142 | return oList.getSelectedItem(); 143 | }, 144 | success : function (oListItem) { 145 | this.iRememberTheListItem(oListItem); 146 | }, 147 | errorMessage : "The list does not have a selected item so nothing can be remembered" 148 | }); 149 | }, 150 | 151 | iRememberTheIdOfListItemAtPosition : function (iPosition) { 152 | return this.waitFor({ 153 | id : "list", 154 | viewName : sViewName, 155 | matchers : function (oList) { 156 | return oList.getItems()[iPosition]; 157 | }, 158 | success : function (oListItem) { 159 | this.iRememberTheListItem(oListItem); 160 | }, 161 | errorMessage : "The list does not have an item at the index " + iPosition 162 | }); 163 | }, 164 | 165 | iRememberAnIdOfAnObjectThatsNotInTheList : function () { 166 | return this.waitFor(this.createAWaitForAnEntitySet({ 167 | entitySet : "Objects", 168 | success : function (aEntityData) { 169 | this.waitFor({ 170 | id : "list", 171 | viewName : sViewName, 172 | matchers : new AggregationFilled({name: "items"}), 173 | success : function (oList) { 174 | var sCurrentId, 175 | aItemsNotInTheList = aEntityData.filter(function (oObject) { 176 | return !oList.getItems().some(function (oListItem) { 177 | return oListItem.getBindingContext().getProperty("ObjectID") === oObject.ObjectID; 178 | }); 179 | }); 180 | 181 | if (!aItemsNotInTheList.length) { 182 | // Not enough items all of them are displayed so we take the last one 183 | sCurrentId = aEntityData[aEntityData.length - 1].ObjectID; 184 | } else { 185 | sCurrentId = aItemsNotInTheList[0].ObjectID; 186 | } 187 | 188 | var oCurrentItem = this.getContext().currentItem; 189 | // Construct a binding path since the list item is not created yet and we only have the id. 190 | oCurrentItem.bindingPath = "/" + oList.getModel().createKey("Objects", { 191 | ObjectID : sCurrentId 192 | }); 193 | oCurrentItem.id = sCurrentId; 194 | }, 195 | errorMessage : "the model does not have a item that is not in the list" 196 | }); 197 | } 198 | })); 199 | }, 200 | 201 | iPressOnTheObjectAtPosition : function (iPositon) { 202 | return this.waitFor({ 203 | id : "list", 204 | viewName : sViewName, 205 | matchers : function (oList) { 206 | return oList.getItems()[iPositon]; 207 | }, 208 | actions : new Press(), 209 | errorMessage : "List 'list' in view '" + sViewName + "' does not contain an ObjectListItem at position '" + iPositon + "'" 210 | }); 211 | }, 212 | 213 | iSearchForTheFirstObject : function (){ 214 | var sFirstObjectTitle; 215 | 216 | return this.waitFor({ 217 | id : "list", 218 | viewName : sViewName, 219 | matchers: new AggregationFilled({name : "items"}), 220 | success : function (oList) { 221 | sFirstObjectTitle = oList.getItems()[0].getTitle(); 222 | return this.iSearchForValue(new EnterText({text: sFirstObjectTitle}), new Press()); 223 | }, 224 | errorMessage : "Did not find list items while trying to search for the first item." 225 | }); 226 | }, 227 | 228 | iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh : function () { 229 | var fireRefreshButtonPressedOnSearchField = function (oSearchField) { 230 | 231 | /*eslint-disable new-cap */ 232 | var oEvent = jQuery.Event("touchend"); 233 | /*eslint-enable new-cap */ 234 | oEvent.originalEvent = {refreshButtonPressed: true, id: oSearchField.getId()}; 235 | oEvent.target = oSearchField; 236 | oEvent.srcElement = oSearchField; 237 | jQuery.extend(oEvent, oEvent.originalEvent); 238 | 239 | oSearchField.fireSearch(oEvent); 240 | }; 241 | return this.iSearchForValue([new EnterText({text: sSomethingThatCannotBeFound}), fireRefreshButtonPressedOnSearchField]); 242 | }, 243 | 244 | iSearchForValue : function (aActions) { 245 | return this.waitFor({ 246 | id : "searchField", 247 | viewName : sViewName, 248 | actions: aActions, 249 | errorMessage : "Failed to find search field in Master view.'" 250 | }); 251 | }, 252 | 253 | iClearTheSearch : function () { 254 | //can not use 'EnterText' action to enter empty strings (yet) 255 | var fnClearSearchField = function(oSearchField) { 256 | oSearchField.clear(); 257 | }; 258 | return this.iSearchForValue([fnClearSearchField]); 259 | }, 260 | 261 | iSearchForSomethingWithNoResults : function () { 262 | return this.iSearchForValue([new EnterText({text: sSomethingThatCannotBeFound}), new Press()]); 263 | }, 264 | 265 | iRememberTheListItem : function (oListItem) { 266 | var oBindingContext = oListItem.getBindingContext(); 267 | this.getContext().currentItem = { 268 | bindingPath: oBindingContext.getPath(), 269 | id: oBindingContext.getProperty("ObjectID"), 270 | title: oBindingContext.getProperty("Name") 271 | }; 272 | } 273 | }, 274 | 275 | assertions : { 276 | 277 | iShouldSeeTheBusyIndicator : function () { 278 | return this.waitFor({ 279 | id : "list", 280 | viewName : sViewName, 281 | success : function (oList) { 282 | // we set the list busy, so we need to query the parent of the app 283 | Opa5.assert.ok(oList.getBusy(), "The master list is busy"); 284 | }, 285 | errorMessage : "The master list is not busy." 286 | }); 287 | }, 288 | 289 | theListGroupShouldBeFilteredOnUnitNumberValue20OrLess : function () { 290 | return this.theListShouldBeFilteredOnUnitNumberValue(20, false, {iLow : 1, iHigh : 2}); 291 | }, 292 | 293 | theListShouldContainAGroupHeader : function () { 294 | return this.waitFor({ 295 | controlType : "sap.m.GroupHeaderListItem", 296 | viewName : sViewName, 297 | success : function () { 298 | Opa5.assert.ok(true, "Master list is grouped"); 299 | }, 300 | errorMessage : "Master list is not grouped" 301 | }); 302 | }, 303 | 304 | theListShouldContainOnlyFormattedUnitNumbers : function () { 305 | return this.theUnitNumbersShouldHaveTwoDecimals("sap.m.ObjectListItem", 306 | sViewName, 307 | "Numbers in ObjectListItems numbers are properly formatted", 308 | "List has no entries which can be checked for their formatting"); 309 | }, 310 | 311 | theListHeaderDisplaysZeroHits : function () { 312 | return this.waitFor({ 313 | viewName : sViewName, 314 | id : "page", 315 | matchers : new PropertyStrictEquals({name : "title", value : "<Objects> (0)"}), 316 | success : function () { 317 | Opa5.assert.ok(true, "The list header displays zero hits"); 318 | }, 319 | errorMessage : "The list header still has items" 320 | }); 321 | }, 322 | 323 | theListHasEntries : function () { 324 | return this.waitFor({ 325 | viewName : sViewName, 326 | id : "list", 327 | matchers : new AggregationFilled({ 328 | name : "items" 329 | }), 330 | success : function () { 331 | Opa5.assert.ok(true, "The list has items"); 332 | }, 333 | errorMessage : "The list had no items" 334 | }); 335 | }, 336 | 337 | theListShouldNotContainGroupHeaders : function () { 338 | function fnIsGroupHeader (oElement) { 339 | return oElement.getMetadata().getName() === "sap.m.GroupHeaderListItem"; 340 | } 341 | 342 | return this.waitFor({ 343 | viewName : sViewName, 344 | id : "list", 345 | matchers : function (oList) { 346 | return !oList.getItems().some(fnIsGroupHeader); 347 | }, 348 | success : function() { 349 | Opa5.assert.ok(true, "Master list does not contain a group header although grouping has been removed."); 350 | }, 351 | errorMessage : "Master list still contains a group header although grouping has been removed." 352 | }); 353 | }, 354 | 355 | theListShouldBeSortedAscendingOnUnitNumber : function () { 356 | return this.theListShouldBeSortedAscendingOnField("UnitNumber"); 357 | }, 358 | 359 | theListShouldBeSortedAscendingOnName : function () { 360 | return this.theListShouldBeSortedAscendingOnField("Name"); 361 | }, 362 | 363 | theListShouldBeSortedAscendingOnField : function (sField) { 364 | function fnCheckSort (oList){ 365 | var oLastValue = null, 366 | fnIsOrdered = function (oElement) { 367 | if (!oElement.getBindingContext()) { 368 | return false; 369 | } 370 | 371 | var oCurrentValue = oElement.getBindingContext().getProperty(sField); 372 | 373 | if (oCurrentValue === undefined) { 374 | return false; 375 | } 376 | 377 | if (!oLastValue || oCurrentValue >= oLastValue){ 378 | oLastValue = oCurrentValue; 379 | } else { 380 | return false; 381 | } 382 | return true; 383 | }; 384 | 385 | return oList.getItems().every(fnIsOrdered); 386 | } 387 | 388 | return this.waitFor({ 389 | viewName : sViewName, 390 | id : "list", 391 | matchers : fnCheckSort, 392 | success : function() { 393 | Opa5.assert.ok(true, "Master list has been sorted correctly for field '" + sField + "'."); 394 | }, 395 | errorMessage : "Master list has not been sorted correctly for field '" + sField + "'." 396 | }); 397 | }, 398 | 399 | theListShouldBeFilteredOnUnitNumberValue : function(iThreshhold, bGreaterThan, oRange) { 400 | 401 | function fnCheckFilter (oList){ 402 | var fnIsGreaterThanMaxValue = function (oElement) { 403 | if (bGreaterThan) { 404 | return oElement.getBindingContext().getProperty("UnitNumber") < iThreshhold; 405 | } 406 | return oElement.getBindingContext().getProperty("UnitNumber") > iThreshhold; 407 | }; 408 | var aItems = oList.getItems(); 409 | if (oRange) { 410 | aItems = aItems.slice(oRange.iLow, oRange.iHigh); 411 | } 412 | 413 | return !aItems.some(fnIsGreaterThanMaxValue); 414 | } 415 | 416 | return this.waitFor({ 417 | id : "list", 418 | viewName : sViewName, 419 | matchers : fnCheckFilter, 420 | success : function(){ 421 | Opa5.assert.ok(true, "Master list has been filtered correctly with filter value '" + iThreshhold + "'."); 422 | }, 423 | errorMessage : "Master list has not been filtered correctly with filter value '" + iThreshhold + "'." 424 | }); 425 | }, 426 | 427 | theMasterListShouldBeFilteredOnUnitNumberValueMoreThanTheGroupBoundary : function(){ 428 | return this.theListShouldBeFilteredOnUnitNumberValue(iGroupingBoundary, true); 429 | }, 430 | 431 | theMasterListShouldBeFilteredOnUnitNumberValueLessThanTheGroupBoundary : function(){ 432 | return this.theListShouldBeFilteredOnUnitNumberValue(iGroupingBoundary); 433 | }, 434 | 435 | iShouldSeeTheList : function () { 436 | return this.waitFor({ 437 | id : "list", 438 | viewName : sViewName, 439 | success : function (oList) { 440 | Opa5.assert.ok(oList, "Found the object List"); 441 | }, 442 | errorMessage : "Can't see the master list." 443 | }); 444 | }, 445 | 446 | theListShowsOnlyObjectsWithTheSearchStringInTheirTitle : function () { 447 | this.waitFor({ 448 | id : "list", 449 | viewName : sViewName, 450 | matchers : new AggregationFilled({name : "items"}), 451 | check : function(oList) { 452 | var sTitle = oList.getItems()[0].getTitle(), 453 | bEveryItemContainsTheTitle = oList.getItems().every(function (oItem) { 454 | return oItem.getTitle().indexOf(sTitle) !== -1; 455 | }); 456 | return bEveryItemContainsTheTitle; 457 | }, 458 | success : function (oList) { 459 | Opa5.assert.ok(true, "Every item did contain the title"); 460 | }, 461 | errorMessage : "The list did not have items" 462 | }); 463 | }, 464 | 465 | theListShouldHaveNEntries : function (iObjIndex) { 466 | return this.waitFor({ 467 | id : "list", 468 | viewName : sViewName, 469 | matchers : [ new AggregationLengthEquals({name : "items", length : iObjIndex}) ], 470 | success : function (oList) { 471 | Opa5.assert.strictEqual(oList.getItems().length, iObjIndex, "The list has x items"); 472 | }, 473 | errorMessage : "List does not have " + iObjIndex + " entries." 474 | }); 475 | }, 476 | 477 | theListShouldHaveAllEntries : function () { 478 | var aAllEntities, 479 | iExpectedNumberOfItems; 480 | // retrieve all Objects to be able to check for the total amount 481 | this.waitFor(this.createAWaitForAnEntitySet({ 482 | entitySet : "Objects", 483 | success : function (aEntityData) { 484 | aAllEntities = aEntityData; 485 | } 486 | })); 487 | 488 | return this.waitFor({ 489 | id : "list", 490 | viewName : sViewName, 491 | matchers : function (oList) { 492 | // If there are less items in the list than the growingThreshold, only check for this number. 493 | iExpectedNumberOfItems = Math.min(oList.getGrowingThreshold(), aAllEntities.length); 494 | return new AggregationLengthEquals({name : "items", length : iExpectedNumberOfItems}).isMatching(oList); 495 | }, 496 | success : function (oList) { 497 | Opa5.assert.strictEqual(oList.getItems().length, iExpectedNumberOfItems, "The growing list displays all items"); 498 | }, 499 | errorMessage : "List does not display all entries." 500 | }); 501 | }, 502 | 503 | iShouldSeeTheNoDataTextForNoSearchResults : function () { 504 | return this.waitFor({ 505 | id : "list", 506 | viewName : sViewName, 507 | success : function (oList) { 508 | Opa5.assert.strictEqual(oList.getNoDataText(), oList.getModel("i18n").getProperty("masterListNoDataWithFilterOrSearchText"), "the list should show the no data text for search and filter"); 509 | }, 510 | errorMessage : "list does not show the no data text for search and filter" 511 | }); 512 | }, 513 | 514 | theHeaderShouldDisplayAllEntries : function () { 515 | return this.waitFor({ 516 | id : "list", 517 | viewName : sViewName, 518 | success : function (oList) { 519 | var iExpectedLength = oList.getBinding("items").getLength(); 520 | this.waitFor({ 521 | id : "page", 522 | viewName : sViewName, 523 | matchers : new PropertyStrictEquals({name : "title", value : "<Objects> (" + iExpectedLength + ")"}), 524 | success : function () { 525 | Opa5.assert.ok(true, "The master page header displays " + iExpectedLength + " items"); 526 | }, 527 | errorMessage : "The master page header does not display " + iExpectedLength + " items." 528 | }); 529 | }, 530 | errorMessage : "Header does not display the number of items in the list" 531 | }); 532 | }, 533 | 534 | theFirstItemShouldBeSelected : function () { 535 | return this.waitFor({ 536 | id : "list", 537 | viewName : sViewName, 538 | matchers : new AggregationFilled({name : "items"}), 539 | success : function (oList) { 540 | Opa5.assert.strictEqual(oList.getItems()[0], oList.getSelectedItem(), "The first object is selected"); 541 | }, 542 | errorMessage : "The first object is not selected." 543 | }); 544 | }, 545 | 546 | theListShouldHaveNoSelection : function () { 547 | return this.waitFor({ 548 | id : "list", 549 | viewName : sViewName, 550 | matchers : function(oList) { 551 | return !oList.getSelectedItem(); 552 | }, 553 | success : function (oList) { 554 | Opa5.assert.strictEqual(oList.getSelectedItems().length, 0, "The list selection is removed"); 555 | }, 556 | errorMessage : "List selection was not removed" 557 | }); 558 | }, 559 | 560 | theRememberedListItemShouldBeSelected : function () { 561 | this.waitFor({ 562 | id : "list", 563 | viewName : sViewName, 564 | matchers : function (oList) { 565 | return oList.getSelectedItem(); 566 | }, 567 | success : function (oSelectedItem) { 568 | Opa5.assert.strictEqual(oSelectedItem.getTitle(), this.getContext().currentItem.title, "The list selection is incorrect"); 569 | }, 570 | errorMessage : "The list has no selection" 571 | }); 572 | } 573 | 574 | } 575 | 576 | } 577 | 578 | }); 579 | 580 | }); --------------------------------------------------------------------------------