├── .che └── project.json ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .user.project.json ├── README.md ├── lib ├── middleware │ └── odataProxy.js └── tasks │ └── babel.js ├── neo-app.json ├── odata.json ├── package-lock.json ├── package.json ├── proxy.js ├── readme.txt ├── ui5.yaml └── webapp ├── Component.js ├── controller ├── App.controller.js ├── BaseController.js ├── Detail.controller.js ├── DetailObjectNotFound.controller.js ├── DetailObjectNotFound.js ├── ErrorHandler.js ├── ListSelector.js ├── Master.controller.js └── NotFound.controller.js ├── i18n └── i18n.properties ├── index.html ├── libs └── observable-slim.js ├── localService ├── metadata.xml └── mockserver.js ├── manifest.json ├── model ├── BaseObject.js ├── Person.js ├── Skill.js ├── formatter.js └── models.js ├── service ├── CoreService.js └── PersonService.js ├── state └── PersonState.js ├── test.html ├── test ├── initMockServer.js ├── integration │ ├── AllJourneys.js │ ├── BusyJourney.js │ ├── BusyJourneyPhone.js │ ├── MasterJourney.js │ ├── NavigationJourney.js │ ├── NavigationJourneyPhone.js │ ├── NotFoundJourney.js │ ├── NotFoundJourneyPhone.js │ ├── PhoneJourneys.js │ ├── arrangements │ │ └── Startup.js │ ├── opaTests.qunit.html │ ├── opaTests.qunit.js │ ├── opaTestsPhone.qunit.html │ ├── opaTestsPhone.qunit.js │ └── pages │ │ ├── App.js │ │ ├── Browser.js │ │ ├── Common.js │ │ ├── Detail.js │ │ ├── Master.js │ │ └── NotFound.js ├── mockServer.html ├── testsuite.qunit.html ├── testsuite.qunit.js └── unit │ ├── AllTests.js │ ├── controller │ └── ListSelector.js │ ├── model │ ├── formatter.js │ └── models.js │ ├── unitTests.qunit.html │ └── unitTests.qunit.js └── view ├── App.view.xml ├── Detail.view.xml ├── DetailObjectNotFound.view.xml ├── Master.view.xml ├── NotFound.view.xml └── ViewSettingsDialog.fragment.xml /.che/project.json: -------------------------------------------------------------------------------- 1 | {"type":"sap.web","builders":{"configs":{}},"runners":{"configs":{}},"attributes":{"sap.watt.common.setting":["{\"projectType\":[\"sap.watt.uitools.ide.web\",\"sap.watt.uitools.ide.fiori\"],\"build\":{\"targetFolder\":\"dist\",\"sourceFolder\":\"webapp\",\"excludedFolders\":[\"test\"],\"excludedFiles\":[\"test.html\"]},\"dataBinding\":{\"/webapp/view/App.view.xml\":{\"entitySet\":\"UNBINDKEY\"},\"/webapp/view/NotFound.view.xml\":{\"entitySet\":\"UNBINDKEY\"},\"/webapp/view/DetailObjectNotFound.view.xml\":{\"entitySet\":\"UNBINDKEY\"},\"/webapp/view/DetailNoObjectsAvailable.view.xml\":{\"entitySet\":\"UNBINDKEY\"},\"/webapp/view/Master.view.xml\":{\"entitySet\":\"Persons\"},\"/webapp/view/Detail.view.xml\":{\"entitySet\":\"Persons\"}},\"generation\":[{\"templateId\":\"sap.ui.ui5-template-plugin.2masterdetail\",\"templateVersion\":\"1.66.1\",\"dateTimeStamp\":\"Tue, 04 Jun 2019 17:55:34 GMT\"}],\"translation\":{\"translationDomain\":\"\",\"supportedLanguages\":\"en,fr,de\",\"defaultLanguage\":\"en\",\"defaultI18NPropertyFile\":\"i18n.properties\",\"resourceModelName\":\"i18n\"},\"mockpreview\":{\"mockUri\":\"/sap/opu/odata/sap/ZGW_UI5CON_SRV\",\"metadataFilePath\":\"\",\"loadJSONFiles\":false,\"loadCustomRequests\":false,\"mockRequestsFilePath\":\"\"},\"basevalidator\":{\"services\":{\"xml\":\"fioriXmlAnalysis\",\"js\":\"fioriJsValidator\"}},\"codeCheckingTriggers\":{\"notifyBeforePush\":false,\"notifyBeforePushLevel\":\"Error\",\"blockPush\":false,\"blockPushLevel\":\"Error\"}}"]},"mixinTypes":[]} -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webapp/libs/*.js 2 | webapp/libs/**/*.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 9, 8 | "sourceType": "module" 9 | }, 10 | "globals": { 11 | "sap": true, 12 | "jQuery": true 13 | }, 14 | "rules": { 15 | "block-scoped-var": 1, 16 | "brace-style": [2, "1tbs", { 17 | "allowSingleLine": true 18 | }], 19 | "consistent-this": 2, 20 | "no-div-regex": 2, 21 | "no-floating-decimal": 2, 22 | "no-self-compare": 2, 23 | "no-mixed-spaces-and-tabs": [2, true], 24 | "no-nested-ternary": 2, 25 | "no-unused-vars": [2, { 26 | "vars": "all", 27 | "args": "none" 28 | }], 29 | "radix": 2, 30 | "keyword-spacing": 2, 31 | "space-unary-ops": 2, 32 | "wrap-iife": [2, "any"], 33 | "camelcase": 1, 34 | "consistent-return": 1, 35 | "max-nested-callbacks": [1, 3], 36 | "new-cap": 1, 37 | "no-extra-boolean-cast": 1, 38 | "no-lonely-if": 1, 39 | "no-new": 1, 40 | "no-new-wrappers": 1, 41 | "no-redeclare": 1, 42 | "no-unused-expressions": 1, 43 | "no-use-before-define": [1, "nofunc"], 44 | "no-warning-comments": 1, 45 | "strict": 1, 46 | "valid-jsdoc": [1, { 47 | "requireReturn": false 48 | }], 49 | "default-case": 1, 50 | "dot-notation": 0, 51 | "eol-last": 0, 52 | "eqeqeq": 0, 53 | "no-trailing-spaces": 0, 54 | "no-underscore-dangle": 0, 55 | "quotes": 0, 56 | "key-spacing": 0, 57 | "comma-spacing": 0, 58 | "no-multi-spaces": 0, 59 | "no-shadow": 0, 60 | "no-irregular-whitespace": 0 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.user.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "run": [ 3 | { 4 | "filePath": "/PersonSkills/webapp/index.html", 5 | "previewMode": 1, 6 | "dataMode": 1, 7 | "workspace": "withoutWorkspace", 8 | "urlParameters": [ 9 | { 10 | "paramName": "", 11 | "paramValue": "", 12 | "paramActive": false 13 | }, 14 | { 15 | "paramName": "", 16 | "paramValue": "", 17 | "paramActive": false 18 | } 19 | ], 20 | "hashParameter": "", 21 | "ui5ActiveVersion": "snapshot", 22 | "ui5VerSource": null, 23 | "isDefaultVersion": 1, 24 | "_metadata": { 25 | "id": "2622620", 26 | "runnerId": "webapprunner", 27 | "displayName": "App" 28 | } 29 | }, 30 | { 31 | "filePath": "/PersonSkills/webapp/test/mockServer.html", 32 | "previewMode": 1, 33 | "dataMode": 1, 34 | "workspace": "withoutWorkspace", 35 | "urlParameters": [ 36 | { 37 | "paramName": "", 38 | "paramValue": "", 39 | "paramActive": false 40 | }, 41 | { 42 | "paramName": "", 43 | "paramValue": "", 44 | "paramActive": false 45 | } 46 | ], 47 | "hashParameter": "", 48 | "ui5ActiveVersion": "snapshot", 49 | "ui5VerSource": null, 50 | "isDefaultVersion": 1, 51 | "_metadata": { 52 | "id": "7185718", 53 | "runnerId": "webapprunner", 54 | "displayName": "App (Mock Server)" 55 | } 56 | }, 57 | { 58 | "filePath": "/PersonSkills/webapp/test/unit/unitTests.qunit.html", 59 | "previewMode": 1, 60 | "dataMode": 1, 61 | "workspace": "withoutWorkspace", 62 | "ui5ActiveVersion": "snapshot", 63 | "ui5VerSource": null, 64 | "isDefaultVersion": 1, 65 | "urlParameters": [ 66 | { 67 | "paramName": "", 68 | "paramValue": "", 69 | "paramActive": false 70 | }, 71 | { 72 | "paramName": "", 73 | "paramValue": "", 74 | "paramActive": false 75 | } 76 | ], 77 | "_metadata": { 78 | "id": "2852428", 79 | "runnerId": "webapprunner", 80 | "displayName": "Unit Tests" 81 | } 82 | }, 83 | { 84 | "filePath": "/PersonSkills/webapp/test/integration/opaTests.qunit.html", 85 | "previewMode": 1, 86 | "dataMode": 1, 87 | "workspace": "withoutWorkspace", 88 | "ui5ActiveVersion": "snapshot", 89 | "ui5VerSource": null, 90 | "isDefaultVersion": 1, 91 | "urlParameters": [ 92 | { 93 | "paramName": "", 94 | "paramValue": "", 95 | "paramActive": false 96 | }, 97 | { 98 | "paramName": "", 99 | "paramValue": "", 100 | "paramActive": false 101 | } 102 | ], 103 | "hashParameter": "", 104 | "_metadata": { 105 | "id": "2186555", 106 | "runnerId": "webapprunner", 107 | "displayName": "OPA Tests" 108 | } 109 | }, 110 | { 111 | "filePath": "/PersonSkills/webapp/test/testsuite.qunit.html", 112 | "previewMode": 1, 113 | "dataMode": 1, 114 | "workspace": "withoutWorkspace", 115 | "ui5ActiveVersion": "snapshot", 116 | "ui5VerSource": null, 117 | "isDefaultVersion": 1, 118 | "urlParameters": [ 119 | { 120 | "paramName": "", 121 | "paramValue": "", 122 | "paramActive": false 123 | }, 124 | { 125 | "paramName": "", 126 | "paramValue": "", 127 | "paramActive": false 128 | } 129 | ], 130 | "hashParameter": "", 131 | "_metadata": { 132 | "id": "5031676", 133 | "runnerId": "webapprunner", 134 | "displayName": "Test Suite (Unit, OPA)" 135 | } 136 | } 137 | ] 138 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI5ToolsExampleApp 2 | UI5ToolsExampleApp 3 | -------------------------------------------------------------------------------- /lib/middleware/odataProxy.js: -------------------------------------------------------------------------------- 1 | module.exports = function ({ 2 | resources, 3 | options 4 | }) { 5 | const fs = require('fs'); 6 | const httpProxy = require('http-proxy'); 7 | const proxy = new httpProxy.createProxyServer(); 8 | const odata = fs.readFileSync('./odata.json'); 9 | const routing = JSON.parse(odata); 10 | return function (req, res, next) { 11 | res.header('Access-Control-Allow-Origin', '*'); 12 | res.header('Access-Control-Allow-Credentials', 'true'); 13 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 14 | res.header('Access-Control-Allow-Headers', 'X-Requested-With, Accept, Origin, Referer, User-Agent, Content-Type, Authorization, X-Mindflash-SessionID'); 15 | // intercept OPTIONS method 16 | if ('OPTIONS' === req.method) { 17 | res.header(200); 18 | console.log(req.method + '(options): ' + req.url); 19 | next(); 20 | return; 21 | } 22 | var dirname = req.url.replace(/^\/([^\/]*).*$/, '$1'); //get root directory name eg sdk, app, sap 23 | if (!routing[dirname]) { 24 | console.log(req.method + ': ' + req.url); 25 | next(); 26 | return; 27 | } 28 | console.log(req.method + ' (redirect): ' + routing[dirname].target + req.url); 29 | proxy.web(req, res, routing[dirname], function (err) { 30 | if (err) { 31 | next(err); 32 | } 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /lib/tasks/babel.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors'); 2 | const emoji = require('node-emoji'); 3 | const babel = require("@babel/core"); 4 | const pathregen = require("regenerator-runtime/path").path; 5 | const resourceFactory = require("@ui5/fs").resourceFactory; 6 | const fs = require('fs'); 7 | const path = require("path"); 8 | const icons = ["hatching_chick", "baby_chick", "hatched_chick", "bird"]; 9 | const baseLogTask = "info".green + " babel:".magenta; 10 | /** 11 | * Custom task example 12 | * 13 | * @param {Object} parameters Parameters 14 | * @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files 15 | * @param {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files 16 | * @param {Object} parameters.options Options 17 | * @param {string} parameters.options.projectName Project name 18 | * @param {string} [parameters.options.configuration] Task configuration if given in ui5.yaml 19 | * @returns {Promise} Promise resolving with undefined once data has been written 20 | */ 21 | module.exports = async function ({ 22 | workspace, 23 | dependencies, 24 | options 25 | }) { 26 | const jsResources = await workspace.byGlob("**/*.js"); 27 | 28 | console.info("\n" + baseLogTask + "Add regenerator-runtime".green + emoji.get('palm_tree')); 29 | //get path of Component 30 | const componentResource = jsResources.find((jsResource) => jsResource.getPath().includes("Component.js")); 31 | const toPath = componentResource.getPath(); 32 | const pathPrefix = toPath.replace("Component.js", ""); 33 | //get path of regenerator in node_modules 34 | const pathRegenerator = pathregen.substr(pathregen.indexOf("node_modules") + 13).replace("\\", "/"); 35 | //build full path for regenerator in current project 36 | const virtualPathRegenerator = pathPrefix + pathRegenerator; 37 | //get code of regenereator 38 | const runtimeCode = fs.readFileSync(pathregen, 'utf8'); 39 | //create resource 40 | const runtimeResource = resourceFactory.createResource({ 41 | path: virtualPathRegenerator, 42 | string: runtimeCode 43 | }); 44 | //save regenerator to workspace 45 | await workspace.write(runtimeResource); 46 | 47 | console.info(baseLogTask + "Include regenerator-runtime".green + emoji.get('palm_tree')); 48 | //add require regenerator for development prupose 49 | let componentSource = await componentResource.getString(); 50 | const requirePath = virtualPathRegenerator.replace("/resources/", "").replace(".js", ""); 51 | componentSource = "// development mode: load the regenerator runtime synchronously\nif(!window.regeneratorRuntime){sap.ui.requireSync(\"" + requirePath + "\")}" + componentSource; 52 | componentResource.setString(componentSource); 53 | await workspace.write(componentResource); 54 | 55 | console.info(baseLogTask + "Start tranformation".green + emoji.get('palm_tree')); 56 | const filteredResources = jsResources.filter(resource => { 57 | return (!resource.getPath().includes("/libs/")); 58 | }); 59 | let iconIdx = 0; 60 | const transformCode = async resource => { 61 | var source = await resource.getString(); 62 | console.info(baseLogTask + "Transforming:".blue + emoji.get(icons[iconIdx]) + resource.getPath()); 63 | iconIdx = iconIdx >= (icons.length - 1) ? 0 : ++iconIdx; 64 | var {code,map,ast} = babel.transformSync(source, { 65 | presets: [["@babel/preset-env"]], 66 | plugins: [["@babel/plugin-transform-modules-commonjs", {"strictMode": false}]] 67 | }); 68 | resource.setString(code); 69 | return resource; 70 | }; 71 | const transformedResources = await Promise.all(filteredResources.map(resource => transformCode(resource))); 72 | console.info(baseLogTask + "Tranformation finished".green + emoji.get('palm_tree')); 73 | 74 | console.info(baseLogTask + "Start updating files".green); 75 | await Promise.all(transformedResources.map((resource) => { 76 | return workspace.write(resource); 77 | })); 78 | console.info(baseLogTask + "Updating files finished".green); 79 | 80 | console.info(baseLogTask + "Babel task finished" + emoji.get('white_check_mark')) 81 | }; 82 | -------------------------------------------------------------------------------- /neo-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "path": "/webapp/resources", 5 | "target": { 6 | "type": "service", 7 | "name": "sapui5", 8 | "entryPath": "/resources" 9 | }, 10 | "description": "SAPUI5 Resources" 11 | }, 12 | { 13 | "path": "/webapp/test-resources", 14 | "target": { 15 | "type": "service", 16 | "name": "sapui5", 17 | "entryPath": "/test-resources" 18 | }, 19 | "description": "SAPUI5 Test Resources" 20 | }, 21 | { 22 | "path": "/resources", 23 | "target": { 24 | "type": "service", 25 | "name": "sapui5", 26 | "entryPath": "/resources" 27 | }, 28 | "description": "SAPUI5 Resources" 29 | }, 30 | { 31 | "path": "/test-resources", 32 | "target": { 33 | "type": "service", 34 | "name": "sapui5", 35 | "entryPath": "/test-resources" 36 | }, 37 | "description": "SAPUI5 Test Resources" 38 | }, 39 | { 40 | "path": "/sap/opu/odata", 41 | "target": { 42 | "type": "destination", 43 | "name": "FG2", 44 | "entryPath": "/sap/opu/odata" 45 | }, 46 | "description": "FG2" 47 | } 48 | ], 49 | "welcomeFile": "/webapp/index.html", 50 | "sendWelcomeFileRedirect": true 51 | } -------------------------------------------------------------------------------- /odata.json: -------------------------------------------------------------------------------- 1 | { 2 | "sap": { 3 | "target": "http://10.0.xx.xx", 4 | "auth": "lemaiwo:xxxx" 5 | } 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PersonSkills", 3 | "version": "0.0.1", 4 | "description": "", 5 | "private": true, 6 | "devDependencies": { 7 | "@ui5/cli": "^1.7.0", 8 | "@ui5/fs": "^1.1.2", 9 | "eslint": "^6.2.2", 10 | "express": "latest", 11 | "http-proxy": "^1.17.0", 12 | "npm-run-all": "^4.1.5", 13 | "rimraf": "^2.7.1", 14 | "serve": "^10.1.2", 15 | "@babel/core": "^7.3.4", 16 | "@babel/preset-env": "^7.3.4", 17 | "node-emoji": "^1.10.0", 18 | "colors": "^1.3.3", 19 | "regenerator-runtime": "^0.13.3", 20 | "nwabap-ui5uploader": "^0.3.4", 21 | "npm-watch": "^0.5.0" 22 | }, 23 | "dependencies": { 24 | "@openui5/sap.f": "^1.69.1", 25 | "@openui5/sap.m": "^1.69.1", 26 | "@openui5/sap.ui.core": "^1.69.1", 27 | "@openui5/themelib_sap_belize": "^1.69.1" 28 | }, 29 | "scripts": { 30 | "start": "npm-run-all sapbuild --parallel watch start:dist proxy", 31 | "start:webapp": "ui5 serve", 32 | "start:dist": "serve dist", 33 | "lint": "eslint webapp", 34 | "build": "rimraf dist && ui5 build -a", 35 | "sapbuild": "rimraf dist && ui5 build", 36 | "proxy": "node proxy.js", 37 | "deploy": "npx nwabap upload --base ./dist --conn_server \"$ABAP_DEVELOPMENT_SERVER_HOST\" --conn_user \"$ABAP_DEVELOPMENT_USER\" --conn_password \"$ABAP_DEVELOPMENT_PASSWORD\" --abap_package \"$ABAP_PACKAGE\" --abap_bsp \"$ABAP_APPLICATION_NAME\" --abap_bsp_text \"$ABAP_APPLICATION_DESC\" --abap_transport \"$CI_COMMIT_TITLE\"", 38 | "watch": "npm-watch sapbuild" 39 | }, 40 | "watch": { 41 | "sapbuild": { 42 | "patterns": [ 43 | "webapp", 44 | "ui5.yaml" 45 | ], 46 | "extensions": "js,json,xml,html,properties", 47 | "delay": 500, 48 | "runOnChangeOnly": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /proxy.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'), 3 | httpProxy = require('http-proxy'), 4 | fs = require('fs'), 5 | proxy = new httpProxy.createProxyServer(); 6 | 7 | const appRoute = { 8 | target: 'http://localhost:5000' 9 | }; 10 | const routing = JSON.parse(fs.readFileSync('./odata.json')); 11 | 12 | var allowCrossDomain = function(req, res) { 13 | res.header('Access-Control-Allow-Origin', '*'); 14 | res.header('Access-Control-Allow-Credentials', 'true'); 15 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 16 | res.header('Access-Control-Allow-Headers', 'X-Requested-With, Accept, Origin, Referer, User-Agent, Content-Type, Authorization, X-Mindflash-SessionID'); 17 | // intercept OPTIONS method 18 | if ('OPTIONS' === req.method) { 19 | res.header(200); 20 | } else { 21 | var dirname = req.url.replace(/^\/([^\/]*).*$/, '$1'); 22 | var route = routing[dirname] || appRoute; 23 | console.log(req.method + ': ' + route.target + req.url); 24 | proxy.web(req, res, route); 25 | } 26 | }; 27 | 28 | var app = express(); 29 | app.use(allowCrossDomain); 30 | app.listen(8005); 31 | console.log("Proxy started on http://localhost:8005"); 32 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | #### SAP Fiori Master-Detail Application Template #### 2 | 3 | This is your copy of the "SAP Fiori Worklist Application" template - ready for your custom developments! 4 | You can find the template version in your workspace in the manifest.json file under "sourceTemplate". 5 | 6 | This app can be deployed standalone or registered to an SAP Fiori Launchpad depending on the selection you initially 7 | made in the template wizard. All entry points (*.html) can be located via the test.html file in the test folder or the 8 | respective SAP Web IDE run configurations. 9 | 10 | SAP Web IDE templates are kept up to date with the latest best practices and recommendations, but they are not updated 11 | automatically! To migrate to a newer version of the template, re-create the template and copy over the changes as needed. 12 | 13 | For more information and documentation of all template features, please refer to the SAPUI5 Demo Kit: 14 | https://sapui5.hana.ondemand.com/#/topic/a460a7348a6c431a8bd967ab9fb8d918 15 | 16 | #### Happy Development! #### -------------------------------------------------------------------------------- /ui5.yaml: -------------------------------------------------------------------------------- 1 | specVersion: '1.0' 2 | metadata: 3 | name: PersonSkills 4 | type: application 5 | server: 6 | customMiddleware: 7 | - name: odataProxy 8 | beforeMiddleware: serveResources 9 | builder: 10 | bundles: 11 | - bundleDefinition: 12 | name: be/wl/PersonSkills/Component-preload.js 13 | defaultFileTypes: 14 | - ".js" 15 | - ".json" 16 | - ".xml" 17 | - ".html" 18 | - ".library" 19 | sections: 20 | - mode: raw 21 | filters: 22 | - be/wl/PersonSkills/regenerator-runtime/runtime.js 23 | - mode: preload 24 | filters: 25 | - be/wl/PersonSkills/manifest.json 26 | - be/wl/PersonSkills/controller/** 27 | - be/wl/PersonSkills/Component.js 28 | - be/wl/PersonSkills/i18n/** 29 | - be/wl/PersonSkills/model/** 30 | - be/wl/PersonSkills/ui5fixes/** 31 | - be/wl/PersonSkills/util/** 32 | - be/wl/PersonSkills/view/** 33 | - be/wl/PersonSkills/libs/** 34 | - be/wl/PersonSkills/test/** 35 | - be/wl/PersonSkills/service/** 36 | - be/wl/PersonSkills/state/** 37 | - be/wl/PersonSkills/localService/** 38 | resolve: false 39 | sort: true 40 | declareModules: false 41 | bundleOptions: 42 | optimize: true 43 | usePredefineCalls: true 44 | customTasks: 45 | - name: babel 46 | afterTask: replaceVersion 47 | --- 48 | specVersion: "1.0" 49 | kind: extension 50 | type: server-middleware 51 | metadata: 52 | name: odataProxy 53 | middleware: 54 | path: lib/middleware/odataProxy.js 55 | --- 56 | specVersion: "1.0" 57 | kind: extension 58 | type: task 59 | metadata: 60 | name: babel 61 | task: 62 | path: lib/tasks/babel.js -------------------------------------------------------------------------------- /webapp/Component.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/UIComponent", 3 | "sap/ui/Device", 4 | "./model/models", 5 | "./controller/ListSelector", 6 | "./controller/ErrorHandler", 7 | "./service/PersonService", 8 | "./state/PersonState" 9 | ], function (UIComponent, Device, models, ListSelector, ErrorHandler,PersonService,PersonState) { 10 | "use strict"; 11 | 12 | return UIComponent.extend("be.wl.PersonSkills.Component", { 13 | 14 | metadata : { 15 | manifest : "json" 16 | }, 17 | PERSON:"Person", 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 | init : function () { 25 | this._oPersonService = new PersonService(this.getModel()); 26 | this._oPersonState = new PersonState(this._oPersonService); 27 | this.oListSelector = new ListSelector(); 28 | this._oErrorHandler = new ErrorHandler(this); 29 | 30 | // set the device model 31 | this.setModel(models.createDeviceModel(), "device"); 32 | 33 | // call the base component's init function and create the App view 34 | UIComponent.prototype.init.apply(this, arguments); 35 | 36 | // create the views based on the url/hash 37 | this.getRouter().initialize(); 38 | }, 39 | getService: function (sService) { 40 | return this["_o" + sService + "Service"]; 41 | }, 42 | getState: function (sState) { 43 | return this["_o" + sState + "State"]; 44 | }, 45 | 46 | /** 47 | * The component is destroyed by UI5 automatically. 48 | * In this method, the ListSelector and ErrorHandler are destroyed. 49 | * @public 50 | * @override 51 | */ 52 | destroy : function () { 53 | this.oListSelector.destroy(); 54 | this._oErrorHandler.destroy(); 55 | // call the base component's destroy function 56 | UIComponent.prototype.destroy.apply(this, arguments); 57 | }, 58 | 59 | /** 60 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy 61 | * design mode class should be set, which influences the size appearance of some controls. 62 | * @public 63 | * @return {string} css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set 64 | */ 65 | getContentDensityClass : function() { 66 | if (this._sContentDensityClass === undefined) { 67 | // check whether FLP has already set the content density class; do nothing in this case 68 | // eslint-disable-next-line sap-no-proprietary-browser-api 69 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) { 70 | this._sContentDensityClass = ""; 71 | } else if (!Device.support.touch) { // apply "compact" mode if touch is not supported 72 | this._sContentDensityClass = "sapUiSizeCompact"; 73 | } else { 74 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table 75 | this._sContentDensityClass = "sapUiSizeCozy"; 76 | } 77 | } 78 | return this._sContentDensityClass; 79 | } 80 | 81 | }); 82 | }); -------------------------------------------------------------------------------- /webapp/controller/App.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel" 4 | ], function (BaseController, JSONModel) { 5 | "use strict"; 6 | 7 | return BaseController.extend("be.wl.PersonSkills.controller.App", { 8 | 9 | onInit : function () { 10 | var oViewModel, 11 | fnSetAppNotBusy, 12 | iOriginalBusyDelay = this.getView().getBusyIndicatorDelay(); 13 | 14 | oViewModel = new JSONModel({ 15 | busy : true, 16 | delay : 0, 17 | layout : "OneColumn", 18 | previousLayout : "", 19 | actionButtonsInfo : { 20 | midColumn : { 21 | fullScreen : false 22 | } 23 | } 24 | }); 25 | this.setModel(oViewModel, "appView"); 26 | 27 | fnSetAppNotBusy = function() { 28 | oViewModel.setProperty("/busy", false); 29 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 30 | }; 31 | 32 | // since then() has no "reject"-path attach to the MetadataFailed-Event to disable the busy indicator in case of an error 33 | this.getOwnerComponent().getModel().metadataLoaded().then(fnSetAppNotBusy); 34 | this.getOwnerComponent().getModel().attachMetadataFailed(fnSetAppNotBusy); 35 | 36 | // apply content density mode to root view 37 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass()); 38 | } 39 | 40 | }); 41 | }); -------------------------------------------------------------------------------- /webapp/controller/BaseController.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/mvc/Controller", 3 | "sap/ui/core/routing/History" 4 | ], function (Controller, History) { 5 | "use strict"; 6 | 7 | return Controller.extend("be.wl.PersonSkills.controller.BaseController", { 8 | /** 9 | * Convenience method for accessing the router in every controller of the application. 10 | * @public 11 | * @returns {sap.ui.core.routing.Router} the router for this component 12 | */ 13 | getIndexFromPath: function (oSource) { 14 | var sPath = oSource.getBindingContext("pers").getPath(); 15 | return parseInt(sPath.substr(sPath.lastIndexOf("/") + 1)); 16 | }, 17 | getRouter : function () { 18 | return this.getOwnerComponent().getRouter(); 19 | }, 20 | 21 | /** 22 | * Convenience method for getting the view model by name in every controller of the application. 23 | * @public 24 | * @param {string} sName the model name 25 | * @returns {sap.ui.model.Model} the model instance 26 | */ 27 | getModel : function (sName) { 28 | return this.getView().getModel(sName); 29 | }, 30 | 31 | /** 32 | * Convenience method for setting the view model in every controller of the application. 33 | * @public 34 | * @param {sap.ui.model.Model} oModel the model instance 35 | * @param {string} sName the model name 36 | * @returns {sap.ui.mvc.View} the view instance 37 | */ 38 | setModel : function (oModel, sName) { 39 | return this.getView().setModel(oModel, sName); 40 | }, 41 | 42 | /** 43 | * Convenience method for getting the resource bundle. 44 | * @public 45 | * @returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component 46 | */ 47 | getResourceBundle : function () { 48 | return this.getOwnerComponent().getModel("i18n").getResourceBundle(); 49 | }, 50 | 51 | /** 52 | * Event handler for navigating back. 53 | * It there is a history entry we go one step back in the browser history 54 | * If not, it will replace the current entry of the browser history with the master route. 55 | * @public 56 | */ 57 | onNavBack : function() { 58 | var sPreviousHash = History.getInstance().getPreviousHash(); 59 | 60 | if (sPreviousHash !== undefined) { 61 | // eslint-disable-next-line sap-no-history-manipulation 62 | history.go(-1); 63 | } else { 64 | this.getRouter().navTo("master", {}, true); 65 | } 66 | } 67 | 68 | }); 69 | 70 | }); -------------------------------------------------------------------------------- /webapp/controller/Detail.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel", 4 | "../model/formatter", 5 | "sap/m/library" 6 | ], function (BaseController, JSONModel, formatter, mobileLibrary) { 7 | "use strict"; 8 | 9 | // shortcut for sap.m.URLHelper 10 | var URLHelper = mobileLibrary.URLHelper; 11 | 12 | return BaseController.extend("be.wl.PersonSkills.controller.Detail", { 13 | 14 | formatter: formatter, 15 | 16 | /* =========================================================== */ 17 | /* lifecycle methods */ 18 | /* =========================================================== */ 19 | 20 | onInit: function () { 21 | // Model used to manipulate control states. The chosen values make sure, 22 | // detail page is busy indication immediately so there is no break in 23 | // between the busy indication for loading the view's meta data 24 | var oViewModel = new JSONModel({ 25 | busy: false, 26 | delay: 0, 27 | lineItemListTitle: this.getResourceBundle().getText("detailLineItemTableHeading") 28 | }); 29 | 30 | this.PersonState = this.getOwnerComponent().getState(this.getOwnerComponent().PERSON); 31 | this.setModel(this.PersonState.getModel(), "pers"); 32 | 33 | this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this); 34 | 35 | this.setModel(oViewModel, "detailView"); 36 | }, 37 | onSave: function (oEvent) { 38 | this.PersonState.newPerson().then((id) => { 39 | this.getRouter().navTo("object", { 40 | objectId: id 41 | }, true); 42 | }); 43 | }, 44 | onDeleteSkill:function(oEvent){ 45 | this.PersonState.deletePersonSkill(this.getIndexFromPath(oEvent.getSource())); 46 | }, 47 | /* =========================================================== */ 48 | /* event handlers */ 49 | /* =========================================================== */ 50 | 51 | /** 52 | * Event handler when the share by E-Mail button has been clicked 53 | * @public 54 | */ 55 | onSendEmailPress: function () { 56 | var oViewModel = this.getModel("detailView"); 57 | 58 | URLHelper.triggerEmail( 59 | null, 60 | oViewModel.getProperty("/shareSendEmailSubject"), 61 | oViewModel.getProperty("/shareSendEmailMessage") 62 | ); 63 | }, 64 | 65 | /** 66 | * Binds the view to the object path and expands the aggregated line items. 67 | * @function 68 | * @param {sap.ui.base.Event} oEvent pattern match event in route 'object' 69 | * @private 70 | */ 71 | _onObjectMatched: function (oEvent) { 72 | var sObjectId = oEvent.getParameter("arguments").objectId; 73 | this.getModel("appView").setProperty("/layout", "TwoColumnsMidExpanded"); 74 | if (sObjectId === "NEW") { 75 | this.PersonState.createPerson(); 76 | } else { 77 | this.getModel().metadataLoaded().then(() => { 78 | var sObjectPath = this.getModel().createKey("Persons", { 79 | Id: sObjectId 80 | }); 81 | this.getOwnerComponent().oListSelector.selectAListItem("/" + sObjectPath); 82 | 83 | this.PersonState.getPerson(sObjectId); 84 | }); 85 | } 86 | }, 87 | 88 | /** 89 | * Set the full screen mode to false and navigate to master page 90 | */ 91 | onCloseDetailPress: function () { 92 | this.getModel("appView").setProperty("/actionButtonsInfo/midColumn/fullScreen", false); 93 | // No item should be selected on master after detail page is closed 94 | this.getOwnerComponent().oListSelector.clearMasterListSelection(); 95 | this.getRouter().navTo("master"); 96 | }, 97 | 98 | /** 99 | * Toggle between full and non full screen mode. 100 | */ 101 | toggleFullScreen: function () { 102 | var bFullScreen = this.getModel("appView").getProperty("/actionButtonsInfo/midColumn/fullScreen"); 103 | this.getModel("appView").setProperty("/actionButtonsInfo/midColumn/fullScreen", !bFullScreen); 104 | if (!bFullScreen) { 105 | // store current layout and go full screen 106 | this.getModel("appView").setProperty("/previousLayout", this.getModel("appView").getProperty("/layout")); 107 | this.getModel("appView").setProperty("/layout", "MidColumnFullScreen"); 108 | } else { 109 | // reset to previous layout 110 | this.getModel("appView").setProperty("/layout", this.getModel("appView").getProperty("/previousLayout")); 111 | } 112 | } 113 | }); 114 | 115 | }); -------------------------------------------------------------------------------- /webapp/controller/DetailObjectNotFound.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController" 3 | ], function (BaseController) { 4 | "use strict"; 5 | 6 | return BaseController.extend("be.wl.PersonSkills.controller.DetailObjectNotFound", {}); 7 | }); -------------------------------------------------------------------------------- /webapp/controller/DetailObjectNotFound.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController" 3 | ], function (BaseController) { 4 | "use strict"; 5 | 6 | return BaseController.extend("be.wl.PersonSkills.controller.DetailObjectNotFound", {}); 7 | }); 8 | -------------------------------------------------------------------------------- /webapp/controller/ErrorHandler.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/base/Object", 3 | "sap/m/MessageBox" 4 | ], function (UI5Object, MessageBox) { 5 | "use strict"; 6 | 7 | return UI5Object.extend("be.wl.PersonSkills.controller.ErrorHandler", { 8 | 9 | /** 10 | * Handles application errors by automatically attaching to the model events and displaying errors when needed. 11 | * @class 12 | * @param {sap.ui.core.UIComponent} oComponent reference to the app's component 13 | * @public 14 | * @alias be.wl.PersonSkills.controller.ErrorHandler 15 | */ 16 | constructor : function (oComponent) { 17 | this._oResourceBundle = oComponent.getModel("i18n").getResourceBundle(); 18 | this._oComponent = oComponent; 19 | this._oModel = oComponent.getModel(); 20 | this._bMessageOpen = false; 21 | this._sErrorText = this._oResourceBundle.getText("errorText"); 22 | 23 | this._oModel.attachMetadataFailed(function (oEvent) { 24 | var oParams = oEvent.getParameters(); 25 | this._showServiceError(oParams.response); 26 | }, this); 27 | 28 | this._oModel.attachRequestFailed(function (oEvent) { 29 | var oParams = oEvent.getParameters(); 30 | // An entity that was not found in the service is also throwing a 404 error in oData. 31 | // We already cover this case with a notFound target so we skip it here. 32 | // A request that cannot be sent to the server is a technical error that we have to handle though 33 | if (oParams.response.statusCode !== "404" || (oParams.response.statusCode === 404 && oParams.response.responseText.indexOf("Cannot POST") === 0)) { 34 | this._showServiceError(oParams.response); 35 | } 36 | }, this); 37 | }, 38 | 39 | /** 40 | * Shows a {@link sap.m.MessageBox} when a service call has failed. 41 | * Only the first error message will be display. 42 | * @param {string} sDetails a technical error to be displayed on request 43 | * @private 44 | */ 45 | _showServiceError : function (sDetails) { 46 | if (this._bMessageOpen) { 47 | return; 48 | } 49 | this._bMessageOpen = true; 50 | MessageBox.error( 51 | this._sErrorText, 52 | { 53 | id : "serviceErrorMessageBox", 54 | details : sDetails, 55 | styleClass : this._oComponent.getContentDensityClass(), 56 | actions : [MessageBox.Action.CLOSE], 57 | onClose : function () { 58 | this._bMessageOpen = false; 59 | }.bind(this) 60 | } 61 | ); 62 | } 63 | 64 | }); 65 | 66 | }); -------------------------------------------------------------------------------- /webapp/controller/ListSelector.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/base/Object", 3 | "sap/base/Log" 4 | ], function (BaseObject, Log) { 5 | "use strict"; 6 | 7 | return BaseObject.extend("be.wl.PersonSkills.controller.ListSelector", { 8 | 9 | /** 10 | * Provides a convenience API for selecting list items. All the functions will wait until the initial load of the a List passed to the instance by the setBoundMasterList 11 | * function. 12 | * @class 13 | * @public 14 | * @alias be.wl.PersonSkills.controller.ListSelector 15 | */ 16 | 17 | constructor : function () { 18 | this._oWhenListHasBeenSet = new Promise(function (fnResolveListHasBeenSet) { 19 | this._fnResolveListHasBeenSet = fnResolveListHasBeenSet; 20 | }.bind(this)); 21 | // This promise needs to be created in the constructor, since it is allowed to 22 | // invoke selectItem functions before calling setBoundMasterList 23 | this.oWhenListLoadingIsDone = new Promise(function (fnResolve, fnReject) { 24 | // Used to wait until the setBound masterList function is invoked 25 | this._oWhenListHasBeenSet 26 | .then(function (oList) { 27 | oList.getBinding("items").attachEventOnce("dataReceived", 28 | function () { 29 | if (this._oList.getItems().length) { 30 | fnResolve({ 31 | list : oList 32 | }); 33 | } else { 34 | // No items in the list 35 | fnReject({ 36 | list : oList 37 | }); 38 | } 39 | }.bind(this) 40 | ); 41 | }.bind(this)); 42 | }.bind(this)); 43 | }, 44 | 45 | /** 46 | * A bound list should be passed in here. Should be done, before the list has received its initial data from the server. 47 | * May only be invoked once per ListSelector instance. 48 | * @param {sap.m.List} oList The list all the select functions will be invoked on. 49 | * @public 50 | */ 51 | setBoundMasterList : function (oList) { 52 | this._oList = oList; 53 | this._fnResolveListHasBeenSet(oList); 54 | }, 55 | 56 | /** 57 | * Tries to select and scroll to a list item with a matching binding context. If there are no items matching the binding context or the ListMode is none, 58 | * no selection/scrolling will happen 59 | * @param {string} sBindingPath the binding path matching the binding path of a list item 60 | * @public 61 | */ 62 | selectAListItem : function (sBindingPath) { 63 | 64 | this.oWhenListLoadingIsDone.then( 65 | function () { 66 | var oList = this._oList, 67 | oSelectedItem; 68 | 69 | if (oList.getMode() === "None") { 70 | return; 71 | } 72 | 73 | oSelectedItem = oList.getSelectedItem(); 74 | 75 | // skip update if the current selection is already matching the object path 76 | if (oSelectedItem && oSelectedItem.getBindingContext().getPath() === sBindingPath) { 77 | return; 78 | } 79 | 80 | oList.getItems().some(function (oItem) { 81 | if (oItem.getBindingContext() && oItem.getBindingContext().getPath() === sBindingPath) { 82 | oList.setSelectedItem(oItem); 83 | return true; 84 | } 85 | }); 86 | }.bind(this), 87 | function () { 88 | Log.warning("Could not select the list item with the path" + sBindingPath + " because the list encountered an error or had no items"); 89 | } 90 | ); 91 | }, 92 | 93 | /** 94 | * Removes all selections from master list. 95 | * Does not trigger 'selectionChange' event on master list, though. 96 | * @public 97 | */ 98 | clearMasterListSelection : function () { 99 | //use promise to make sure that 'this._oList' is available 100 | this._oWhenListHasBeenSet.then(function () { 101 | this._oList.removeSelections(true); 102 | }.bind(this)); 103 | } 104 | }); 105 | }); -------------------------------------------------------------------------------- /webapp/controller/Master.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel", 4 | "sap/ui/model/Filter", 5 | "sap/ui/model/Sorter", 6 | "sap/ui/model/FilterOperator", 7 | "sap/m/GroupHeaderListItem", 8 | "sap/ui/Device", 9 | "sap/ui/core/Fragment", 10 | "../model/formatter" 11 | ], function (BaseController, JSONModel, Filter, Sorter, FilterOperator, GroupHeaderListItem, Device, Fragment, formatter) { 12 | "use strict"; 13 | 14 | return BaseController.extend("be.wl.PersonSkills.controller.Master", { 15 | 16 | formatter: formatter, 17 | 18 | /* =========================================================== */ 19 | /* lifecycle methods */ 20 | /* =========================================================== */ 21 | 22 | /** 23 | * Called when the master list controller is instantiated. It sets up the event handling for the master/detail communication and other lifecycle tasks. 24 | * @public 25 | */ 26 | onInit : function () { 27 | // Control state model 28 | var oList = this.byId("list"), 29 | oViewModel = this._createViewModel(), 30 | // Put down master list's original value for busy indicator delay, 31 | // so it can be restored later on. Busy handling on the master list is 32 | // taken care of by the master list itself. 33 | iOriginalBusyDelay = oList.getBusyIndicatorDelay(); 34 | 35 | this._oGroupFunctions = { 36 | Id : function(oContext) { 37 | var iNumber = oContext.getProperty('Id'), 38 | key, text; 39 | if (iNumber <= 20) { 40 | key = "LE20"; 41 | text = this.getResourceBundle().getText("masterGroup1Header1"); 42 | } else { 43 | key = "GT20"; 44 | text = this.getResourceBundle().getText("masterGroup1Header2"); 45 | } 46 | return { 47 | key: key, 48 | text: text 49 | }; 50 | }.bind(this) 51 | }; 52 | 53 | this._oList = oList; 54 | // keeps the filter and search state 55 | this._oListFilterState = { 56 | aFilter : [], 57 | aSearch : [] 58 | }; 59 | 60 | this.setModel(oViewModel, "masterView"); 61 | // Make sure, busy indication is showing immediately so there is no 62 | // break after the busy indication for loading the view's meta data is 63 | // ended (see promise 'oWhenMetadataIsLoaded' in AppController) 64 | oList.attachEventOnce("updateFinished", function(){ 65 | // Restore original busy indicator delay for the list 66 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 67 | }); 68 | 69 | this.getView().addEventDelegate({ 70 | onBeforeFirstShow: function () { 71 | this.getOwnerComponent().oListSelector.setBoundMasterList(oList); 72 | }.bind(this) 73 | }); 74 | 75 | this.getRouter().getRoute("master").attachPatternMatched(this._onMasterMatched, this); 76 | this.getRouter().attachBypassed(this.onBypassed, this); 77 | }, 78 | 79 | /* =========================================================== */ 80 | /* event handlers */ 81 | /* =========================================================== */ 82 | 83 | /** 84 | * After list data is available, this handler method updates the 85 | * master list counter 86 | * @param {sap.ui.base.Event} oEvent the update finished event 87 | * @public 88 | */ 89 | onUpdateFinished : function (oEvent) { 90 | // update the master list object counter after new data is loaded 91 | this._updateListItemCount(oEvent.getParameter("total")); 92 | }, 93 | 94 | /** 95 | * Event handler for the master search field. Applies current 96 | * filter value and triggers a new search. If the search field's 97 | * 'refresh' button has been pressed, no new search is triggered 98 | * and the list binding is refresh instead. 99 | * @param {sap.ui.base.Event} oEvent the search event 100 | * @public 101 | */ 102 | onSearch : function (oEvent) { 103 | if (oEvent.getParameters().refreshButtonPressed) { 104 | // Search field's 'refresh' button has been pressed. 105 | // This is visible if you select any master list item. 106 | // In this case no new search is triggered, we only 107 | // refresh the list binding. 108 | this.onRefresh(); 109 | return; 110 | } 111 | 112 | var sQuery = oEvent.getParameter("query"); 113 | 114 | if (sQuery) { 115 | this._oListFilterState.aSearch = [new Filter("Firstname", FilterOperator.Contains, sQuery)]; 116 | } else { 117 | this._oListFilterState.aSearch = []; 118 | } 119 | this._applyFilterSearch(); 120 | 121 | }, 122 | 123 | /** 124 | * Event handler for refresh event. Keeps filter, sort 125 | * and group settings and refreshes the list binding. 126 | * @public 127 | */ 128 | onRefresh : function () { 129 | this._oList.getBinding("items").refresh(); 130 | }, 131 | 132 | /** 133 | * Event handler for the filter, sort and group buttons to open the ViewSettingsDialog. 134 | * @param {sap.ui.base.Event} oEvent the button press event 135 | * @public 136 | */ 137 | onOpenViewSettings : function (oEvent) { 138 | var sDialogTab = "filter"; 139 | if (oEvent.getSource() instanceof sap.m.Button) { 140 | var sButtonId = oEvent.getSource().getId(); 141 | if (sButtonId.match("sort")) { 142 | sDialogTab = "sort"; 143 | } else if (sButtonId.match("group")) { 144 | sDialogTab = "group"; 145 | } 146 | } 147 | // load asynchronous XML fragment 148 | if (!this.byId("viewSettingsDialog")) { 149 | Fragment.load({ 150 | id: this.getView().getId(), 151 | name: "be.wl.PersonSkills.view.ViewSettingsDialog", 152 | controller: this 153 | }).then(function(oDialog){ 154 | // connect dialog to the root view of this component (models, lifecycle) 155 | this.getView().addDependent(oDialog); 156 | oDialog.addStyleClass(this.getOwnerComponent().getContentDensityClass()); 157 | oDialog.open(sDialogTab); 158 | }.bind(this)); 159 | } else { 160 | this.byId("viewSettingsDialog").open(sDialogTab); 161 | } 162 | }, 163 | 164 | /** 165 | * Event handler called when ViewSettingsDialog has been confirmed, i.e. 166 | * has been closed with 'OK'. In the case, the currently chosen filters, sorters or groupers 167 | * are applied to the master list, which can also mean that they 168 | * are removed from the master list, in case they are 169 | * removed in the ViewSettingsDialog. 170 | * @param {sap.ui.base.Event} oEvent the confirm event 171 | * @public 172 | */ 173 | onConfirmViewSettingsDialog : function (oEvent) { 174 | var aFilterItems = oEvent.getParameters().filterItems, 175 | aFilters = [], 176 | aCaptions = []; 177 | 178 | // update filter state: 179 | // combine the filter array and the filter string 180 | aFilterItems.forEach(function (oItem) { 181 | switch (oItem.getKey()) { 182 | case "Filter1" : 183 | aFilters.push(new Filter("Id", FilterOperator.LE, 100)); 184 | break; 185 | case "Filter2" : 186 | aFilters.push(new Filter("Id", FilterOperator.GT, 100)); 187 | break; 188 | default : 189 | break; 190 | } 191 | aCaptions.push(oItem.getText()); 192 | }); 193 | 194 | this._oListFilterState.aFilter = aFilters; 195 | this._updateFilterBar(aCaptions.join(", ")); 196 | this._applyFilterSearch(); 197 | this._applySortGroup(oEvent); 198 | }, 199 | 200 | /** 201 | * Apply the chosen sorter and grouper to the master list 202 | * @param {sap.ui.base.Event} oEvent the confirm event 203 | * @private 204 | */ 205 | _applySortGroup: function (oEvent) { 206 | var mParams = oEvent.getParameters(), 207 | sPath, 208 | bDescending, 209 | aSorters = []; 210 | // apply sorter to binding 211 | // (grouping comes before sorting) 212 | if (mParams.groupItem) { 213 | sPath = mParams.groupItem.getKey(); 214 | bDescending = mParams.groupDescending; 215 | var vGroup = this._oGroupFunctions[sPath]; 216 | aSorters.push(new Sorter(sPath, bDescending, vGroup)); 217 | } 218 | sPath = mParams.sortItem.getKey(); 219 | bDescending = mParams.sortDescending; 220 | aSorters.push(new Sorter(sPath, bDescending)); 221 | this._oList.getBinding("items").sort(aSorters); 222 | }, 223 | 224 | /** 225 | * Event handler for the list selection event 226 | * @param {sap.ui.base.Event} oEvent the list selectionChange event 227 | * @public 228 | */ 229 | onSelectionChange : function (oEvent) { 230 | var oList = oEvent.getSource(), 231 | bSelected = oEvent.getParameter("selected"); 232 | 233 | // skip navigation when deselecting an item in multi selection mode 234 | if (!(oList.getMode() === "MultiSelect" && !bSelected)) { 235 | // get the list item, either from the listItem parameter or from the event's source itself (will depend on the device-dependent mode). 236 | this._showDetail(oEvent.getParameter("listItem") || oEvent.getSource()); 237 | } 238 | }, 239 | 240 | /** 241 | * Event handler for the bypassed event, which is fired when no routing pattern matched. 242 | * If there was an object selected in the master list, that selection is removed. 243 | * @public 244 | */ 245 | onBypassed : function () { 246 | this._oList.removeSelections(true); 247 | }, 248 | 249 | /** 250 | * Used to create GroupHeaders with non-capitalized caption. 251 | * These headers are inserted into the master list to 252 | * group the master list's items. 253 | * @param {Object} oGroup group whose text is to be displayed 254 | * @public 255 | * @returns {sap.m.GroupHeaderListItem} group header with non-capitalized caption. 256 | */ 257 | createGroupHeader : function (oGroup) { 258 | return new GroupHeaderListItem({ 259 | title : oGroup.text, 260 | upperCase : false 261 | }); 262 | }, 263 | 264 | /** 265 | * Event handler for navigating back. 266 | * We navigate back in the browser historz 267 | * @public 268 | */ 269 | onNavBack : function() { 270 | // eslint-disable-next-line sap-no-history-manipulation 271 | history.go(-1); 272 | }, 273 | 274 | /* =========================================================== */ 275 | /* begin: internal methods */ 276 | /* =========================================================== */ 277 | 278 | 279 | _createViewModel : function() { 280 | return new JSONModel({ 281 | isFilterBarVisible: false, 282 | filterBarLabel: "", 283 | delay: 0, 284 | title: this.getResourceBundle().getText("masterTitleCount", [0]), 285 | noDataText: this.getResourceBundle().getText("masterListNoDataText"), 286 | sortBy: "Firstname", 287 | groupBy: "None" 288 | }); 289 | }, 290 | 291 | _onMasterMatched : function() { 292 | //Set the layout property of the FCL control to 'OneColumn' 293 | this.getModel("appView").setProperty("/layout", "OneColumn"); 294 | }, 295 | 296 | /** 297 | * Shows the selected item on the detail page 298 | * On phones a additional history entry is created 299 | * @param {sap.m.ObjectListItem} oItem selected Item 300 | * @private 301 | */ 302 | _showDetail : function (oItem) { 303 | var bReplace = !Device.system.phone; 304 | // set the layout property of FCL control to show two columns 305 | this.getModel("appView").setProperty("/layout", "TwoColumnsMidExpanded"); 306 | this.getRouter().navTo("object", { 307 | objectId : oItem.getBindingContext().getProperty("Id") 308 | }, bReplace); 309 | }, 310 | onCreatePerson:function(oEvent){ 311 | var bReplace = !Device.system.phone; 312 | // set the layout property of FCL control to show two columns 313 | this.getOwnerComponent().oListSelector.clearMasterListSelection(); 314 | this.getModel("appView").setProperty("/layout", "TwoColumnsMidExpanded"); 315 | this.getRouter().navTo("object", { 316 | objectId : "NEW" 317 | }, bReplace); 318 | }, 319 | 320 | /** 321 | * Sets the item count on the master list header 322 | * @param {integer} iTotalItems the total number of items in the list 323 | * @private 324 | */ 325 | _updateListItemCount : function (iTotalItems) { 326 | var sTitle; 327 | // only update the counter if the length is final 328 | if (this._oList.getBinding("items").isLengthFinal()) { 329 | sTitle = this.getResourceBundle().getText("masterTitleCount", [iTotalItems]); 330 | this.getModel("masterView").setProperty("/title", sTitle); 331 | } 332 | }, 333 | 334 | /** 335 | * Internal helper method to apply both filter and search state together on the list binding 336 | * @private 337 | */ 338 | _applyFilterSearch : function () { 339 | var aFilters = this._oListFilterState.aSearch.concat(this._oListFilterState.aFilter), 340 | oViewModel = this.getModel("masterView"); 341 | this._oList.getBinding("items").filter(aFilters, "Application"); 342 | // changes the noDataText of the list in case there are no filter results 343 | if (aFilters.length !== 0) { 344 | oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataWithFilterOrSearchText")); 345 | } else if (this._oListFilterState.aSearch.length > 0) { 346 | // only reset the no data text to default when no new search was triggered 347 | oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataText")); 348 | } 349 | }, 350 | 351 | /** 352 | * Internal helper method that sets the filter bar visibility property and the label's caption to be shown 353 | * @param {string} sFilterBarText the selected filter value 354 | * @private 355 | */ 356 | _updateFilterBar : function (sFilterBarText) { 357 | var oViewModel = this.getModel("masterView"); 358 | oViewModel.setProperty("/isFilterBarVisible", (this._oListFilterState.aFilter.length > 0)); 359 | oViewModel.setProperty("/filterBarLabel", this.getResourceBundle().getText("masterFilterBarText", [sFilterBarText])); 360 | } 361 | 362 | }); 363 | 364 | }); -------------------------------------------------------------------------------- /webapp/controller/NotFound.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController" 3 | ], function (BaseController) { 4 | "use strict"; 5 | 6 | return BaseController.extend("be.wl.PersonSkills.controller.NotFound", { 7 | 8 | onInit: function () { 9 | this.getRouter().getTarget("notFound").attachDisplay(this._onNotFoundDisplayed, this); 10 | }, 11 | 12 | _onNotFoundDisplayed : function () { 13 | this.getModel("appView").setProperty("/layout", "OneColumn"); 14 | } 15 | }); 16 | }); -------------------------------------------------------------------------------- /webapp/i18n/i18n.properties: -------------------------------------------------------------------------------- 1 | # This is the resource bundle for PersonSkills 2 | 3 | #XTIT: Application name 4 | appTitle=PersonSkills 5 | 6 | #YDES: Application description 7 | appDescription=PersonSkills 8 | 9 | #~~~ Master View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | #XTIT: Master view title with placeholder for the number of items 12 | masterTitleCount= ({0}) 13 | 14 | #XTOL: Tooltip for the search field 15 | masterSearchTooltip=Enter an name or a part of it. 16 | 17 | #XBLI: text for a list with no data 18 | masterListNoDataText=No are currently available 19 | 20 | #XBLI: text for a list with no data with filter or search 21 | masterListNoDataWithFilterOrSearchText=No matching found 22 | 23 | #XSEL: Option to sort the master list by Firstname 24 | masterSort1=Sort By 25 | 26 | #XSEL: Option to sort the master list by Id 27 | masterSort2=Sort By 28 | 29 | #XSEL: Option to filter the master list by Id 30 | masterFilterName= 31 | 32 | #XSEL: Option to not filter the master list 33 | masterFilterNone=none 34 | 35 | 36 | #XSEL: Option to filter the master list by if the value is less than 100 37 | masterFilter1=<100 38 | 39 | #XSEL: Option to filter the master list by if the value is greater than 100 40 | masterFilter2=>100 41 | 42 | #YMSG: Filter text that is displayed above the master list 43 | masterFilterBarText=Filtered by {0} 44 | 45 | #XSEL: Option to not group the master list 46 | masterGroupNone=(Not grouped) 47 | 48 | #XSEL: Option to group the master list by Id 49 | masterGroup1= Group 50 | 51 | #XGRP: Group header Id 52 | masterGroup1Header1= 20 or less 53 | 54 | #XGRP: Group header Id 55 | masterGroup1Header2= higher than 20 56 | 57 | #~~~ Detail View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | #XTOL: Icon Tab Bar Info 60 | detailIconTabBarInfo=Info 61 | 62 | #XTOL: Icon Tab Bar Attachments 63 | detailIconTabBarAttachments=Attachments 64 | 65 | #XTOL: Tooltip text for close column button 66 | closeColumn=Close 67 | 68 | #XBLI: Text for the PersonHasSkills table with no data 69 | detailLineItemTableNoDataText=No 70 | 71 | #XTIT: Title of the PersonHasSkills table 72 | detailLineItemTableHeading= 73 | 74 | #XTIT: Title of the PersonHasSkills table 75 | detailLineItemTableHeadingCount= ({0}) 76 | 77 | #XGRP: Title for the SkillName column in the PersonHasSkills table 78 | detailLineItemTableIDColumn= 79 | 80 | #XGRP: Title for the Score column in the PersonHasSkills table 81 | detailLineItemTableUnitNumberColumn= 82 | 83 | #XTIT: Send E-Mail subject 84 | shareSendEmailObjectSubject= {0} 85 | 86 | #YMSG: Send E-Mail message 87 | shareSendEmailObjectMessage= {0} (id: {1})\r\n{2} 88 | 89 | #XBUT: Text for the send e-mail button 90 | sendEmail=Send E-Mail 91 | 92 | #XTIT: Title text for the price 93 | priceTitle=Price 94 | 95 | #~~~ Not Found View ~~~~~~~~~~~~~~~~~~~~~~~ 96 | 97 | #XTIT: Not found view title 98 | notFoundTitle=Not Found 99 | 100 | #YMSG: The Persons not found text is displayed when there is no Persons with this id 101 | noObjectFoundText=This is not available 102 | 103 | #YMSG: The not found text is displayed when there was an error loading the resource (404 error) 104 | notFoundText=The requested resource was not found 105 | 106 | #~~~ Not Available View ~~~~~~~~~~~~~~~~~~~~~~~ 107 | 108 | #XTIT: Master view title 109 | notAvailableViewTitle= 110 | 111 | #~~~ Error Handling ~~~~~~~~~~~~~~~~~~~~~~~ 112 | 113 | #YMSG: Error dialog description 114 | errorText=Sorry, a technical error occurred! Please try again later. -------------------------------------------------------------------------------- /webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PersonSkills 7 | 8 | 19 | 20 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /webapp/libs/observable-slim.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Observable Slim 3 | * Version 0.1.5 4 | * https://github.com/elliotnb/observable-slim 5 | * 6 | * Licensed under the MIT license: 7 | * http://www.opensource.org/licenses/MIT 8 | * 9 | * Observable Slim is a singleton that allows you to observe changes made to an object and any nested 10 | * children of that object. It is intended to assist with one-way data binding, that is, in MVC parlance, 11 | * reflecting changes in the model to the view. Observable Slim aspires to be as lightweight and easily 12 | * understood as possible. Minifies down to roughly 3000 characters. 13 | */ 14 | var ObservableSlim = (function() { 15 | var paths = []; 16 | // An array that stores all of the observables created through the public create() method below. 17 | var observables = []; 18 | // An array of all the objects that we have assigned Proxies to 19 | var targets = []; 20 | 21 | // An array of arrays containing the Proxies created for each target object. targetsProxy is index-matched with 22 | // 'targets' -- together, the pair offer a Hash table where the key is not a string nor number, but the actual target object 23 | var targetsProxy = []; 24 | 25 | // this variable tracks duplicate proxies assigned to the same target. 26 | // the 'set' handler below will trigger the same change on all other Proxies tracking the same target. 27 | // however, in order to avoid an infinite loop of Proxies triggering and re-triggering one another, we use dupProxy 28 | // to track that a given Proxy was modified from the 'set' handler 29 | var dupProxy = null; 30 | 31 | var _getProperty = function(obj, path) { 32 | return path.split('.').reduce(function(prev, curr) { 33 | return prev ? prev[curr] : undefined 34 | }, obj || self) 35 | }; 36 | 37 | /* Function: _create 38 | Private internal function that is invoked to create a new ES6 Proxy whose changes we can observe through 39 | the Observerable.observe() method. 40 | Parameters: 41 | target - required, plain JavaScript object that we want to observe for changes. 42 | domDelay - batch up changes on a 10ms delay so a series of changes can be processed in one DOM update. 43 | originalObservable - object, the original observable created by the user, exists for recursion purposes, 44 | allows one observable to observe change on any nested/child objects. 45 | originalPath - array of objects, each object having the properties 'target' and 'property' -- target referring to the observed object itself 46 | and property referring to the name of that object in the nested structure. the path of the property in relation to the target 47 | on the original observable, exists for recursion purposes, allows one observable to observe change on any nested/child objects. 48 | Returns: 49 | An ES6 Proxy object. 50 | */ 51 | var _create = function(target, domDelay, originalObservable, originalPath) { 52 | 53 | var observable = originalObservable || null; 54 | 55 | // record the nested path taken to access this object -- if there was no path then we provide the first empty entry 56 | var path = originalPath || [{"target":target,"property":""}]; 57 | paths.push(path); 58 | 59 | // in order to accurately report the "previous value" of the "length" property on an Array 60 | // we must use a helper property because intercepting a length change is not always possible as of 8/13/2018 in 61 | // Chrome -- the new `length` value is already set by the time the `set` handler is invoked 62 | if (target instanceof Array) target.__length = target.length; 63 | 64 | var changes = []; 65 | 66 | /* Function: _getPath 67 | Returns a string of the nested path (in relation to the top-level observed object) 68 | of the property being modified or deleted. 69 | Parameters: 70 | target - the object whose property is being modified or deleted. 71 | property - the string name of the property 72 | jsonPointer - optional, set to true if the string path should be formatted as a JSON pointer. 73 | Returns: 74 | String of the nested path (e.g., hello.testing.1.bar or, if JSON pointer, /hello/testing/1/bar 75 | */ 76 | var _getPath = function(target, property, jsonPointer) { 77 | 78 | var fullPath = ""; 79 | var lastTarget = null; 80 | 81 | // loop over each item in the path and append it to full path 82 | for (var i = 0; i < path.length; i++) { 83 | 84 | // if the current object was a member of an array, it's possible that the array was at one point 85 | // mutated and would cause the position of the current object in that array to change. we perform an indexOf 86 | // lookup here to determine the current position of that object in the array before we add it to fullPath 87 | if (lastTarget instanceof Array && !isNaN(path[i].property)) { 88 | path[i].property = lastTarget.indexOf(path[i].target); 89 | } 90 | 91 | fullPath = fullPath + "." + path[i].property 92 | lastTarget = path[i].target; 93 | } 94 | 95 | // add the current property 96 | fullPath = fullPath + "." + property; 97 | 98 | // remove the beginning two dots -- ..foo.bar becomes foo.bar (the first item in the nested chain doesn't have a property name) 99 | fullPath = fullPath.substring(2); 100 | 101 | if (jsonPointer === true) fullPath = "/" + fullPath.replace(/\./g, "/"); 102 | 103 | return fullPath; 104 | }; 105 | 106 | var _notifyObservers = function(numChanges) { 107 | 108 | // if the observable is paused, then we don't want to execute any of the observer functions 109 | if (observable.paused === true) return; 110 | 111 | // execute observer functions on a 10ms settimeout, this prevents the observer functions from being executed 112 | // separately on every change -- this is necessary because the observer functions will often trigger UI updates 113 | if (domDelay === true) { 114 | setTimeout(function() { 115 | if (numChanges === changes.length) { 116 | 117 | // we create a copy of changes before passing it to the observer functions because even if the observer function 118 | // throws an error, we still need to ensure that changes is reset to an empty array so that old changes don't persist 119 | var changesCopy = changes.slice(0); 120 | changes = []; 121 | 122 | // invoke any functions that are observing changes 123 | for (var i = 0; i < observable.observers.length; i++) observable.observers[i](changesCopy); 124 | 125 | } 126 | },10); 127 | } else { 128 | 129 | // we create a copy of changes before passing it to the observer functions because even if the observer function 130 | // throws an error, we still need to ensure that changes is reset to an empty array so that old changes don't persist 131 | var changesCopy = changes.slice(0); 132 | changes = []; 133 | 134 | // invoke any functions that are observing changes 135 | for (var i = 0; i < observable.observers.length; i++) observable.observers[i](changesCopy); 136 | 137 | } 138 | }; 139 | 140 | var handler = { 141 | get: function(target, property) { 142 | 143 | // implement a simple check for whether or not the object is a proxy, this helps the .create() method avoid 144 | // creating Proxies of Proxies. 145 | if (property === "__getTarget") { 146 | return target; 147 | } else if (property === "__isProxy") { 148 | return true; 149 | // from the perspective of a given observable on a parent object, return the parent object of the given nested object 150 | } else if (property === "__getParent") { 151 | return function(i) { 152 | if (typeof i === "undefined") var i = 1; 153 | var parentPath = _getPath(target, "__getParent").split("."); 154 | parentPath.splice(-(i+1),(i+1)); 155 | return _getProperty(observable.parentProxy, parentPath.join(".")); 156 | } 157 | // return the full path of the current object relative to the parent observable 158 | } else if (property === "__getPath") { 159 | // strip off the 12 characters for ".__getParent" 160 | var parentPath = _getPath(target, "__getParent"); 161 | return parentPath.slice(0, -12); 162 | } 163 | 164 | // for performance improvements, we assign this to a variable so we do not have to lookup the property value again 165 | var targetProp = target[property]; 166 | if (target instanceof Date && targetProp instanceof Function && targetProp !== null) { 167 | return targetProp.bind(target); 168 | } 169 | 170 | // if we are traversing into a new object, then we want to record path to that object and return a new observable. 171 | // recursively returning a new observable allows us a single Observable.observe() to monitor all changes on 172 | // the target object and any objects nested within. 173 | if (targetProp instanceof Object && targetProp !== null && target.hasOwnProperty(property)) { 174 | 175 | // if we've found a proxy nested on the object, then we want to retrieve the original object behind that proxy 176 | if (targetProp.__isProxy === true) targetProp = targetProp.__getTarget; 177 | 178 | // if the object accessed by the user (targetProp) already has a __targetPosition AND the object 179 | // stored at target[targetProp.__targetPosition] is not null, then that means we are already observing this object 180 | // we might be able to return a proxy that we've already created for the object 181 | if (targetProp.__targetPosition > -1 && targets[targetProp.__targetPosition] !== null) { 182 | 183 | // loop over the proxies that we've created for this object 184 | var ttp = targetsProxy[targetProp.__targetPosition]; 185 | for (var i = 0, l = ttp.length; i < l; i++) { 186 | 187 | // if we find a proxy that was setup for this particular observable, then return that proxy 188 | if (observable === ttp[i].observable) { 189 | return ttp[i].proxy; 190 | } 191 | } 192 | } 193 | 194 | // if we're arrived here, then that means there is no proxy for the object the user just accessed, so we 195 | // have to create a new proxy for it 196 | 197 | // create a shallow copy of the path array -- if we didn't create a shallow copy then all nested objects would share the same path array and the path wouldn't be accurate 198 | var newPath = path.slice(0); 199 | newPath.push({"target":targetProp,"property":property}); 200 | return _create(targetProp, domDelay, observable, newPath); 201 | } else { 202 | return targetProp; 203 | } 204 | }, 205 | deleteProperty: function(target, property) { 206 | 207 | // was this change an original change or was it a change that was re-triggered below 208 | var originalChange = true; 209 | if (dupProxy === proxy) { 210 | originalChange = false; 211 | dupProxy = null; 212 | } 213 | 214 | // in order to report what the previous value was, we must make a copy of it before it is deleted 215 | var previousValue = Object.assign({}, target); 216 | 217 | // record the deletion that just took place 218 | changes.push({ 219 | "type":"delete" 220 | ,"target":target 221 | ,"property":property 222 | ,"newValue":null 223 | ,"previousValue":previousValue[property] 224 | ,"currentPath":_getPath(target, property) 225 | ,"jsonPointer":_getPath(target, property, true) 226 | ,"proxy":proxy 227 | }); 228 | 229 | if (originalChange === true) { 230 | 231 | // perform the delete that we've trapped if changes are not paused for this observable 232 | if (!observable.changesPaused) delete target[property]; 233 | 234 | for (var a = 0, l = targets.length; a < l; a++) if (target === targets[a]) break; 235 | 236 | // loop over each proxy and see if the target for this change has any other proxies 237 | var currentTargetProxy = targetsProxy[a] || []; 238 | 239 | var b = currentTargetProxy.length; 240 | while (b--) { 241 | // if the same target has a different proxy 242 | if (currentTargetProxy[b].proxy !== proxy) { 243 | // !!IMPORTANT!! store the proxy as a duplicate proxy (dupProxy) -- this will adjust the behavior above appropriately (that is, 244 | // prevent a change on dupProxy from re-triggering the same change on other proxies) 245 | dupProxy = currentTargetProxy[b].proxy; 246 | 247 | // make the same delete on the different proxy for the same target object. it is important that we make this change *after* we invoke the same change 248 | // on any other proxies so that the previousValue can show up correct for the other proxies 249 | delete currentTargetProxy[b].proxy[property]; 250 | } 251 | } 252 | 253 | } 254 | 255 | _notifyObservers(changes.length); 256 | 257 | return true; 258 | 259 | }, 260 | set: function(target, property, value, receiver) { 261 | 262 | // if the value we're assigning is an object, then we want to ensure 263 | // that we're assigning the original object, not the proxy, in order to avoid mixing 264 | // the actual targets and proxies -- creates issues with path logging if we don't do this 265 | if (value && value.__isProxy) value = value.__getTarget; 266 | 267 | // was this change an original change or was it a change that was re-triggered below 268 | var originalChange = true; 269 | if (dupProxy === proxy) { 270 | originalChange = false; 271 | dupProxy = null; 272 | } 273 | 274 | // improve performance by saving direct references to the property 275 | var targetProp = target[property]; 276 | 277 | // Only record this change if: 278 | // 1. the new value differs from the old one 279 | // 2. OR if this proxy was not the original proxy to receive the change 280 | // 3. OR the modified target is an array and the modified property is "length" and our helper property __length indicates that the array length has changed 281 | // 282 | // Regarding #3 above: mutations of arrays via .push or .splice actually modify the .length before the set handler is invoked 283 | // so in order to accurately report the correct previousValue for the .length, we have to use a helper property. 284 | if (targetProp !== value || originalChange === false || (property === "length" && target instanceof Array && target.__length !== value)) { 285 | 286 | var foundObservable = true; 287 | 288 | var typeOfTargetProp = (typeof targetProp); 289 | 290 | // determine if we're adding something new or modifying somethat that already existed 291 | var type = "update"; 292 | if (typeOfTargetProp === "undefined") type = "add"; 293 | 294 | // store the change that just occurred. it is important that we store the change before invoking the other proxies so that the previousValue is correct 295 | changes.push({ 296 | "type":type 297 | ,"target":target 298 | ,"property":property 299 | ,"newValue":value 300 | ,"previousValue":receiver[property] 301 | ,"currentPath":_getPath(target, property) 302 | ,"jsonPointer":_getPath(target, property, true) 303 | ,"proxy":proxy 304 | }); 305 | 306 | // mutations of arrays via .push or .splice actually modify the .length before the set handler is invoked 307 | // so in order to accurately report the correct previousValue for the .length, we have to use a helper property. 308 | if (property === "length" && target instanceof Array && target.__length !== value) { 309 | changes[changes.length-1].previousValue = target.__length; 310 | target.__length = value; 311 | } 312 | 313 | // !!IMPORTANT!! if this proxy was the first proxy to receive the change, then we need to go check and see 314 | // if there are other proxies for the same project. if there are, then we will modify those proxies as well so the other 315 | // observers can be modified of the change that has occurred. 316 | if (originalChange === true) { 317 | 318 | // because the value actually differs than the previous value 319 | // we need to store the new value on the original target object, 320 | // but only as long as changes have not been paused 321 | if (!observable.changesPaused) target[property] = value; 322 | 323 | 324 | foundObservable = false; 325 | 326 | var targetPosition = target.__targetPosition; 327 | var z = targetsProxy[targetPosition].length; 328 | 329 | // find the parent target for this observable -- if the target for that observable has not been removed 330 | // from the targets array, then that means the observable is still active and we should notify the observers of this change 331 | while (z--) { 332 | if (observable === targetsProxy[targetPosition][z].observable) { 333 | if (targets[targetsProxy[targetPosition][z].observable.parentTarget.__targetPosition] !== null) { 334 | foundObservable = true; 335 | break; 336 | } 337 | } 338 | } 339 | 340 | // if we didn't find an observable for this proxy, then that means .remove(proxy) was likely invoked 341 | // so we no longer need to notify any observer function about the changes, but we still need to update the 342 | // value of the underlying original objectm see below: target[property] = value; 343 | if (foundObservable) { 344 | 345 | // loop over each proxy and see if the target for this change has any other proxies 346 | var currentTargetProxy = targetsProxy[targetPosition]; 347 | for (var b = 0, l = currentTargetProxy.length; b < l; b++) { 348 | // if the same target has a different proxy 349 | if (currentTargetProxy[b].proxy !== proxy) { 350 | 351 | // !!IMPORTANT!! store the proxy as a duplicate proxy (dupProxy) -- this will adjust the behavior above appropriately (that is, 352 | // prevent a change on dupProxy from re-triggering the same change on other proxies) 353 | dupProxy = currentTargetProxy[b].proxy; 354 | 355 | // invoke the same change on the different proxy for the same target object. it is important that we make this change *after* we invoke the same change 356 | // on any other proxies so that the previousValue can show up correct for the other proxies 357 | currentTargetProxy[b].proxy[property] = value; 358 | 359 | } 360 | } 361 | 362 | // if the property being overwritten is an object, then that means this observable 363 | // will need to stop monitoring this object and any nested objects underneath the overwritten object else they'll become 364 | // orphaned and grow memory usage. we excute this on a setTimeout so that the clean-up process does not block 365 | // the UI rendering -- there's no need to execute the clean up immediately 366 | setTimeout(function() { 367 | 368 | if (typeOfTargetProp === "object" && targetProp !== null) { 369 | 370 | // check if the to-be-overwritten target property still exists on the target object 371 | // if it does still exist on the object, then we don't want to stop observing it. this resolves 372 | // an issue where array .sort() triggers objects to be overwritten, but instead of being overwritten 373 | // and discarded, they are shuffled to a new position in the array 374 | var keys = Object.keys(target); 375 | for (var i = 0, l = keys.length; i < l; i++) { 376 | if (target[keys[i]] === targetProp) return; 377 | } 378 | 379 | var stillExists = false; 380 | 381 | // now we perform the more expensive search recursively through the target object. 382 | // if we find the targetProp (that was just overwritten) still exists somewhere else 383 | // further down in the object, then we still need to observe the targetProp on this observable. 384 | (function iterate(target) { 385 | var keys = Object.keys(target); 386 | for (var i = 0, l = keys.length; i < l; i++) { 387 | 388 | var property = keys[i]; 389 | var nestedTarget = target[property]; 390 | 391 | if (nestedTarget instanceof Object && nestedTarget !== null) iterate(nestedTarget); 392 | if (nestedTarget === targetProp) { 393 | stillExists = true; 394 | return; 395 | } 396 | }; 397 | })(target); 398 | 399 | // even though targetProp was overwritten, if it still exists somewhere else on the object, 400 | // then we don't want to remove the observable for that object (targetProp) 401 | if (stillExists === true) return; 402 | 403 | // loop over each property and recursively invoke the `iterate` function for any 404 | // objects nested on targetProp 405 | (function iterate(obj) { 406 | 407 | var keys = Object.keys(obj); 408 | for (var i = 0, l = keys.length; i < l; i++) { 409 | var objProp = obj[keys[i]]; 410 | if (objProp instanceof Object && objProp !== null) iterate(objProp); 411 | } 412 | 413 | // if there are any existing target objects (objects that we're already observing)... 414 | var c = -1; 415 | for (var i = 0, l = targets.length; i < l; i++) { 416 | if (obj === targets[i]) { 417 | c = i; 418 | break; 419 | } 420 | } 421 | if (c > -1) { 422 | 423 | // ...then we want to determine if the observables for that object match our current observable 424 | var currentTargetProxy = targetsProxy[c]; 425 | var d = currentTargetProxy.length; 426 | 427 | while (d--) { 428 | // if we do have an observable monitoring the object thats about to be overwritten 429 | // then we can remove that observable from the target object 430 | if (observable === currentTargetProxy[d].observable) { 431 | currentTargetProxy.splice(d,1); 432 | break; 433 | } 434 | } 435 | 436 | // if there are no more observables assigned to the target object, then we can remove 437 | // the target object altogether. this is necessary to prevent growing memory consumption particularly with large data sets 438 | if (currentTargetProxy.length == 0) { 439 | // targetsProxy.splice(c,1); 440 | targets[c] = null; 441 | } 442 | } 443 | 444 | })(targetProp) 445 | } 446 | },10000); 447 | } 448 | 449 | // TO DO: the next block of code resolves test case #29, but it results in poor IE11 performance with very large objects. 450 | // UPDATE: need to re-evaluate IE11 performance due to major performance overhaul from 12/23/2018. 451 | // 452 | // if the value we've just set is an object, then we'll need to iterate over it in order to initialize the 453 | // observers/proxies on all nested children of the object 454 | /* if (value instanceof Object && value !== null) { 455 | (function iterate(proxy) { 456 | var target = proxy.__getTarget; 457 | var keys = Object.keys(target); 458 | for (var i = 0, l = keys.length; i < l; i++) { 459 | var property = keys[i]; 460 | if (target[property] instanceof Object && target[property] !== null) iterate(proxy[property]); 461 | }; 462 | })(proxy[property]); 463 | }; */ 464 | 465 | }; 466 | 467 | if (foundObservable) { 468 | // notify the observer functions that the target has been modified 469 | _notifyObservers(changes.length); 470 | } 471 | 472 | } 473 | return true; 474 | } 475 | } 476 | 477 | var __targetPosition = target.__targetPosition; 478 | if (!(__targetPosition > -1)) { 479 | Object.defineProperty(target, "__targetPosition", { 480 | value: targets.length 481 | ,writable: false 482 | ,enumerable: false 483 | ,configurable: false 484 | }); 485 | } 486 | 487 | // create the proxy that we'll use to observe any changes 488 | var proxy = new Proxy(target, handler); 489 | 490 | // we don't want to create a new observable if this function was invoked recursively 491 | if (observable === null) { 492 | observable = {"parentTarget":target, "domDelay":domDelay, "parentProxy":proxy, "observers":[],"paused":false,"path":path,"changesPaused":false}; 493 | observables.push(observable); 494 | } 495 | 496 | // store the proxy we've created so it isn't re-created unnecessairly via get handler 497 | var proxyItem = {"target":target,"proxy":proxy,"observable":observable}; 498 | 499 | // if we have already created a Proxy for this target object then we add it to the corresponding array 500 | // on targetsProxy (targets and targetsProxy work together as a Hash table indexed by the actual target object). 501 | if (__targetPosition > -1) { 502 | 503 | // the targets array is set to null for the position of this particular object, then we know that 504 | // the observable was removed some point in time for this object -- so we need to set the reference again 505 | if (targets[__targetPosition] === null) { 506 | targets[__targetPosition] = target; 507 | } 508 | 509 | targetsProxy[__targetPosition].push(proxyItem); 510 | 511 | // else this is a target object that we had not yet created a Proxy for, so we must add it to targets, 512 | // and push a new array on to targetsProxy containing the new Proxy 513 | } else { 514 | targets.push(target); 515 | targetsProxy.push([proxyItem]); 516 | } 517 | 518 | return proxy; 519 | }; 520 | 521 | return { 522 | /* Method: 523 | Public method that is invoked to create a new ES6 Proxy whose changes we can observe 524 | through the Observerable.observe() method. 525 | Parameters 526 | target - Object, required, plain JavaScript object that we want to observe for changes. 527 | domDelay - Boolean, required, if true, then batch up changes on a 10ms delay so a series of changes can be processed in one DOM update. 528 | observer - Function, optional, will be invoked when a change is made to the proxy. 529 | Returns: 530 | An ES6 Proxy object. 531 | */ 532 | create: function(target, domDelay, observer) { 533 | 534 | // test if the target is a Proxy, if it is then we need to retrieve the original object behind the Proxy. 535 | // we do not allow creating proxies of proxies because -- given the recursive design of ObservableSlim -- it would lead to sharp increases in memory usage 536 | if (target.__isProxy === true) { 537 | var target = target.__getTarget; 538 | //if it is, then we should throw an error. we do not allow creating proxies of proxies 539 | // because -- given the recursive design of ObservableSlim -- it would lead to sharp increases in memory usage 540 | //throw new Error("ObservableSlim.create() cannot create a Proxy for a target object that is also a Proxy."); 541 | } 542 | 543 | // fire off the _create() method -- it will create a new observable and proxy and return the proxy 544 | var proxy = _create(target, domDelay); 545 | 546 | // assign the observer function 547 | if (typeof observer === "function") this.observe(proxy, observer); 548 | 549 | // recursively loop over all nested objects on the proxy we've just created 550 | // this will allow the top observable to observe any changes that occur on a nested object 551 | (function iterate(proxy) { 552 | var target = proxy.__getTarget; 553 | var keys = Object.keys(target); 554 | for (var i = 0, l = keys.length; i < l; i++) { 555 | var property = keys[i]; 556 | if (target[property] instanceof Object && target[property] !== null) iterate(proxy[property]); 557 | } 558 | })(proxy); 559 | 560 | return proxy; 561 | 562 | }, 563 | 564 | /* Method: observe 565 | This method is used to add a new observer function to an existing proxy. 566 | Parameters: 567 | proxy - the ES6 Proxy returned by the create() method. We want to observe changes made to this object. 568 | observer - this function will be invoked when a change is made to the observable (not to be confused with the 569 | observer defined in the create() method). 570 | Returns: 571 | Nothing. 572 | */ 573 | observe: function(proxy, observer) { 574 | // loop over all the observables created by the _create() function 575 | var i = observables.length; 576 | while (i--) { 577 | if (observables[i].parentProxy === proxy) { 578 | observables[i].observers.push(observer); 579 | break; 580 | } 581 | }; 582 | }, 583 | 584 | /* Method: pause 585 | This method will prevent any observer functions from being invoked when a change occurs to a proxy. 586 | Parameters: 587 | proxy - the ES6 Proxy returned by the create() method. 588 | */ 589 | pause: function(proxy) { 590 | var i = observables.length; 591 | var foundMatch = false; 592 | while (i--) { 593 | if (observables[i].parentProxy === proxy) { 594 | observables[i].paused = true; 595 | foundMatch = true; 596 | break; 597 | } 598 | }; 599 | 600 | if (foundMatch == false) throw new Error("ObseravableSlim could not pause observable -- matching proxy not found."); 601 | }, 602 | 603 | /* Method: resume 604 | This method will resume execution of any observer functions when a change is made to a proxy. 605 | Parameters: 606 | proxy - the ES6 Proxy returned by the create() method. 607 | */ 608 | resume: function(proxy) { 609 | var i = observables.length; 610 | var foundMatch = false; 611 | while (i--) { 612 | if (observables[i].parentProxy === proxy) { 613 | observables[i].paused = false; 614 | foundMatch = true; 615 | break; 616 | } 617 | }; 618 | 619 | if (foundMatch == false) throw new Error("ObseravableSlim could not resume observable -- matching proxy not found."); 620 | }, 621 | 622 | /* Method: pauseChanges 623 | This method will prevent any changes (i.e., set, and deleteProperty) from being written to the target 624 | object. However, the observer functions will still be invoked to let you know what changes WOULD have 625 | been made. This can be useful if the changes need to be approved by an external source before the 626 | changes take effect. 627 | Parameters: 628 | proxy - the ES6 Proxy returned by the create() method. 629 | */ 630 | pauseChanges: function(proxy){ 631 | var i = observables.length; 632 | var foundMatch = false; 633 | while (i--) { 634 | if (observables[i].parentProxy === proxy) { 635 | observables[i].changesPaused = true; 636 | foundMatch = true; 637 | break; 638 | } 639 | }; 640 | 641 | if (foundMatch == false) throw new Error("ObseravableSlim could not pause changes on observable -- matching proxy not found."); 642 | }, 643 | 644 | /* Method: resumeChanges 645 | This method will resume the changes that were taking place prior to the call to pauseChanges(). 646 | Parameters: 647 | proxy - the ES6 Proxy returned by the create() method. 648 | */ 649 | resumeChanges: function(proxy){ 650 | var i = observables.length; 651 | var foundMatch = false; 652 | while (i--) { 653 | if (observables[i].parentProxy === proxy) { 654 | observables[i].changesPaused = false; 655 | foundMatch = true; 656 | break; 657 | } 658 | }; 659 | 660 | if (foundMatch == false) throw new Error("ObseravableSlim could not resume changes on observable -- matching proxy not found."); 661 | }, 662 | 663 | /* Method: remove 664 | This method will remove the observable and proxy thereby preventing any further callback observers for 665 | changes occuring to the target object. 666 | Parameters: 667 | proxy - the ES6 Proxy returned by the create() method. 668 | */ 669 | remove: function(proxy) { 670 | 671 | var matchedObservable = null; 672 | var foundMatch = false; 673 | 674 | var c = observables.length; 675 | while (c--) { 676 | if (observables[c].parentProxy === proxy) { 677 | matchedObservable = observables[c]; 678 | foundMatch = true; 679 | break; 680 | } 681 | }; 682 | 683 | var a = targetsProxy.length; 684 | while (a--) { 685 | var b = targetsProxy[a].length; 686 | while (b--) { 687 | if (targetsProxy[a][b].observable === matchedObservable) { 688 | targetsProxy[a].splice(b,1); 689 | 690 | // if there are no more proxies for this target object 691 | // then we null out the position for this object on the targets array 692 | // since we are essentially no longer observing this object. 693 | // we do not splice it off the targets array, because if we re-observe the same 694 | // object at a later time, the property __targetPosition cannot be redefined. 695 | if (targetsProxy[a].length === 0) { 696 | targets[a] = null; 697 | }; 698 | } 699 | }; 700 | }; 701 | 702 | if (foundMatch === true) { 703 | observables.splice(c,1); 704 | } 705 | } 706 | }; 707 | })(); 708 | 709 | // Export in a try catch to prevent this from erroring out on older browsers 710 | try { module.exports = ObservableSlim; } catch (err) {}; -------------------------------------------------------------------------------- /webapp/localService/metadata.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 11 | 12 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /webapp/localService/mockserver.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/util/MockServer", 3 | "sap/ui/model/json/JSONModel", 4 | "sap/base/Log", 5 | "sap/base/util/UriParameters" 6 | ], function (MockServer, JSONModel, Log, UriParameters) { 7 | "use strict"; 8 | 9 | var oMockServer, 10 | _sAppPath = "be/wl/PersonSkills/", 11 | _sJsonFilesPath = _sAppPath + "localService/mockdata"; 12 | 13 | var oMockServerInterface = { 14 | 15 | /** 16 | * Initializes the mock server asynchronously. 17 | * You can configure the delay with the URL parameter "serverDelay". 18 | * The local mock data in this folder is returned instead of the real data for testing. 19 | * @protected 20 | * @param {object} [oOptionsParameter] init parameters for the mockserver 21 | * @returns{Promise} a promise that is resolved when the mock server has been started 22 | */ 23 | init : function (oOptionsParameter) { 24 | var oOptions = oOptionsParameter || {}; 25 | 26 | return new Promise(function(fnResolve, fnReject) { 27 | var sManifestUrl = sap.ui.require.toUrl(_sAppPath + "manifest.json"), 28 | oManifestModel = new JSONModel(sManifestUrl); 29 | 30 | oManifestModel.attachRequestCompleted(function () { 31 | var oUriParameters = new UriParameters(window.location.href), 32 | // parse manifest for local metadata URI 33 | sJsonFilesUrl = sap.ui.require.toUrl(_sJsonFilesPath), 34 | oMainDataSource = oManifestModel.getProperty("/sap.app/dataSources/mainService"), 35 | sMetadataUrl = sap.ui.require.toUrl(_sAppPath + oMainDataSource.settings.localUri), 36 | // ensure there is a trailing slash 37 | sMockServerUrl = /.*\/$/.test(oMainDataSource.uri) ? oMainDataSource.uri : oMainDataSource.uri + "/"; 38 | 39 | // create a mock server instance or stop the existing one to reinitialize 40 | if (!oMockServer) { 41 | oMockServer = new MockServer({ 42 | rootUri: sMockServerUrl 43 | }); 44 | } else { 45 | oMockServer.stop(); 46 | } 47 | 48 | // configure mock server with the given options or a default delay of 0.5s 49 | MockServer.config({ 50 | autoRespond : true, 51 | autoRespondAfter : (oOptions.delay || oUriParameters.get("serverDelay") || 500) 52 | }); 53 | 54 | // simulate all requests using mock data 55 | oMockServer.simulate(sMetadataUrl, { 56 | sMockdataBaseUrl : sJsonFilesUrl, 57 | bGenerateMissingMockData : true 58 | }); 59 | 60 | var aRequests = oMockServer.getRequests(); 61 | 62 | // compose an error response for each request 63 | var fnResponse = function (iErrCode, sMessage, aRequest) { 64 | aRequest.response = function(oXhr){ 65 | oXhr.respond(iErrCode, {"Content-Type": "text/plain;charset=utf-8"}, sMessage); 66 | }; 67 | }; 68 | 69 | // simulate metadata errors 70 | if (oOptions.metadataError || oUriParameters.get("metadataError")) { 71 | aRequests.forEach(function (aEntry) { 72 | if (aEntry.path.toString().indexOf("$metadata") > -1) { 73 | fnResponse(500, "metadata Error", aEntry); 74 | } 75 | }); 76 | } 77 | 78 | // simulate request errors 79 | var sErrorParam = oOptions.errorType || oUriParameters.get("errorType"), 80 | iErrorCode = sErrorParam === "badRequest" ? 400 : 500; 81 | if (sErrorParam) { 82 | aRequests.forEach(function (aEntry) { 83 | fnResponse(iErrorCode, sErrorParam, aEntry); 84 | }); 85 | } 86 | 87 | // custom mock behaviour may be added here 88 | 89 | // set requests and start the server 90 | oMockServer.setRequests(aRequests); 91 | oMockServer.start(); 92 | 93 | Log.info("Running the app with mock data"); 94 | fnResolve(); 95 | }); 96 | 97 | oManifestModel.attachRequestFailed(function () { 98 | var sError = "Failed to load application manifest"; 99 | 100 | Log.error(sError); 101 | fnReject(new Error(sError)); 102 | }); 103 | }); 104 | }, 105 | 106 | /** 107 | * @public returns the mockserver of the app, should be used in integration tests 108 | * @returns {sap.ui.core.util.MockServer} the mockserver instance 109 | */ 110 | getMockServer : function () { 111 | return oMockServer; 112 | } 113 | }; 114 | 115 | return oMockServerInterface; 116 | }); -------------------------------------------------------------------------------- /webapp/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "_version": "1.12.0", 3 | "sap.app": { 4 | "id": "be.wl.PersonSkills", 5 | "type": "application", 6 | "i18n": "i18n/i18n.properties", 7 | "title": "{{appTitle}}", 8 | "description": "{{appDescription}}", 9 | "applicationVersion": { 10 | "version": "1.0.0" 11 | }, 12 | "resources": "resources.json", 13 | "dataSources": { 14 | "mainService": { 15 | "uri": "/sap/opu/odata/sap/ZGW_UI5CON_SRV/", 16 | "type": "OData", 17 | "settings": { 18 | "odataVersion": "2.0", 19 | "localUri": "localService/metadata.xml" 20 | } 21 | } 22 | }, 23 | "sourceTemplate": { 24 | "id": "sap.ui.ui5-template-plugin.2masterdetail", 25 | "version": "1.66.1" 26 | } 27 | }, 28 | "sap.ui": { 29 | "technology": "UI5", 30 | "icons": { 31 | "icon": "sap-icon://detail-view", 32 | "favIcon": "", 33 | "phone": "", 34 | "phone@2": "", 35 | "tablet": "", 36 | "tablet@2": "" 37 | }, 38 | "deviceTypes": { 39 | "desktop": true, 40 | "tablet": true, 41 | "phone": true 42 | } 43 | }, 44 | "sap.ui5": { 45 | "rootView": { 46 | "viewName": "be.wl.PersonSkills.view.App", 47 | "type": "XML", 48 | "async": true, 49 | "id": "app" 50 | }, 51 | "dependencies": { 52 | "minUI5Version": "1.60.0", 53 | "libs": { 54 | "sap.ui.core": {}, 55 | "sap.m": {}, 56 | "sap.f": {} 57 | } 58 | }, 59 | "contentDensities": { 60 | "compact": true, 61 | "cozy": true 62 | }, 63 | "models": { 64 | "i18n": { 65 | "type": "sap.ui.model.resource.ResourceModel", 66 | "settings": { 67 | "bundleName": "be.wl.PersonSkills.i18n.i18n" 68 | } 69 | }, 70 | "": { 71 | "dataSource": "mainService", 72 | "preload": true 73 | } 74 | }, 75 | "routing": { 76 | "config": { 77 | "routerClass": "sap.f.routing.Router", 78 | "viewType": "XML", 79 | "viewPath": "be.wl.PersonSkills.view", 80 | "controlId": "layout", 81 | "controlAggregation": "beginColumnPages", 82 | "bypassed": { 83 | "target": "notFound" 84 | }, 85 | "async": true 86 | }, 87 | "routes": [{ 88 | "pattern": "", 89 | "name": "master", 90 | "target": "master" 91 | }, { 92 | "pattern": "Persons/{objectId}", 93 | "name": "object", 94 | "target": ["master", "object"] 95 | }], 96 | "targets": { 97 | "master": { 98 | "viewName": "Master", 99 | "viewLevel": 1, 100 | "viewId": "master" 101 | }, 102 | "object": { 103 | "viewName": "Detail", 104 | "viewId": "detail", 105 | "viewLevel": 1, 106 | "controlAggregation": "midColumnPages" 107 | }, 108 | "detailObjectNotFound": { 109 | "viewName": "DetailObjectNotFound", 110 | "viewId": "detailObjectNotFound", 111 | "controlAggregation": "midColumnPages" 112 | }, 113 | "notFound": { 114 | "viewName": "NotFound", 115 | "viewId": "notFound" 116 | } 117 | } 118 | }, 119 | "flexEnabled": true, 120 | "resources": { 121 | "js": [{ 122 | "uri": "libs/observable-slim.js" 123 | }] 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /webapp/model/BaseObject.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/base/Object", 3 | "sap/ui/model/json/JSONModel" 4 | ], function (Object, JSONModel) { 5 | "use strict"; 6 | return Object.extend("be.wl.PersonSkills.model.BaseObject", { 7 | constructor: function (data) { 8 | this.copyValues(data); 9 | 10 | if (this.isState) { 11 | this.initDirtyCheck(); 12 | this.getModel().attachPropertyChange((oProperty) => { 13 | // this.isDirty = this.isDirtyCheck(); 14 | // sap && sap.ushell && sap.ushell.Container && sap.ushell.Container.setDirtyFlag(this.isDirty); 15 | // this["update" + oProperty.getParameter("path")] && this["update" + oProperty.getParameter("path").substr(1)](); 16 | var fChangeFunction = this.getChangeFunction(oProperty.getParameter("path")); 17 | this.callChangeFunction(fChangeFunction, oProperty); 18 | if (oProperty.getParameter("context")) { 19 | fChangeFunction = this.getChangeFunction(oProperty.getParameter("context").getPath() + "/" + oProperty.getParameter("path")); 20 | this.callChangeFunction(fChangeFunction, oProperty.getParameter("context").getObject(), oProperty); 21 | //call parent 22 | var sPath = oProperty.getParameter("context").getPath(); 23 | var sParent = sPath.split("/")[sPath.split("/").length - 1]; 24 | if (!isNaN(parseInt(sParent))) { //in case of integer it's probably an array and we need to go one level up 25 | sPath = sPath.split("/").slice(0, sPath.split("/").length - 1).join("/"); 26 | } 27 | var sSourcePath = sPath.split("/").slice(0, sPath.split("/").length - 1).join("/"); 28 | var oSource = (sSourcePath && oProperty.getParameter("context").getModel().getProperty(sSourcePath)); 29 | fChangeFunction = this.getChangeFunction(sPath); 30 | this.callChangeFunction(fChangeFunction, (oSource || oProperty.getParameter("context").getObject()), oProperty); 31 | 32 | } 33 | }, this); 34 | } 35 | }, 36 | copyFieldsToObject: function (aFields) { 37 | return this.copyFields(this, {}, aFields); 38 | }, 39 | copyFieldsToThis: function (oFrom, aFields) { 40 | return this.copyFields(oFrom, this, aFields); 41 | }, 42 | copyFields: function (oFrom, oTo, aFields) { 43 | for (var prop in oFrom) { 44 | if (aFields.find((field) => field === prop)) { 45 | oTo[prop] = oFrom[prop]; 46 | } 47 | } 48 | return oTo; 49 | }, 50 | initDirtyCheck: function () { 51 | this.isDirty = false; 52 | this.enableDirtyFlag(); 53 | this.updateModel(); 54 | }, 55 | disableDirtyFlag: function () { 56 | this.setDirtyFlag(false); 57 | }, 58 | enableDirtyFlag: function () { 59 | this.setDirtyFlag(this.isDirty); 60 | }, 61 | setDirtyFlag: function (bIsDirty) { 62 | sap && sap.ushell && sap.ushell.Container && sap.ushell.Container.setDirtyFlag(bIsDirty); 63 | }, 64 | getChangeFunction: function (sPath) { 65 | sPath = sPath.substr(0, 1) === "/" ? sPath.substr(1) : sPath; 66 | return sPath.split("/").reduce(function (prev, 67 | curr, 68 | idx, array) { 69 | if (idx === array.length - 1) { 70 | return prev[curr + "Changed"]; 71 | } 72 | return curr && curr.length > 0 && prev ? prev[curr] : prev; 73 | }, this.data); 74 | }, 75 | callChangeFunction: function (fChangeFunction, scope, args) { 76 | fChangeFunction && fChangeFunction.apply(scope, args); 77 | }, 78 | copyValues: function (data) { 79 | if (data) { 80 | for (var field in data) { 81 | switch (typeof (data[field])) { 82 | case "object": 83 | if (data[field] && data[field]["results"]) { 84 | this[field] = data[field]["results"]; 85 | } 86 | break; 87 | default: 88 | this[field] = data[field]; 89 | } 90 | } 91 | } 92 | }, 93 | getModel: function () { 94 | if (!this.model) { 95 | this.model = new JSONModel(this.data, true); 96 | //this.model.setData(this); 97 | } 98 | return this.model; 99 | }, 100 | updateModel: function (bHardRefresh) { 101 | if (this.model) { 102 | this.model.refresh(bHardRefresh ? true : false); 103 | } 104 | }, 105 | getData: function () { 106 | var req = jQuery.extend({}, this); 107 | delete req["model"]; 108 | return req; 109 | }, 110 | fnMap: function (oObject) { 111 | var obj = {}; 112 | for (var prop in oObject) { 113 | if (oObject.hasOwnProperty(prop) && typeof (oObject[prop]) !== "object") { 114 | obj[prop] = oObject[prop]; 115 | } 116 | } 117 | return obj; 118 | } 119 | }); 120 | }); -------------------------------------------------------------------------------- /webapp/model/Person.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseObject", 3 | "./Skill" 4 | ], function (BaseObject, Skill) { 5 | "use strict"; 6 | return BaseObject.extend("be.wl.PersonSkills.model.Person", { 7 | constructor: function (data) { 8 | BaseObject.call(this, data); 9 | this.Skills = []; 10 | if (data) { 11 | this.Birthdate = data.Birthdate; 12 | this.setSkills(data.PersonHasSkills.results); 13 | } 14 | 15 | Object.defineProperty(this, "Total", { 16 | get: () => { 17 | return (this.getSkills().reduce((iTotal, oSkill) => { 18 | iTotal += oSkill.Score 19 | return iTotal; 20 | }, 0) / this.getSkills().length )|| 0; 21 | } 22 | }); 23 | }, 24 | SkillsChanged: function (oEvent) { 25 | if (!this.Skills.some((oSkill) => oSkill.isEmpty())) { 26 | this.addEmptySkill(); 27 | } 28 | }, 29 | deleteSkill: function (iIndex) { 30 | this.Skills.splice(iIndex, 1); 31 | }, 32 | addEmptySkill: function () { 33 | this.Skills.push(new Skill({ 34 | SkillName: "", 35 | Score: "" 36 | })); 37 | }, 38 | setSkills: function (aSkills) { 39 | this.Skills = aSkills.map((oSkill) => new Skill(oSkill)); 40 | }, 41 | getSkills: function () { 42 | return this.Skills.filter((oSkill) => oSkill.isNotEmpty()).map((oSkill) => oSkill.getJSON()); 43 | }, 44 | getJSON: function () { 45 | return { 46 | Id: this.Id || 0, 47 | Firstname: this.Firstname || "", 48 | Lastname: this.Lastname || "", 49 | Company: "", 50 | Birthdate: this.Birthdate || "", 51 | PersonHasSkills: this.getSkills() 52 | }; 53 | } 54 | }); 55 | }); -------------------------------------------------------------------------------- /webapp/model/Skill.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseObject" 3 | ], function (BaseObject) { 4 | "use strict"; 5 | return BaseObject.extend("be.wl.PersonSkills.model.Skill", { 6 | constructor: function (data) { 7 | BaseObject.call(this, data); 8 | this.Editable = true; 9 | this.Deletable = false; 10 | }, 11 | SkillNameChanged: function (oEvent) { 12 | this.changeEditable(); 13 | }, 14 | ScoreChanged: function (oEvent) { 15 | this.changeEditable(); 16 | }, 17 | changeEditable: function () { 18 | this.Editable = !(this.SkillName && this.Score); 19 | this.Deletable = !!(this.SkillName || this.Score); 20 | }, 21 | isEmpty:function(){ 22 | return this.SkillName === "" || this.Score === 0; 23 | }, 24 | isNotEmpty:function(){ 25 | return this.SkillName !== "" && this.Score >= 0; 26 | }, 27 | getJSON: function () { 28 | return { 29 | Id: this.Id || 0, 30 | PersonId: this.PersonId || 0, 31 | SkillName: this.SkillName || "", 32 | Score: this.Score || 0 33 | }; 34 | } 35 | }); 36 | }); -------------------------------------------------------------------------------- /webapp/model/formatter.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([], function () { 2 | "use strict"; 3 | 4 | return { 5 | /** 6 | * Rounds the currency value to 2 digits 7 | * 8 | * @public 9 | * @param {string} sValue value to be formatted 10 | * @returns {string} formatted currency value with 2 digits 11 | */ 12 | currencyValue : function (sValue) { 13 | if (!sValue) { 14 | return ""; 15 | } 16 | 17 | return parseFloat(sValue).toFixed(2); 18 | } 19 | }; 20 | }); -------------------------------------------------------------------------------- /webapp/model/models.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/model/json/JSONModel", 3 | "sap/ui/Device" 4 | ], function (JSONModel, Device) { 5 | "use strict"; 6 | 7 | return { 8 | createDeviceModel : function () { 9 | var oModel = new JSONModel(Device); 10 | oModel.setDefaultBindingMode("OneWay"); 11 | return oModel; 12 | } 13 | }; 14 | }); -------------------------------------------------------------------------------- /webapp/service/CoreService.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/base/Object" 3 | ], function (Object) { 4 | "use strict"; 5 | 6 | return Object.extend("be.wl.PersonSkills.service.CoreService", { 7 | constructor: function (model) { 8 | Object.call(this); 9 | if (model) { 10 | this.setModel(model); 11 | } 12 | }, 13 | setModel: function (model) { 14 | this.model = model; 15 | }, 16 | odata: function (url) { 17 | var me = this; 18 | var core = { 19 | ajax: function (type, url, data, parameters) { 20 | var promise = new Promise(function (resolve, reject) { 21 | var args = []; 22 | var params = {}; 23 | args.push(url); 24 | if (data) { 25 | args.push(data); 26 | } 27 | if (parameters) { 28 | params = parameters; 29 | } 30 | params.success = function (result, response) { 31 | resolve({ 32 | data: result, 33 | response: response 34 | }); 35 | }; 36 | params.error = function (error) { 37 | reject(error); 38 | }; 39 | args.push(params); 40 | me.model[type].apply(me.model, args); 41 | }); 42 | return promise; 43 | } 44 | }; 45 | 46 | return { 47 | 'get': function (params) { 48 | return core.ajax('read', url, false, params); 49 | }, 50 | 'post': function (data, params) { 51 | return core.ajax('create', url, data, params); 52 | }, 53 | 'put': function (data, params) { 54 | return core.ajax('update', url, data, params); 55 | }, 56 | 'delete': function (params) { 57 | return core.ajax('remove', url, false, params); 58 | } 59 | }; 60 | }, 61 | http: function (url) { 62 | var core = { 63 | ajax: function (method, url, headers, args, mimetype) { 64 | var promise = new Promise(function (resolve, reject) { 65 | var client = new XMLHttpRequest(); 66 | var uri = url; 67 | if (args && method === 'GET') { 68 | uri += '?'; 69 | var argcount = 0; 70 | for (var key in args) { 71 | if (args.hasOwnProperty(key)) { 72 | if (argcount++) { 73 | uri += '&'; 74 | } 75 | uri += encodeURIComponent(key) + '=' + encodeURIComponent(args[key]); 76 | } 77 | } 78 | } 79 | if (args && (method === 'POST' || method === 'PUT')) { 80 | var data = {}; 81 | for (var keyp in args) { 82 | if (args.hasOwnProperty(keyp)) { 83 | data[keyp] = args[keyp]; 84 | } 85 | } 86 | } 87 | client.open(method, uri); 88 | 89 | if (method === 'POST' || method === 'PUT') { 90 | client.setRequestHeader("accept", "application/json"); 91 | client.setRequestHeader("content-type", "application/json"); 92 | } 93 | for (var keyh in headers) { 94 | if (headers.hasOwnProperty(keyh)) { 95 | client.setRequestHeader(keyh, headers[keyh]); 96 | } 97 | } 98 | if (data) { 99 | client.send(JSON.stringify(data)); 100 | } else { 101 | client.send(); 102 | } 103 | client.onload = function () { 104 | if (this.status == 200) { 105 | resolve(this.response); 106 | } else { 107 | reject(this.statusText); 108 | } 109 | }; 110 | client.onerror = function () { 111 | reject(this.statusText); 112 | }; 113 | }); 114 | return promise; 115 | } 116 | }; 117 | 118 | return { 119 | 'get': function (headers, args) { 120 | return core.ajax('GET', url, headers, args); 121 | }, 122 | 'post': function (headers, args) { 123 | return core.ajax('POST', url, headers, args); 124 | }, 125 | 'put': function (headers, args) { 126 | return core.ajax('PUT', url, headers, args); 127 | }, 128 | 'delete': function (headers, args) { 129 | return core.ajax('DELETE', url, headers, args); 130 | } 131 | }; 132 | } 133 | }); 134 | }); -------------------------------------------------------------------------------- /webapp/service/PersonService.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./CoreService", 3 | "sap/ui/model/Sorter" 4 | ], function (CoreService, Sorter) { 5 | "use strict"; 6 | 7 | var PersonService = CoreService.extend("be.wl.PersonSkills.service.PersonService", { 8 | constructor: function (model) { 9 | CoreService.call(this, model); 10 | }, 11 | getPerson: function (id) { 12 | var sObjectPath = this.model.createKey("/Persons", { 13 | Id: id 14 | }); 15 | var mParameters = { 16 | urlParameters: { 17 | $expand: "PersonHasSkills" 18 | } 19 | }; 20 | return this.odata(sObjectPath).get(mParameters); 21 | }, 22 | createPerson:function(oPerson){ 23 | var oData = oPerson.getJSON(); 24 | return this.odata("/Persons").post(oData); 25 | } 26 | }); 27 | return PersonService; 28 | }); -------------------------------------------------------------------------------- /webapp/state/PersonState.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "../model/BaseObject", 3 | "../model/Person", 4 | "../libs/observable-slim" 5 | ], function (BaseObject, Person, obs) { 6 | "use strict"; 7 | var PersonState = BaseObject.extend("be.wl.PersonSkills.state.PersonState", { 8 | constructor: function (oService) { 9 | this.data = { 10 | Person : new Person(), 11 | display : true 12 | }; 13 | this.PersonService = oService; 14 | BaseObject.call(this, { 15 | isState: true 16 | }); 17 | 18 | }, 19 | createPerson: function () { 20 | // this.Person = new Person(); 21 | var oPerson = new Person(); 22 | this.data.Person = ObservableSlim.create(oPerson, true, (changes)=>{ 23 | console.log(JSON.stringify(changes)); 24 | this.updateModel(); 25 | }); 26 | this.data.Person.addEmptySkill(); 27 | this.data.display = false; 28 | // this.updateModel(); 29 | }, 30 | getPerson: async function (id) { 31 | // return this.PersonService.getPerson(id).then((result) => { 32 | let result = await this.PersonService.getPerson(id); 33 | this.data.Person = new Person(result.data); 34 | this.data.display = true; 35 | this.updateModel(); 36 | return this.data.Person; 37 | // }); 38 | }, 39 | newPerson: function () { 40 | return this.PersonService.createPerson(this.data.Person).then((result) => result.data.Id); 41 | }, 42 | deletePersonSkill: function (iIndex) { 43 | this.data.Person.deleteSkill(iIndex); 44 | // this.updateModel(); 45 | } 46 | }); 47 | return PersonState; 48 | }); -------------------------------------------------------------------------------- /webapp/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Testing Overview 5 | 6 | 7 | 8 | 9 |

Testing Overview

10 |

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

11 | 12 | 23 | 24 | -------------------------------------------------------------------------------- /webapp/test/initMockServer.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "../localService/mockserver", 3 | "sap/m/MessageBox" 4 | ], function (mockserver, MessageBox) { 5 | "use strict"; 6 | 7 | var aMockservers = []; 8 | 9 | // initialize the mock server 10 | aMockservers.push(mockserver.init()); 11 | 12 | Promise.all(aMockservers).catch(function (oError) { 13 | MessageBox.error(oError.message); 14 | }).finally(function () { 15 | // initialize the embedded component on the HTML page 16 | sap.ui.require(["sap/ui/core/ComponentSupport"]); 17 | }); 18 | }); -------------------------------------------------------------------------------- /webapp/test/integration/AllJourneys.js: -------------------------------------------------------------------------------- 1 | // We cannot provide stable mock data out of the template. 2 | // If you introduce mock data, by adding .json files in your webapp/localService/mockdata folder you have to provide the following minimum data: 3 | // * At least 3 Persons in the list 4 | // * All 3 Persons have at least one PersonHasSkills 5 | 6 | sap.ui.define([ 7 | "sap/ui/test/Opa5", 8 | "./arrangements/Startup", 9 | "./MasterJourney", 10 | "./NavigationJourney", 11 | "./NotFoundJourney", 12 | "./BusyJourney" 13 | ], function (Opa5, Startup) { 14 | "use strict"; 15 | Opa5.extendConfig({ 16 | arrangements: new Startup(), 17 | viewNamespace: "be.wl.PersonSkills.view.", 18 | autoWait: true 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /webapp/test/integration/BusyJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "sap/ui/Device", 6 | "./pages/App", 7 | "./pages/Master" 8 | ], function (opaTest, Device) { 9 | "use strict"; 10 | 11 | var iDelay = (Device.browser.msie || Device.browser.edge) ? 1500 : 1000; 12 | 13 | QUnit.module("Desktop busy indication"); 14 | 15 | opaTest("Should see a global busy indication while loading the metadata", function (Given, When, Then) { 16 | // Arrangements 17 | Given.iStartMyApp({delay : iDelay}); 18 | 19 | // Assertions 20 | Then.onTheAppPage.iShouldSeeTheBusyIndicator(); 21 | }); 22 | 23 | opaTest("Should see a busy indication on the master after loading the metadata", function (Given, When, Then) { 24 | // Assertions 25 | Then.onTheMasterPage.iShouldSeeTheBusyIndicator(); 26 | 27 | // Cleanup 28 | Then.iTeardownMyApp(); 29 | }); 30 | 31 | }); -------------------------------------------------------------------------------- /webapp/test/integration/BusyJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "sap/ui/Device", 6 | "./pages/App", 7 | "./pages/Master" 8 | ], function (opaTest, Device) { 9 | "use strict"; 10 | 11 | var iDelay = (Device.browser.msie || Device.browser.edge) ? 1500 : 1000; 12 | 13 | QUnit.module("Phone busy indication"); 14 | 15 | opaTest("Should see a global busy indication while loading the metadata", function (Given, When, Then) { 16 | // Arrangements 17 | Given.iStartMyApp({delay : iDelay}); 18 | 19 | // Assertions 20 | Then.onTheAppPage.iShouldSeeTheBusyIndicator(); 21 | }); 22 | 23 | opaTest("Should see a busy indication on the master after loading the metadata", function (Given, When, Then) { 24 | // Assertions 25 | Then.onTheMasterPage.iShouldSeeTheBusyIndicator(); 26 | 27 | //Cleanup 28 | Then.iTeardownMyApp(); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /webapp/test/integration/MasterJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Master" 6 | ], function (opaTest) { 7 | "use strict"; 8 | 9 | QUnit.module("Master List"); 10 | 11 | opaTest("Should see the master list with all entries", function (Given, When, Then) { 12 | // Arrangements 13 | Given.iStartMyApp(); 14 | 15 | // Assertions 16 | Then.onTheMasterPage.iShouldSeeTheList(). 17 | and.theListShouldHaveAllEntries(). 18 | and.theHeaderShouldDisplayAllEntries(). 19 | and.theListShouldContainOnlyFormattedUnitNumbers(); 20 | }); 21 | 22 | opaTest("Search for the First object should deliver results that contain the firstObject in the name", function (Given, When, Then) { 23 | //Actions 24 | When.onTheMasterPage.iSearchForTheFirstObject(); 25 | 26 | // Assertions 27 | Then.onTheMasterPage.theListShowsOnlyObjectsWithTheSearchStringInTheirTitle(); 28 | }); 29 | 30 | opaTest("Entering something that cannot be found into search field and pressing search field's refresh should leave the list as it was", function (Given, When, Then) { 31 | //Actions 32 | When.onTheMasterPage.iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh(); 33 | 34 | // Assertions 35 | Then.onTheMasterPage.theListHasEntries(); 36 | }); 37 | 38 | opaTest("Entering something that cannot be found into search field and pressing 'search' should display the list's 'not found' message", function (Given, When, Then) { 39 | //Actions 40 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 41 | 42 | // Assertions 43 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(). 44 | and.theListHeaderDisplaysZeroHits(); 45 | }); 46 | 47 | opaTest("Should display items again if the searchfield is emptied", function (Given, When, Then) { 48 | //Actions 49 | When.onTheMasterPage.iClearTheSearch(); 50 | 51 | // Assertions 52 | Then.onTheMasterPage.theListShouldHaveAllEntries(); 53 | }); 54 | 55 | opaTest("MasterList Sorting on Name", function(Given, When, Then) { 56 | // Actions 57 | When.onTheMasterPage.iSortTheListOnName(); 58 | 59 | // Assertions 60 | Then.onTheMasterPage.theListShouldBeSortedAscendingOnName(); 61 | }); 62 | 63 | opaTest("MasterList Filtering on UnitNumber less than 100", function(Given, When, Then) { 64 | // Action 65 | When.onTheMasterPage.iFilterTheListOnUnitNumber(); 66 | 67 | // Assertion 68 | Then.onTheMasterPage.theListShouldBeFilteredOnUnitNumber(); 69 | }); 70 | 71 | opaTest("MasterList remove filter should display all items", function(Given, When, Then) { 72 | // Action 73 | When.onTheMasterPage.iOpenViewSettingsDialog(). 74 | and.iPressResetInViewSelectionDialog(). 75 | and.iPressOKInViewSelectionDialog(); 76 | 77 | // Assertion 78 | Then.onTheMasterPage.theListShouldHaveAllEntries(); 79 | }); 80 | 81 | 82 | opaTest("MasterList Sorting on UnitNumber", function(Given, When, Then) { 83 | // Actions 84 | When.onTheMasterPage.iSortTheListOnUnitNumber(); 85 | 86 | // Assertions 87 | Then.onTheMasterPage.theListShouldBeSortedAscendingOnUnitNumber(); 88 | }); 89 | 90 | opaTest("MasterList grouping created group headers", function(Given, When, Then) { 91 | // Action 92 | When.onTheMasterPage.iGroupTheList(); 93 | 94 | // Assertion 95 | Then.onTheMasterPage.theListShouldContainAGroupHeader(); 96 | }); 97 | 98 | opaTest("Remove grouping from MasterList delivers initial list", function(Given, When, Then) { 99 | // Action 100 | When.onTheMasterPage.iRemoveListGrouping(); 101 | 102 | // Assertion 103 | Then.onTheMasterPage.theListShouldNotContainGroupHeaders(). 104 | and.theListShouldHaveAllEntries(); 105 | }); 106 | 107 | opaTest("Grouping the master list and sorting it should deliver the initial list", function(Given, When, Then) { 108 | // Action 109 | When.onTheMasterPage.iGroupTheList(). 110 | and.iSortTheListOnUnitNumber(); 111 | 112 | // Assertion 113 | Then.onTheMasterPage.theListShouldContainAGroupHeader(); 114 | 115 | // Cleanup 116 | Then.iTeardownMyApp(); 117 | }); 118 | 119 | }); -------------------------------------------------------------------------------- /webapp/test/integration/NavigationJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Master", 6 | "./pages/Detail", 7 | "./pages/Browser", 8 | "./pages/App" 9 | ], function (opaTest) { 10 | "use strict"; 11 | 12 | QUnit.module("Desktop navigation"); 13 | 14 | opaTest("Should navigate on press", function (Given, When, Then) { 15 | // Arrangements 16 | Given.iStartMyApp(); 17 | 18 | // Actions 19 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1). 20 | and.iPressOnTheObjectAtPosition(1); 21 | 22 | // Assertions 23 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(). 24 | and.iShouldSeeHeaderActionButtons(); 25 | Then.onTheBrowserPage.iShouldSeeTheHashForTheRememberedObject(); 26 | }); 27 | 28 | opaTest("Should press full screen toggle button: The app shows one column", function (Given, When, Then) { 29 | // Actions 30 | When.onTheDetailPage.iPressTheHeaderActionButton("enterFullScreen"); 31 | 32 | // Assertions 33 | Then.onTheAppPage.theAppShowsFCLDesign("MidColumnFullScreen"); 34 | Then.onTheDetailPage.iShouldSeeTheFullScreenToggleButton("exitFullScreen"); 35 | }); 36 | 37 | opaTest("Should press full screen toggle button: The app shows two columns", function (Given, When, Then) { 38 | // Actions 39 | When.onTheDetailPage.iPressTheHeaderActionButton("exitFullScreen"); 40 | 41 | // Assertions 42 | Then.onTheAppPage.theAppShowsFCLDesign("TwoColumnsMidExpanded"); 43 | Then.onTheDetailPage.iShouldSeeTheFullScreenToggleButton("enterFullScreen"); 44 | }); 45 | 46 | opaTest("Should react on hash change", function (Given, When, Then) { 47 | // Actions 48 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1); 49 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 50 | 51 | // Assertions 52 | Then.onTheDetailPage.iShouldSeeTheRememberedObject().and.iShouldSeeNoBusyIndicator(); 53 | Then.onTheMasterPage.theRememberedListItemShouldBeSelected(); 54 | }); 55 | 56 | 57 | opaTest("Detail Page Shows Object Details", function (Given, When, Then) { 58 | 59 | // Assertions 60 | Then.onTheDetailPage.iShouldSeeTheObjectLineItemsList(). 61 | and.theDetailViewShouldContainOnlyFormattedUnitNumbers(). 62 | and.theLineItemsListShouldHaveTheCorrectNumberOfItems(). 63 | and.theLineItemsHeaderShouldDisplayTheAmountOfEntries(); 64 | 65 | }); 66 | 67 | opaTest("Navigate to an object not on the client: no item should be selected and the object page should be displayed", function (Given, When, Then) { 68 | //Actions 69 | When.onTheMasterPage.iRememberAnIdOfAnObjectThatsNotInTheList(); 70 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 71 | 72 | // Assertions 73 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(); 74 | }); 75 | 76 | opaTest("Should press close column button: The app shows one columns", function (Given, When, Then) { 77 | // Actions 78 | When.onTheDetailPage.iPressTheHeaderActionButton("closeColumn"); 79 | 80 | // Assertions 81 | Then.onTheAppPage.theAppShowsFCLDesign("OneColumn"); 82 | Then.onTheMasterPage.theListShouldHaveNoSelection(); 83 | 84 | // Cleanup 85 | Then.iTeardownMyApp(); 86 | }); 87 | 88 | opaTest("Start the App and simulate metadata error: MessageBox should be shown", function (Given, When, Then) { 89 | //Arrangement 90 | Given.iStartMyApp({ 91 | delay : 1000, 92 | metadataError : true 93 | }); 94 | 95 | // Assertions 96 | Then.onTheAppPage.iShouldSeeTheMessageBox(); 97 | 98 | // Actions 99 | When.onTheAppPage.iCloseTheMessageBox(); 100 | 101 | // Cleanup 102 | Then.iTeardownMyApp(); 103 | }); 104 | 105 | opaTest("Start the App and simulate bad request error: MessageBox should be shown", function (Given, When, Then) { 106 | //Arrangement 107 | Given.iStartMyApp({ 108 | delay : 1000, 109 | errorType : 'serverError' 110 | }); 111 | 112 | // Assertions 113 | Then.onTheAppPage.iShouldSeeTheMessageBox(); 114 | 115 | // Actions 116 | When.onTheAppPage.iCloseTheMessageBox(); 117 | 118 | // Cleanup 119 | Then.iTeardownMyApp(); 120 | }); 121 | 122 | }); -------------------------------------------------------------------------------- /webapp/test/integration/NavigationJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Master", 6 | "./pages/Browser", 7 | "./pages/Detail" 8 | ], function (opaTest) { 9 | "use strict"; 10 | 11 | QUnit.module("Phone navigation"); 12 | 13 | opaTest("Should see the objects list", function (Given, When, Then) { 14 | // Arrangements 15 | Given.iStartMyApp(); 16 | 17 | // Assertions 18 | Then.onTheMasterPage.iShouldSeeTheList(); 19 | Then.onTheBrowserPage.iShouldSeeAnEmptyHash(); 20 | }); 21 | 22 | opaTest("Should react on hash change", function (Given, When, Then) { 23 | // Actions 24 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(1); 25 | When.onTheBrowserPage.iChangeTheHashToTheRememberedItem(); 26 | 27 | // Assertions 28 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(); 29 | }); 30 | 31 | opaTest("Detail Page Shows Object Details", function (Given, When, Then) { 32 | // Assertions 33 | Then.onTheDetailPage.iShouldSeeTheObjectLineItemsList(). 34 | and.theLineItemsListShouldHaveTheCorrectNumberOfItems(). 35 | and.theLineItemsHeaderShouldDisplayTheAmountOfEntries(); 36 | }); 37 | 38 | opaTest("Should navigate on press", function (Given, When, Then) { 39 | // Actions 40 | When.onTheDetailPage.iPressTheHeaderActionButton("closeColumn"); 41 | When.onTheMasterPage.iRememberTheIdOfListItemAtPosition(2). 42 | and.iPressOnTheObjectAtPosition(2); 43 | 44 | // Assertions 45 | Then.onTheDetailPage.iShouldSeeTheRememberedObject(); 46 | 47 | // Cleanup 48 | Then.iTeardownMyApp(); 49 | }); 50 | 51 | }); -------------------------------------------------------------------------------- /webapp/test/integration/NotFoundJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Master", 6 | "./pages/NotFound", 7 | "./pages/Browser" 8 | ], function (opaTest) { 9 | "use strict"; 10 | 11 | QUnit.module("Desktop not found"); 12 | 13 | opaTest("Should see the resource not found page when navigating to an invalid hash", function (Given, When, Then) { 14 | //Arrangement 15 | Given.iStartMyApp(); 16 | 17 | //Actions 18 | When.onTheBrowserPage.iChangeTheHashToSomethingInvalid(); 19 | 20 | // Assertions 21 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 22 | and.theNotFoundPageShouldSayResourceNotFound(); 23 | 24 | // Cleanup 25 | Then.iTeardownMyApp(); 26 | }); 27 | 28 | opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) { 29 | // Arrangements 30 | Given.iStartMyApp({hash : "somethingThatDoesNotExist"}); 31 | 32 | // Assertions 33 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 34 | and.theNotFoundPageShouldSayResourceNotFound(); 35 | 36 | // Cleanup 37 | Then.iTeardownMyApp(); 38 | }); 39 | 40 | opaTest("Should see the not found master and detail page if an invalid object id has been called", function (Given, When, Then) { 41 | // Arrangements 42 | Given.iStartMyApp({hash : "/Persons/SomeInvalidObjectId"}); 43 | 44 | // Assertions 45 | Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage(). 46 | and.theNotFoundPageShouldSayObjectNotFound(); 47 | 48 | // Cleanup 49 | Then.iTeardownMyApp(); 50 | }); 51 | 52 | opaTest("Should see the not found text for no search results", function (Given, When, Then) { 53 | // Arrangements 54 | Given.iStartMyApp(); 55 | 56 | //Actions 57 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 58 | 59 | // Assertions 60 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(); 61 | 62 | // Cleanup 63 | Then.iTeardownMyApp(); 64 | }); 65 | 66 | }); -------------------------------------------------------------------------------- /webapp/test/integration/NotFoundJourneyPhone.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/NotFound", 6 | "./pages/Master" 7 | ], function (opaTest) { 8 | "use strict"; 9 | 10 | QUnit.module("Phone not found"); 11 | 12 | opaTest("Should see the not found page if the hash is something that matches no route", function (Given, When, Then) { 13 | // Arrangements 14 | Given.iStartMyApp({hash : "somethingThatDoesNotExist"}); 15 | 16 | // Assertions 17 | Then.onTheNotFoundPage.iShouldSeeTheNotFoundPage(). 18 | and.theNotFoundPageShouldSayResourceNotFound(); 19 | }); 20 | 21 | opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) { 22 | // Actions 23 | When.onTheNotFoundPage.iPressTheBackButton("NotFound"); 24 | 25 | // Assertions 26 | Then.onTheMasterPage.iShouldSeeTheList(); 27 | 28 | // Cleanup 29 | Then.iTeardownMyApp(); 30 | }); 31 | 32 | opaTest("Should see the not found detail page if an invalid object id has been called", function (Given, When, Then) { 33 | // Arrangements 34 | Given.iStartMyApp({hash : "/Persons/SomeInvalidObjectId"}); 35 | 36 | // Assertions 37 | Then.onTheNotFoundPage.iShouldSeeTheObjectNotFoundPage(). 38 | and.theNotFoundPageShouldSayObjectNotFound(); 39 | }); 40 | 41 | opaTest("Should end up on the master list, if the back button is pressed", function (Given, When, Then) { 42 | // Actions 43 | When.onTheNotFoundPage.iPressTheBackButton("DetailObjectNotFound"); 44 | 45 | // Assertions 46 | Then.onTheMasterPage.iShouldSeeTheList(); 47 | 48 | // Cleanup 49 | Then.iTeardownMyApp(); 50 | }); 51 | 52 | 53 | opaTest("Should see the not found text for no search results", function (Given, When, Then) { 54 | // Arrangements 55 | Given.iStartMyApp(); 56 | 57 | // Actions 58 | When.onTheMasterPage.iSearchForSomethingWithNoResults(); 59 | 60 | // Assertions 61 | Then.onTheMasterPage.iShouldSeeTheNoDataTextForNoSearchResults(); 62 | 63 | // Cleanup 64 | Then.iTeardownMyApp(); 65 | }); 66 | 67 | }); -------------------------------------------------------------------------------- /webapp/test/integration/PhoneJourneys.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "./arrangements/Startup", 4 | "./NavigationJourneyPhone", 5 | "./NotFoundJourneyPhone", 6 | "./BusyJourneyPhone" 7 | ], function (Opa5, Startup) { 8 | "use strict"; 9 | 10 | Opa5.extendConfig({ 11 | arrangements: new Startup(), 12 | viewNamespace: "be.wl.PersonSkills.view.", 13 | autoWait: true 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /webapp/test/integration/arrangements/Startup.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "be/wl/PersonSkills/localService/mockserver", 4 | "sap/ui/model/odata/v2/ODataModel" 5 | ], function(Opa5, mockserver, ODataModel) { 6 | "use strict"; 7 | 8 | return Opa5.extend("be.wl.PersonSkills.test.integration.arrangements.Startup", { 9 | 10 | /** 11 | * Initializes mock server, then starts the app component 12 | * @param {object} oOptionsParameter An object that contains the configuration for starting up the app 13 | * @param {integer} oOptionsParameter.delay A custom delay to start the app with 14 | * @param {string} [oOptionsParameter.hash] The in-app hash can also be passed separately for better readability in tests 15 | * @param {boolean} [oOptionsParameter.autoWait=true] Automatically wait for pending requests while the application is starting up 16 | */ 17 | iStartMyApp : function (oOptionsParameter) { 18 | var oOptions = oOptionsParameter || {}; 19 | 20 | this._clearSharedData(); 21 | 22 | // start the app with a minimal delay to make tests fast but still async to discover basic timing issues 23 | oOptions.delay = oOptions.delay || 1; 24 | 25 | // configure mock server with the current options 26 | var oMockServerInitialized = mockserver.init(oOptions); 27 | 28 | this.iWaitForPromise(oMockServerInitialized); 29 | // start the app UI component 30 | this.iStartMyUIComponent({ 31 | componentConfig: { 32 | name: "be.wl.PersonSkills", 33 | async: true 34 | }, 35 | hash: oOptions.hash, 36 | autoWait: oOptions.autoWait 37 | }); 38 | }, 39 | _clearSharedData: function () { 40 | // clear shared metadata in ODataModel to allow tests for loading the metadata 41 | ODataModel.mSharedData = { server: {}, service: {}, meta: {} }; 42 | } 43 | }); 44 | }); -------------------------------------------------------------------------------- /webapp/test/integration/opaTests.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Integration tests for PersonSkills 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /webapp/test/integration/opaTests.qunit.js: -------------------------------------------------------------------------------- 1 | /* global QUnit */ 2 | 3 | QUnit.config.autostart = false; 4 | 5 | sap.ui.getCore().attachInit(function() { 6 | "use strict"; 7 | 8 | sap.ui.require([ 9 | "be/wl/PersonSkills/test/integration/AllJourneys" 10 | ], function() { 11 | QUnit.start(); 12 | }); 13 | }); -------------------------------------------------------------------------------- /webapp/test/integration/opaTestsPhone.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Integration tests for PersonSkills 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /webapp/test/integration/opaTestsPhone.qunit.js: -------------------------------------------------------------------------------- 1 | /* global QUnit */ 2 | 3 | QUnit.config.autostart = false; 4 | 5 | sap.ui.getCore().attachInit(function() { 6 | "use strict"; 7 | 8 | sap.ui.require([ 9 | "be/wl/PersonSkills/test/integration/PhoneJourneys" 10 | ], function() { 11 | QUnit.start(); 12 | }); 13 | }); -------------------------------------------------------------------------------- /webapp/test/integration/pages/App.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/matchers/PropertyStrictEquals" 4 | ], function(Opa5, PropertyStrictEquals) { 5 | "use strict"; 6 | 7 | var sViewName = "App", 8 | sAppControl = "app"; 9 | 10 | Opa5.createPageObjects({ 11 | onTheAppPage : { 12 | 13 | actions : { 14 | 15 | iCloseTheMessageBox : function () { 16 | return this.waitFor({ 17 | id: "serviceErrorMessageBox", 18 | autoWait: false, 19 | success: function (oMessageBox) { 20 | oMessageBox.destroy(); 21 | Opa5.assert.ok(true, "The MessageBox was closed"); 22 | } 23 | }); 24 | } 25 | }, 26 | 27 | assertions : { 28 | 29 | iShouldSeeTheBusyIndicator : function () { 30 | return this.waitFor({ 31 | id : sAppControl, 32 | viewName : sViewName, 33 | matchers: new PropertyStrictEquals({ 34 | name: "busy", 35 | value: true 36 | }), 37 | autoWait: false, 38 | success : function () { 39 | Opa5.assert.ok(true, "The app is busy"); 40 | }, 41 | errorMessage : "The app is not busy" 42 | }); 43 | }, 44 | 45 | iShouldSeeTheMessageBox : function () { 46 | return this.waitFor({ 47 | searchOpenDialogs: true, 48 | controlType: "sap.m.Dialog", 49 | matchers : new PropertyStrictEquals({ name: "type", value: "Message"}), 50 | success: function () { 51 | Opa5.assert.ok(true, "The correct MessageBox was shown"); 52 | } 53 | }); 54 | }, 55 | 56 | theAppShowsFCLDesign: function (sLayout) { 57 | return this.waitFor({ 58 | id : "layout", 59 | viewName : "App", 60 | matchers : new PropertyStrictEquals({name: "layout", value: sLayout}), 61 | success : function () { 62 | Opa5.assert.ok(true, "the app shows " + sLayout + " layout"); 63 | }, 64 | errorMessage : "The app does not show " + sLayout + " layout" 65 | }); 66 | } 67 | 68 | } 69 | 70 | } 71 | 72 | }); 73 | 74 | }); -------------------------------------------------------------------------------- /webapp/test/integration/pages/Browser.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5" 3 | ], function(Opa5) { 4 | "use strict"; 5 | 6 | Opa5.createPageObjects({ 7 | onTheBrowserPage : { 8 | 9 | actions : { 10 | 11 | iChangeTheHashToObjectN : function (iObjIndex) { 12 | return this.waitFor(this.createAWaitForAnEntitySet({ 13 | entitySet : "Objects", 14 | success : function (aEntitySet) { 15 | Opa5.getHashChanger().setHash("/Persons/" + aEntitySet[iObjIndex].Id); 16 | } 17 | })); 18 | }, 19 | 20 | iChangeTheHashToTheRememberedItem : function () { 21 | return this.waitFor({ 22 | success : function () { 23 | var sObjectId = this.getContext().currentItem.id; 24 | Opa5.getHashChanger().setHash("/Persons/" + sObjectId); 25 | } 26 | }); 27 | }, 28 | 29 | iChangeTheHashToSomethingInvalid : function () { 30 | return this.waitFor({ 31 | success : function () { 32 | Opa5.getHashChanger().setHash("/somethingInvalid"); 33 | } 34 | }); 35 | } 36 | 37 | }, 38 | 39 | assertions : { 40 | 41 | iShouldSeeTheHashForObjectN : function (iObjIndex) { 42 | return this.waitFor(this.createAWaitForAnEntitySet({ 43 | entitySet : "Objects", 44 | success : function (aEntitySet) { 45 | var oHashChanger = Opa5.getHashChanger(), 46 | sHash = oHashChanger.getHash(); 47 | Opa5.assert.strictEqual(sHash, "Persons/" + aEntitySet[iObjIndex].Id, "The Hash is correct"); 48 | } 49 | })); 50 | }, 51 | iShouldSeeTheHashForTheRememberedObject : function () { 52 | return this.waitFor({ 53 | success : function () { 54 | var sObjectId = this.getContext().currentItem.id, 55 | oHashChanger = Opa5.getHashChanger(), 56 | sHash = oHashChanger.getHash(); 57 | Opa5.assert.strictEqual(sHash, "Persons/" + sObjectId, "The Hash is not correct"); 58 | } 59 | }); 60 | }, 61 | iShouldSeeAnEmptyHash : function () { 62 | return this.waitFor({ 63 | success : function () { 64 | var oHashChanger = Opa5.getHashChanger(), 65 | sHash = oHashChanger.getHash(); 66 | Opa5.assert.strictEqual(sHash, "", "The Hash should be empty"); 67 | }, 68 | errorMessage : "The Hash is not Correct!" 69 | }); 70 | } 71 | 72 | } 73 | 74 | } 75 | 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /webapp/test/integration/pages/Common.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5" 3 | ], function(Opa5) { 4 | "use strict"; 5 | 6 | return Opa5.extend("be.wl.PersonSkills.test.integration.pages.Common", { 7 | 8 | createAWaitForAnEntitySet : function (oOptions) { 9 | return { 10 | success: function () { 11 | var aEntitySet; 12 | 13 | var oMockServerInitialized = this.getMockServer().then(function (oMockServer) { 14 | aEntitySet = oMockServer.getEntitySetData(oOptions.entitySet); 15 | }); 16 | 17 | this.iWaitForPromise(oMockServerInitialized); 18 | return this.waitFor({ 19 | success : function () { 20 | oOptions.success.call(this, aEntitySet); 21 | } 22 | }); 23 | } 24 | }; 25 | }, 26 | 27 | getMockServer : function () { 28 | return new Promise(function (success) { 29 | Opa5.getWindow().sap.ui.require(["be/wl/PersonSkills/localService/mockserver"], function (mockserver) { 30 | success(mockserver.getMockServer()); 31 | }); 32 | }); 33 | } 34 | 35 | }); 36 | 37 | }); -------------------------------------------------------------------------------- /webapp/test/integration/pages/Detail.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "./Common", 5 | "sap/ui/test/matchers/AggregationLengthEquals", 6 | "sap/ui/test/matchers/AggregationFilled", 7 | "sap/ui/test/matchers/PropertyStrictEquals" 8 | ], function(Opa5, Press, Common, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals) { 9 | "use strict"; 10 | 11 | var sViewName = "Detail"; 12 | 13 | Opa5.createPageObjects({ 14 | onTheDetailPage : { 15 | 16 | baseClass : Common, 17 | 18 | actions : { 19 | 20 | iPressTheHeaderActionButton: function (sId) { 21 | return this.waitFor({ 22 | id : sId, 23 | viewName : sViewName, 24 | actions: new Press(), 25 | errorMessage : "Did not find the button with id" + sId + " on detail page" 26 | }); 27 | } 28 | }, 29 | 30 | assertions : { 31 | 32 | 33 | iShouldSeeNoBusyIndicator : function () { 34 | return this.waitFor({ 35 | id : "detailPage", 36 | viewName : sViewName, 37 | matchers : function (oPage) { 38 | return !oPage.getBusy(); 39 | }, 40 | success : function (oPage) { 41 | // we set the view busy, so we need to query the parent of the app 42 | Opa5.assert.ok(!oPage.getBusy(), "The detail view is not busy"); 43 | }, 44 | errorMessage : "The detail view is busy." 45 | }); 46 | }, 47 | 48 | theObjectPageShowsTheFirstObject : function () { 49 | return this.iShouldBeOnTheObjectNPage(0); 50 | }, 51 | 52 | iShouldBeOnTheObjectNPage : function (iObjIndex) { 53 | return this.waitFor(this.createAWaitForAnEntitySet({ 54 | entitySet : "Persons", 55 | success : function (aEntitySet) { 56 | var sItemName = aEntitySet[iObjIndex].Name; 57 | 58 | this.waitFor({ 59 | controlType : "sap.m.ObjectHeader", 60 | viewName : sViewName, 61 | matchers : new PropertyStrictEquals({name : "title", value: aEntitySet[iObjIndex].Name}), 62 | success : function () { 63 | Opa5.assert.ok(true, "was on the first object page with the name " + sItemName); 64 | }, 65 | errorMessage : "First object is not shown" 66 | }); 67 | } 68 | })); 69 | }, 70 | 71 | iShouldSeeTheRememberedObject : function () { 72 | return this.waitFor({ 73 | success : function () { 74 | var sBindingPath = this.getContext().currentItem.bindingPath; 75 | this._waitForPageBindingPath(sBindingPath); 76 | } 77 | }); 78 | }, 79 | 80 | _waitForPageBindingPath : function (sBindingPath) { 81 | return this.waitFor({ 82 | id : "detailPage", 83 | viewName : sViewName, 84 | matchers : function (oPage) { 85 | return oPage.getBindingContext() && oPage.getBindingContext().getPath() === sBindingPath; 86 | }, 87 | success : function (oPage) { 88 | Opa5.assert.strictEqual(oPage.getBindingContext().getPath(), sBindingPath, "was on the remembered detail page"); 89 | }, 90 | errorMessage : "Remembered object " + sBindingPath + " is not shown" 91 | }); 92 | }, 93 | 94 | iShouldSeeTheObjectLineItemsList : function () { 95 | return this.waitFor({ 96 | id : "lineItemsList", 97 | viewName : sViewName, 98 | success : function (oList) { 99 | Opa5.assert.ok(oList, "Found the line items list."); 100 | } 101 | }); 102 | }, 103 | 104 | theLineItemsListShouldHaveTheCorrectNumberOfItems : function () { 105 | return this.waitFor(this.createAWaitForAnEntitySet({ 106 | entitySet : "PersonHasSkills", 107 | success : function (aEntitySet) { 108 | 109 | return this.waitFor({ 110 | id : "lineItemsList", 111 | viewName : sViewName, 112 | matchers : new AggregationFilled({name : "items"}), 113 | check: function (oList) { 114 | 115 | var sObjectID = oList.getBindingContext().getProperty("Id"); 116 | 117 | var iLength = aEntitySet.filter(function (oLineItem) { 118 | return oLineItem.Id === sObjectID; 119 | }).length; 120 | 121 | return oList.getItems().length === iLength; 122 | }, 123 | success : function () { 124 | Opa5.assert.ok(true, "The list has the correct number of items"); 125 | }, 126 | errorMessage : "The list does not have the correct number of items.\nHint: This test needs suitable mock data in localService directory which can be generated via SAP Web IDE" 127 | }); 128 | } 129 | })); 130 | }, 131 | 132 | theDetailViewShouldContainOnlyFormattedUnitNumbers : function () { 133 | var rTwoDecimalPlaces = /^-?\d+\.\d{2}$/; 134 | return this.waitFor({ 135 | id : "objectHeaderNumber", 136 | viewName : sViewName, 137 | success : function (oNumberControl) { 138 | Opa5.assert.ok(rTwoDecimalPlaces.test(oNumberControl.getNumber()), "Object numbers are properly formatted"); 139 | }, 140 | errorMessage : "Object view has no entries which can be checked for their formatting" 141 | }); 142 | }, 143 | 144 | theLineItemsHeaderShouldDisplayTheAmountOfEntries : function () { 145 | return this.waitFor({ 146 | id : "lineItemsList", 147 | viewName : sViewName, 148 | matchers : new AggregationFilled({name : "items"}), 149 | success : function (oList) { 150 | var iNumberOfItems = oList.getItems().length; 151 | return this.waitFor({ 152 | id : "lineItemsTitle", 153 | viewName : sViewName, 154 | matchers : new PropertyStrictEquals({name: "text", value: " (" + iNumberOfItems + ")"}), 155 | success : function () { 156 | Opa5.assert.ok(true, "The line item list displays " + iNumberOfItems + " items"); 157 | }, 158 | errorMessage : "The line item list does not display " + iNumberOfItems + " items." 159 | }); 160 | } 161 | }); 162 | }, 163 | iShouldSeeHeaderActionButtons: function () { 164 | return this.waitFor({ 165 | id : ["closeColumn", "enterFullScreen"], 166 | viewName : sViewName, 167 | success : function () { 168 | Opa5.assert.ok(true, "The action buttons are visible"); 169 | }, 170 | errorMessage : "The action buttons were not found" 171 | }); 172 | }, 173 | 174 | iShouldSeeTheFullScreenToggleButton : function (sId) { 175 | return this.waitFor({ 176 | id : sId, 177 | viewName : sViewName, 178 | errorMessage : "The toggle button" + sId + "was not found" 179 | }); 180 | } 181 | 182 | } 183 | 184 | } 185 | 186 | }); 187 | 188 | }); 189 | -------------------------------------------------------------------------------- /webapp/test/integration/pages/Master.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "./Common", 5 | "sap/ui/test/actions/EnterText", 6 | "sap/ui/test/matchers/AggregationLengthEquals", 7 | "sap/ui/test/matchers/AggregationFilled", 8 | "sap/ui/test/matchers/PropertyStrictEquals" 9 | ], function(Opa5, Press, Common, EnterText, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals) { 10 | "use strict"; 11 | 12 | var sViewName = "Master", 13 | sSomethingThatCannotBeFound = "*#-Q@@||"; 14 | 15 | Opa5.createPageObjects({ 16 | onTheMasterPage : { 17 | 18 | baseClass : Common, 19 | 20 | actions : { 21 | 22 | iSortTheListOnName : function () { 23 | return this.iChooseASorter("sortButton", "Sort By "); 24 | }, 25 | iSortTheListOnUnitNumber : function () { 26 | return this.iChooseASorter("sortButton", "Sort By "); 27 | }, 28 | 29 | iFilterTheListOnUnitNumber : function () { 30 | return this.iMakeASelection("filterButton", "", "<100 "); 31 | }, 32 | 33 | iGroupTheList : function () { 34 | return this.iChooseASorter("groupButton", " Group"); 35 | }, 36 | 37 | iRemoveListGrouping : function () { 38 | return this.iChooseASorter("groupButton", "None"); 39 | }, 40 | iOpenViewSettingsDialog : function () { 41 | return this.waitFor({ 42 | id : "filterButton", 43 | viewName : sViewName, 44 | actions : new Press(), 45 | errorMessage : "Did not find the 'filter' button." 46 | }); 47 | }, 48 | iPressOKInViewSelectionDialog : function () { 49 | return this.waitFor({ 50 | searchOpenDialogs : true, 51 | controlType : "sap.m.Button", 52 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "text", value : "OK"}), 53 | actions : new Press(), 54 | errorMessage : "Did not find the ViewSettingDialog's 'OK' button." 55 | }); 56 | }, 57 | 58 | iPressResetInViewSelectionDialog : function () { 59 | return this.waitFor({ 60 | searchOpenDialogs : true, 61 | controlType : "sap.m.Button", 62 | matchers : new Opa5.matchers.PropertyStrictEquals({name : "icon", value : "sap-icon://clear-filter"}), 63 | actions : new Press(), 64 | errorMessage : "Did not find the ViewSettingDialog's 'Reset' button." 65 | }); 66 | }, 67 | 68 | iMakeASelection : function (sSelect, sItem, sOption) { 69 | return this.waitFor({ 70 | id : sSelect, 71 | viewName : sViewName, 72 | actions : new Press(), 73 | success : function () { 74 | this.waitFor({ 75 | controlType: "sap.m.StandardListItem", 76 | matchers: new PropertyStrictEquals({name: "title", value: sItem}), 77 | searchOpenDialogs: true, 78 | actions: new Press(), 79 | success: function () { 80 | this.waitFor({ 81 | controlType: "sap.m.StandardListItem", 82 | matchers : new PropertyStrictEquals({name: "title", value: sOption}), 83 | searchOpenDialogs: true, 84 | actions : new Press(), 85 | success: function () { 86 | this.waitFor({ 87 | controlType: "sap.m.Button", 88 | matchers: new PropertyStrictEquals({name: "text", value: "OK"}), 89 | searchOpenDialogs: true, 90 | actions: new Press(), 91 | errorMessage: "The ok button in the dialog was not found and could not be pressed" 92 | }); 93 | }, 94 | errorMessage : "Did not find the" + sOption + "in" + sItem 95 | }); 96 | }, 97 | errorMessage : "Did not find the " + sItem + " element in select" 98 | }); 99 | }, 100 | errorMessage : "Did not find the " + sSelect + " select" 101 | }); 102 | }, 103 | 104 | iRememberTheSelectedItem : function () { 105 | return this.waitFor({ 106 | id : "list", 107 | viewName : sViewName, 108 | matchers : function (oList) { 109 | return oList.getSelectedItem(); 110 | }, 111 | success : function (oListItem) { 112 | this.iRememberTheListItem(oListItem); 113 | }, 114 | errorMessage : "The list does not have a selected item so nothing can be remembered" 115 | }); 116 | }, 117 | 118 | iChooseASorter: function (sSelect, sSort) { 119 | return this.waitFor({ 120 | id : sSelect, 121 | viewName : sViewName, 122 | actions : new Press(), 123 | success : function () { 124 | this.waitFor({ 125 | controlType: "sap.m.StandardListItem", 126 | matchers : new PropertyStrictEquals({name: "title", value: sSort}), 127 | searchOpenDialogs: true, 128 | actions : new Press(), 129 | success : function () { 130 | this.waitFor({ 131 | controlType: "sap.m.Button", 132 | matchers: new PropertyStrictEquals({name: "text", value: "OK"}), 133 | searchOpenDialogs: true, 134 | actions: new Press(), 135 | errorMessage: "The ok button in the dialog was not found and could not be pressed" 136 | }); 137 | }, 138 | errorMessage : "Did not find the" + sSort + " element in select" 139 | }); 140 | }, 141 | errorMessage : "Did not find the " + sSelect + " select" 142 | }); 143 | }, 144 | iRememberTheIdOfListItemAtPosition : function (iPosition) { 145 | return this.waitFor({ 146 | id : "list", 147 | viewName : sViewName, 148 | matchers : function (oList) { 149 | return oList.getItems()[iPosition]; 150 | }, 151 | success : function (oListItem) { 152 | this.iRememberTheListItem(oListItem); 153 | }, 154 | errorMessage : "The list does not have an item at the index " + iPosition 155 | }); 156 | }, 157 | iRememberAnIdOfAnObjectThatsNotInTheList : function () { 158 | return this.waitFor(this.createAWaitForAnEntitySet({ 159 | entitySet : "Persons", 160 | success : function (aEntityData) { 161 | this.waitFor({ 162 | id : "list", 163 | viewName : sViewName, 164 | matchers : new AggregationFilled({name: "items"}), 165 | success : function (oList) { 166 | var sCurrentId, 167 | aItemsNotInTheList = aEntityData.filter(function (oObject) { 168 | return !oList.getItems().some(function (oListItem) { 169 | return oListItem.getBindingContext().getProperty("Id") === oObject.Id; 170 | }); 171 | }); 172 | 173 | if (!aItemsNotInTheList.length) { 174 | // Not enough items all of them are displayed so we take the last one 175 | sCurrentId = aEntityData[aEntityData.length - 1].Id; 176 | } else { 177 | sCurrentId = aItemsNotInTheList[0].Id; 178 | } 179 | 180 | var oCurrentItem = this.getContext().currentItem; 181 | // Construct a binding path since the list item is not created yet and we only have the id. 182 | oCurrentItem.bindingPath = "/" + oList.getModel().createKey("Persons", { 183 | Id : sCurrentId 184 | }); 185 | oCurrentItem.id = sCurrentId; 186 | }, 187 | errorMessage : "the model does not have a item that is not in the list" 188 | }); 189 | } 190 | })); 191 | }, 192 | iPressOnTheObjectAtPosition : function (iPositon) { 193 | return this.waitFor({ 194 | id : "list", 195 | viewName : sViewName, 196 | matchers : function (oList) { 197 | return oList.getItems()[iPositon]; 198 | }, 199 | actions : new Press(), 200 | errorMessage : "List 'list' in view '" + sViewName + "' does not contain an ObjectListItem at position '" + iPositon + "'" 201 | }); 202 | }, 203 | iSearchForTheFirstObject : function (){ 204 | var sFirstObjectTitle; 205 | return this.waitFor({ 206 | id : "list", 207 | viewName : sViewName, 208 | matchers: new AggregationFilled({name : "items"}), 209 | success : function (oList) { 210 | sFirstObjectTitle = oList.getItems()[0].getTitle(); 211 | return this.iSearchForValue(new EnterText({text: sFirstObjectTitle}), new Press()); 212 | }, 213 | errorMessage : "Did not find list items while trying to search for the first item." 214 | }); 215 | }, 216 | iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh : function () { 217 | return this.iSearchForValue(function (oSearchField) { 218 | oSearchField.setValue(sSomethingThatCannotBeFound); 219 | oSearchField.fireSearch({refreshButtonPressed : true}); 220 | }); 221 | }, 222 | iSearchForValue : function (aActions) { 223 | return this.waitFor({ 224 | id : "searchField", 225 | viewName : sViewName, 226 | actions: aActions, 227 | errorMessage : "Failed to find search field in Master view.'" 228 | }); 229 | }, 230 | 231 | iClearTheSearch : function () { 232 | //can not use 'EnterText' action to enter empty strings (yet) 233 | var fnClearSearchField = function(oSearchField) { 234 | oSearchField.clear(); 235 | }; 236 | return this.iSearchForValue([fnClearSearchField]); 237 | }, 238 | 239 | iSearchForSomethingWithNoResults : function () { 240 | return this.iSearchForValue([new EnterText({text: sSomethingThatCannotBeFound}), new Press()]); 241 | }, 242 | 243 | iRememberTheListItem : function (oListItem) { 244 | var oBindingContext = oListItem.getBindingContext(); 245 | this.getContext().currentItem = { 246 | bindingPath: oBindingContext.getPath(), 247 | id: oBindingContext.getProperty("Id"), 248 | title: oBindingContext.getProperty("Firstname") 249 | }; 250 | } 251 | }, 252 | 253 | assertions : { 254 | 255 | iShouldSeeTheBusyIndicator : function () { 256 | return this.waitFor({ 257 | id: "list", 258 | viewName: sViewName, 259 | matchers: new PropertyStrictEquals({ 260 | name: "busy", 261 | value: true 262 | }), 263 | autoWait: false, 264 | success : function () { 265 | Opa5.assert.ok(true, "The master list is busy"); 266 | }, 267 | errorMessage : "The master list is not busy." 268 | }); 269 | }, 270 | 271 | theListShouldContainAGroupHeader : function () { 272 | return this.waitFor({ 273 | controlType : "sap.m.GroupHeaderListItem", 274 | viewName : sViewName, 275 | success : function () { 276 | Opa5.assert.ok(true, "Master list is grouped"); 277 | }, 278 | errorMessage : "Master list is not grouped" 279 | }); 280 | }, 281 | 282 | theListShouldContainOnlyFormattedUnitNumbers : function () { 283 | var rTwoDecimalPlaces = /^-?\d+\.\d{2}$/; 284 | return this.waitFor({ 285 | controlType : "sap.m.ObjectListItem", 286 | viewName : sViewName, 287 | success : function (aNumberControls) { 288 | Opa5.assert.ok(aNumberControls.every(function(oNumberControl){ 289 | return rTwoDecimalPlaces.test(oNumberControl.getNumber()); 290 | }), 291 | "Numbers in ObjectListItems numbers are properly formatted"); 292 | }, 293 | errorMessage : "List has no entries which can be checked for their formatting" 294 | }); 295 | }, 296 | 297 | theListHeaderDisplaysZeroHits : function () { 298 | return this.waitFor({ 299 | viewName : sViewName, 300 | id: "masterPageTitle", 301 | autoWait: false, 302 | matchers: new PropertyStrictEquals({name : "text", value : " (0)"}), 303 | success: function () { 304 | Opa5.assert.ok(true, "The list header displays zero hits"); 305 | }, 306 | errorMessage: "The list header still has items" 307 | }); 308 | }, 309 | 310 | theListHasEntries : function () { 311 | return this.waitFor({ 312 | viewName : sViewName, 313 | id : "list", 314 | matchers : new AggregationFilled({ 315 | name : "items" 316 | }), 317 | success : function () { 318 | Opa5.assert.ok(true, "The list has items"); 319 | }, 320 | errorMessage : "The list had no items" 321 | }); 322 | }, 323 | 324 | theListShouldNotContainGroupHeaders : function () { 325 | function fnIsGroupHeader (oElement) { 326 | return oElement.getMetadata().getName() === "sap.m.GroupHeaderListItem"; 327 | } 328 | 329 | return this.waitFor({ 330 | viewName : sViewName, 331 | id : "list", 332 | matchers : function (oList) { 333 | return !oList.getItems().some(fnIsGroupHeader); 334 | }, 335 | success : function() { 336 | Opa5.assert.ok(true, "Master list does not contain a group header although grouping has been removed."); 337 | }, 338 | errorMessage : "Master list still contains a group header although grouping has been removed." 339 | }); 340 | }, 341 | 342 | theListShouldBeFilteredOnUnitNumber : function () { 343 | return this.theListShouldBeFilteredOnFieldUsingComparator("Id", 100); 344 | }, 345 | 346 | 347 | theListShouldBeSortedAscendingOnUnitNumber : function () { 348 | return this.theListShouldBeSortedAscendingOnField("Id"); 349 | }, 350 | 351 | theListShouldBeSortedAscendingOnName : function () { 352 | return this.theListShouldBeSortedAscendingOnField("Firstname"); 353 | }, 354 | 355 | theListShouldBeFilteredOnFieldUsingComparator : function (sField, iComparator) { 356 | function fnCheckFilter(oList){ 357 | var fnIsFiltered = function (oElement) { 358 | if (!oElement.getBindingContext()) { 359 | return false; 360 | } else { 361 | var iValue = oElement.getBindingContext().getProperty(sField); 362 | if (iValue > iComparator) { 363 | return false; 364 | } else { 365 | return true; 366 | } 367 | } 368 | }; 369 | 370 | return oList.getItems().every(fnIsFiltered); 371 | } 372 | 373 | return this.waitFor({ 374 | viewName : sViewName, 375 | id : "list", 376 | matchers : fnCheckFilter, 377 | success : function() { 378 | Opa5.assert.ok(true, "Master list has been filtered correctly for field '" + sField + "'."); 379 | }, 380 | errorMessage : "Master list has not been filtered correctly for field '" + sField + "'." 381 | }); 382 | }, 383 | iShouldSeeTheList : function () { 384 | return this.waitFor({ 385 | id : "list", 386 | viewName : sViewName, 387 | success : function (oList) { 388 | Opa5.assert.ok(oList, "Found the object List"); 389 | }, 390 | errorMessage : "Can't see the master list." 391 | }); 392 | }, 393 | 394 | theListShowsOnlyObjectsWithTheSearchStringInTheirTitle : function () { 395 | this.waitFor({ 396 | id : "list", 397 | viewName : sViewName, 398 | matchers : new AggregationFilled({name : "items"}), 399 | check : function(oList) { 400 | var sTitle = oList.getItems()[0].getTitle(), 401 | bEveryItemContainsTheTitle = oList.getItems().every(function (oItem) { 402 | return oItem.getTitle().indexOf(sTitle) !== -1; 403 | }); 404 | return bEveryItemContainsTheTitle; 405 | }, 406 | success : function (oList) { 407 | Opa5.assert.ok(true, "Every item did contain the title"); 408 | }, 409 | errorMessage : "The list did not have items" 410 | }); 411 | }, 412 | theListShouldHaveAllEntries : function () { 413 | var aAllEntities, 414 | iExpectedNumberOfItems; 415 | // retrieve all Persons to be able to check for the total amount 416 | this.waitFor(this.createAWaitForAnEntitySet({ 417 | entitySet : "Persons", 418 | success : function (aEntityData) { 419 | aAllEntities = aEntityData; 420 | } 421 | })); 422 | 423 | return this.waitFor({ 424 | id : "list", 425 | viewName : sViewName, 426 | matchers : function (oList) { 427 | // If there are less items in the list than the growingThreshold, only check for this number. 428 | iExpectedNumberOfItems = Math.min(oList.getGrowingThreshold(), aAllEntities.length); 429 | return new AggregationLengthEquals({name : "items", length : iExpectedNumberOfItems}).isMatching(oList); 430 | }, 431 | success : function (oList) { 432 | Opa5.assert.strictEqual(oList.getItems().length, iExpectedNumberOfItems, "The growing list displays all items"); 433 | }, 434 | errorMessage : "List does not display all entries." 435 | }); 436 | }, 437 | 438 | iShouldSeeTheNoDataTextForNoSearchResults : function () { 439 | return this.waitFor({ 440 | id : "list", 441 | viewName : sViewName, 442 | success : function (oList) { 443 | Opa5.assert.strictEqual(oList.getNoDataText(), oList.getModel("i18n").getProperty("masterListNoDataWithFilterOrSearchText"), "the list should show the no data text for search and filter"); 444 | }, 445 | errorMessage : "list does not show the no data text for search and filter" 446 | }); 447 | }, 448 | 449 | theHeaderShouldDisplayAllEntries : function () { 450 | return this.waitFor({ 451 | id : "list", 452 | viewName : sViewName, 453 | success : function (oList) { 454 | var iExpectedLength = oList.getBinding("items").getLength(); 455 | this.waitFor({ 456 | id : "masterPageTitle", 457 | viewName : sViewName, 458 | matchers : new PropertyStrictEquals({name : "text", value : " (" + iExpectedLength + ")"}), 459 | success : function () { 460 | Opa5.assert.ok(true, "The master page header displays " + iExpectedLength + " items"); 461 | }, 462 | errorMessage : "The master page header does not display " + iExpectedLength + " items." 463 | }); 464 | }, 465 | errorMessage : "Header does not display the number of items in the list" 466 | }); 467 | }, 468 | 469 | theListShouldHaveNoSelection : function () { 470 | return this.waitFor({ 471 | id : "list", 472 | viewName : sViewName, 473 | matchers : function(oList) { 474 | return !oList.getSelectedItem(); 475 | }, 476 | success : function (oList) { 477 | Opa5.assert.strictEqual(oList.getSelectedItems().length, 0, "The list selection is removed"); 478 | }, 479 | errorMessage : "List selection was not removed" 480 | }); 481 | }, 482 | 483 | theRememberedListItemShouldBeSelected : function () { 484 | this.waitFor({ 485 | id : "list", 486 | viewName : sViewName, 487 | matchers : function (oList) { 488 | return oList.getSelectedItem(); 489 | }, 490 | success : function (oSelectedItem) { 491 | Opa5.assert.strictEqual(oSelectedItem.getTitle(), this.getContext().currentItem.title, "The list selection is incorrect.\nHint: If the master list shows integer numbers, use toString function to convert the second parameter to string"); 492 | }, 493 | errorMessage : "The list has no selection" 494 | }); 495 | }, 496 | theListShouldBeSortedAscendingOnField : function (sField) { 497 | function fnCheckSort (oList){ 498 | var oLastValue = null, 499 | fnSortByField = function (oElement) { 500 | if (!oElement.getBindingContext()) { 501 | return false; 502 | } 503 | 504 | var oCurrentValue = oElement.getBindingContext().getProperty(sField); 505 | 506 | if (oCurrentValue === undefined) { 507 | return false; 508 | } 509 | 510 | if (!oLastValue || oCurrentValue >= oLastValue){ 511 | oLastValue = oCurrentValue; 512 | } else { 513 | return false; 514 | } 515 | return true; 516 | }; 517 | 518 | return oList.getItems().every(fnSortByField); 519 | } 520 | 521 | return this.waitFor({ 522 | viewName : sViewName, 523 | id : "list", 524 | matchers : fnCheckSort, 525 | success : function() { 526 | Opa5.assert.ok(true, "Master list has been sorted correctly for field '" + sField + "'."); 527 | }, 528 | errorMessage : "Master list has not been sorted correctly for field '" + sField + "'." 529 | }); 530 | } 531 | } 532 | } 533 | }); 534 | }); -------------------------------------------------------------------------------- /webapp/test/integration/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "sap/ui/test/matchers/PropertyStrictEquals" 5 | ], function(Opa5, Press, PropertyStrictEquals) { 6 | "use strict"; 7 | 8 | var sNotFoundPageId = "page", 9 | sNotFoundView = "NotFound", 10 | sDetailNotFoundView = "DetailObjectNotFound"; 11 | 12 | Opa5.createPageObjects({ 13 | onTheNotFoundPage : { 14 | 15 | actions : { 16 | 17 | iPressTheBackButton : function (sViewName) { 18 | return this.waitFor({ 19 | viewName : sViewName, 20 | controlType : "sap.m.Button", 21 | matchers: new PropertyStrictEquals({name : "type", value : "Back"}), 22 | actions : new Press(), 23 | errorMessage : "Did not find the back button" 24 | }); 25 | } 26 | }, 27 | 28 | assertions : { 29 | 30 | iShouldSeeTheNotFoundGeneralPage : function (sPageId, sPageViewName) { 31 | return this.waitFor({ 32 | controlType : "sap.m.MessagePage", 33 | viewName : sPageViewName, 34 | success : function () { 35 | Opa5.assert.ok(true, "Shows the message page"); 36 | }, 37 | errorMessage : "Did not reach the empty page" 38 | }); 39 | }, 40 | 41 | iShouldSeeTheNotFoundPage : function () { 42 | return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundPageId, sNotFoundView); 43 | }, 44 | 45 | iShouldSeeTheObjectNotFoundPage : function () { 46 | return this.iShouldSeeTheNotFoundGeneralPage(sNotFoundPageId, sDetailNotFoundView); 47 | }, 48 | 49 | theNotFoundPageShouldSayResourceNotFound : function () { 50 | return this.waitFor({ 51 | id : sNotFoundPageId, 52 | viewName : sNotFoundView, 53 | success : function (oPage) { 54 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("notFoundTitle"), "The not found text is shown as title"); 55 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("notFoundText"), "The resource not found text is shown"); 56 | }, 57 | errorMessage : "Did not display the resource not found text" 58 | }); 59 | }, 60 | 61 | theNotFoundPageShouldSayObjectNotFound : function () { 62 | return this.waitFor({ 63 | id : sNotFoundPageId, 64 | viewName : sDetailNotFoundView, 65 | success : function (oPage) { 66 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("detailTitle"), "The object text is shown as title"); 67 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("noObjectFoundText"), "The object not found text is shown"); 68 | }, 69 | errorMessage : "Did not display the object not found text" 70 | }); 71 | } 72 | } 73 | } 74 | }); 75 | }); -------------------------------------------------------------------------------- /webapp/test/mockServer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PersonSkills 7 | 8 | 19 | 20 | 21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /webapp/test/testsuite.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | QUnit test suite for PersonSkills 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /webapp/test/testsuite.qunit.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line sap-no-global-define 2 | window.suite = function () { 3 | "use strict"; 4 | /* eslint-disable new-cap */ 5 | var oSuite = new parent.jsUnitTestSuite(), 6 | sContextPath = location.pathname.substring(0, location.pathname.lastIndexOf("/") + 1); 7 | 8 | oSuite.addTestPage(sContextPath + "unit/unitTests.qunit.html"); 9 | oSuite.addTestPage(sContextPath + "integration/opaTests.qunit.html"); 10 | 11 | return oSuite; 12 | }; -------------------------------------------------------------------------------- /webapp/test/unit/AllTests.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./model/models", 3 | "./model/formatter", 4 | "./controller/ListSelector" 5 | ], function() { 6 | "use strict"; 7 | }); -------------------------------------------------------------------------------- /webapp/test/unit/controller/ListSelector.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "be/wl/PersonSkills/controller/ListSelector" 5 | ], function(ListSelector) { 6 | "use strict"; 7 | 8 | QUnit.module("Initialization", { 9 | beforeEach : function () { 10 | this.oListSelector = new ListSelector(); 11 | }, 12 | afterEach : function () { 13 | this.oListSelector.destroy(); 14 | } 15 | }); 16 | 17 | QUnit.test("Should initialize the List loading promise", function (assert) { 18 | // Arrange 19 | var done = assert.async(), 20 | fnRejectSpy = this.spy(), 21 | fnResolveSpy = this.spy(); 22 | 23 | // Act 24 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 25 | 26 | // Assert 27 | setTimeout(function () { 28 | assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise"); 29 | assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise"); 30 | done(); 31 | }, 0); 32 | }); 33 | 34 | QUnit.module("List loading", { 35 | beforeEach : function () { 36 | this.oListSelector = new ListSelector(); 37 | }, 38 | afterEach : function () { 39 | this.oListSelector.destroy(); 40 | } 41 | }); 42 | 43 | function createListStub (bCreateListItem, sBindingPath) { 44 | var fnGetParameter = function () { 45 | return true; 46 | }, 47 | oDataStub = { 48 | getParameter : fnGetParameter 49 | }, 50 | fnAttachEventOnce = function (sEventName, fnCallback) { 51 | fnCallback(oDataStub); 52 | }, 53 | fnGetBinding = this.stub().returns({ 54 | attachEventOnce : fnAttachEventOnce 55 | }), 56 | fnAttachEvent = function (sEventName, fnCallback, oContext) { 57 | fnCallback.apply(oContext); 58 | }, 59 | oListItemStub = { 60 | getBindingContext : this.stub().returns({ 61 | getPath : this.stub().returns(sBindingPath) 62 | }) 63 | }, 64 | aListItems = []; 65 | 66 | if (bCreateListItem) { 67 | aListItems.push(oListItemStub); 68 | } 69 | 70 | return { 71 | attachEvent : fnAttachEvent, 72 | attachEventOnce : fnAttachEventOnce, 73 | getBinding : fnGetBinding, 74 | getItems : this.stub().returns(aListItems) 75 | }; 76 | } 77 | 78 | QUnit.test("Should resolve the list loading promise, if the list has items", function (assert) { 79 | // Arrange 80 | var done = assert.async(), 81 | fnRejectSpy = this.spy(), 82 | fnResolveSpy = function (sBindingPath) { 83 | // Assert 84 | assert.strictEqual(sBindingPath, sBindingPath, "Did pass the binding path"); 85 | assert.strictEqual(fnRejectSpy.callCount, 0, "Did not reject the promise"); 86 | done(); 87 | }; 88 | 89 | // Act 90 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 91 | this.oListSelector.setBoundMasterList(createListStub.call(this, true, "anything")); 92 | }); 93 | 94 | QUnit.test("Should reject the list loading promise, if the list has no items", function (assert) { 95 | // Arrange 96 | var done = assert.async(), 97 | fnResolveSpy = this.spy(), 98 | fnRejectSpy = function () { 99 | // Assert 100 | assert.strictEqual(fnResolveSpy.callCount, 0, "Did not resolve the promise"); 101 | done(); 102 | }; 103 | 104 | // Act 105 | this.oListSelector.oWhenListLoadingIsDone.then(fnResolveSpy, fnRejectSpy); 106 | this.oListSelector.setBoundMasterList(createListStub.call(this, false)); 107 | }); 108 | 109 | QUnit.module("Selecting item in the list", { 110 | beforeEach : function () { 111 | this.oListSelector = new ListSelector(); 112 | this.oListSelector.oWhenListLoadingIsDone = { 113 | then : function (fnAct) { 114 | this.fnAct = fnAct; 115 | }.bind(this) 116 | }; 117 | }, 118 | afterEach : function () { 119 | this.oListSelector.destroy(); 120 | } 121 | }); 122 | 123 | function createStubbedListItem (sBindingPath) { 124 | return { 125 | getBindingContext : this.stub().returns({ 126 | getPath : this.stub().returns(sBindingPath) 127 | }) 128 | }; 129 | } 130 | 131 | QUnit.test("Should select an Item of the list when it is loaded and the binding contexts match", function (assert) { 132 | // Arrange 133 | var sBindingPath = "anything", 134 | oListItemToSelect = createStubbedListItem.call(this, sBindingPath), 135 | oSelectedListItemStub = createStubbedListItem.call(this, "a different binding path"); 136 | 137 | this.oListSelector._oList = { 138 | getMode : this.stub().returns("SingleSelectMaster"), 139 | getSelectedItem : this.stub().returns(oSelectedListItemStub), 140 | getItems : this.stub().returns([ oSelectedListItemStub, oListItemToSelect, createListStub.call(this, "yet another list binding") ]), 141 | setSelectedItem : function (oItem) { 142 | //Assert 143 | assert.strictEqual(oItem, oListItemToSelect, "Did select the list item with a matching binding context"); 144 | } 145 | }; 146 | 147 | // Act 148 | this.oListSelector.selectAListItem(sBindingPath); 149 | // Resolve list loading 150 | this.fnAct(); 151 | }); 152 | 153 | QUnit.test("Should not select an Item of the list when it is already selected", function (assert) { 154 | // Arrange 155 | var sBindingPath = "anything", 156 | oSelectedListItemStub = createStubbedListItem.call(this, sBindingPath); 157 | 158 | this.oListSelector._oList = { 159 | getMode: this.stub().returns("SingleSelectMaster"), 160 | getSelectedItem : this.stub().returns(oSelectedListItemStub) 161 | }; 162 | 163 | // Act 164 | this.oListSelector.selectAListItem(sBindingPath); 165 | // Resolve list loading 166 | this.fnAct(); 167 | 168 | // Assert 169 | assert.ok(true, "did not fail"); 170 | }); 171 | 172 | QUnit.test("Should not select an item of the list when the list has the selection mode none", function (assert) { 173 | // Arrange 174 | var sBindingPath = "anything"; 175 | 176 | this.oListSelector._oList = { 177 | getMode : this.stub().returns("None") 178 | }; 179 | 180 | // Act 181 | this.oListSelector.selectAListItem(sBindingPath); 182 | // Resolve list loading 183 | this.fnAct(); 184 | 185 | // Assert 186 | assert.ok(true, "did not fail"); 187 | }); 188 | 189 | }); -------------------------------------------------------------------------------- /webapp/test/unit/model/formatter.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/m/Text", 5 | "be/wl/PersonSkills/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 | }); -------------------------------------------------------------------------------- /webapp/test/unit/model/models.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "be/wl/PersonSkills/model/models" 5 | ], function (models) { 6 | "use strict"; 7 | 8 | QUnit.module("createDeviceModel", { 9 | afterEach : function () { 10 | this.oDeviceModel.destroy(); 11 | } 12 | }); 13 | 14 | function isPhoneTestCase(assert, bIsPhone) { 15 | // Arrange 16 | this.stub(sap.ui.Device, "system", { phone : bIsPhone }); 17 | 18 | // System under test 19 | this.oDeviceModel = models.createDeviceModel(); 20 | 21 | // Assert 22 | assert.strictEqual(this.oDeviceModel.getData().system.phone, bIsPhone, "IsPhone property is correct"); 23 | } 24 | 25 | QUnit.test("Should initialize a device model for desktop", function (assert) { 26 | isPhoneTestCase.call(this, assert, false); 27 | }); 28 | 29 | QUnit.test("Should initialize a device model for phone", function (assert) { 30 | isPhoneTestCase.call(this, assert, true); 31 | }); 32 | 33 | function isTouchTestCase(assert, bIsTouch) { 34 | // Arrange 35 | this.stub(sap.ui.Device, "support", { touch : bIsTouch }); 36 | 37 | // System under test 38 | this.oDeviceModel = models.createDeviceModel(); 39 | 40 | // Assert 41 | assert.strictEqual(this.oDeviceModel.getData().support.touch, bIsTouch, "IsTouch property is correct"); 42 | } 43 | 44 | QUnit.test("Should initialize a device model for non touch devices", function (assert) { 45 | isTouchTestCase.call(this, assert, false); 46 | }); 47 | 48 | QUnit.test("Should initialize a device model for touch devices", function (assert) { 49 | isTouchTestCase.call(this, assert, true); 50 | }); 51 | 52 | QUnit.test("The binding mode of the device model should be one way", function (assert) { 53 | 54 | // System under test 55 | this.oDeviceModel = models.createDeviceModel(); 56 | 57 | // Assert 58 | assert.strictEqual(this.oDeviceModel.getDefaultBindingMode(), "OneWay", "Binding mode is correct"); 59 | }); 60 | }); -------------------------------------------------------------------------------- /webapp/test/unit/unitTests.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unit tests for PersonSkills 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /webapp/test/unit/unitTests.qunit.js: -------------------------------------------------------------------------------- 1 | /* global QUnit */ 2 | QUnit.config.autostart = false; 3 | 4 | sap.ui.getCore().attachInit(function () { 5 | "use strict"; 6 | 7 | sap.ui.require([ 8 | "be/wl/PersonSkills/test/unit/AllTests" 9 | ], function () { 10 | QUnit.start(); 11 | }); 12 | }); -------------------------------------------------------------------------------- /webapp/view/App.view.xml: -------------------------------------------------------------------------------- 1 | 8 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /webapp/view/Detail.view.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | </semantic:titleHeading> 7 | <semantic:headerContent> 8 | <ObjectAttribute title="{i18n>priceTitle}"/> 9 | <ObjectNumber id="objectHeaderNumber" number="{ path: 'pers>/Person/Id', formatter: '.formatter.currencyValue' }"/> 10 | </semantic:headerContent> 11 | <semantic:content> 12 | <l:VerticalLayout width="100%"> 13 | <f:Form id="generalform" editable="true" visible="{= !${pers>/display}}"> 14 | <f:layout> 15 | <f:ResponsiveGridLayout labelSpanXL="3" labelSpanL="3" labelSpanM="4" labelSpanS="12" adjustLabelSpan="false" emptySpanXL="0" emptySpanL="4" 16 | emptySpanM="0" emptySpanS="0" columnsXL="2" columnsL="1" columnsM="1" singleContainerFullSize="false"/> 17 | </f:layout> 18 | <f:formContainers> 19 | <f:FormContainer title=" "> 20 | <f:formElements> 21 | <f:FormElement label="Firstname"> 22 | <f:fields> 23 | <Input value="{pers>/Person/Firstname}"/> 24 | </f:fields> 25 | </f:FormElement> 26 | <f:FormElement label="Lastname"> 27 | <f:fields> 28 | <Input value="{pers>/Person/Lastname}"/> 29 | </f:fields> 30 | </f:FormElement> 31 | <f:FormElement label="Birthdate"> 32 | <f:fields> 33 | <DatePicker 34 | value="{path: 'pers>/Person/Birthdate', type: 'sap.ui.model.type.Date', formatOptions: { pattern: 'dd/MM/YYYY' , UTC:true}}"/> 35 | </f:fields> 36 | </f:FormElement> 37 | </f:formElements> 38 | </f:FormContainer> 39 | </f:formContainers> 40 | </f:Form> 41 | <Table visible="{pers>/display}" id="lineItemsList" width="auto" items="{pers>/Person/Skills}" 42 | noDataText="{i18n>detailLineItemTableNoDataText}" busyIndicatorDelay="{detailView>/lineItemTableDelay}"> 43 | <headerToolbar> 44 | <Toolbar> 45 | <Title id="lineItemsTitle" text="{detailView>/lineItemListTitle}" titleStyle="H3" level="H3"/> 46 | </Toolbar> 47 | </headerToolbar> 48 | <columns> 49 | <Column> 50 | <Text text="{i18n>detailLineItemTableIDColumn}"/> 51 | </Column> 52 | <Column minScreenWidth="Tablet" demandPopin="true" hAlign="End"> 53 | <Text text="{i18n>detailLineItemTableUnitNumberColumn}"/> 54 | </Column> 55 | </columns> 56 | <items> 57 | <ColumnListItem> 58 | <cells> 59 | <ObjectIdentifier title="{pers>SkillName}" text="{Id}"/> 60 | <ObjectNumber number="{ path: 'pers>Score', formatter: '.formatter.currencyValue' }"/> 61 | </cells> 62 | </ColumnListItem> 63 | </items> 64 | </Table> 65 | <Table items="{pers>/Person/Skills}" fixedLayout="false" visible="{= !${pers>/display}}"> 66 | <headerToolbar> 67 | <Toolbar> 68 | <Title text="Average: {pers>/Person/Total}" titleStyle="H3" level="H3"/> 69 | </Toolbar> 70 | </headerToolbar> 71 | <columns> 72 | <Column > 73 | <Text text="Skill"/> 74 | </Column> 75 | <Column > 76 | <Text text="Score"/> 77 | </Column> 78 | <Column > 79 | <Text text="Edit"/> 80 | </Column> 81 | <Column > 82 | <Text text="Delete"/> 83 | </Column> 84 | </columns> 85 | <items> 86 | <ColumnListItem> 87 | <cells> 88 | <Input value="{pers>SkillName}" editable="{pers>Editable}"/> 89 | <Input value="{path:'pers>Score',type: 'sap.ui.model.type.Integer'}" editable="{pers>Editable}"/> 90 | <l:VerticalLayout width="100%"> 91 | <ToggleButton icon="sap-icon://{= ${pers>Editable}?'display':'edit' }" pressed="{pers>Editable}" /> 92 | </l:VerticalLayout> 93 | <Button icon="sap-icon://delete" visible="{pers>Deletable}" press=".onDeleteSkill"/> 94 | </cells> 95 | </ColumnListItem> 96 | </items> 97 | </Table> 98 | </l:VerticalLayout> 99 | </semantic:content> 100 | <semantic:footerMainAction > 101 | <semantic:FooterMainAction visible="{= !${pers>/display}}" text="Save" press=".onSave"/> 102 | </semantic:footerMainAction> 103 | <!--Semantic ShareMenu Buttons--> 104 | <semantic:sendEmailAction> 105 | <semantic:SendEmailAction id="shareEmail" press=".onSendEmailPress"/> 106 | </semantic:sendEmailAction> 107 | <semantic:closeAction> 108 | <semantic:CloseAction id="closeColumn" press=".onCloseDetailPress"/> 109 | </semantic:closeAction> 110 | <semantic:fullScreenAction> 111 | <semantic:FullScreenAction id="enterFullScreen" 112 | visible="{= !${device>/system/phone} && !${appView>/actionButtonsInfo/midColumn/fullScreen}}" press=".toggleFullScreen"/> 113 | </semantic:fullScreenAction> 114 | <semantic:exitFullScreenAction> 115 | <semantic:ExitFullScreenAction id="exitFullScreen" 116 | visible="{= !${device>/system/phone} && ${appView>/actionButtonsInfo/midColumn/fullScreen}}" press=".toggleFullScreen"/> 117 | </semantic:exitFullScreenAction> 118 | </semantic:SemanticPage> 119 | </mvc:View> -------------------------------------------------------------------------------- /webapp/view/DetailObjectNotFound.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="be.wl.PersonSkills.controller.DetailObjectNotFound" 3 | xmlns="sap.m" 4 | xmlns:mvc="sap.ui.core.mvc"> 5 | 6 | <MessagePage 7 | id="page" 8 | title="{i18n>detailTitle}" 9 | text="{i18n>noObjectFoundText}" 10 | icon="sap-icon://product" 11 | description="" 12 | showNavButton="{= 13 | ${device>/system/phone} || 14 | ${device>/system/tablet} && 15 | ${device>/orientation/portrait} 16 | }" 17 | navButtonPress=".onNavBack"> 18 | </MessagePage> 19 | 20 | </mvc:View> -------------------------------------------------------------------------------- /webapp/view/Master.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="be.wl.PersonSkills.controller.Master" 3 | xmlns="sap.m" 4 | xmlns:semantic="sap.f.semantic" 5 | xmlns:mvc="sap.ui.core.mvc"> 6 | 7 | <semantic:SemanticPage 8 | id="masterPage" 9 | preserveHeaderStateOnScroll="true" 10 | toggleHeaderOnTitleClick="false"> 11 | 12 | <semantic:addAction> 13 | <semantic:AddAction press=".onCreatePerson"/> 14 | </semantic:addAction> 15 | <semantic:titleHeading> 16 | <Title 17 | id="masterPageTitle" 18 | text="{masterView>/title}" 19 | level="H2"/> 20 | </semantic:titleHeading> 21 | <semantic:content> 22 | <!-- For client side filtering add this to the items attribute: parameters: {operationMode: 'Client'}}" --> 23 | <List 24 | id="list" 25 | width="auto" 26 | class="sapFDynamicPageAlignContent" 27 | items="{ 28 | path: '/Persons', 29 | sorter: { 30 | path: 'Firstname', 31 | descending: false 32 | }, 33 | groupHeaderFactory: '.createGroupHeader' 34 | }" 35 | busyIndicatorDelay="{masterView>/delay}" 36 | noDataText="{masterView>/noDataText}" 37 | mode="{= ${device>/system/phone} ? 'None' : 'SingleSelectMaster'}" 38 | growing="true" 39 | growingScrollToLoad="true" 40 | updateFinished=".onUpdateFinished" 41 | selectionChange=".onSelectionChange"> 42 | <infoToolbar> 43 | <Toolbar 44 | active="true" 45 | id="filterBar" 46 | visible="{masterView>/isFilterBarVisible}" 47 | press=".onOpenViewSettings"> 48 | <Title 49 | id="filterBarLabel" 50 | text="{masterView>/filterBarLabel}" 51 | level="H3"/> 52 | </Toolbar> 53 | </infoToolbar> 54 | <headerToolbar> 55 | <OverflowToolbar> 56 | <SearchField 57 | id="searchField" 58 | showRefreshButton="true" 59 | tooltip="{i18n>masterSearchTooltip}" 60 | search=".onSearch" 61 | width="auto"> 62 | <layoutData> 63 | <OverflowToolbarLayoutData 64 | minWidth="150px" 65 | maxWidth="240px" 66 | shrinkable="true" 67 | priority="NeverOverflow"/> 68 | </layoutData> 69 | </SearchField> 70 | <ToolbarSpacer/> 71 | <Button 72 | id="sortButton" 73 | press=".onOpenViewSettings" 74 | icon="sap-icon://sort" 75 | type="Transparent"/> 76 | <Button 77 | id="filterButton" 78 | press=".onOpenViewSettings" 79 | icon="sap-icon://filter" 80 | type="Transparent"/> 81 | <Button 82 | id="groupButton" 83 | press=".onOpenViewSettings" 84 | icon="sap-icon://group-2" 85 | type="Transparent"/> 86 | </OverflowToolbar> 87 | </headerToolbar> 88 | <items> 89 | <ObjectListItem 90 | type="Navigation" 91 | press=".onSelectionChange" 92 | title="{Firstname}" 93 | number="{ 94 | path: 'Id', 95 | formatter: '.formatter.currencyValue' 96 | }" 97 | > 98 | </ObjectListItem> 99 | </items> 100 | </List> 101 | </semantic:content> 102 | </semantic:SemanticPage> 103 | </mvc:View> -------------------------------------------------------------------------------- /webapp/view/NotFound.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="be.wl.PersonSkills.controller.NotFound" 3 | xmlns="sap.m" 4 | xmlns:mvc="sap.ui.core.mvc"> 5 | 6 | <MessagePage 7 | id="page" 8 | title="{i18n>notFoundTitle}" 9 | text="{i18n>notFoundText}" 10 | icon="sap-icon://document" 11 | showNavButton="true" 12 | navButtonPress=".onNavBack"> 13 | </MessagePage> 14 | 15 | </mvc:View> -------------------------------------------------------------------------------- /webapp/view/ViewSettingsDialog.fragment.xml: -------------------------------------------------------------------------------- 1 | <core:FragmentDefinition 2 | xmlns="sap.m" 3 | xmlns:core="sap.ui.core"> 4 | 5 | <ViewSettingsDialog 6 | id="viewSettingsDialog" 7 | confirm=".onConfirmViewSettingsDialog"> 8 | <sortItems> 9 | <ViewSettingsItem 10 | text="{i18n>masterSort1}" 11 | key="Firstname" 12 | selected="true"/> 13 | <ViewSettingsItem 14 | text="{i18n>masterSort2}" 15 | key="Id"/> 16 | </sortItems> 17 | <filterItems> 18 | <ViewSettingsFilterItem 19 | id="filterItems" 20 | text="{i18n>masterFilterName}" 21 | multiSelect="false"> 22 | <items> 23 | <ViewSettingsItem 24 | id="viewFilter1" 25 | text="{i18n>masterFilter1}" 26 | key="Filter1"/> 27 | <ViewSettingsItem 28 | id="viewFilter2" 29 | text="{i18n>masterFilter2}" 30 | key="Filter2"/> 31 | </items> 32 | </ViewSettingsFilterItem> 33 | </filterItems> 34 | <groupItems> 35 | <ViewSettingsItem 36 | text="{i18n>masterGroup1}" 37 | key="Id"/> 38 | </groupItems> 39 | </ViewSettingsDialog> 40 | </core:FragmentDefinition> --------------------------------------------------------------------------------