├── demo-project ├── .gitignore ├── packages │ ├── server │ │ ├── .env.example │ │ ├── ui5.yaml │ │ ├── index.js │ │ ├── package.json │ │ ├── .eslintrc.json │ │ └── lib │ │ │ └── middleware.js │ ├── app │ │ ├── webapp │ │ │ ├── test │ │ │ │ ├── unit │ │ │ │ │ ├── AllTests.js │ │ │ │ │ ├── unitTests.qunit.js │ │ │ │ │ ├── helper │ │ │ │ │ │ └── FakeI18nModel.js │ │ │ │ │ ├── unitTests.qunit.html │ │ │ │ │ └── model │ │ │ │ │ │ ├── formatter.js │ │ │ │ │ │ └── models.js │ │ │ │ ├── integration │ │ │ │ │ ├── opaTests.qunit.js │ │ │ │ │ ├── AllJourneys.js │ │ │ │ │ ├── opaTests.qunit.html │ │ │ │ │ ├── ObjectJourney.js │ │ │ │ │ ├── pages │ │ │ │ │ │ ├── shareOptions.js │ │ │ │ │ │ ├── Browser.js │ │ │ │ │ │ ├── Common.js │ │ │ │ │ │ ├── App.js │ │ │ │ │ │ ├── NotFound.js │ │ │ │ │ │ ├── Object.js │ │ │ │ │ │ └── Worklist.js │ │ │ │ │ ├── WorklistJourney.js │ │ │ │ │ ├── arrangements │ │ │ │ │ │ └── Startup.js │ │ │ │ │ ├── NotFoundJourney.js │ │ │ │ │ └── NavigationJourney.js │ │ │ │ ├── testsuite.qunit.html │ │ │ │ └── testsuite.qunit.js │ │ │ ├── view │ │ │ │ ├── App.view.xml │ │ │ │ ├── NotFound.view.xml │ │ │ │ ├── ObjectNotFound.view.xml │ │ │ │ ├── Object.view.xml │ │ │ │ └── Worklist.view.xml │ │ │ ├── model │ │ │ │ ├── models.js │ │ │ │ └── formatter.js │ │ │ ├── controller │ │ │ │ ├── NotFound.controller.js │ │ │ │ ├── App.controller.js │ │ │ │ ├── BaseController.js │ │ │ │ ├── ErrorHandler.js │ │ │ │ ├── Object.controller.js │ │ │ │ └── Worklist.controller.js │ │ │ ├── index.html │ │ │ ├── test.html │ │ │ ├── manifest.json │ │ │ ├── i18n │ │ │ │ └── i18n.properties │ │ │ └── Component.js │ │ ├── ui5.yaml │ │ ├── package.json │ │ └── .eslintrc.json │ ├── theme-library │ │ ├── src │ │ │ ├── sap │ │ │ │ ├── ui │ │ │ │ │ ├── core │ │ │ │ │ │ └── themes │ │ │ │ │ │ │ └── ui5con20 │ │ │ │ │ │ │ ├── library.source.less │ │ │ │ │ │ │ └── base.less │ │ │ │ │ └── layout │ │ │ │ │ │ └── themes │ │ │ │ │ │ └── ui5con20 │ │ │ │ │ │ └── library.source.less │ │ │ │ ├── f │ │ │ │ │ └── themes │ │ │ │ │ │ └── ui5con20 │ │ │ │ │ │ └── library.source.less │ │ │ │ └── m │ │ │ │ │ └── themes │ │ │ │ │ └── ui5con20 │ │ │ │ │ └── library.source.less │ │ │ └── ui5con20 │ │ │ │ └── library │ │ │ │ └── themes │ │ │ │ └── ui5con20 │ │ │ │ └── library.source.less │ │ ├── package.json │ │ └── ui5.yaml │ └── library │ │ ├── src │ │ └── ui5con20 │ │ │ └── library │ │ │ ├── themes │ │ │ ├── base │ │ │ │ ├── library.source.less │ │ │ │ └── Chart.less │ │ │ ├── sap_fiori_3 │ │ │ │ └── library.source.less │ │ │ └── sap_fiori_3_dark │ │ │ │ └── library.source.less │ │ │ ├── .library │ │ │ ├── library.js │ │ │ ├── ChartRenderer.js │ │ │ └── Chart.js │ │ ├── package.json │ │ ├── ui5.yaml │ │ ├── test │ │ └── ui5con20 │ │ │ └── library │ │ │ └── qunit │ │ │ ├── Chart.qunit.html │ │ │ └── Chart.qunit.js │ │ └── .eslintrc.json ├── .editorconfig ├── package.json └── README.md ├── demo-nodejs-api ├── webapp │ ├── view │ │ ├── App.view.xml │ │ ├── NotFound.view.xml │ │ ├── ObjectNotFound.view.xml │ │ ├── Object.view.xml │ │ └── Worklist.view.xml │ ├── controller │ │ ├── NotFound.controller.js │ │ ├── App.controller.js │ │ ├── ErrorHandler.js │ │ ├── BaseController.js │ │ ├── Object.controller.js │ │ └── Worklist.controller.js │ ├── manifest.json │ ├── i18n │ │ └── i18n.properties │ └── Component.js ├── README.md ├── package.json └── createPreload.js ├── README.md ├── .gitignore └── LICENSE.txt /demo-project/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /demo-project/packages/server/.env.example: -------------------------------------------------------------------------------- 1 | UI5CON_DEMO_GITHUB_USER= 2 | UI5CON_DEMO_GITHUB_TOKEN= -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/unit/AllTests.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./model/formatter", 3 | "./model/models" 4 | ], function() { 5 | "use strict"; 6 | }); -------------------------------------------------------------------------------- /demo-project/packages/theme-library/src/sap/ui/core/themes/ui5con20/library.source.less: -------------------------------------------------------------------------------- 1 | @import "../sap_fiori_3_dark/library.source.less"; 2 | @import "base.less"; 3 | -------------------------------------------------------------------------------- /demo-project/packages/server/ui5.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "2.1" 2 | kind: extension 3 | type: server-middleware 4 | metadata: 5 | name: backend 6 | middleware: 7 | path: lib/middleware.js 8 | -------------------------------------------------------------------------------- /demo-project/packages/theme-library/src/sap/f/themes/ui5con20/library.source.less: -------------------------------------------------------------------------------- 1 | @import "../sap_fiori_3_dark/library.source.less"; 2 | @import "../../../../sap/ui/core/themes/ui5con20/base.less"; 3 | -------------------------------------------------------------------------------- /demo-project/packages/theme-library/src/sap/m/themes/ui5con20/library.source.less: -------------------------------------------------------------------------------- 1 | @import "../sap_fiori_3_dark/library.source.less"; 2 | @import "../../../../sap/ui/core/themes/ui5con20/base.less"; 3 | -------------------------------------------------------------------------------- /demo-project/packages/theme-library/src/sap/ui/layout/themes/ui5con20/library.source.less: -------------------------------------------------------------------------------- 1 | @import "../sap_fiori_3_dark/library.source.less"; 2 | @import "../../../../../sap/ui/core/themes/ui5con20/base.less"; 3 | -------------------------------------------------------------------------------- /demo-project/packages/theme-library/src/ui5con20/library/themes/ui5con20/library.source.less: -------------------------------------------------------------------------------- 1 | @import "../sap_fiori_3_dark/library.source.less"; 2 | @import "../../../../sap/ui/core/themes/ui5con20/base.less"; 3 | -------------------------------------------------------------------------------- /demo-project/packages/library/src/ui5con20/library/themes/base/library.source.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${copyright} 3 | */ 4 | 5 | @import "../../../../sap/ui/core/themes/base/base.less"; 6 | @import "../../../../sap/ui/core/themes/base/global.less"; 7 | 8 | @import "Chart.less"; 9 | -------------------------------------------------------------------------------- /demo-project/packages/theme-library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui5con20/theme-library", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "UI5con Demo Theme Library", 6 | "scripts": {}, 7 | "dependencies": { 8 | "@ui5con20/library": "^1.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo-project/packages/library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui5con20/library", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "UI5con Demo Library", 6 | "scripts": { 7 | "test": "eslint ./src ./test" 8 | }, 9 | "dependencies": { 10 | "chartist": "^0.11.4" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /demo-project/packages/library/src/ui5con20/library/themes/sap_fiori_3/library.source.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${copyright} 3 | */ 4 | 5 | @import "../base/library.source.less"; 6 | @import "../../../../sap/ui/core/themes/sap_fiori_3/base.less"; 7 | @import "../../../../sap/ui/core/themes/sap_fiori_3/global.less"; 8 | -------------------------------------------------------------------------------- /demo-project/packages/app/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 | "ui5con20/app/test/unit/AllTests" 9 | ], function () { 10 | QUnit.start(); 11 | }); 12 | }); -------------------------------------------------------------------------------- /demo-project/packages/library/src/ui5con20/library/themes/sap_fiori_3_dark/library.source.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${copyright} 3 | */ 4 | 5 | @import "../base/library.source.less"; 6 | @import "../../../../sap/ui/core/themes/sap_fiori_3_dark/base.less"; 7 | @import "../../../../sap/ui/core/themes/sap_fiori_3_dark/global.less"; 8 | -------------------------------------------------------------------------------- /demo-project/packages/app/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 | "ui5con20/app/test/integration/AllJourneys" 10 | ], function() { 11 | QUnit.start(); 12 | }); 13 | }); -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/view/App.view.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/testsuite.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | QUnit test suite for UI5con 2020 Demo App 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/view/App.view.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo-project/packages/theme-library/ui5.yaml: -------------------------------------------------------------------------------- 1 | specVersion: '2.1' 2 | metadata: 3 | name: theme-library 4 | type: theme-library 5 | framework: 6 | name: OpenUI5 7 | version: "1.79.0" 8 | libraries: 9 | - name: sap.f 10 | - name: sap.m 11 | - name: sap.ui.core 12 | - name: sap.ui.layout 13 | - name: themelib_sap_fiori_3 14 | -------------------------------------------------------------------------------- /demo-nodejs-api/README.md: -------------------------------------------------------------------------------- 1 | # demo-nodejs-api 2 | 3 | Example of how to use the UI5 Tooling Node.js API to create a Component preload. 4 | 5 | ## Getting started 6 | 7 | Install dependencies with npm 8 | ``` 9 | npm install 10 | ``` 11 | 12 | ## Run 13 | Run `npm run build` to run the `createPreload.js` script that produces a Component preload (`dist/Component-preload.js`) 14 | -------------------------------------------------------------------------------- /demo-project/packages/app/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 | 9 | createDeviceModel : function () { 10 | var oModel = new JSONModel(Device); 11 | oModel.setDefaultBindingMode("OneWay"); 12 | return oModel; 13 | } 14 | 15 | }; 16 | }); -------------------------------------------------------------------------------- /demo-project/packages/library/src/ui5con20/library/themes/base/Chart.less: -------------------------------------------------------------------------------- 1 | /* Basic styling from chartist */ 2 | @import (inline) "../../thirdparty/chartist/chartist.css"; 3 | 4 | /* Overrides with UI5 theming parameters */ 5 | .ct-label { 6 | fill: @sapUiNeutralText; 7 | color: @sapUiNeutralText; 8 | } 9 | 10 | .ct-grid { 11 | stroke: @sapUiContentDisabledTextColor; 12 | } 13 | -------------------------------------------------------------------------------- /demo-project/.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | 9 | [*.{css,html,js,less,txt,json,yml,md}] 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | indent_size = 4 13 | insert_final_newline = true 14 | 15 | [*.{yml,yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/AllJourneys.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "./arrangements/Startup", 4 | "./WorklistJourney", 5 | "./NavigationJourney", 6 | "./NotFoundJourney", 7 | "./ObjectJourney" 8 | ], function (Opa5, Startup) { 9 | "use strict"; 10 | 11 | Opa5.extendConfig({ 12 | arrangements: new Startup(), 13 | viewNamespace: "ui5con20.app.view.", 14 | autoWait: true 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /demo-nodejs-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-nodejs-api", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "UI5con Node.js API Demo", 6 | "main": "createPreload.js", 7 | "scripts": { 8 | "build": "node createPreload.js" 9 | }, 10 | "devDependencies": { 11 | "@openui5/sap.ui.core": "^1.85.2", 12 | "@ui5/builder": "^2.7.2", 13 | "@ui5/fs": "^2.0.6", 14 | "@ui5/project": "^2.2.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/controller/NotFound.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController" 3 | ], function (BaseController) { 4 | "use strict"; 5 | 6 | return BaseController.extend("ui5con20.demo.nodejs.controller.NotFound", { 7 | 8 | /** 9 | * Navigates to the worklist when the link is pressed 10 | * @public 11 | */ 12 | onLinkPressed : function () { 13 | this.getRouter().navTo("worklist"); 14 | } 15 | 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/controller/NotFound.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController" 3 | ], function (BaseController) { 4 | "use strict"; 5 | 6 | return BaseController.extend("ui5con20.app.controller.NotFound", { 7 | 8 | /** 9 | * Navigates to the worklist when the link is pressed 10 | * @public 11 | */ 12 | onLinkPressed : function () { 13 | this.getRouter().navTo("worklist"); 14 | } 15 | 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/testsuite.qunit.js: -------------------------------------------------------------------------------- 1 | window.suite = function () { 2 | "use strict"; 3 | /* eslint-disable new-cap */ 4 | var oSuite = new parent.jsUnitTestSuite(), 5 | sContextPath = location.pathname.substring(0, location.pathname.lastIndexOf("/") + 1); 6 | 7 | oSuite.addTestPage(sContextPath + "unit/unitTests.qunit.html"); 8 | oSuite.addTestPage(sContextPath + "integration/opaTests.qunit.html"); 9 | 10 | return oSuite; 11 | }; 12 | -------------------------------------------------------------------------------- /demo-project/packages/server/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const middleware = require("./lib/middleware"); 3 | const path = require("path"); 4 | const app = express(); 5 | const port = 3001; 6 | 7 | const appDir = path.dirname(require.resolve("@ui5con20/app/dist/index.html")); 8 | 9 | app.use(express.static(appDir)); 10 | app.use("/data", middleware()); 11 | 12 | app.listen(port, () => console.log(`App listening at http://localhost:${port}`)); 13 | -------------------------------------------------------------------------------- /demo-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-project", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "UI5con Demo Project", 6 | "scripts": { 7 | "build": "cd packages/app && ui5 build --all", 8 | "start": "cd packages/app && ui5 serve -o index.html", 9 | "serve-prod": "cd packages/server && yarn start" 10 | }, 11 | "workspaces": [ 12 | "packages/*" 13 | ], 14 | "devDependencies": { 15 | "@ui5/cli": "^2.9.3", 16 | "eslint": "^7.19.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/view/NotFound.view.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/view/NotFound.view.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/model/formatter.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([], function () { 2 | "use strict"; 3 | 4 | return { 5 | 6 | /** 7 | * Rounds the number unit value to 2 digits 8 | * @public 9 | * @param {string} sValue the number string to be rounded 10 | * @returns {string} sValue with 2 digits rounded 11 | */ 12 | numberUnit : function (sValue) { 13 | if (!sValue) { 14 | return ""; 15 | } 16 | return parseFloat(sValue).toFixed(2); 17 | } 18 | 19 | }; 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/view/ObjectNotFound.view.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/view/ObjectNotFound.view.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo-project/packages/library/src/ui5con20/library/.library: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ui5con20.library 5 | SAP SE 6 | ${copyright} 7 | ${version} 8 | 9 | UI5con 2020 demo library 10 | 11 | 12 | 13 | sap.ui.core 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/unit/helper/FakeI18nModel.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/model/Model" 3 | ], function (Model) { 4 | "use strict"; 5 | 6 | return Model.extend("ui5con20.app.test.unit.helper.FakeI18nModel", { 7 | 8 | constructor : function (mTexts) { 9 | Model.call(this); 10 | this.mTexts = mTexts || {}; 11 | }, 12 | 13 | getResourceBundle : function () { 14 | return { 15 | getText : function (sTextName) { 16 | return this.mTexts[sTextName]; 17 | }.bind(this) 18 | }; 19 | } 20 | 21 | }); 22 | 23 | }); -------------------------------------------------------------------------------- /demo-project/packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui5con20/server", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "UI5con Demo Server", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "eslint ./index.js ./lib", 9 | "start": "node index.js" 10 | }, 11 | "dependencies": { 12 | "dotenv": "^8.2.0", 13 | "express": "^4.17.1", 14 | "parseurl": "^1.3.3", 15 | "request": "^2.88.2" 16 | }, 17 | "devDependencies": { 18 | "@ui5con20/app": "^1.0.0" 19 | }, 20 | "ui5": { 21 | "dependencies": [] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo-project/packages/app/ui5.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "2.1" 2 | metadata: 3 | name: app 4 | type: application 5 | framework: 6 | name: OpenUI5 7 | version: "1.79.0" 8 | libraries: 9 | - name: sap.f 10 | - name: sap.m 11 | - name: sap.ui.core 12 | server: 13 | customMiddleware: 14 | - name: ui5-middleware-livereload 15 | afterMiddleware: compression 16 | configuration: 17 | extraExts: "xml,json,properties" 18 | port: 35729 19 | watchPath: "webapp" 20 | - name: backend 21 | mountPath: /data 22 | afterMiddleware: compression 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui5con20-ui5-tooling 2 | 3 | The repository contains the content for the UI5con ON AIR 2020 session *UI5 Tooling 2020*. 4 | 5 | [Slides](https://github.com/RandomByte/talks/blob/master/UI5con_ON_AIR_2020/UI5con2020_UI5_Tooling_2020.pdf) 6 | [Recording](https://www.youtube.com/watch?v=8IHoVJLKN34) 7 | 8 | ## [demo-nodejs-api](./demo-nodejs-api) 9 | 10 | Example of how to use the UI5 Tooling Node.js API to create a Component preload. 11 | 12 | ## [demo-project](./demo-project) 13 | 14 | Monorepo setup containing an application, library, theme-library and server. 15 | -------------------------------------------------------------------------------- /demo-project/packages/library/src/ui5con20/library/library.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${copyright} 3 | */ 4 | 5 | /** 6 | * Initialization Code of ui5con20.library 7 | */ 8 | sap.ui.define([ 9 | "sap/ui/core/library" // library dependency 10 | ], function() { 11 | "use strict"; 12 | 13 | // delegate further initialization of this library to the Core 14 | sap.ui.getCore().initLibrary({ 15 | name : "ui5con20.library", 16 | version: "${version}", 17 | dependencies : ["sap.ui.core"], 18 | controls: [ 19 | "ui5con20.library.Chart" 20 | ] 21 | }); 22 | 23 | return ui5con20.library; 24 | }); 25 | -------------------------------------------------------------------------------- /demo-project/packages/library/ui5.yaml: -------------------------------------------------------------------------------- 1 | specVersion: '2.1' 2 | metadata: 3 | name: library 4 | type: library 5 | 6 | framework: 7 | name: OpenUI5 8 | version: "1.79.0" 9 | libraries: 10 | - name: sap.ui.core 11 | 12 | --- 13 | specVersion: "2.1" 14 | kind: extension 15 | type: project-shim 16 | metadata: 17 | name: chartist-shim 18 | shims: 19 | configurations: 20 | chartist: 21 | specVersion: "2.1" 22 | type: module 23 | metadata: 24 | name: chartist 25 | resources: 26 | configuration: 27 | paths: 28 | /resources/ui5con20/library/thirdparty/chartist/: "dist" 29 | -------------------------------------------------------------------------------- /demo-project/packages/library/test/ui5con20/library/qunit/Chart.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test: ui5con20.library.Chart 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /demo-project/packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui5con20/app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "UI5con Demo App", 6 | "scripts": { 7 | "build": "ui5 build", 8 | "test": "eslint ./webapp", 9 | "start": "ui5 serve -o index.html" 10 | }, 11 | "dependencies": { 12 | "@ui5con20/library": "^1.0.0", 13 | "@ui5con20/theme-library": "^1.0.0" 14 | }, 15 | "devDependencies": { 16 | "@ui5con20/server": "^1.0.0", 17 | "ui5-middleware-livereload": "^0.5.1" 18 | }, 19 | "ui5": { 20 | "dependencies": [ 21 | "@ui5con20/library", 22 | "@ui5con20/server", 23 | "@ui5con20/theme-library", 24 | "ui5-middleware-livereload" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo-project/packages/theme-library/src/sap/ui/core/themes/ui5con20/base.less: -------------------------------------------------------------------------------- 1 | @sapBaseColor: #191717; 2 | 3 | @sapList_Background: @sapBaseColor; 4 | @sapField_Background: @sapBaseColor; 5 | @sapBackgroundColor: @sapBaseColor; 6 | 7 | @sapBrandColor: #ffa42c; 8 | @sapUiAccent6: @sapBrandColor; 9 | 10 | @sapUiShellBackground: linear-gradient(343deg,#fb5b38 54%,#fdbb2d); 11 | 12 | @sapTextColor: #fff; 13 | @sapGroup_TitleTextColor: #ffa42c; 14 | @sapField_TextColor: #fff; 15 | @sapNeutralColor: #fff; 16 | 17 | @sapSelectedColor: rgba(255,162,66,.2); 18 | @sapList_SelectionBackgroundColor: @sapSelectedColor; 19 | 20 | @sapList_HeaderBackground: @sapGroup_ContentBackground; 21 | @sapPageFooter_Background: @sapGroup_ContentBackground; 22 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UI5con 2020 Demo App 7 | 8 | 19 | 20 | 21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /demo-project/README.md: -------------------------------------------------------------------------------- 1 | # demo-project 2 | 3 | Monorepo setup containing an application, library, theme-library and server. 4 | 5 | ## Getting started 6 | 7 | Install and link dependencies with Yarn 8 | ``` 9 | yarn 10 | ``` 11 | 12 | Copy the example env file 13 | ``` 14 | cp packages/server/.env.example packages/server/.env 15 | ``` 16 | 17 | Create a [GitHub personal access token](https://github.com/settings/tokens/new) (read-only / no scopes required). 18 | Add your GitHub user and token to `packages/server/.env` 19 | 20 | ## Development 21 | Run `yarn start` to serve and open the application in the browser 22 | 23 | ## Building 24 | Run `yarn build` to build the app with all dependencies 25 | 26 | ## Deployment 27 | Run `yarn serve-prod` to host a local webserver with the built application 28 | -------------------------------------------------------------------------------- /demo-project/packages/library/src/ui5con20/library/ChartRenderer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${copyright} 3 | */ 4 | 5 | // Provides default renderer for controlsap.m.Avatar 6 | sap.ui.define(["./library"], function (/* library */) { 7 | "use strict"; 8 | 9 | var ChartRenderer = { 10 | apiVersion: 2 11 | }; 12 | 13 | /** 14 | * Renders the HTML for the given control, using the provided {@link sap.ui.core.RenderManager}. 15 | * 16 | * @param {sap.ui.core.RenderManager} oRm the RenderManager that can be used for writing to the Render-Output-Buffer 17 | * @param {sap.ui.core.Control} oChart an object representation of the control that should be rendered 18 | */ 19 | ChartRenderer.render = function (oRm, oChart) { 20 | oRm.openStart("div", oChart); 21 | oRm.openEnd(); 22 | oRm.renderControl(oChart.getAggregation("_html")); 23 | oRm.close("div"); 24 | }; 25 | 26 | return ChartRenderer; 27 | }); 28 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/opaTests.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Integration tests for UI5con 2020 Demo App 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /demo-nodejs-api/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("ui5con20.demo.nodejs.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 | }); 18 | this.setModel(oViewModel, "appView"); 19 | 20 | fnSetAppNotBusy = function() { 21 | oViewModel.setProperty("/busy", false); 22 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 23 | }; 24 | 25 | this.getOwnerComponent().getDataLoaded().then(fnSetAppNotBusy, fnSetAppNotBusy); 26 | 27 | // apply content density mode to root view 28 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass()); 29 | } 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /demo-project/packages/app/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("ui5con20.app.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 | }); 18 | this.setModel(oViewModel, "appView"); 19 | 20 | fnSetAppNotBusy = function() { 21 | oViewModel.setProperty("/busy", false); 22 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 23 | }; 24 | 25 | this.getOwnerComponent().getDataLoaded().then(fnSetAppNotBusy, fnSetAppNotBusy); 26 | 27 | // apply content density mode to root view 28 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass()); 29 | } 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /demo-project/packages/library/test/ui5con20/library/qunit/Chart.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(["ui5con20/library/Chart"], function(Chart) { 8 | 9 | QUnit.test("Chart", function(assert) { 10 | var oChart = new Chart({ 11 | data: { 12 | series: [ 13 | [ { value: 1 }, { value: 2 } ], 14 | [ { value: 3 }, { value: 4 } ] 15 | ] 16 | }, 17 | options: { 18 | fullWidth: true, 19 | chartPadding: { 20 | top: 20, 21 | right: 50, 22 | bottom: 30, 23 | left: 10 24 | }, 25 | showArea: true, 26 | showPoint: true, 27 | axisX: { 28 | labelOffset: { 29 | x: -10, 30 | y: 10 31 | } 32 | }, 33 | axisY: { 34 | onlyInteger: true 35 | } 36 | } 37 | }); 38 | 39 | oChart.placeAt("qunit-fixture"); 40 | 41 | assert.ok(true); 42 | }) 43 | 44 | QUnit.start(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /demo-project/packages/app/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 | 18 | 19 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/unit/unitTests.qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Unit tests for UI5con 2020 Demo App 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/view/Object.view.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 16 | 19 | </semantic:titleHeading> 20 | 21 | <semantic:headerContent> 22 | <ObjectNumber 23 | number="{ 24 | path: 'stars', 25 | type: 'sap.ui.model.type.Float' 26 | }" 27 | unit="Github Stars"/> 28 | <ObjectNumber 29 | number="{ 30 | path: 'downloadsTotal', 31 | type: 'sap.ui.model.type.Float' 32 | }" 33 | unit="npm Downloads"/> 34 | </semantic:headerContent> 35 | 36 | <semantic:content> 37 | <lib:Chart 38 | id="chart" 39 | data="{downloadsRange}"/> 40 | </semantic:content> 41 | 42 | </semantic:SemanticPage> 43 | </mvc:View> 44 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/unit/model/formatter.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "ui5con20/app/model/formatter" 5 | ], function (formatter) { 6 | "use strict"; 7 | 8 | QUnit.module("Number unit"); 9 | 10 | function numberUnitValueTestCase(assert, sValue, fExpectedNumber) { 11 | // Act 12 | var fNumber = formatter.numberUnit(sValue); 13 | 14 | // Assert 15 | assert.strictEqual(fNumber, fExpectedNumber, "The rounding was correct"); 16 | } 17 | 18 | QUnit.test("Should round down a 3 digit number", function (assert) { 19 | numberUnitValueTestCase.call(this, assert, "3.123", "3.12"); 20 | }); 21 | 22 | QUnit.test("Should round up a 3 digit number", function (assert) { 23 | numberUnitValueTestCase.call(this, assert, "3.128", "3.13"); 24 | }); 25 | 26 | QUnit.test("Should round a negative number", function (assert) { 27 | numberUnitValueTestCase.call(this, assert, "-3", "-3.00"); 28 | }); 29 | 30 | QUnit.test("Should round an empty string", function (assert) { 31 | numberUnitValueTestCase.call(this, assert, "", ""); 32 | }); 33 | 34 | QUnit.test("Should round a zero", function (assert) { 35 | numberUnitValueTestCase.call(this, assert, "0", "0.00"); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/view/Object.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="ui5con20.demo.nodejs.controller.Object" 3 | xmlns="sap.m" 4 | xmlns:mvc="sap.ui.core.mvc" 5 | xmlns:lib="ui5con20.library" 6 | xmlns:semantic="sap.f.semantic"> 7 | 8 | <semantic:SemanticPage 9 | id="page" 10 | headerPinnable="false" 11 | toggleHeaderOnTitleClick="false" 12 | busy="{objectView>/busy}" 13 | busyIndicatorDelay="{objectView>/delay}"> 14 | 15 | <semantic:titleHeading> 16 | <Title 17 | text="{repositoryName}" 18 | level="H2"/> 19 | </semantic:titleHeading> 20 | 21 | <semantic:headerContent> 22 | <ObjectNumber 23 | number="{ 24 | path: 'stars', 25 | type: 'sap.ui.model.type.Float' 26 | }" 27 | unit="Github Stars"/> 28 | <ObjectNumber 29 | number="{ 30 | path: 'downloadsTotal', 31 | type: 'sap.ui.model.type.Float' 32 | }" 33 | unit="npm Downloads"/> 34 | </semantic:headerContent> 35 | 36 | 37 | <semantic:content> 38 | <lib:Chart 39 | id="chart" 40 | data="{downloadsRange}"/> 41 | </semantic:content> 42 | 43 | <semantic:sendEmailAction> 44 | <semantic:SendEmailAction id="shareEmail" press=".onShareEmailPress"/> 45 | </semantic:sendEmailAction> 46 | 47 | 48 | </semantic:SemanticPage> 49 | </mvc:View> 50 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/ObjectJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Worklist", 6 | "./pages/Browser", 7 | "./pages/Object", 8 | "./pages/App" 9 | ], function (opaTest) { 10 | "use strict"; 11 | 12 | QUnit.module("Object"); 13 | 14 | opaTest("Should remember the first item", function (Given, When, Then) { 15 | // Arrangements 16 | Given.iStartMyApp(); 17 | 18 | //Actions 19 | When.onTheWorklistPage.iRememberTheItemAtPosition(1); 20 | 21 | // Assertions 22 | Then.onTheWorklistPage.theTitleShouldDisplayTheTotalAmountOfItems(); 23 | 24 | // Cleanup 25 | Then.iTeardownMyApp(); 26 | }); 27 | 28 | opaTest("Should start the app with remembered item", function (Given, When, Then) { 29 | // Arrangements 30 | Given.iRestartTheAppWithTheRememberedItem({ 31 | delay: 1000, 32 | autoWait: false 33 | }); 34 | 35 | //Actions 36 | When.onTheAppPage.iWaitUntilTheAppBusyIndicatorIsGone(); 37 | 38 | // Assertions 39 | Then.onTheObjectPage.iShouldSeeTheObjectViewsBusyIndicator(). 40 | and.theObjectViewsBusyIndicatorDelayIsRestored(). 41 | and.iShouldSeeTheRememberedObject(). 42 | and.theObjectViewShouldContainOnlyFormattedUnitNumbers(); 43 | 44 | // Cleanup 45 | Then.iTeardownMyApp(); 46 | }); 47 | 48 | 49 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/pages/shareOptions.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 | return { 9 | 10 | createActions : function (sViewName) { 11 | return { 12 | iPressOnTheShareButton : function () { 13 | return this.waitFor({ 14 | controlType : "sap.m.Button", 15 | viewName : sViewName, 16 | matchers : new PropertyStrictEquals({ 17 | name : "icon", 18 | value : "sap-icon://action" 19 | }), 20 | actions : new Press(), 21 | errorMessage : "Did not find the share button" 22 | }); 23 | } 24 | }; 25 | }, 26 | 27 | createAssertions : function (sViewName) { 28 | return { 29 | 30 | iShouldSeeTheShareEmailButton : function () { 31 | return this.waitFor({ 32 | viewName : sViewName, 33 | id : "shareEmail-button", 34 | matchers : new PropertyStrictEquals({ 35 | name : "icon", 36 | value : "sap-icon://email" 37 | }), 38 | success : function () { 39 | Opa5.assert.ok(true, "The E-Mail button is visible"); 40 | }, 41 | errorMessage : "The E-Mail button was not found" 42 | }); 43 | } 44 | 45 | }; 46 | 47 | } 48 | 49 | }; 50 | 51 | }); -------------------------------------------------------------------------------- /demo-project/packages/server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 2017 7 | }, 8 | "rules": { 9 | "block-scoped-var": 1, 10 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 11 | "consistent-this": 2, 12 | "no-div-regex": 2, 13 | "no-floating-decimal": 2, 14 | "no-self-compare": 2, 15 | "no-mixed-spaces-and-tabs": [2, true], 16 | "no-nested-ternary": 2, 17 | "no-unused-vars": [2, {"vars":"all", "args":"none"}], 18 | "radix": 2, 19 | "keyword-spacing": 2, 20 | "space-unary-ops": 2, 21 | "wrap-iife": [2, "any"], 22 | 23 | "camelcase": 1, 24 | "consistent-return": 1, 25 | "max-nested-callbacks": [1, 3], 26 | "new-cap": 1, 27 | "no-extra-boolean-cast": 1, 28 | "no-lonely-if": 1, 29 | "no-new": 1, 30 | "no-new-wrappers": 1, 31 | "no-redeclare": 1, 32 | "no-unused-expressions": 1, 33 | "no-use-before-define": [1, "nofunc"], 34 | "no-warning-comments": 1, 35 | "valid-jsdoc": [1, { 36 | "requireReturn": false 37 | }], 38 | "default-case": 1, 39 | 40 | "dot-notation": 0, 41 | "eol-last": 0, 42 | "eqeqeq": 0, 43 | "no-trailing-spaces": 0, 44 | "no-underscore-dangle": 0, 45 | "quotes": 0, 46 | "key-spacing": 0, 47 | "comma-spacing": 0, 48 | "no-multi-spaces": 0, 49 | "no-shadow": 0, 50 | "no-irregular-whitespace": 0 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /demo-project/packages/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "globals": { 6 | "sap": true, 7 | "jQuery": true 8 | }, 9 | "rules": { 10 | "block-scoped-var": 1, 11 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 12 | "consistent-this": 2, 13 | "no-div-regex": 2, 14 | "no-floating-decimal": 2, 15 | "no-self-compare": 2, 16 | "no-mixed-spaces-and-tabs": [2, true], 17 | "no-nested-ternary": 2, 18 | "no-unused-vars": [2, {"vars":"all", "args":"none"}], 19 | "radix": 2, 20 | "keyword-spacing": 2, 21 | "space-unary-ops": 2, 22 | "wrap-iife": [2, "any"], 23 | 24 | "camelcase": 1, 25 | "consistent-return": 1, 26 | "max-nested-callbacks": [1, 3], 27 | "new-cap": 1, 28 | "no-extra-boolean-cast": 1, 29 | "no-lonely-if": 1, 30 | "no-new": 1, 31 | "no-new-wrappers": 1, 32 | "no-redeclare": 1, 33 | "no-unused-expressions": 1, 34 | "no-use-before-define": [1, "nofunc"], 35 | "no-warning-comments": 1, 36 | "strict": 1, 37 | "valid-jsdoc": [1, { 38 | "requireReturn": false 39 | }], 40 | "default-case": 1, 41 | 42 | "dot-notation": 0, 43 | "eol-last": 0, 44 | "eqeqeq": 0, 45 | "no-trailing-spaces": 0, 46 | "no-underscore-dangle": 0, 47 | "quotes": 0, 48 | "key-spacing": 0, 49 | "comma-spacing": 0, 50 | "no-multi-spaces": 0, 51 | "no-shadow": 0, 52 | "no-irregular-whitespace": 0 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/pages/Browser.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "./Common" 4 | ], function(Opa5, Common) { 5 | "use strict"; 6 | 7 | Opa5.createPageObjects({ 8 | onTheBrowser : { 9 | baseClass : Common, 10 | 11 | actions : { 12 | 13 | iPressOnTheBackwardsButton : function () { 14 | return this.waitFor({ 15 | success : function () { 16 | // manipulate history directly for testing purposes 17 | Opa5.getWindow().history.back(); 18 | } 19 | }); 20 | }, 21 | 22 | iPressOnTheForwardsButton : function () { 23 | return this.waitFor({ 24 | success : function () { 25 | // manipulate history directly for testing purposes 26 | Opa5.getWindow().history.forward(); 27 | } 28 | }); 29 | }, 30 | 31 | iChangeTheHashToSomethingInvalid : function () { 32 | return this.waitFor({ 33 | success : function () { 34 | Opa5.getHashChanger().setHash("somethingInvalid"); 35 | } 36 | }); 37 | }, 38 | 39 | iChangeTheHashToTheRememberedItem : function () { 40 | return this.waitFor({ 41 | success : function () { 42 | var sObjectId = this.getContext().currentItem.id; 43 | Opa5.getHashChanger().setHash("Pony/" + sObjectId); 44 | } 45 | }); 46 | } 47 | }, 48 | 49 | assertions: {} 50 | } 51 | 52 | }); 53 | }); -------------------------------------------------------------------------------- /demo-project/packages/library/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "globals": { 6 | "sap": true, 7 | "jQuery": true 8 | }, 9 | "rules": { 10 | "block-scoped-var": 1, 11 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 12 | "consistent-this": 2, 13 | "no-div-regex": 2, 14 | "no-floating-decimal": 2, 15 | "no-self-compare": 2, 16 | "no-mixed-spaces-and-tabs": [2, true], 17 | "no-nested-ternary": 2, 18 | "no-unused-vars": [2, {"vars":"all", "args":"none"}], 19 | "radix": 2, 20 | "keyword-spacing": 2, 21 | "space-unary-ops": 2, 22 | "wrap-iife": [2, "any"], 23 | 24 | "camelcase": 1, 25 | "consistent-return": 1, 26 | "max-nested-callbacks": [1, 3], 27 | "new-cap": 1, 28 | "no-extra-boolean-cast": 1, 29 | "no-lonely-if": 1, 30 | "no-new": 1, 31 | "no-new-wrappers": 1, 32 | "no-redeclare": 1, 33 | "no-unused-expressions": 1, 34 | "no-use-before-define": [1, "nofunc"], 35 | "no-warning-comments": 1, 36 | "strict": 1, 37 | "valid-jsdoc": [1, { 38 | "requireReturn": false 39 | }], 40 | "default-case": 1, 41 | 42 | "dot-notation": 0, 43 | "eol-last": 0, 44 | "eqeqeq": 0, 45 | "no-trailing-spaces": 0, 46 | "no-underscore-dangle": 0, 47 | "quotes": 0, 48 | "key-spacing": 0, 49 | "comma-spacing": 0, 50 | "no-multi-spaces": 0, 51 | "no-shadow": 0, 52 | "no-irregular-whitespace": 0 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo-nodejs-api/createPreload.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const {resourceFactory} = require("@ui5/fs"); 3 | const {FileSystem, Memory} = require("@ui5/fs").adapters; 4 | const {generateComponentPreload} = require("@ui5/builder").tasks; 5 | const {normalizer} = require("@ui5/project"); 6 | 7 | async function getCoreDependencyReader() { 8 | const coreTree = await normalizer.generateProjectTree({ 9 | cwd: path.dirname(require.resolve("@openui5/sap.ui.core/package.json")) 10 | }); 11 | return (await resourceFactory.createCollectionsForTree(coreTree)).source; 12 | } 13 | 14 | async function main() { 15 | const projectName = "UI5con Node.js API Demo"; 16 | 17 | const sources = new FileSystem({ 18 | virBasePath: "/resources/ui5con20/demo/nodejs/", 19 | fsBasePath: "./webapp", 20 | }); 21 | 22 | const dependencies = await getCoreDependencyReader(); 23 | 24 | const target = new FileSystem({ 25 | virBasePath: "/resources/ui5con20/demo/nodejs/", 26 | fsBasePath: "./dist" 27 | }); 28 | 29 | const workspace = resourceFactory.createWorkspace({ 30 | virBasePath: "/", 31 | writer: target, 32 | reader: sources, 33 | name: projectName 34 | }); 35 | 36 | await generateComponentPreload({ 37 | workspace, 38 | dependencies, 39 | options: { 40 | projectName, 41 | namespaces: [ 42 | "ui5con20/demo/nodejs" 43 | ] 44 | } 45 | }); 46 | } 47 | 48 | main().catch((err) => { 49 | console.log(err); 50 | process.exit(1); 51 | }); 52 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/controller/BaseController.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/mvc/Controller", 3 | "sap/ui/core/UIComponent" 4 | ], function (Controller, UIComponent) { 5 | "use strict"; 6 | 7 | return Controller.extend("ui5con20.app.controller.BaseController", { 8 | /** 9 | * Convenience method for accessing the router. 10 | * @public 11 | * @returns {sap.ui.core.routing.Router} the router for this component 12 | */ 13 | getRouter : function () { 14 | return UIComponent.getRouterFor(this); 15 | }, 16 | 17 | /** 18 | * Convenience method for getting the view model by name. 19 | * @public 20 | * @param {string} [sName] the model name 21 | * @returns {sap.ui.model.Model} the model instance 22 | */ 23 | getModel : function (sName) { 24 | return this.getView().getModel(sName); 25 | }, 26 | 27 | /** 28 | * Convenience method for setting the view model. 29 | * @public 30 | * @param {sap.ui.model.Model} oModel the model instance 31 | * @param {string} sName the model name 32 | * @returns {sap.ui.mvc.View} the view instance 33 | */ 34 | setModel : function (oModel, sName) { 35 | return this.getView().setModel(oModel, sName); 36 | }, 37 | 38 | /** 39 | * Getter for the resource bundle. 40 | * @public 41 | * @returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component 42 | */ 43 | getResourceBundle : function () { 44 | return this.getOwnerComponent().getModel("i18n").getResourceBundle(); 45 | } 46 | 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /demo-project/packages/app/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("ui5con20.app.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 | theUnitNumbersShouldHaveTwoDecimals : function (sControlType, sViewName, sSuccessMsg, sErrMsg) { 28 | var rTwoDecimalPlaces = /^-?\d+\.\d{2}$/; 29 | 30 | return this.waitFor({ 31 | controlType : sControlType, 32 | viewName : sViewName, 33 | success : function (aNumberControls) { 34 | Opa5.assert.ok(aNumberControls.every(function(oNumberControl){ 35 | return rTwoDecimalPlaces.test(oNumberControl.getNumber()); 36 | }), 37 | sSuccessMsg); 38 | }, 39 | errorMessage : sErrMsg 40 | }); 41 | }, 42 | 43 | getMockServer : function () { 44 | return new Promise(function (success) { 45 | Opa5.getWindow().sap.ui.require(["ui5con20/app/localService/mockserver"], function (mockserver) { 46 | success(mockserver.getMockServer()); 47 | }); 48 | }); 49 | } 50 | 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /demo-project/packages/app/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("ui5con20.app.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 ui5con20.app.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 | 24 | /** 25 | * Shows a {@link sap.m.MessageBox} when a service call has failed. 26 | * Only the first error message will be display. 27 | * @param {string} sDetails a technical error to be displayed on request 28 | * @private 29 | */ 30 | _showServiceError : function (sDetails) { 31 | if (this._bMessageOpen) { 32 | return; 33 | } 34 | this._bMessageOpen = true; 35 | MessageBox.error( 36 | this._sErrorText, 37 | { 38 | id : "serviceErrorMessageBox", 39 | details: sDetails, 40 | styleClass: this._oComponent.getContentDensityClass(), 41 | actions: [MessageBox.Action.CLOSE], 42 | onClose: function () { 43 | this._bMessageOpen = false; 44 | }.bind(this) 45 | } 46 | ); 47 | } 48 | }); 49 | }); -------------------------------------------------------------------------------- /demo-nodejs-api/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("ui5con20.demo.nodejs.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 ui5con20.demo.nodejs.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 | 24 | /** 25 | * Shows a {@link sap.m.MessageBox} when a service call has failed. 26 | * Only the first error message will be display. 27 | * @param {string} sDetails a technical error to be displayed on request 28 | * @private 29 | */ 30 | _showServiceError : function (sDetails) { 31 | if (this._bMessageOpen) { 32 | return; 33 | } 34 | this._bMessageOpen = true; 35 | MessageBox.error( 36 | this._sErrorText, 37 | { 38 | id : "serviceErrorMessageBox", 39 | details: sDetails, 40 | styleClass: this._oComponent.getContentDensityClass(), 41 | actions: [MessageBox.Action.CLOSE], 42 | onClose: function () { 43 | this._bMessageOpen = false; 44 | }.bind(this) 45 | } 46 | ); 47 | } 48 | }); 49 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/unit/model/models.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "ui5con20/app/model/models", 5 | "sap/ui/Device" 6 | ], function (models, Device) { 7 | "use strict"; 8 | 9 | QUnit.module("createDeviceModel", { 10 | afterEach : function () { 11 | this.oDeviceModel.destroy(); 12 | } 13 | }); 14 | 15 | function isPhoneTestCase(assert, bIsPhone) { 16 | // Arrange 17 | this.stub(Device, "system", { phone : bIsPhone }); 18 | 19 | // System under test 20 | this.oDeviceModel = models.createDeviceModel(); 21 | 22 | // Assert 23 | assert.strictEqual(this.oDeviceModel.getData().system.phone, bIsPhone, "IsPhone property is correct"); 24 | } 25 | 26 | QUnit.test("Should initialize a device model for desktop", function (assert) { 27 | isPhoneTestCase.call(this, assert, false); 28 | }); 29 | 30 | QUnit.test("Should initialize a device model for phone", function (assert) { 31 | isPhoneTestCase.call(this, assert, true); 32 | }); 33 | 34 | function isTouchTestCase(assert, bIsTouch) { 35 | // Arrange 36 | this.stub(Device, "support", { touch : bIsTouch }); 37 | 38 | // System under test 39 | this.oDeviceModel = models.createDeviceModel(); 40 | 41 | // Assert 42 | assert.strictEqual(this.oDeviceModel.getData().support.touch, bIsTouch, "IsTouch property is correct"); 43 | } 44 | 45 | QUnit.test("Should initialize a device model for non touch devices", function (assert) { 46 | isTouchTestCase.call(this, assert, false); 47 | }); 48 | 49 | QUnit.test("Should initialize a device model for touch devices", function (assert) { 50 | isTouchTestCase.call(this, assert, true); 51 | }); 52 | 53 | QUnit.test("The binding mode of the device model should be one way", function (assert) { 54 | 55 | // System under test 56 | this.oDeviceModel = models.createDeviceModel(); 57 | 58 | // Assert 59 | assert.strictEqual(this.oDeviceModel.getDefaultBindingMode(), "OneWay", "Binding mode is correct"); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/controller/BaseController.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/mvc/Controller", 3 | "sap/ui/core/UIComponent", 4 | "sap/m/library" 5 | ], function (Controller, UIComponent, mobileLibrary) { 6 | "use strict"; 7 | 8 | // shortcut for sap.m.URLHelper 9 | var URLHelper = mobileLibrary.URLHelper; 10 | 11 | return Controller.extend("ui5con20.demo.nodejs.controller.BaseController", { 12 | /** 13 | * Convenience method for accessing the router. 14 | * @public 15 | * @returns {sap.ui.core.routing.Router} the router for this component 16 | */ 17 | getRouter : function () { 18 | return UIComponent.getRouterFor(this); 19 | }, 20 | 21 | /** 22 | * Convenience method for getting the view model by name. 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. 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 | * Getter for 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 when the share by E-Mail button has been clicked 53 | * @public 54 | */ 55 | onShareEmailPress : function () { 56 | var oViewModel = (this.getModel("objectView") || this.getModel("worklistView")); 57 | URLHelper.triggerEmail( 58 | null, 59 | oViewModel.getProperty("/shareSendEmailSubject"), 60 | oViewModel.getProperty("/shareSendEmailMessage") 61 | ); 62 | } }); 63 | 64 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/pages/App.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/matchers/PropertyStrictEquals", 4 | "./Common" 5 | ], function(Opa5, PropertyStrictEquals, Common) { 6 | "use strict"; 7 | 8 | var sViewName = "App", 9 | sAppId = "app"; 10 | 11 | Opa5.createPageObjects({ 12 | onTheAppPage : { 13 | baseClass : Common, 14 | 15 | actions : { 16 | 17 | iWaitUntilTheAppBusyIndicatorIsGone : function () { 18 | return this.waitFor({ 19 | id : sAppId, 20 | viewName : sViewName, 21 | matchers: new PropertyStrictEquals({ 22 | name: "busy", 23 | value: false 24 | }), 25 | autoWait: false, 26 | success : function () { 27 | Opa5.assert.ok(true, "The app is not busy"); 28 | }, 29 | errorMessage : "The app is busy" 30 | }); 31 | }, 32 | iCloseTheMessageBox : function () { 33 | return this.waitFor({ 34 | id : "serviceErrorMessageBox", 35 | autoWait: false, 36 | success : function (oMessageBox) { 37 | oMessageBox.close(); 38 | Opa5.assert.ok(true, "The MessageBox was closed"); 39 | } 40 | }); 41 | } 42 | }, 43 | 44 | assertions : { 45 | 46 | iShouldSeeTheBusyIndicatorForTheWholeApp : function () { 47 | return this.waitFor({ 48 | id : sAppId, 49 | viewName : sViewName, 50 | matchers : new PropertyStrictEquals({ 51 | name : "busy", 52 | value : true 53 | }), 54 | autoWait: false, 55 | success : function () { 56 | // we set the view busy, so we need to query the parent of the app 57 | Opa5.assert.ok(true, "The app is busy"); 58 | }, 59 | errorMessage : "The App is not busy" 60 | }); 61 | }, 62 | iShouldSeeTheMessageBox : function () { 63 | return this.waitFor({ 64 | id : "serviceErrorMessageBox", 65 | autoWait: false, 66 | success : function () { 67 | Opa5.assert.ok(true, "The correct MessageBox was shown"); 68 | } 69 | }); 70 | } 71 | } 72 | } 73 | }); 74 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/WorklistJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "sap/ui/Device", 6 | "./pages/Worklist", 7 | "./pages/App" 8 | ], function (opaTest, Device) { 9 | "use strict"; 10 | 11 | var iDelay = (Device.browser.msie || Device.browser.edge) ? 1500 : 1000; 12 | 13 | QUnit.module("Worklist"); 14 | 15 | opaTest("Should see the table with all entries", function (Given, When, Then) { 16 | // Arrangements 17 | Given.iStartMyApp(); 18 | 19 | // Assertions 20 | Then.onTheWorklistPage.theTableShouldHaveAllEntries(). 21 | and.theTableShouldContainOnlyFormattedUnitNumbers(). 22 | and.theTitleShouldDisplayTheTotalAmountOfItems(); 23 | }); 24 | 25 | opaTest("Search for the First object should deliver results that contain the firstObject in the name", function (Given, When, Then) { 26 | //Actions 27 | When.onTheWorklistPage.iSearchForTheFirstObject(); 28 | 29 | // Assertions 30 | Then.onTheWorklistPage.theTableShowsOnlyObjectsWithTheSearchStringInTheirTitle(); 31 | }); 32 | 33 | 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) { 34 | //Actions 35 | When.onTheWorklistPage.iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh(); 36 | 37 | // Assertions 38 | Then.onTheWorklistPage.theTableHasEntries(); 39 | 40 | // Cleanup 41 | Then.iTeardownMyApp(); 42 | }); 43 | 44 | 45 | opaTest("Should see the busy indicator on app view while worklist view metadata is loaded", function (Given, When, Then) { 46 | // Arrangements 47 | Given.iStartMyApp({ 48 | delay: iDelay, 49 | autoWait: false 50 | }); 51 | 52 | // Assertions 53 | Then.onTheAppPage.iShouldSeeTheBusyIndicatorForTheWholeApp(); 54 | }); 55 | 56 | opaTest("Should see the busy indicator on worklist table after metadata is loaded", function (Given, When, Then) { 57 | //Actions 58 | When.onTheAppPage.iWaitUntilTheAppBusyIndicatorIsGone(); 59 | 60 | // Assertions 61 | Then.onTheWorklistPage.iShouldSeeTheWorklistTableBusyIndicator(); 62 | 63 | // Cleanup 64 | Then.iTeardownMyApp(); 65 | }); 66 | 67 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/arrangements/Startup.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "ui5con20/app/localService/mockserver", 4 | "sap/ui/model/odata/v2/ODataModel" 5 | ], function(Opa5, mockserver, ODataModel) { 6 | "use strict"; 7 | 8 | return Opa5.extend("ui5con20.app.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: "ui5con20.app", 33 | async: true 34 | }, 35 | hash: oOptions.hash, 36 | autoWait: oOptions.autoWait 37 | }); 38 | }, 39 | iRestartTheAppWithTheRememberedItem : function (oOptions) { 40 | var sObjectId; 41 | this.waitFor({ 42 | success : function () { 43 | sObjectId = this.getContext().currentItem.id; 44 | } 45 | }); 46 | 47 | this.waitFor({ 48 | success : function() { 49 | oOptions.hash = "Pony/" + sObjectId; 50 | this.iStartMyApp(oOptions); 51 | } 52 | }); 53 | }, 54 | _clearSharedData: function () { 55 | // clear shared metadata in ODataModel to allow tests for loading the metadata 56 | ODataModel.mSharedData = { server: {}, service: {}, meta: {} }; 57 | } 58 | }); 59 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/NotFoundJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "./pages/Worklist", 6 | "./pages/Browser", 7 | "./pages/NotFound", 8 | "./pages/App" 9 | ], function (opaTest) { 10 | "use strict"; 11 | 12 | QUnit.module("NotFound"); 13 | 14 | opaTest("Should see the resource not found page when changing to an invalid hash", function (Given, When, Then) { 15 | //Arrangement 16 | Given.iStartMyApp(); 17 | 18 | //Actions 19 | When.onTheBrowser.iChangeTheHashToSomethingInvalid(); 20 | 21 | // Assertions 22 | Then.onTheNotFoundPage.iShouldSeeResourceNotFound(); 23 | }); 24 | 25 | opaTest("Clicking the 'Show my worklist' link on the 'Resource not found' page should bring me back to the worklist", function (Given, When, Then) { 26 | //Actions 27 | When.onTheAppPage.iWaitUntilTheAppBusyIndicatorIsGone(); 28 | When.onTheNotFoundPage.iPressTheNotFoundShowWorklistLink(); 29 | 30 | // Assertions 31 | Then.onTheWorklistPage.iShouldSeeTheTable(); 32 | }); 33 | 34 | opaTest("Should see the not found text for no search results", function (Given, When, Then) { 35 | //Actions 36 | When.onTheWorklistPage.iSearchForSomethingWithNoResults(); 37 | 38 | // Assertions 39 | Then.onTheWorklistPage.iShouldSeeTheNoDataTextForNoSearchResults(); 40 | }); 41 | 42 | opaTest("Clicking the back button should take me back to the not found page", function (Given, When, Then) { 43 | //Actions 44 | When.onTheBrowser.iPressOnTheBackwardsButton(); 45 | 46 | // Assertions 47 | Then.onTheNotFoundPage.iShouldSeeResourceNotFound(); 48 | 49 | // Cleanup 50 | Then.iTeardownMyApp(); 51 | }); 52 | 53 | opaTest("Should see the 'Object not found' page if an invalid object id has been called", function (Given, When, Then) { 54 | //Arrangement 55 | Given.iStartMyApp({hash : "Pony/SomeInvalidObjectId"}); 56 | 57 | // Assertions 58 | Then.onTheNotFoundPage.iShouldSeeObjectNotFound(); 59 | }); 60 | 61 | opaTest("Clicking the 'Show my worklist' link on the 'Object not found' page should bring me back to the worklist", function (Given, When, Then) { 62 | //Actions 63 | When.onTheAppPage.iWaitUntilTheAppBusyIndicatorIsGone(); 64 | When.onTheNotFoundPage.iPressTheObjectNotFoundShowWorklistLink(); 65 | 66 | // Assertions 67 | Then.onTheWorklistPage.iShouldSeeTheTable(); 68 | 69 | // Cleanup 70 | Then.iTeardownMyApp(); 71 | }); 72 | 73 | }); -------------------------------------------------------------------------------- /demo-project/packages/library/src/ui5con20/library/Chart.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${copyright} 3 | */ 4 | 5 | // Provides a shim for the chartist library 6 | sap.ui.loader.config({ 7 | shim: { 8 | "ui5con20/library/thirdparty/chartist/chartist": { 9 | amd: true, 10 | exports: "Chartist" 11 | } 12 | } 13 | }); 14 | 15 | // Provides control ui5con20.library.Chart 16 | sap.ui.define([ 17 | "sap/ui/core/Control", 18 | "sap/ui/core/HTML", 19 | "./ChartRenderer", 20 | "./thirdparty/chartist/chartist", 21 | "./library" 22 | ], function(Control, HTML, ChartRenderer, Chartist /*, library*/) { 23 | "use strict"; 24 | 25 | var Chart = Control.extend("ui5con20.library.Chart", { 26 | metadata: { 27 | properties: { 28 | "data": { 29 | type: "object" 30 | }, 31 | "options": { 32 | type: "object" 33 | }, 34 | "type": { 35 | type: "string", 36 | defaultValue: "Line" 37 | } 38 | }, 39 | aggregations: { 40 | "_html": { 41 | type: "sap.ui.core.HTML", 42 | multiple: false, 43 | visibility: "hidden" 44 | } 45 | }, 46 | events: { 47 | chartInitialized: {} 48 | } 49 | }, 50 | init: function() { 51 | var sHtmlId = this.getId() + "-html"; 52 | this.setAggregation("_html", new HTML(sHtmlId, { 53 | content: "<div id=\"" + sHtmlId + "\"></div>" 54 | })); 55 | }, 56 | onAfterRendering: function() { 57 | if (!this._chartistChart) { 58 | this._chartistChart = new Chartist[this.getType()](this.getAggregation("_html").getDomRef(), this.getData(), this.getOptions()); 59 | this.fireChartInitialized({ 60 | chart: this._chartistChart 61 | }); 62 | } 63 | }, 64 | setData: function(data) { 65 | this.setProperty("data", data, /* bSuppressInvalidate */ true); 66 | if (this._chartistChart) { 67 | this._chartistChart.update(data || {}); 68 | } 69 | return this; 70 | }, 71 | setOptions: function(options, bSuppressInvalidate) { 72 | this.setProperty("options", options, bSuppressInvalidate); 73 | if (this._chartistChart) { 74 | this._chartistChart.detach(); 75 | } 76 | this._chartistChart = new Chartist[this.getType()](this.getAggregation("_html").getDomRef(), this.getData(), this.getOptions()); 77 | }, 78 | setType: function(type, bSuppressInvalidate) { 79 | this.setProperty("type", type, bSuppressInvalidate); 80 | if (this._chartistChart) { 81 | this._chartistChart.detach(); 82 | } 83 | this._chartistChart = new Chartist[this.getType()](this.getAggregation("_html").getDomRef(), this.getData(), this.getOptions()); 84 | }, 85 | renderer: ChartRenderer 86 | }); 87 | 88 | return Chart; 89 | }); 90 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/view/Worklist.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="ui5con20.app.controller.Worklist" 3 | xmlns="sap.m" 4 | xmlns:mvc="sap.ui.core.mvc" 5 | xmlns:semantic="sap.f.semantic"> 6 | 7 | <semantic:SemanticPage 8 | id="page" 9 | headerPinnable="false" 10 | toggleHeaderOnTitleClick="false"> 11 | 12 | <semantic:titleHeading> 13 | <Title 14 | text="{i18n>worklistTitle}" 15 | level="H2"/> 16 | </semantic:titleHeading> 17 | 18 | <semantic:content> 19 | <Table 20 | id="table" 21 | width="auto" 22 | items="{ 23 | path: '/Repositories', 24 | sorter: { 25 | path: 'repositoryName', 26 | descending: false 27 | } 28 | }" 29 | noDataText="{worklistView>/tableNoDataText}" 30 | busyIndicatorDelay="{worklistView>/tableBusyDelay}" 31 | growing="true" 32 | growingScrollToLoad="true" 33 | updateFinished=".onUpdateFinished"> 34 | 35 | <headerToolbar> 36 | <OverflowToolbar> 37 | <Title 38 | id="tableHeader" 39 | text="{worklistView>/worklistTableTitle}" 40 | level="H3"/> 41 | <ToolbarSpacer /> 42 | <SearchField 43 | id="searchField" 44 | tooltip="{i18n>worklistSearchTooltip}" 45 | search=".onSearch"> 46 | <layoutData> 47 | <OverflowToolbarLayoutData 48 | maxWidth="200px" 49 | priority="NeverOverflow"/> 50 | </layoutData> 51 | </SearchField> 52 | </OverflowToolbar> 53 | </headerToolbar> 54 | 55 | <columns> 56 | <Column id="nameColumn"> 57 | <Text text="{i18n>tableNameColumnTitle}" id="nameColumnTitle"/> 58 | </Column> 59 | <Column id="starNumberColumn" hAlign="End"> 60 | <Text text="{i18n>starNumberColumnTitle}" id="starNumberColumnTitle"/> 61 | </Column> 62 | <Column id="downloadNumberColumn" hAlign="End"> 63 | <Text text="{i18n>downloadNumberColumnTitle}" id="downloadNumberColumnTitle"/> 64 | </Column> 65 | </columns> 66 | 67 | <items> 68 | <ColumnListItem 69 | type="Navigation" 70 | press=".onPress"> 71 | <cells> 72 | <ObjectIdentifier 73 | title="{repositoryName}"/> 74 | <ObjectNumber 75 | number="{ 76 | path: 'stars', 77 | type: 'sap.ui.model.type.Float' 78 | }" 79 | unit="⭐️"/> 80 | <ObjectNumber 81 | number="{ 82 | path: 'downloadsTotal', 83 | type: 'sap.ui.model.type.Float' 84 | }" 85 | unit="📦"/> 86 | </cells> 87 | </ColumnListItem> 88 | </items> 89 | </Table> 90 | </semantic:content> 91 | 92 | </semantic:SemanticPage> 93 | 94 | </mvc:View> -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/view/Worklist.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View 2 | controllerName="ui5con20.demo.nodejs.controller.Worklist" 3 | xmlns="sap.m" 4 | xmlns:mvc="sap.ui.core.mvc" 5 | xmlns:semantic="sap.f.semantic"> 6 | 7 | <semantic:SemanticPage 8 | id="page" 9 | headerPinnable="false" 10 | toggleHeaderOnTitleClick="false"> 11 | 12 | <semantic:titleHeading> 13 | <Title 14 | text="{i18n>worklistTitle}" 15 | level="H2"/> 16 | </semantic:titleHeading> 17 | 18 | <semantic:content> 19 | <Table 20 | id="table" 21 | width="auto" 22 | items="{ 23 | path: '/Repositories', 24 | sorter: { 25 | path: 'repositoryName', 26 | descending: false 27 | } 28 | }" 29 | noDataText="{worklistView>/tableNoDataText}" 30 | busyIndicatorDelay="{worklistView>/tableBusyDelay}" 31 | growing="true" 32 | growingScrollToLoad="true" 33 | updateFinished=".onUpdateFinished"> 34 | 35 | <headerToolbar> 36 | <OverflowToolbar> 37 | <Title 38 | id="tableHeader" 39 | text="{worklistView>/worklistTableTitle}" 40 | level="H3"/> 41 | <ToolbarSpacer /> 42 | <SearchField 43 | id="searchField" 44 | tooltip="{i18n>worklistSearchTooltip}" 45 | search=".onSearch"> 46 | <layoutData> 47 | <OverflowToolbarLayoutData 48 | maxWidth="200px" 49 | priority="NeverOverflow"/> 50 | </layoutData> 51 | </SearchField> 52 | </OverflowToolbar> 53 | </headerToolbar> 54 | 55 | <columns> 56 | <Column id="nameColumn"> 57 | <Text text="{i18n>tableNameColumnTitle}" id="nameColumnTitle"/> 58 | </Column> 59 | <Column id="starNumberColumn" hAlign="End"> 60 | <Text text="{i18n>starNumberColumnTitle}" id="starNumberColumnTitle"/> 61 | </Column> 62 | <Column id="downloadNumberColumn" hAlign="End"> 63 | <Text text="{i18n>downloadNumberColumnTitle}" id="downloadNumberColumnTitle"/> 64 | </Column> 65 | </columns> 66 | 67 | <items> 68 | <ColumnListItem 69 | type="Navigation" 70 | press=".onPress"> 71 | <cells> 72 | <ObjectIdentifier 73 | title="{repositoryName}"/> 74 | <ObjectNumber 75 | number="{ 76 | path: 'stars', 77 | type: 'sap.ui.model.type.Float' 78 | }" 79 | unit="⭐️"/> 80 | <ObjectNumber 81 | number="{ 82 | path: 'downloadsTotal', 83 | type: 'sap.ui.model.type.Float' 84 | }" 85 | unit="📦"/> 86 | </cells> 87 | </ColumnListItem> 88 | </items> 89 | </Table> 90 | </semantic:content> 91 | 92 | <semantic:sendEmailAction> 93 | <semantic:SendEmailAction id="shareEmail" press=".onShareEmailPress"/> 94 | </semantic:sendEmailAction> 95 | 96 | 97 | </semantic:SemanticPage> 98 | 99 | </mvc:View> -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "_version": "1.12.0", 3 | 4 | "sap.app": { 5 | "id": "ui5con20.app", 6 | "type": "application", 7 | "i18n": "i18n/i18n.properties", 8 | "title": "{{appTitle}}", 9 | "description": "{{appDescription}}", 10 | "applicationVersion": { 11 | "version": "1.0.0" 12 | }, 13 | "ach": "set-ach", 14 | "resources": "resources.json" 15 | }, 16 | 17 | "sap.fiori": { 18 | "registrationIds": [ 19 | ], 20 | "archeType": "transactional" 21 | }, 22 | 23 | "sap.ui": { 24 | "technology": "UI5", 25 | "icons": { 26 | "icon": "sap-icon://task", 27 | "favIcon": "", 28 | "phone": "", 29 | "phone@2": "", 30 | "tablet": "", 31 | "tablet@2": "" 32 | }, 33 | "deviceTypes": { 34 | "desktop": true, 35 | "tablet": true, 36 | "phone": true 37 | } 38 | }, 39 | 40 | "sap.ui5": { 41 | "rootView": { 42 | "viewName": "ui5con20.app.view.App", 43 | "type": "XML", 44 | "async": true, 45 | "id": "app" 46 | }, 47 | 48 | "dependencies": { 49 | "minUI5Version": "1.66.0", 50 | "libs": { 51 | "sap.ui.core": {}, 52 | "sap.m": {}, 53 | "sap.f": {}, 54 | "ui5con20.library": {} 55 | } 56 | }, 57 | 58 | "contentDensities": { 59 | "compact": true, 60 | "cozy": true 61 | }, 62 | 63 | "models": { 64 | "i18n": { 65 | "type": "sap.ui.model.resource.ResourceModel", 66 | "settings": { 67 | "bundleName": "ui5con20.app.i18n.i18n" 68 | } 69 | }, 70 | "": { 71 | "type": "sap.ui.model.json.JSONModel" 72 | } 73 | }, 74 | 75 | "routing": { 76 | "config": { 77 | "routerClass": "sap.m.routing.Router", 78 | "viewType": "XML", 79 | "viewPath": "ui5con20.app.view", 80 | "controlId": "app", 81 | "controlAggregation": "pages", 82 | "bypassed": { 83 | "target": ["notFound"] 84 | }, 85 | "async": true 86 | }, 87 | 88 | "routes": [ 89 | { 90 | "pattern": "", 91 | "name": "worklist", 92 | "target": ["worklist"] 93 | }, 94 | { 95 | "pattern": "Repository/{objectId}", 96 | "name": "object", 97 | "target": ["object"] 98 | } 99 | ], 100 | 101 | "targets": { 102 | "worklist": { 103 | "viewName": "Worklist", 104 | "viewId": "worklist", 105 | "viewLevel": 1, 106 | "title": "{i18n>worklistViewTitle}" 107 | }, 108 | "object": { 109 | "viewName": "Object", 110 | "viewId": "object", 111 | "viewLevel": 2, 112 | "title": "{i18n>objectViewTitle}" 113 | }, 114 | "objectNotFound": { 115 | "viewName": "ObjectNotFound", 116 | "viewId": "objectNotFound" 117 | }, 118 | "notFound": { 119 | "viewName": "NotFound", 120 | "viewId": "notFound" 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "_version": "1.12.0", 3 | 4 | "sap.app": { 5 | "id": "ui5con20.demo.nodejs", 6 | "type": "application", 7 | "i18n": "i18n/i18n.properties", 8 | "title": "{{appTitle}}", 9 | "description": "{{appDescription}}", 10 | "applicationVersion": { 11 | "version": "1.0.0" 12 | }, 13 | "ach": "set-ach", 14 | "resources": "resources.json" 15 | }, 16 | 17 | "sap.fiori": { 18 | "registrationIds": [ 19 | ], 20 | "archeType": "transactional" 21 | }, 22 | 23 | "sap.ui": { 24 | "technology": "UI5", 25 | "icons": { 26 | "icon": "sap-icon://task", 27 | "favIcon": "", 28 | "phone": "", 29 | "phone@2": "", 30 | "tablet": "", 31 | "tablet@2": "" 32 | }, 33 | "deviceTypes": { 34 | "desktop": true, 35 | "tablet": true, 36 | "phone": true 37 | } 38 | }, 39 | 40 | "sap.ui5": { 41 | "rootView": { 42 | "viewName": "ui5con20.demo.nodejs.view.App", 43 | "type": "XML", 44 | "async": true, 45 | "id": "app" 46 | }, 47 | 48 | "dependencies": { 49 | "minUI5Version": "1.66.0", 50 | "libs": { 51 | "sap.ui.core": {}, 52 | "sap.m": {}, 53 | "sap.f": {}, 54 | "ui5con20.library": {} 55 | } 56 | }, 57 | 58 | "contentDensities": { 59 | "compact": true, 60 | "cozy": true 61 | }, 62 | 63 | "models": { 64 | "i18n": { 65 | "type": "sap.ui.model.resource.ResourceModel", 66 | "settings": { 67 | "bundleName": "ui5con20.demo.nodejs.i18n.i18n" 68 | } 69 | }, 70 | "": { 71 | "type": "sap.ui.model.json.JSONModel" 72 | } 73 | }, 74 | 75 | "routing": { 76 | "config": { 77 | "routerClass": "sap.m.routing.Router", 78 | "viewType": "XML", 79 | "viewPath": "ui5con20.demo.nodejs.view", 80 | "controlId": "app", 81 | "controlAggregation": "pages", 82 | "bypassed": { 83 | "target": ["notFound"] 84 | }, 85 | "async": true 86 | }, 87 | 88 | "routes": [ 89 | { 90 | "pattern": "", 91 | "name": "worklist", 92 | "target": ["worklist"] 93 | }, 94 | { 95 | "pattern": "Repository/{objectId}", 96 | "name": "object", 97 | "target": ["object"] 98 | } 99 | ], 100 | 101 | "targets": { 102 | "worklist": { 103 | "viewName": "Worklist", 104 | "viewId": "worklist", 105 | "viewLevel": 1, 106 | "title": "{i18n>worklistViewTitle}" 107 | }, 108 | "object": { 109 | "viewName": "Object", 110 | "viewId": "object", 111 | "viewLevel": 2, 112 | "title": "{i18n>objectViewTitle}" 113 | }, 114 | "objectNotFound": { 115 | "viewName": "ObjectNotFound", 116 | "viewId": "objectNotFound" 117 | }, 118 | "notFound": { 119 | "viewName": "NotFound", 120 | "viewId": "notFound" 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/NavigationJourney.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | sap.ui.define([ 4 | "sap/ui/test/opaQunit", 5 | "sap/ui/Device", 6 | "./pages/Worklist", 7 | "./pages/Browser", 8 | "./pages/Object", 9 | "./pages/App" 10 | ], function (opaTest, Device) { 11 | "use strict"; 12 | 13 | var iDelay = (Device.browser.msie || Device.browser.edge) ? 1500 : 1000; 14 | 15 | QUnit.module("Navigation"); 16 | 17 | opaTest("Should see the objects list", function (Given, When, Then) { 18 | // Arrangements 19 | Given.iStartMyApp(); 20 | 21 | // Assertions 22 | Then.onTheWorklistPage.iShouldSeeTheTable(); 23 | }); 24 | 25 | opaTest("Should react on hash change", function (Given, When, Then) { 26 | // Actions 27 | When.onTheWorklistPage.iRememberTheItemAtPosition(2); 28 | When.onTheBrowser.iChangeTheHashToTheRememberedItem(); 29 | 30 | // Assertions 31 | Then.onTheObjectPage.iShouldSeeTheRememberedObject(). 32 | and.theViewIsNotBusyAnymore(); 33 | }); 34 | 35 | opaTest("Should go back to the TablePage", function (Given, When, Then) { 36 | // Actions 37 | When.onTheBrowser.iPressOnTheBackwardsButton(); 38 | 39 | // Assertions 40 | Then.onTheWorklistPage.iShouldSeeTheTable(); 41 | }); 42 | 43 | opaTest("Object Page shows the correct object Details", function (Given, When, Then) { 44 | // Actions 45 | When.onTheWorklistPage.iRememberTheItemAtPosition(1). 46 | and.iPressATableItemAtPosition(1); 47 | 48 | // Assertions 49 | Then.onTheObjectPage.iShouldSeeTheRememberedObject(); 50 | }); 51 | 52 | opaTest("Should be on the table page again when browser back is pressed", function (Given, When, Then) { 53 | // Actions 54 | When.onTheBrowser.iPressOnTheBackwardsButton(); 55 | 56 | // Assertions 57 | Then.onTheWorklistPage.iShouldSeeTheTable(); 58 | }); 59 | 60 | opaTest("Should be on the object page again when browser forwards is pressed", function (Given, When, Then) { 61 | // Actions 62 | When.onTheBrowser.iPressOnTheForwardsButton(); 63 | 64 | // Assertions 65 | Then.onTheObjectPage.iShouldSeeTheRememberedObject(); 66 | 67 | // Cleanup 68 | Then.iTeardownMyApp(); 69 | }); 70 | 71 | opaTest("Start the App and simulate metadata error: MessageBox should be shown", function (Given, When, Then) { 72 | //Arrangement 73 | Given.iStartMyApp({ 74 | delay: iDelay, 75 | metadataError: true 76 | }); 77 | 78 | //Assertions 79 | Then.onTheAppPage.iShouldSeeTheMessageBox(); 80 | 81 | // Actions 82 | When.onTheAppPage.iCloseTheMessageBox(); 83 | 84 | // Cleanup 85 | Then.iTeardownMyApp(); 86 | }); 87 | 88 | opaTest("Start the App and simulate bad request error: MessageBox should be shown", function (Given, When, Then) { 89 | //Arrangement 90 | Given.iStartMyApp({ 91 | delay: iDelay, 92 | errorType: "serverError" 93 | }); 94 | 95 | //Assertions 96 | Then.onTheAppPage.iShouldSeeTheMessageBox(); 97 | 98 | // Actions 99 | When.onTheAppPage.iCloseTheMessageBox(); 100 | 101 | // Cleanup 102 | Then.iTeardownMyApp(); 103 | }); 104 | 105 | }); -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/i18n/i18n.properties: -------------------------------------------------------------------------------- 1 | # This is the resource bundle for UI5con 2020 Demo App 2 | # __ldi.translation.uuid= 3 | 4 | #XTIT: Application name 5 | appTitle=UI5con 2020 Demo App 6 | 7 | #YDES: Application description 8 | appDescription=UI5con 2020 Demo App 9 | 10 | #~~~ Worklist View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | #XTIT: Worklist view title 12 | worklistViewTitle=Manage Repositories 13 | 14 | #XTIT: Worklist page title 15 | worklistTitle=UI5con 2020 Demo App 16 | 17 | #XTIT: Table view title 18 | worklistTableTitle=Repositories 19 | 20 | #XTOL: Tooltip for the search field 21 | worklistSearchTooltip=Enter an Repository name or a part of it. 22 | 23 | #XBLI: text for a table with no data with filter or search 24 | worklistNoDataWithSearchText=No matching Repositories found 25 | 26 | #XTIT: Table view title with placeholder for the number of items 27 | worklistTableTitleCount=Repositories ({0}) 28 | 29 | #XTIT: The title of the column containing the Repository of Repository 30 | tableNameColumnTitle=Repository 31 | 32 | #XTIT: The title of the column containing the Repository and the unit of measure 33 | starNumberColumnTitle=GitHub Stars 34 | 35 | #XTIT: The title of the column containing the Repository and the unit of measure 36 | downloadNumberColumnTitle=npm Downloads 37 | 38 | #XBLI: text for a table with no data 39 | tableNoDataText=No Repositories are currently available 40 | 41 | #XLNK: text for link in 'not found' pages 42 | backToWorklist=Show UI5con 2020 Demo App 43 | 44 | #~~~ Object View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | #XTIT: Object view title 46 | objectViewTitle=Repository Details 47 | 48 | #XTIT: Object page title 49 | objectTitle=Repository 50 | 51 | 52 | #XTIT: Label for the Repository 53 | RepositoryLabel=Repository 54 | 55 | #XTIT: Label for the Repository 56 | RepositoryLabel=Repository 57 | 58 | 59 | #~~~ Share Menu Options ~~~~~~~~~~~~~~~~~~~~~~~ 60 | 61 | #XTIT: Send E-Mail subject 62 | shareSendEmailWorklistSubject=<Email subject PLEASE REPLACE ACCORDING TO YOUR USE CASE> 63 | 64 | #YMSG: Send E-Mail message 65 | shareSendEmailWorklistMessage=<Email body PLEASE REPLACE ACCORDING TO YOUR USE CASE>\r\n{0} 66 | 67 | #XTIT: Send E-Mail subject 68 | shareSendEmailObjectSubject=<Email subject including object identifier PLEASE REPLACE ACCORDING TO YOUR USE CASE> {0} 69 | 70 | #YMSG: Send E-Mail message 71 | shareSendEmailObjectMessage=<Email body PLEASE REPLACE ACCORDING TO YOUR USE CASE> {0} (id: {1})\r\n{2} 72 | 73 | 74 | #~~~ Not Found View ~~~~~~~~~~~~~~~~~~~~~~~ 75 | 76 | #XTIT: Not found view title 77 | notFoundTitle=Not Found 78 | 79 | #YMSG: The Repository not found text is displayed when there is no Repository with this id 80 | noObjectFoundText=This Repository is not available 81 | 82 | #YMSG: The Repository not available text is displayed when there is no data when starting the app 83 | noObjectsAvailableText=No Repositories are currently available 84 | 85 | #YMSG: The not found text is displayed when there was an error loading the resource (404 error) 86 | notFoundText=The requested resource was not found 87 | 88 | #~~~ Error Handling ~~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | #YMSG: Error dialog description 91 | errorText=Sorry, a technical error occurred! Please try again later. -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/i18n/i18n.properties: -------------------------------------------------------------------------------- 1 | # This is the resource bundle for UI5con 2020 Demo App 2 | # __ldi.translation.uuid= 3 | 4 | #XTIT: Application name 5 | appTitle=UI5con 2020 Demo App 6 | 7 | #YDES: Application description 8 | appDescription=UI5con 2020 Demo App 9 | 10 | #~~~ Worklist View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | #XTIT: Worklist view title 12 | worklistViewTitle=Manage Repositories 13 | 14 | #XTIT: Worklist page title 15 | worklistTitle=UI5con 2020 Demo App 16 | 17 | #XTIT: Table view title 18 | worklistTableTitle=Repositories 19 | 20 | #XTOL: Tooltip for the search field 21 | worklistSearchTooltip=Enter an Repository name or a part of it. 22 | 23 | #XBLI: text for a table with no data with filter or search 24 | worklistNoDataWithSearchText=No matching Repositories found 25 | 26 | #XTIT: Table view title with placeholder for the number of items 27 | worklistTableTitleCount=Repositories ({0}) 28 | 29 | #XTIT: The title of the column containing the Repository of Repository 30 | tableNameColumnTitle=Repository 31 | 32 | #XTIT: The title of the column containing the Repository and the unit of measure 33 | starNumberColumnTitle=GitHub Stars 34 | 35 | #XTIT: The title of the column containing the Repository and the unit of measure 36 | downloadNumberColumnTitle=npm Downloads 37 | 38 | #XBLI: text for a table with no data 39 | tableNoDataText=No Repositories are currently available 40 | 41 | #XLNK: text for link in 'not found' pages 42 | backToWorklist=Show UI5con 2020 Demo App 43 | 44 | #~~~ Object View ~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | #XTIT: Object view title 46 | objectViewTitle=Repository Details 47 | 48 | #XTIT: Object page title 49 | objectTitle=Repository 50 | 51 | 52 | #XTIT: Label for the Repository 53 | RepositoryLabel=Repository 54 | 55 | #XTIT: Label for the Repository 56 | RepositoryLabel=Repository 57 | 58 | 59 | #~~~ Share Menu Options ~~~~~~~~~~~~~~~~~~~~~~~ 60 | 61 | #XTIT: Send E-Mail subject 62 | shareSendEmailWorklistSubject=<Email subject PLEASE REPLACE ACCORDING TO YOUR USE CASE> 63 | 64 | #YMSG: Send E-Mail message 65 | shareSendEmailWorklistMessage=<Email body PLEASE REPLACE ACCORDING TO YOUR USE CASE>\r\n{0} 66 | 67 | #XTIT: Send E-Mail subject 68 | shareSendEmailObjectSubject=<Email subject including object identifier PLEASE REPLACE ACCORDING TO YOUR USE CASE> {0} 69 | 70 | #YMSG: Send E-Mail message 71 | shareSendEmailObjectMessage=<Email body PLEASE REPLACE ACCORDING TO YOUR USE CASE> {0} (id: {1})\r\n{2} 72 | 73 | 74 | #~~~ Not Found View ~~~~~~~~~~~~~~~~~~~~~~~ 75 | 76 | #XTIT: Not found view title 77 | notFoundTitle=Not Found 78 | 79 | #YMSG: The Repository not found text is displayed when there is no Repository with this id 80 | noObjectFoundText=This Repository is not available 81 | 82 | #YMSG: The Repository not available text is displayed when there is no data when starting the app 83 | noObjectsAvailableText=No Repositories are currently available 84 | 85 | #YMSG: The not found text is displayed when there was an error loading the resource (404 error) 86 | notFoundText=The requested resource was not found 87 | 88 | #~~~ Error Handling ~~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | #YMSG: Error dialog description 91 | errorText=Sorry, a technical error occurred! Please try again later. -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "./Common" 5 | ], function(Opa5, Press, Common) { 6 | "use strict"; 7 | 8 | Opa5.createPageObjects({ 9 | onTheNotFoundPage : { 10 | baseClass : Common, 11 | 12 | actions : { 13 | 14 | iWaitUntilISeeObjectNotFoundPage : function () { 15 | return this.waitFor({ 16 | id : "page", 17 | viewName : "ObjectNotFound", 18 | success : function (oPage) { 19 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("objectTitle"), "the object text is shown as title"); 20 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("noObjectFoundText"), "the object not found text is shown"); 21 | }, 22 | errorMessage : "Did not display the object not found text" 23 | }); 24 | }, 25 | 26 | iWaitUntilISeeResourceNotFoundPage : function () { 27 | return this.waitFor({ 28 | id : "page", 29 | viewName : "NotFound", 30 | success : function (oPage) { 31 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("notFoundTitle"), "the not found title is shown as title"); 32 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("notFoundText"), "the not found text is shown"); 33 | }, 34 | errorMessage : "Did not display the object not found text" 35 | }); 36 | }, 37 | 38 | iPressTheObjectNotFoundShowWorklistLink : function () { 39 | return this.waitFor({ 40 | id : "link", 41 | viewName : "ObjectNotFound", 42 | actions : new Press(), 43 | errorMessage : "Did not find the link on the not found page" 44 | }); 45 | }, 46 | 47 | iPressTheNotFoundShowWorklistLink : function () { 48 | return this.waitFor({ 49 | id : "link", 50 | viewName : "NotFound", 51 | actions : new Press(), 52 | errorMessage : "Did not find the link on the not found page" 53 | }); 54 | } 55 | }, 56 | 57 | assertions : { 58 | 59 | iShouldSeeObjectNotFound : function () { 60 | return this.waitFor({ 61 | id : "page", 62 | viewName : "ObjectNotFound", 63 | success: function (oPage) { 64 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("objectTitle"), "the object text is shown as title"); 65 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("noObjectFoundText"), "the object not found text is shown"); 66 | }, 67 | errorMessage: "Did not display the object not found text" 68 | }); 69 | }, 70 | 71 | iShouldSeeResourceNotFound : function () { 72 | return this.waitFor({ 73 | id : "page", 74 | viewName : "NotFound", 75 | success: function (oPage) { 76 | Opa5.assert.strictEqual(oPage.getTitle(), oPage.getModel("i18n").getProperty("notFoundTitle"), "the not found title is shown as title"); 77 | Opa5.assert.strictEqual(oPage.getText(), oPage.getModel("i18n").getProperty("notFoundText"), "the not found text is shown"); 78 | }, 79 | errorMessage: "Did not display the object not found text" 80 | }); 81 | } 82 | 83 | } 84 | 85 | } 86 | 87 | }); 88 | 89 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/Component.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/UIComponent", 3 | "sap/ui/Device", 4 | "./model/models", 5 | "./controller/ErrorHandler" 6 | ], function (UIComponent, Device, models, ErrorHandler) { 7 | "use strict"; 8 | 9 | return UIComponent.extend("ui5con20.app.Component", { 10 | 11 | metadata : { 12 | manifest: "json" 13 | }, 14 | 15 | /** 16 | * The component is initialized by UI5 automatically during the startup of the app and calls the init method once. 17 | * In this function, the device models are set and the router is initialized. 18 | * @public 19 | * @override 20 | */ 21 | init : function () { 22 | // call the base component's init function 23 | UIComponent.prototype.init.apply(this, arguments); 24 | 25 | // initialize the error handler with the component 26 | this._oErrorHandler = new ErrorHandler(this); 27 | 28 | // set the device model 29 | this.setModel(models.createDeviceModel(), "device"); 30 | 31 | // create the views based on the url/hash 32 | this.getRouter().initialize(); 33 | 34 | this._dataLoaded = this.loadData([ 35 | "SAP/ui5-tooling", 36 | "SAP/ui5-cli", 37 | "SAP/ui5-project", 38 | "SAP/ui5-server", 39 | "SAP/ui5-builder", 40 | "SAP/ui5-fs", 41 | "SAP/ui5-logger" 42 | ]); 43 | }, 44 | 45 | loadData: function(repositories) { 46 | var oModel = this.getModel(); 47 | return Promise.all(repositories.map(function(repositoryName) { 48 | 49 | // return { 50 | // repositoryName, 51 | // stars: 200, 52 | // downloads: 100000 53 | // }; 54 | return fetch("data/" + repositoryName) 55 | .then(function(response) { 56 | return response.json(); 57 | }); 58 | })).then(function(data) { 59 | console.log(data); 60 | 61 | oModel.setProperty("/Repositories", data); 62 | }); 63 | }, 64 | 65 | getDataLoaded: function() { 66 | return this._dataLoaded; 67 | }, 68 | 69 | /** 70 | * The component is destroyed by UI5 automatically. 71 | * In this method, the ErrorHandler is destroyed. 72 | * @public 73 | * @override 74 | */ 75 | destroy : function () { 76 | this._oErrorHandler.destroy(); 77 | // call the base component's destroy function 78 | UIComponent.prototype.destroy.apply(this, arguments); 79 | }, 80 | 81 | /** 82 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy 83 | * design mode class should be set, which influences the size appearance of some controls. 84 | * @public 85 | * @return {string} css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set 86 | */ 87 | getContentDensityClass : function() { 88 | if (this._sContentDensityClass === undefined) { 89 | // check whether FLP has already set the content density class; do nothing in this case 90 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) { 91 | this._sContentDensityClass = ""; 92 | } else if (!Device.support.touch) { // apply "compact" mode if touch is not supported 93 | this._sContentDensityClass = "sapUiSizeCompact"; 94 | } else { 95 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table 96 | this._sContentDensityClass = "sapUiSizeCozy"; 97 | } 98 | } 99 | return this._sContentDensityClass; 100 | } 101 | 102 | }); 103 | 104 | }); 105 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/Component.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/core/UIComponent", 3 | "sap/ui/Device", 4 | "./model/models", 5 | "./controller/ErrorHandler" 6 | ], function (UIComponent, Device, models, ErrorHandler) { 7 | "use strict"; 8 | 9 | return UIComponent.extend("ui5con20.demo.nodejs.Component", { 10 | 11 | metadata : { 12 | manifest: "json" 13 | }, 14 | 15 | /** 16 | * The component is initialized by UI5 automatically during the startup of the app and calls the init method once. 17 | * In this function, the device models are set and the router is initialized. 18 | * @public 19 | * @override 20 | */ 21 | init : function () { 22 | // call the base component's init function 23 | UIComponent.prototype.init.apply(this, arguments); 24 | 25 | // initialize the error handler with the component 26 | this._oErrorHandler = new ErrorHandler(this); 27 | 28 | // set the device model 29 | this.setModel(models.createDeviceModel(), "device"); 30 | 31 | // create the views based on the url/hash 32 | this.getRouter().initialize(); 33 | 34 | this._dataLoaded = this.loadData([ 35 | "SAP/ui5-tooling", 36 | "SAP/ui5-cli", 37 | "SAP/ui5-project", 38 | "SAP/ui5-server", 39 | "SAP/ui5-builder", 40 | "SAP/ui5-fs", 41 | "SAP/ui5-logger" 42 | ]); 43 | }, 44 | 45 | loadData: function(repositories) { 46 | var oModel = this.getModel(); 47 | return Promise.all(repositories.map(function(repositoryName) { 48 | 49 | // return { 50 | // repositoryName, 51 | // stars: 200, 52 | // downloads: 100000 53 | // }; 54 | return fetch(`data/${repositoryName}`) 55 | .then(function(response) { 56 | return response.json(); 57 | }); 58 | })).then(function(data) { 59 | console.log(data); 60 | 61 | oModel.setProperty("/Repositories", data); 62 | }); 63 | }, 64 | 65 | getDataLoaded: function() { 66 | return this._dataLoaded; 67 | }, 68 | 69 | /** 70 | * The component is destroyed by UI5 automatically. 71 | * In this method, the ErrorHandler is destroyed. 72 | * @public 73 | * @override 74 | */ 75 | destroy : function () { 76 | this._oErrorHandler.destroy(); 77 | // call the base component's destroy function 78 | UIComponent.prototype.destroy.apply(this, arguments); 79 | }, 80 | 81 | /** 82 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy 83 | * design mode class should be set, which influences the size appearance of some controls. 84 | * @public 85 | * @return {string} css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set 86 | */ 87 | getContentDensityClass : function() { 88 | if (this._sContentDensityClass === undefined) { 89 | // check whether FLP has already set the content density class; do nothing in this case 90 | // eslint-disable-next-line sap-no-proprietary-browser-api 91 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) { 92 | this._sContentDensityClass = ""; 93 | } else if (!Device.support.touch) { // apply "compact" mode if touch is not supported 94 | this._sContentDensityClass = "sapUiSizeCompact"; 95 | } else { 96 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table 97 | this._sContentDensityClass = "sapUiSizeCozy"; 98 | } 99 | } 100 | return this._sContentDensityClass; 101 | } 102 | 103 | }); 104 | 105 | }); -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/pages/Object.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "sap/ui/test/matchers/PropertyStrictEquals", 5 | "./Common", 6 | "./shareOptions" 7 | ], function (Opa5, Press, PropertyStrictEquals, Common, shareOptions) { 8 | "use strict"; 9 | 10 | var sViewName = "Object"; 11 | 12 | Opa5.createPageObjects({ 13 | onTheObjectPage: { 14 | baseClass : Common, 15 | 16 | actions : Object.assign({ 17 | iPressTheBackButton : function () { 18 | return this.waitFor({ 19 | id : "page", 20 | viewName : sViewName, 21 | actions: new Press(), 22 | errorMessage : "Did not find the nav button on object page" 23 | }); 24 | } 25 | 26 | }, shareOptions.createActions(sViewName)), 27 | 28 | assertions: Object.assign({ 29 | 30 | iShouldSeeTheRememberedObject : function () { 31 | return this.waitFor({ 32 | success : function () { 33 | var sBindingPath = this.getContext().currentItem.bindingPath; 34 | this.waitFor({ 35 | id : "page", 36 | viewName : sViewName, 37 | matchers : function (oPage) { 38 | return oPage.getBindingContext() && oPage.getBindingContext().getPath() === sBindingPath; 39 | }, 40 | success : function (oPage) { 41 | Opa5.assert.strictEqual(oPage.getBindingContext().getPath(), sBindingPath, "was on the remembered detail page"); 42 | }, 43 | errorMessage : "Remembered object " + sBindingPath + " is not shown" 44 | }); 45 | } 46 | }); 47 | }, 48 | 49 | iShouldSeeTheObjectViewsBusyIndicator : function () { 50 | return this.waitFor({ 51 | id : "page", 52 | viewName : sViewName, 53 | matchers: new PropertyStrictEquals({ 54 | name : "busy", 55 | value: true 56 | }), 57 | autoWait: false, 58 | success : function (oPage) { 59 | Opa5.assert.ok(oPage.getBusy(), "The object view is busy"); 60 | }, 61 | errorMessage : "The object view is not busy" 62 | }); 63 | }, 64 | 65 | theViewIsNotBusyAnymore : function () { 66 | return this.waitFor({ 67 | id : "page", 68 | viewName : sViewName, 69 | matchers: new PropertyStrictEquals({ 70 | name : "busy", 71 | value: false 72 | }), 73 | autoWait: false, 74 | success : function (oPage) { 75 | Opa5.assert.ok(!oPage.getBusy(), "The object view is not busy"); 76 | }, 77 | errorMessage : "The object view is busy" 78 | }); 79 | }, 80 | 81 | theObjectViewsBusyIndicatorDelayIsZero : function () { 82 | return this.waitFor({ 83 | id : "page", 84 | viewName : sViewName, 85 | success : function (oPage) { 86 | Opa5.assert.strictEqual(oPage.getBusyIndicatorDelay(), 0, "The object view's busy indicator delay is zero."); 87 | }, 88 | errorMessage : "The object view's busy indicator delay is not zero." 89 | }); 90 | }, 91 | 92 | theObjectViewsBusyIndicatorDelayIsRestored : function () { 93 | return this.waitFor({ 94 | id : "page", 95 | viewName : sViewName, 96 | matchers: new PropertyStrictEquals({ 97 | name : "busyIndicatorDelay", 98 | value: 1000 99 | }), 100 | success : function () { 101 | Opa5.assert.ok(true, "The object view's busy indicator delay default is restored."); 102 | }, 103 | errorMessage : "The object view's busy indicator delay is still zero." 104 | }); 105 | }, 106 | 107 | theObjectViewShouldContainOnlyFormattedUnitNumbers : function () { 108 | return this.theUnitNumbersShouldHaveTwoDecimals("sap.m.ObjectNumber", 109 | sViewName, 110 | "Object numbers are properly formatted", 111 | "Object view has no entries which can be checked for their formatting"); 112 | }, 113 | 114 | theShareTileButtonShouldContainTheRememberedObjectName : function () { 115 | return this.waitFor({ 116 | id : "shareTile", 117 | viewName : sViewName, 118 | matchers : function (oButton) { 119 | var sObjectName = this.getContext().currentItem.name; 120 | var sTitle = oButton.getTitle(); 121 | return sTitle && sTitle.indexOf(sObjectName) > -1; 122 | }.bind(this), 123 | success : function () { 124 | Opa5.assert.ok(true, "The Save as Tile button contains the object name"); 125 | }, 126 | errorMessage : "The Save as Tile did not contain the object name" 127 | }); 128 | } 129 | 130 | }, shareOptions.createAssertions(sViewName)) 131 | 132 | } 133 | 134 | }); 135 | 136 | }); -------------------------------------------------------------------------------- /demo-project/packages/server/lib/middleware.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | const request = require("request"); 3 | const parseurl = require("parseurl"); 4 | const path = require("path"); 5 | 6 | const gitHubBaseUrl = "https://api.github.com"; 7 | const npmBaseUrl = "https://api.npmjs.org"; 8 | 9 | require('dotenv').config({ 10 | path: path.join(__dirname, "..", '.env') 11 | }); 12 | 13 | if (!process.env.UI5CON_DEMO_GITHUB_USER || !process.env.UI5CON_DEMO_GITHUB_TOKEN) { 14 | throw new Error("Missing GitHub credentials"); 15 | } 16 | 17 | function formatMonth(monthId) { 18 | const date = new Date(monthId + "-01"); 19 | return (date.getUTCMonth() + 1) + "." + date.getUTCFullYear(); 20 | } 21 | 22 | function formatChartDataMonthly(dailyDownloads) { 23 | const months = {}; 24 | 25 | const lastIdx = dailyDownloads.length - 1; 26 | dailyDownloads.forEach((report, idx) => { 27 | const monthId = report.day.substring(0, 7); 28 | 29 | if (idx === lastIdx) { 30 | // Remove latest month as data is not complete yet 31 | delete months[monthId]; 32 | return; 33 | } 34 | if (!months[monthId]) { 35 | months[monthId] = 0; 36 | } 37 | 38 | months[monthId] += report.downloads; 39 | }); 40 | 41 | const dataPoints = Object.values(months); 42 | 43 | const labels = Object.keys(months).map(formatMonth); 44 | 45 | return { 46 | labels, 47 | series: [dataPoints] 48 | }; 49 | } 50 | 51 | const gitHubClient = request.defaults({ 52 | baseUrl: gitHubBaseUrl, 53 | json: true, 54 | headers: { 55 | "Accept": "application/vnd.github.v3+json; charset=UTF-8", 56 | "Content-Type": "application/json", 57 | "User-Agent": "ui5con-demo-project" 58 | }, 59 | auth: { 60 | user: process.env.UI5CON_DEMO_GITHUB_USER, 61 | password: process.env.UI5CON_DEMO_GITHUB_TOKEN 62 | }, 63 | gzip: true 64 | }); 65 | 66 | const gitHubCache = {}; 67 | function fetchGitHubData(repositoryName) { 68 | if (gitHubCache[repositoryName]) { 69 | return gitHubCache[repositoryName]; 70 | } 71 | return gitHubCache[repositoryName] = new Promise((resolve, reject) => { 72 | gitHubClient.get({ 73 | uri: "/repos" + repositoryName 74 | }, function(err, httpRes, data) { 75 | if (err) { 76 | console.log("GitHub API repo fetch failed:"); 77 | console.error(err); 78 | reject(err); 79 | return; 80 | } 81 | 82 | if (httpRes.statusCode !== 200) { 83 | console.log("GitHub API repo fetch failed:"); 84 | console.error(httpRes.statusCode + ": " + data); 85 | reject(httpRes.statusCode + ": " + data); 86 | return; 87 | } 88 | 89 | resolve({ 90 | "stargazers_count": data.stargazers_count, 91 | "html_url": data.html_url, 92 | "full_name": data.full_name 93 | }); 94 | }); 95 | }); 96 | } 97 | 98 | const npmClient = request.defaults({ 99 | baseUrl: npmBaseUrl, 100 | json: true, 101 | headers: { 102 | "cache": "force-cache", // Force using cache as data should not change anymore 103 | "referrerPolicy": "no-referrer" 104 | }, 105 | gzip: true 106 | }); 107 | 108 | const npmCache = {}; 109 | async function fetchNpmData(repositoryName) { 110 | const npmName = repositoryName.replace("/SAP/ui5-", "@ui5/"); 111 | if (npmName === "@ui5/tooling") { 112 | return {}; 113 | } 114 | 115 | if (npmCache[npmName]) { 116 | return npmCache[npmName]; 117 | } 118 | const nowDateString = new Date().toISOString().split("T")[0]; 119 | return npmCache[npmName] = Promise.all([ 120 | new Promise((resolve, reject) => { 121 | npmClient.get({ 122 | uri: `/downloads/point/2016-01-01:${nowDateString}/${npmName}` 123 | }, function(err, httpRes, data) { 124 | if (err) { 125 | console.log("npm Registry API fetch failed:"); 126 | console.error(err); 127 | reject(err); 128 | return; 129 | } 130 | 131 | if (httpRes.statusCode !== 200) { 132 | console.log("npm Registry API fetch failed:"); 133 | console.error(httpRes.statusCode + ": " + data); 134 | reject(httpRes.statusCode + ": " + data); 135 | return; 136 | } 137 | resolve(data); 138 | }); 139 | }), 140 | new Promise((resolve, reject) => { 141 | npmClient.get({ 142 | uri: `/downloads/range/2016-01-01:${nowDateString}/${npmName}` 143 | }, function(err, httpRes, data) { 144 | if (err) { 145 | console.log("npm Registry API fetch failed:"); 146 | console.error(err); 147 | reject(err); 148 | return; 149 | } 150 | 151 | if (httpRes.statusCode !== 200) { 152 | console.log("npm Registry API fetch failed:"); 153 | console.error(httpRes.statusCode + ": " + data); 154 | reject(httpRes.statusCode + ": " + data); 155 | return; 156 | } 157 | resolve(formatChartDataMonthly(data.downloads)); 158 | }); 159 | }), 160 | ]).then(([total, range]) => { 161 | return { 162 | downloadsTotal: total.downloads, 163 | downloadsRange: range 164 | } 165 | }); 166 | } 167 | 168 | return function (req, res, next) { 169 | let {pathname} = parseurl(req); 170 | pathname = decodeURIComponent(pathname); 171 | 172 | // Try to read a corresponding markdown file 173 | Promise.all([fetchGitHubData(pathname), fetchNpmData(pathname)]) 174 | .then(([gitHubData, npmData]) => { 175 | res.json({ 176 | "stars": gitHubData.stargazers_count, 177 | "html_url": gitHubData.html_url, 178 | "repositoryName": gitHubData.full_name, 179 | "downloadsTotal": npmData.downloadsTotal, 180 | "downloadsRange": npmData.downloadsRange 181 | }); 182 | 183 | }, function(err) { 184 | next(err); 185 | }); 186 | } 187 | }; 188 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/controller/Object.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel", 4 | "sap/ui/core/routing/History", 5 | "../model/formatter" 6 | ], function (BaseController, JSONModel, History, formatter) { 7 | "use strict"; 8 | 9 | return BaseController.extend("ui5con20.app.controller.Object", { 10 | 11 | formatter: formatter, 12 | 13 | /* =========================================================== */ 14 | /* lifecycle methods */ 15 | /* =========================================================== */ 16 | 17 | /** 18 | * Called when the worklist controller is instantiated. 19 | * @public 20 | */ 21 | onInit : function () { 22 | // Model used to manipulate control states. The chosen values make sure, 23 | // detail page is busy indication immediately so there is no break in 24 | // between the busy indication for loading the view's meta data 25 | var iOriginalBusyDelay, 26 | oViewModel = new JSONModel({ 27 | busy : true, 28 | delay : 0 29 | }); 30 | 31 | this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this); 32 | 33 | // Store original busy indicator delay, so it can be restored later on 34 | iOriginalBusyDelay = this.getView().getBusyIndicatorDelay(); 35 | this.setModel(oViewModel, "objectView"); 36 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 37 | 38 | this.initChart(); 39 | }, 40 | 41 | initChart: function() { 42 | var oChart = this.byId("chart"); 43 | oChart.attachChartInitialized(function(oEvent) { 44 | var chart = oEvent.getParameter("chart"); 45 | var dur = 1000; 46 | var easing = Chartist.Svg.Easing.easeOutQuint; 47 | var animationDone; 48 | function draw(data) { 49 | var begin; 50 | if (data.type === "line" || data.type === "area") { 51 | begin = 100 * data.index; 52 | data.element.animate({ 53 | d: { 54 | begin: begin, 55 | dur: dur, 56 | from: data.path.clone().scale(1, 0).translate(0, data.chartRect.height()).stringify(), 57 | to: data.path.clone().stringify(), 58 | easing: easing 59 | } 60 | }); 61 | } else if (data.type === "point") { 62 | begin = 100 * data.seriesIndex; 63 | var animation = { 64 | begin: begin, 65 | dur: dur, 66 | from: data.axisY.chartRect.height(), 67 | to: data.y, 68 | easing: easing 69 | }; 70 | data.element.animate({ 71 | // Need to use shallow clone as the object is being modified by the animate function 72 | y1: Object.assign({}, animation), 73 | y2: Object.assign({}, animation) 74 | }); 75 | } 76 | if (animationDone) { 77 | clearTimeout(animationDone); 78 | } 79 | animationDone = setTimeout(removeDrawHandler, begin + dur); 80 | } 81 | function removeDrawHandler() { 82 | chart.off("draw", draw); 83 | } 84 | 85 | chart.on("draw", draw); 86 | }); 87 | }, 88 | 89 | /* =========================================================== */ 90 | /* event handlers */ 91 | /* =========================================================== */ 92 | 93 | 94 | /** 95 | * Event handler for navigating back. 96 | * It there is a history entry we go one step back in the browser history 97 | * If not, it will replace the current entry of the browser history with the worklist route. 98 | * @public 99 | */ 100 | onNavBack : function() { 101 | var sPreviousHash = History.getInstance().getPreviousHash(); 102 | 103 | if (sPreviousHash !== undefined) { 104 | history.go(-1); 105 | } else { 106 | this.getRouter().navTo("worklist", {}, true); 107 | } 108 | }, 109 | 110 | /* =========================================================== */ 111 | /* internal methods */ 112 | /* =========================================================== */ 113 | 114 | /** 115 | * Binds the view to the object path. 116 | * @function 117 | * @param {sap.ui.base.Event} oEvent pattern match event in route 'object' 118 | * @private 119 | */ 120 | _onObjectMatched : function (oEvent) { 121 | var sObjectId = oEvent.getParameter("arguments").objectId; 122 | this._bindView("/Repositories/" + sObjectId); 123 | }, 124 | 125 | /** 126 | * Binds the view to the object path. 127 | * @function 128 | * @param {string} sObjectPath path to the object to be bound 129 | * @private 130 | */ 131 | _bindView : function (sObjectPath) { 132 | this.getView().bindElement({ 133 | path: sObjectPath, 134 | events: { 135 | change: this._onBindingChange.bind(this) 136 | } 137 | }); 138 | }, 139 | 140 | _onBindingChange : function () { 141 | var oView = this.getView(), 142 | oViewModel = this.getModel("objectView"), 143 | oElementBinding = oView.getElementBinding(); 144 | 145 | // No data for the binding 146 | if (!oElementBinding.getBoundContext()) { 147 | this.getRouter().getTargets().display("objectNotFound"); 148 | return; 149 | } 150 | 151 | var oResourceBundle = this.getResourceBundle(), 152 | oObject = oView.getBindingContext().getObject(), 153 | sObjectId = oObject.repositoryName; 154 | 155 | oViewModel.setProperty("/busy", false); 156 | 157 | oViewModel.setProperty("/shareSendEmailSubject", 158 | oResourceBundle.getText("shareSendEmailObjectSubject", [sObjectId])); 159 | oViewModel.setProperty("/shareSendEmailMessage", 160 | oResourceBundle.getText("shareSendEmailObjectMessage", [sObjectId, location.href])); 161 | } 162 | 163 | }); 164 | 165 | }); 166 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/controller/Object.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel", 4 | "sap/ui/core/routing/History", 5 | "../model/formatter" 6 | ], function (BaseController, JSONModel, History, formatter) { 7 | "use strict"; 8 | 9 | return BaseController.extend("ui5con20.demo.nodejs.controller.Object", { 10 | 11 | formatter: formatter, 12 | 13 | /* =========================================================== */ 14 | /* lifecycle methods */ 15 | /* =========================================================== */ 16 | 17 | /** 18 | * Called when the worklist controller is instantiated. 19 | * @public 20 | */ 21 | onInit : function () { 22 | // Model used to manipulate control states. The chosen values make sure, 23 | // detail page is busy indication immediately so there is no break in 24 | // between the busy indication for loading the view's meta data 25 | var iOriginalBusyDelay, 26 | oViewModel = new JSONModel({ 27 | busy : true, 28 | delay : 0 29 | }); 30 | 31 | this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this); 32 | 33 | // Store original busy indicator delay, so it can be restored later on 34 | iOriginalBusyDelay = this.getView().getBusyIndicatorDelay(); 35 | this.setModel(oViewModel, "objectView"); 36 | oViewModel.setProperty("/delay", iOriginalBusyDelay); 37 | 38 | this.initChart(); 39 | }, 40 | 41 | initChart: function() { 42 | var oChart = this.byId("chart"); 43 | oChart.attachChartInitialized(function(oEvent) { 44 | var chart = oEvent.getParameter("chart"); 45 | var dur = 1000; 46 | var easing = Chartist.Svg.Easing.easeOutQuint; 47 | var animationDone; 48 | function draw(data) { 49 | var begin; 50 | if(data.type === "line" || data.type === "area") { 51 | begin = 100 * data.index; 52 | data.element.animate({ 53 | d: { 54 | begin, 55 | dur, 56 | from: data.path.clone().scale(1, 0).translate(0, data.chartRect.height()).stringify(), 57 | to: data.path.clone().stringify(), 58 | easing 59 | } 60 | }); 61 | } else if (data.type === "point") { 62 | begin = 100 * data.seriesIndex; 63 | var animation = { 64 | begin, 65 | dur, 66 | from: data.axisY.chartRect.height(), 67 | to: data.y, 68 | easing 69 | }; 70 | data.element.animate({ 71 | // Need to use shallow clone as the object is being modified by the animate function 72 | y1: Object.assign({}, animation), 73 | y2: Object.assign({}, animation) 74 | }); 75 | } 76 | if (animationDone) { 77 | clearTimeout(animationDone); 78 | } 79 | animationDone = setTimeout(removeDrawHandler, begin + dur); 80 | } 81 | function removeDrawHandler() { 82 | chart.off("draw", draw); 83 | } 84 | 85 | chart.on("draw", draw); 86 | }); 87 | }, 88 | 89 | /* =========================================================== */ 90 | /* event handlers */ 91 | /* =========================================================== */ 92 | 93 | 94 | /** 95 | * Event handler for navigating back. 96 | * It there is a history entry we go one step back in the browser history 97 | * If not, it will replace the current entry of the browser history with the worklist route. 98 | * @public 99 | */ 100 | onNavBack : function() { 101 | var sPreviousHash = History.getInstance().getPreviousHash(); 102 | 103 | if (sPreviousHash !== undefined) { 104 | history.go(-1); 105 | } else { 106 | this.getRouter().navTo("worklist", {}, true); 107 | } 108 | }, 109 | 110 | /* =========================================================== */ 111 | /* internal methods */ 112 | /* =========================================================== */ 113 | 114 | /** 115 | * Binds the view to the object path. 116 | * @function 117 | * @param {sap.ui.base.Event} oEvent pattern match event in route 'object' 118 | * @private 119 | */ 120 | _onObjectMatched : function (oEvent) { 121 | var sObjectId = oEvent.getParameter("arguments").objectId; 122 | this._bindView("/Repositories/" + sObjectId); 123 | }, 124 | 125 | /** 126 | * Binds the view to the object path. 127 | * @function 128 | * @param {string} sObjectPath path to the object to be bound 129 | * @private 130 | */ 131 | _bindView : function (sObjectPath) { 132 | var oViewModel = this.getModel("objectView"), 133 | oDataModel = this.getModel(); 134 | 135 | this.getView().bindElement({ 136 | path: sObjectPath, 137 | events: { 138 | change: this._onBindingChange.bind(this) 139 | } 140 | }); 141 | }, 142 | 143 | _onBindingChange : function () { 144 | var oView = this.getView(), 145 | oViewModel = this.getModel("objectView"), 146 | oElementBinding = oView.getElementBinding(); 147 | 148 | // No data for the binding 149 | if (!oElementBinding.getBoundContext()) { 150 | this.getRouter().getTargets().display("objectNotFound"); 151 | return; 152 | } 153 | 154 | var oResourceBundle = this.getResourceBundle(), 155 | oObject = oView.getBindingContext().getObject(), 156 | sObjectId = oObject.repositoryName; 157 | 158 | oViewModel.setProperty("/busy", false); 159 | 160 | oViewModel.setProperty("/shareSendEmailSubject", 161 | oResourceBundle.getText("shareSendEmailObjectSubject", [sObjectId])); 162 | oViewModel.setProperty("/shareSendEmailMessage", 163 | oResourceBundle.getText("shareSendEmailObjectMessage", [sObjectId, location.href])); 164 | } 165 | 166 | }); 167 | 168 | }); 169 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/controller/Worklist.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel", 4 | "../model/formatter", 5 | "sap/ui/model/Filter", 6 | "sap/ui/model/FilterOperator" 7 | ], function (BaseController, JSONModel, formatter, Filter, FilterOperator) { 8 | "use strict"; 9 | 10 | return BaseController.extend("ui5con20.app.controller.Worklist", { 11 | 12 | formatter: formatter, 13 | 14 | /* =========================================================== */ 15 | /* lifecycle methods */ 16 | /* =========================================================== */ 17 | 18 | /** 19 | * Called when the worklist controller is instantiated. 20 | * @public 21 | */ 22 | onInit : function () { 23 | var oViewModel, 24 | iOriginalBusyDelay, 25 | oTable = this.byId("table"); 26 | 27 | // Put down worklist table's original value for busy indicator delay, 28 | // so it can be restored later on. Busy handling on the table is 29 | // taken care of by the table itself. 30 | iOriginalBusyDelay = oTable.getBusyIndicatorDelay(); 31 | // keeps the search state 32 | this._aTableSearchState = []; 33 | 34 | // Model used to manipulate control states 35 | oViewModel = new JSONModel({ 36 | worklistTableTitle : this.getResourceBundle().getText("worklistTableTitle"), 37 | shareOnJamTitle: this.getResourceBundle().getText("worklistTitle"), 38 | shareSendEmailSubject: this.getResourceBundle().getText("shareSendEmailWorklistSubject"), 39 | shareSendEmailMessage: this.getResourceBundle().getText("shareSendEmailWorklistMessage", [location.href]), 40 | tableNoDataText : this.getResourceBundle().getText("tableNoDataText"), 41 | tableBusyDelay : 0 42 | }); 43 | this.setModel(oViewModel, "worklistView"); 44 | 45 | // Make sure, busy indication is showing immediately so there is no 46 | // break after the busy indication for loading the view's meta data is 47 | // ended (see promise 'oWhenMetadataIsLoaded' in AppController) 48 | oTable.attachEventOnce("updateFinished", function(){ 49 | // Restore original busy indicator delay for worklist's table 50 | oViewModel.setProperty("/tableBusyDelay", iOriginalBusyDelay); 51 | }); 52 | }, 53 | 54 | /* =========================================================== */ 55 | /* event handlers */ 56 | /* =========================================================== */ 57 | 58 | /** 59 | * Triggered by the table's 'updateFinished' event: after new table 60 | * data is available, this handler method updates the table counter. 61 | * This should only happen if the update was successful, which is 62 | * why this handler is attached to 'updateFinished' and not to the 63 | * table's list binding's 'dataReceived' method. 64 | * @param {sap.ui.base.Event} oEvent the update finished event 65 | * @public 66 | */ 67 | onUpdateFinished : function (oEvent) { 68 | // update the worklist's object counter after the table update 69 | var sTitle, 70 | oTable = oEvent.getSource(), 71 | iTotalItems = oEvent.getParameter("total"); 72 | // only update the counter if the length is final and 73 | // the table is not empty 74 | if (iTotalItems && oTable.getBinding("items").isLengthFinal()) { 75 | sTitle = this.getResourceBundle().getText("worklistTableTitleCount", [iTotalItems]); 76 | } else { 77 | sTitle = this.getResourceBundle().getText("worklistTableTitle"); 78 | } 79 | this.getModel("worklistView").setProperty("/worklistTableTitle", sTitle); 80 | }, 81 | 82 | /** 83 | * Event handler when a table item gets pressed 84 | * @param {sap.ui.base.Event} oEvent the table selectionChange event 85 | * @public 86 | */ 87 | onPress : function (oEvent) { 88 | // The source is the list item that got pressed 89 | this._showObject(oEvent.getSource()); 90 | }, 91 | 92 | /** 93 | * Event handler for navigating back. 94 | * We navigate back in the browser history 95 | * @public 96 | */ 97 | onNavBack : function() { 98 | history.go(-1); 99 | }, 100 | 101 | onSearch : function (oEvent) { 102 | if (oEvent.getParameters().refreshButtonPressed) { 103 | // Search field's 'refresh' button has been pressed. 104 | // This is visible if you select any master list item. 105 | // In this case no new search is triggered, we only 106 | // refresh the list binding. 107 | this.onRefresh(); 108 | } else { 109 | var aTableSearchState = []; 110 | var sQuery = oEvent.getParameter("query"); 111 | 112 | if (sQuery && sQuery.length > 0) { 113 | aTableSearchState = [new Filter("repositoryName", FilterOperator.Contains, sQuery)]; 114 | } 115 | this._applySearch(aTableSearchState); 116 | } 117 | 118 | }, 119 | 120 | /** 121 | * Event handler for refresh event. Keeps filter, sort 122 | * and group settings and refreshes the list binding. 123 | * @public 124 | */ 125 | onRefresh : function () { 126 | var oTable = this.byId("table"); 127 | oTable.getBinding("items").refresh(); 128 | }, 129 | 130 | /* =========================================================== */ 131 | /* internal methods */ 132 | /* =========================================================== */ 133 | 134 | /** 135 | * Shows the selected item on the object page 136 | * On phones a additional history entry is created 137 | * @param {sap.m.ObjectListItem} oItem selected Item 138 | * @private 139 | */ 140 | _showObject : function (oItem) { 141 | this.getRouter().navTo("object", { 142 | objectId: oItem.getBindingContext().getPath().replace("/Repositories/", "") 143 | }); 144 | }, 145 | 146 | /** 147 | * Internal helper method to apply both filter and search state together on the list binding 148 | * @param {sap.ui.model.Filter[]} aTableSearchState An array of filters for the search 149 | * @private 150 | */ 151 | _applySearch: function(aTableSearchState) { 152 | var oTable = this.byId("table"), 153 | oViewModel = this.getModel("worklistView"); 154 | oTable.getBinding("items").filter(aTableSearchState, "Application"); 155 | // changes the noDataText of the list in case there are no filter results 156 | if (aTableSearchState.length !== 0) { 157 | oViewModel.setProperty("/tableNoDataText", this.getResourceBundle().getText("worklistNoDataWithSearchText")); 158 | } 159 | } 160 | 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /demo-nodejs-api/webapp/controller/Worklist.controller.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "./BaseController", 3 | "sap/ui/model/json/JSONModel", 4 | "../model/formatter", 5 | "sap/ui/model/Filter", 6 | "sap/ui/model/FilterOperator" 7 | ], function (BaseController, JSONModel, formatter, Filter, FilterOperator) { 8 | "use strict"; 9 | 10 | return BaseController.extend("ui5con20.demo.nodejs.controller.Worklist", { 11 | 12 | formatter: formatter, 13 | 14 | /* =========================================================== */ 15 | /* lifecycle methods */ 16 | /* =========================================================== */ 17 | 18 | /** 19 | * Called when the worklist controller is instantiated. 20 | * @public 21 | */ 22 | onInit : function () { 23 | var oViewModel, 24 | iOriginalBusyDelay, 25 | oTable = this.byId("table"); 26 | 27 | // Put down worklist table's original value for busy indicator delay, 28 | // so it can be restored later on. Busy handling on the table is 29 | // taken care of by the table itself. 30 | iOriginalBusyDelay = oTable.getBusyIndicatorDelay(); 31 | // keeps the search state 32 | this._aTableSearchState = []; 33 | 34 | // Model used to manipulate control states 35 | oViewModel = new JSONModel({ 36 | worklistTableTitle : this.getResourceBundle().getText("worklistTableTitle"), 37 | shareOnJamTitle: this.getResourceBundle().getText("worklistTitle"), 38 | shareSendEmailSubject: this.getResourceBundle().getText("shareSendEmailWorklistSubject"), 39 | shareSendEmailMessage: this.getResourceBundle().getText("shareSendEmailWorklistMessage", [location.href]), 40 | tableNoDataText : this.getResourceBundle().getText("tableNoDataText"), 41 | tableBusyDelay : 0 42 | }); 43 | this.setModel(oViewModel, "worklistView"); 44 | 45 | // Make sure, busy indication is showing immediately so there is no 46 | // break after the busy indication for loading the view's meta data is 47 | // ended (see promise 'oWhenMetadataIsLoaded' in AppController) 48 | oTable.attachEventOnce("updateFinished", function(){ 49 | // Restore original busy indicator delay for worklist's table 50 | oViewModel.setProperty("/tableBusyDelay", iOriginalBusyDelay); 51 | }); 52 | }, 53 | 54 | /* =========================================================== */ 55 | /* event handlers */ 56 | /* =========================================================== */ 57 | 58 | /** 59 | * Triggered by the table's 'updateFinished' event: after new table 60 | * data is available, this handler method updates the table counter. 61 | * This should only happen if the update was successful, which is 62 | * why this handler is attached to 'updateFinished' and not to the 63 | * table's list binding's 'dataReceived' method. 64 | * @param {sap.ui.base.Event} oEvent the update finished event 65 | * @public 66 | */ 67 | onUpdateFinished : function (oEvent) { 68 | // update the worklist's object counter after the table update 69 | var sTitle, 70 | oTable = oEvent.getSource(), 71 | iTotalItems = oEvent.getParameter("total"); 72 | // only update the counter if the length is final and 73 | // the table is not empty 74 | if (iTotalItems && oTable.getBinding("items").isLengthFinal()) { 75 | sTitle = this.getResourceBundle().getText("worklistTableTitleCount", [iTotalItems]); 76 | } else { 77 | sTitle = this.getResourceBundle().getText("worklistTableTitle"); 78 | } 79 | this.getModel("worklistView").setProperty("/worklistTableTitle", sTitle); 80 | }, 81 | 82 | /** 83 | * Event handler when a table item gets pressed 84 | * @param {sap.ui.base.Event} oEvent the table selectionChange event 85 | * @public 86 | */ 87 | onPress : function (oEvent) { 88 | // The source is the list item that got pressed 89 | this._showObject(oEvent.getSource()); 90 | }, 91 | 92 | /** 93 | * Event handler for navigating back. 94 | * We navigate back in the browser history 95 | * @public 96 | */ 97 | onNavBack : function() { 98 | // eslint-disable-next-line sap-no-history-manipulation 99 | history.go(-1); 100 | }, 101 | 102 | 103 | onSearch : function (oEvent) { 104 | if (oEvent.getParameters().refreshButtonPressed) { 105 | // Search field's 'refresh' button has been pressed. 106 | // This is visible if you select any master list item. 107 | // In this case no new search is triggered, we only 108 | // refresh the list binding. 109 | this.onRefresh(); 110 | } else { 111 | var aTableSearchState = []; 112 | var sQuery = oEvent.getParameter("query"); 113 | 114 | if (sQuery && sQuery.length > 0) { 115 | aTableSearchState = [new Filter("repositoryName", FilterOperator.Contains, sQuery)]; 116 | } 117 | this._applySearch(aTableSearchState); 118 | } 119 | 120 | }, 121 | 122 | /** 123 | * Event handler for refresh event. Keeps filter, sort 124 | * and group settings and refreshes the list binding. 125 | * @public 126 | */ 127 | onRefresh : function () { 128 | var oTable = this.byId("table"); 129 | oTable.getBinding("items").refresh(); 130 | }, 131 | 132 | /* =========================================================== */ 133 | /* internal methods */ 134 | /* =========================================================== */ 135 | 136 | /** 137 | * Shows the selected item on the object page 138 | * On phones a additional history entry is created 139 | * @param {sap.m.ObjectListItem} oItem selected Item 140 | * @private 141 | */ 142 | _showObject : function (oItem) { 143 | this.getRouter().navTo("object", { 144 | objectId: oItem.getBindingContext().getPath().replace("/Repositories/", "") 145 | }); 146 | }, 147 | 148 | /** 149 | * Internal helper method to apply both filter and search state together on the list binding 150 | * @param {sap.ui.model.Filter[]} aTableSearchState An array of filters for the search 151 | * @private 152 | */ 153 | _applySearch: function(aTableSearchState) { 154 | var oTable = this.byId("table"), 155 | oViewModel = this.getModel("worklistView"); 156 | oTable.getBinding("items").filter(aTableSearchState, "Application"); 157 | // changes the noDataText of the list in case there are no filter results 158 | if (aTableSearchState.length !== 0) { 159 | oViewModel.setProperty("/tableNoDataText", this.getResourceBundle().getText("worklistNoDataWithSearchText")); 160 | } 161 | } 162 | 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /demo-project/packages/app/webapp/test/integration/pages/Worklist.js: -------------------------------------------------------------------------------- 1 | sap.ui.define([ 2 | "sap/ui/test/Opa5", 3 | "sap/ui/test/actions/Press", 4 | "sap/ui/test/actions/EnterText", 5 | "sap/ui/test/matchers/AggregationLengthEquals", 6 | "sap/ui/test/matchers/AggregationFilled", 7 | "sap/ui/test/matchers/PropertyStrictEquals", 8 | "./Common", 9 | "./shareOptions" 10 | ], function(Opa5, Press, EnterText, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals, Common, shareOptions) { 11 | "use strict"; 12 | 13 | var sViewName = "Worklist", 14 | sTableId = "table", 15 | sSearchFieldId = "searchField", 16 | sSomethingThatCannotBeFound = "*#-Q@@||"; 17 | 18 | function allItemsInTheListContainTheSearchTerm (aControls) { 19 | var oTable = aControls[0], 20 | oSearchField = aControls[1], 21 | aItems = oTable.getItems(); 22 | 23 | // table needs items 24 | if (aItems.length === 0) { 25 | return false; 26 | } 27 | 28 | return aItems.every(function (oItem) { 29 | return oItem.getCells()[0].getTitle().indexOf(oSearchField.getValue()) !== -1; 30 | }); 31 | } 32 | 33 | function createWaitForItemAtPosition (oOptions) { 34 | var iPosition = oOptions.position; 35 | return { 36 | id : sTableId, 37 | viewName : sViewName, 38 | matchers : function (oTable) { 39 | return oTable.getItems()[iPosition]; 40 | }, 41 | actions : oOptions.actions, 42 | success : oOptions.success, 43 | errorMessage : "Table in view '" + sViewName + "' does not contain an Item at position '" + iPosition + "'" 44 | }; 45 | } 46 | 47 | Opa5.createPageObjects({ 48 | 49 | onTheWorklistPage : { 50 | baseClass : Common, 51 | actions : Object.assign({ 52 | iPressATableItemAtPosition : function (iPosition) { 53 | return this.waitFor(createWaitForItemAtPosition({ 54 | position : iPosition, 55 | actions : new Press() 56 | })); 57 | }, 58 | 59 | iRememberTheItemAtPosition : function (iPosition){ 60 | return this.waitFor(createWaitForItemAtPosition({ 61 | position : iPosition, 62 | success : function (oTableItem) { 63 | var oBindingContext = oTableItem.getBindingContext(); 64 | 65 | // Don't remember objects just strings since IE will not allow accessing objects of destroyed frames 66 | this.getContext().currentItem = { 67 | bindingPath: oBindingContext.getPath(), 68 | id: oBindingContext.getProperty("Pony"), 69 | name: oBindingContext.getProperty("Pony") 70 | }; 71 | } 72 | })); 73 | }, 74 | 75 | iPressOnMoreData : function (){ 76 | return this.waitFor({ 77 | id : sTableId, 78 | viewName : sViewName, 79 | matchers : function (oTable) { 80 | return !!oTable.$("trigger").length; 81 | }, 82 | actions : new Press(), 83 | errorMessage : "The Table does not have a trigger" 84 | }); 85 | }, 86 | 87 | iSearchForTheFirstObject: function() { 88 | var sFirstObjectTitle; 89 | 90 | return this.waitFor({ 91 | id: sTableId, 92 | viewName: sViewName, 93 | matchers: new AggregationFilled({ 94 | name: "items" 95 | }), 96 | success: function(oTable) { 97 | sFirstObjectTitle = oTable.getItems()[0].getCells()[0].getTitle(); 98 | 99 | this.iSearchForValue(sFirstObjectTitle); 100 | 101 | this.waitFor({ 102 | id: [sTableId, sSearchFieldId], 103 | viewName: sViewName, 104 | check : allItemsInTheListContainTheSearchTerm, 105 | errorMessage: "Did not find any table entries or too many while trying to search for the first object." 106 | }); 107 | }, 108 | errorMessage: "Did not find table entries while trying to search for the first object." 109 | }); 110 | }, 111 | 112 | iSearchForValueWithActions : function (aActions) { 113 | return this.waitFor({ 114 | id : sSearchFieldId, 115 | viewName : sViewName, 116 | actions: aActions, 117 | errorMessage : "Failed to find search field in Worklist view.'" 118 | }); 119 | }, 120 | 121 | iSearchForValue : function (sSearchString) { 122 | return this.iSearchForValueWithActions([new EnterText({text : sSearchString}), new Press()]); 123 | }, 124 | 125 | iTypeSomethingInTheSearchThatCannotBeFoundAndTriggerRefresh : function () { 126 | var fnEnterTextAndFireRefreshButtonPressedOnSearchField = function (oSearchField) { 127 | // set the search field value directly as EnterText action triggers a search event 128 | oSearchField.setValue(sSomethingThatCannotBeFound); 129 | 130 | // fire the search to simulate a refresh button press 131 | oSearchField.fireSearch({refreshButtonPressed: true}); 132 | }; 133 | return this.iSearchForValueWithActions(fnEnterTextAndFireRefreshButtonPressedOnSearchField); 134 | }, 135 | 136 | iClearTheSearch : function () { 137 | return this.iSearchForValueWithActions([new EnterText({text: ""}), new Press()]); 138 | }, 139 | 140 | iSearchForSomethingWithNoResults : function () { 141 | return this.iSearchForValueWithActions([new EnterText({text: sSomethingThatCannotBeFound}), new Press()]); 142 | } 143 | 144 | }, shareOptions.createActions(sViewName)), 145 | 146 | assertions: Object.assign({ 147 | 148 | iShouldSeeTheTable : function () { 149 | return this.waitFor({ 150 | id : sTableId, 151 | viewName : sViewName, 152 | success : function (oTable) { 153 | Opa5.assert.ok(oTable, "Found the object Table"); 154 | }, 155 | errorMessage : "Can't see the master Table." 156 | }); 157 | }, 158 | 159 | theTableShowsOnlyObjectsWithTheSearchStringInTheirTitle : function () { 160 | this.waitFor({ 161 | id : [sTableId, sSearchFieldId], 162 | viewName : sViewName, 163 | check: allItemsInTheListContainTheSearchTerm, 164 | success : function () { 165 | Opa5.assert.ok(true, "Every item did contain the title"); 166 | }, 167 | errorMessage : "The table did not have items" 168 | }); 169 | }, 170 | 171 | theTableHasEntries : function () { 172 | return this.waitFor({ 173 | viewName : sViewName, 174 | id : sTableId, 175 | matchers : new AggregationFilled({ 176 | name : "items" 177 | }), 178 | success : function () { 179 | Opa5.assert.ok(true, "The table has entries"); 180 | }, 181 | errorMessage : "The table had no entries" 182 | }); 183 | }, 184 | 185 | theTableShouldHaveAllEntries : function () { 186 | var aAllEntities, 187 | iExpectedNumberOfItems; 188 | 189 | // retrieve all Pony to be able to check for the total amount 190 | this.waitFor(this.createAWaitForAnEntitySet({ 191 | entitySet: "Pony", 192 | success: function (aEntityData) { 193 | aAllEntities = aEntityData; 194 | } 195 | })); 196 | 197 | return this.waitFor({ 198 | id : sTableId, 199 | viewName : sViewName, 200 | matchers : function (oTable) { 201 | // If there are less items in the list than the growingThreshold, only check for this number. 202 | iExpectedNumberOfItems = Math.min(oTable.getGrowingThreshold(), aAllEntities.length); 203 | return new AggregationLengthEquals({name : "items", length : iExpectedNumberOfItems}).isMatching(oTable); 204 | }, 205 | success : function (oTable) { 206 | Opa5.assert.strictEqual(oTable.getItems().length, iExpectedNumberOfItems, "The growing Table has " + iExpectedNumberOfItems + " items"); 207 | }, 208 | errorMessage : "Table does not have all entries." 209 | }); 210 | }, 211 | 212 | theTitleShouldDisplayTheTotalAmountOfItems : function () { 213 | return this.waitFor({ 214 | id : sTableId, 215 | viewName : sViewName, 216 | matchers : new AggregationFilled({name : "items"}), 217 | success : function (oTable) { 218 | var iObjectCount = oTable.getBinding("items").getLength(); 219 | this.waitFor({ 220 | id : "tableHeader", 221 | viewName : sViewName, 222 | matchers : function (oPage) { 223 | var sExpectedText = oPage.getModel("i18n").getResourceBundle().getText("worklistTableTitleCount", [iObjectCount]); 224 | return new PropertyStrictEquals({name : "text", value: sExpectedText}).isMatching(oPage); 225 | }, 226 | success : function () { 227 | Opa5.assert.ok(true, "The Page has a title containing the number " + iObjectCount); 228 | }, 229 | errorMessage : "The Page's header does not container the number of items " + iObjectCount 230 | }); 231 | }, 232 | errorMessage : "The table has no items." 233 | }); 234 | }, 235 | 236 | theTableShouldHaveTheDoubleAmountOfInitialEntries : function () { 237 | var iExpectedNumberOfItems; 238 | 239 | return this.waitFor({ 240 | id : sTableId, 241 | viewName : sViewName, 242 | matchers : function (oTable) { 243 | iExpectedNumberOfItems = oTable.getGrowingThreshold() * 2; 244 | return new AggregationLengthEquals({name : "items", length : iExpectedNumberOfItems}).isMatching(oTable); 245 | }, 246 | success : function () { 247 | Opa5.assert.ok(true, "The growing Table had the double amount: " + iExpectedNumberOfItems + " of entries"); 248 | }, 249 | errorMessage : "Table does not have the double amount of entries." 250 | }); 251 | }, 252 | 253 | theTableShouldContainOnlyFormattedUnitNumbers : function () { 254 | return this.theUnitNumbersShouldHaveTwoDecimals("sap.m.ObjectNumber", 255 | sViewName, 256 | "Object numbers are properly formatted", 257 | "Table has no entries which can be checked for their formatting"); 258 | }, 259 | 260 | iShouldSeeTheWorklistViewsBusyIndicator : function () { 261 | return this.waitFor({ 262 | id : "page", 263 | viewName : sViewName, 264 | success : function (oPage) { 265 | Opa5.assert.ok(oPage.getParent().getBusy(), "The worklist view is busy"); 266 | }, 267 | errorMessage : "The worklist view is not busy" 268 | }); 269 | }, 270 | 271 | iShouldSeeTheWorklistTableBusyIndicator : function () { 272 | return this.waitFor({ 273 | id : "table", 274 | viewName : sViewName, 275 | matchers : new PropertyStrictEquals({ 276 | name : "busy", 277 | value: true 278 | }), 279 | autoWait : false, 280 | success : function () { 281 | Opa5.assert.ok(true, "The worklist table is busy"); 282 | }, 283 | errorMessage : "The worklist table is not busy" 284 | }); 285 | }, 286 | 287 | iShouldSeeTheNoDataTextForNoSearchResults : function () { 288 | return this.waitFor({ 289 | id : sTableId, 290 | viewName : sViewName, 291 | success : function (oTable) { 292 | Opa5.assert.strictEqual( 293 | oTable.getNoDataText(), 294 | oTable.getModel("i18n").getProperty("worklistNoDataWithSearchText"), 295 | "the table should show the no data text for search"); 296 | }, 297 | errorMessage : "table does not show the no data text for search" 298 | }); 299 | } 300 | }, shareOptions.createAssertions(sViewName)) 301 | 302 | } 303 | 304 | }); 305 | 306 | }); -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------