@sap-ux/fiori-freestyle-writer 0.12.9 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/defaults.d.ts +1 -0
  2. package/dist/defaults.js +4 -1
  3. package/dist/index.js +14 -10
  4. package/package.json +2 -2
  5. package/templates/basic/add/webapp/Component.ts +28 -0
  6. package/templates/basic/add/webapp/controller/App.controller.ts +11 -0
  7. package/templates/basic/custom/Controller.ts +11 -0
  8. package/templates/common/add/webapp/model/models.ts +8 -0
  9. package/templates/common/add/webapp/test/flpSandbox.html +12 -7
  10. package/templates/listdetail/add/webapp/Component.ts +68 -0
  11. package/templates/listdetail/add/webapp/controller/App.controller.ts +39 -0
  12. package/templates/listdetail/add/webapp/controller/BaseController.ts +82 -0
  13. package/templates/listdetail/add/webapp/controller/Detail.controller.ts +184 -0
  14. package/templates/listdetail/add/webapp/controller/DetailObjectNotFound.controller.ts +8 -0
  15. package/templates/listdetail/add/webapp/controller/ErrorHandler.ts +61 -0
  16. package/templates/listdetail/add/webapp/controller/List.controller.ts +331 -0
  17. package/templates/listdetail/add/webapp/controller/ListSelector.ts +98 -0
  18. package/templates/listdetail/add/webapp/controller/NotFound.controller.ts +16 -0
  19. package/templates/listdetail/add/webapp/model/formatter.ts +13 -0
  20. package/templates/worklist/add/webapp/Component.ts +64 -0
  21. package/templates/worklist/add/webapp/controller/App.controller.ts +12 -0
  22. package/templates/worklist/add/webapp/controller/BaseController.ts +96 -0
  23. package/templates/worklist/add/webapp/controller/ErrorHandler.ts +74 -0
  24. package/templates/worklist/add/webapp/controller/NotFound.controller.ts +15 -0
  25. package/templates/worklist/add/webapp/controller/Object.controller.ts +79 -0
  26. package/templates/worklist/add/webapp/controller/Worklist.controller.ts +115 -0
  27. package/templates/worklist/add/webapp/model/formatter.ts +13 -0
  28. package/templates/listdetail/add/webapp/controller/DetailObjectNotFound.js +0 -7
@@ -6,4 +6,5 @@ import type { FreestyleApp } from './types';
6
6
  * @param ffApp full config object used by the generate method
7
7
  */
8
8
  export declare function setDefaults(ffApp: FreestyleApp<unknown>): void;
9
+ export declare const escapeFLPText: (s: string) => string;
9
10
  //# sourceMappingURL=defaults.d.ts.map
package/dist/defaults.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.setDefaults = void 0;
6
+ exports.escapeFLPText = exports.setDefaults = void 0;
7
7
  const read_pkg_up_1 = __importDefault(require("read-pkg-up"));
8
8
  const types_1 = require("./types");
9
9
  /**
@@ -48,4 +48,7 @@ function setDefaults(ffApp) {
48
48
  ffApp.appOptions = Object.assign({ loadReuseLibs: true }, ffApp.appOptions);
49
49
  }
50
50
  exports.setDefaults = setDefaults;
51
+ // Specific escaping is required for FLP texts in flpSandbox.html template file
52
+ // Escapes '\' with '\\\\' and '"' with '\"' to correctly render inputs in a secure way
53
+ exports.escapeFLPText = (s) => s.replace(/\\/g, '\\\\').replace(/(")/g, '\\$&');
51
54
  //# sourceMappingURL=defaults.js.map
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ const defaults_1 = require("./defaults");
30
30
  * @returns Reference to a mem-fs-editor
31
31
  */
32
32
  function generate(basePath, data, fs) {
33
- var _a, _b, _c, _d, _e;
33
+ var _a, _b, _c, _d, _e, _f, _g;
34
34
  return __awaiter(this, void 0, void 0, function* () {
35
35
  // Clone rather than modifying callers refs
36
36
  const ffApp = cloneDeep_1.default(data);
@@ -40,14 +40,18 @@ function generate(basePath, data, fs) {
40
40
  // add new and overwrite files from templates e.g.
41
41
  const tmplPath = path_1.join(__dirname, '..', 'templates');
42
42
  // Common files
43
- fs.copyTpl(path_1.join(tmplPath, 'common', 'add', '**/*.*'), basePath, ffApp);
44
- fs.copyTpl(path_1.join(tmplPath, ffApp.template.type, 'add', `**/*.*`), basePath, ffApp, undefined, {});
43
+ const ignore = [((_a = ffApp.appOptions) === null || _a === void 0 ? void 0 : _a.typescript) ? '**/*.js' : '**/*.ts'];
44
+ fs.copyTpl(path_1.join(tmplPath, 'common', 'add'), basePath, Object.assign(Object.assign({}, ffApp), { escapeFLPText: defaults_1.escapeFLPText }), undefined, {
45
+ globOptions: { ignore }
46
+ });
47
+ fs.copyTpl(path_1.join(tmplPath, ffApp.template.type, 'add'), basePath, ffApp, undefined, { globOptions: { ignore } });
45
48
  if (ffApp.template.type === types_1.TemplateType.Basic) {
46
49
  const viewName = ffApp.template.settings.viewName;
47
50
  const viewTarget = path_1.join(basePath, 'webapp', 'view', `${viewName}.view.xml`);
48
51
  fs.copyTpl(path_1.join(tmplPath, ffApp.template.type, 'custom/View.xml'), viewTarget, ffApp);
49
- const controllerTarget = path_1.join(basePath, `webapp/controller/${viewName}.controller.js`);
50
- fs.copyTpl(path_1.join(tmplPath, ffApp.template.type, 'custom/Controller.js'), controllerTarget, ffApp);
52
+ const ext = ((_b = ffApp.appOptions) === null || _b === void 0 ? void 0 : _b.typescript) ? 'ts' : 'js';
53
+ const controllerTarget = path_1.join(basePath, `webapp/controller/${viewName}.controller.${ext}`);
54
+ fs.copyTpl(path_1.join(tmplPath, ffApp.template.type, `custom/Controller.${ext}`), controllerTarget, ffApp);
51
55
  }
52
56
  // Add template specific manifest settings
53
57
  const manifestPath = path_1.join(basePath, 'webapp', 'manifest.json');
@@ -60,12 +64,12 @@ function generate(basePath, data, fs) {
60
64
  fs.extendJSON(packagePath, JSON.parse(ejs_1.render(fs.read(path_1.join(tmplPath, 'common', 'extend', 'package.json')), ffApp, {})));
61
65
  const packageJson = JSON.parse(fs.read(packagePath));
62
66
  packageJson.scripts = Object.assign(packageJson.scripts, Object.assign({}, packageConfig_1.getPackageJsonTasks({
63
- localOnly: !((_a = ffApp.service) === null || _a === void 0 ? void 0 : _a.url),
64
- addMock: !!((_b = ffApp.service) === null || _b === void 0 ? void 0 : _b.metadata),
65
- sapClient: (_c = ffApp.service) === null || _c === void 0 ? void 0 : _c.client,
67
+ localOnly: !((_c = ffApp.service) === null || _c === void 0 ? void 0 : _c.url),
68
+ addMock: !!((_d = ffApp.service) === null || _d === void 0 ? void 0 : _d.metadata),
69
+ sapClient: (_e = ffApp.service) === null || _e === void 0 ? void 0 : _e.client,
66
70
  flpAppId: ffApp.app.flpAppId,
67
- startFile: (_d = data === null || data === void 0 ? void 0 : data.app) === null || _d === void 0 ? void 0 : _d.startFile,
68
- localStartFile: (_e = data === null || data === void 0 ? void 0 : data.app) === null || _e === void 0 ? void 0 : _e.localStartFile
71
+ startFile: (_f = data === null || data === void 0 ? void 0 : data.app) === null || _f === void 0 ? void 0 : _f.startFile,
72
+ localStartFile: (_g = data === null || data === void 0 ? void 0 : data.app) === null || _g === void 0 ? void 0 : _g.localStartFile
69
73
  })));
70
74
  fs.writeJSON(packagePath, packageJson);
71
75
  // Add service to the project if provided
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sap-ux/fiori-freestyle-writer",
3
3
  "description": "SAP Fiori freestyle application writer",
4
- "version": "0.12.9",
4
+ "version": "0.13.2",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "@sap-ux/odata-service-writer": "0.14.6",
24
- "@sap-ux/ui5-application-writer": "0.14.6",
24
+ "@sap-ux/ui5-application-writer": "0.15.1",
25
25
  "@sap-ux/ui5-config": "0.14.5",
26
26
  "ejs": "3.1.7",
27
27
  "i18next": "20.3.2",
@@ -0,0 +1,28 @@
1
+ import BaseComponent from "<%- app.baseComponent %>";
2
+ import { createDeviceModel } from "<%=app.id.replace(/\./g, '/')%>/model/models";
3
+
4
+ /**
5
+ * @namespace <%- app.id %>
6
+ */
7
+ export default class Component extends BaseComponent {
8
+
9
+ public static metadata = {
10
+ manifest: "json"
11
+ };
12
+
13
+ /**
14
+ * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
15
+ * @public
16
+ * @override
17
+ */
18
+ public init() : void {
19
+ // call the base component's init function
20
+ super.init();
21
+
22
+ // enable routing
23
+ this.getRouter().initialize();
24
+
25
+ // set the device model
26
+ this.setModel(createDeviceModel(), "device");
27
+ }
28
+ }
@@ -0,0 +1,11 @@
1
+ import Controller from "sap/ui/core/mvc/Controller";
2
+
3
+ /**
4
+ * @namespace <%- app.id %>.controller
5
+ */
6
+ export default class App extends Controller {
7
+
8
+ public onInit(): void {
9
+
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ import Controller from "sap/ui/core/mvc/Controller";
2
+
3
+ /**
4
+ * @namespace <%- app.id %>.controller
5
+ */
6
+ export default class <%- template.settings.viewName %> extends Controller {
7
+
8
+ public onInit(): void {
9
+
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ import JSONModel from "sap/ui/model/json/JSONModel";
2
+ import Device from "sap/ui/Device";
3
+
4
+ export function createDeviceModel () {
5
+ var model = new JSONModel(Device);
6
+ model.setDefaultBindingMode("OneWay");
7
+ return model;
8
+ }
@@ -44,8 +44,8 @@
44
44
  },
45
45
  applications: {
46
46
  "<%- app.flpAppId %>": {
47
- title: "<%- app.title %>",
48
- description: "<%- app.description %>",
47
+ title: "<%- escapeFLPText(app.title) %>",
48
+ description: "<%- escapeFLPText(app.description) %>",
49
49
  additionalInformation: "SAPUI5.Component=<%- app.id %>",
50
50
  applicationType: "URL",
51
51
  url: "../"
@@ -55,7 +55,7 @@
55
55
  </script>
56
56
 
57
57
  <script src="../test-resources/sap/ushell/bootstrap/sandbox.js" id="sap-ushell-bootstrap"></script>
58
- <!-- Bootstrap the UI5 core library -->
58
+ <!-- Bootstrap the UI5 core library. 'data-sap-ui-frameOptions="allow"'' is a NON-SECURE setting for test environments -->
59
59
  <script id="sap-ui-bootstrap"
60
60
  src="../resources/sap-ui-core.js"
61
61
  data-sap-ui-libs="<%- ui5.ui5Libs %>"
@@ -65,10 +65,15 @@
65
65
  data-sap-ui-compatVersion="edge"
66
66
  data-sap-ui-language="en"
67
67
  data-sap-ui-resourceroots='{"<%- app.id %>": "../"}'
68
- data-sap-ui-frameOptions="allow"> // NON-SECURE setting for testing environment
69
- </script><% if (appOptions.loadReuseLibs) { %>
70
- <script id="locate-reuse-libs" src="../utils/locate-reuse-libs.js" data-sap-ui-manifest-uri="../manifest.json">
71
- </script><% } %>
68
+ data-sap-ui-frameOptions="allow">
69
+ </script><% if (appOptions.loadReuseLibs) { %>
70
+ <script id="locate-reuse-libs" src="../utils/locate-reuse-libs.js" data-sap-ui-manifest-uri="../manifest.json">
71
+ </script><% } else { %>
72
+ <script>
73
+ sap.ui.getCore().attachInit(function () {
74
+ sap.ushell.Container.createRenderer().placeAt("content");
75
+ });
76
+ </script><% } %>
72
77
  </head>
73
78
 
74
79
  <!-- UI Content -->
@@ -0,0 +1,68 @@
1
+ import UIComponent from "sap/ui/core/UIComponent";
2
+ import { support } from "sap/ui/Device";
3
+ import ErrorHandler from "./controller/ErrorHandler";
4
+ import ListSelector from "./controller/ListSelector";
5
+ import { createDeviceModel } from "./model/models";
6
+
7
+ /**
8
+ * @namespace <%- app.id %>
9
+ */
10
+ export default class Component extends UIComponent {
11
+
12
+ public static metadata = {
13
+ manifest: "json"
14
+ };
15
+
16
+ public listSelector: ListSelector;
17
+ private errorHandler: ErrorHandler;
18
+ private contentDensityClass: string;
19
+
20
+ /**
21
+ * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
22
+ */
23
+ public init(): void {
24
+ // call the base component's init function
25
+ super.init();
26
+
27
+ this.listSelector = new ListSelector();
28
+ this.errorHandler = new ErrorHandler(this);
29
+
30
+ // enable routing
31
+ this.getRouter().initialize();
32
+
33
+ // set the device model
34
+ this.setModel(createDeviceModel(), "device");
35
+ }
36
+
37
+ /**
38
+ * The component is destroyed by UI5 automatically.
39
+ * In this method, the ListSelector and ErrorHandler are destroyed.
40
+ */
41
+ public destroy() {
42
+ this.listSelector.destroy();
43
+ this.errorHandler.destroy();
44
+ // call the base component's destroy function
45
+ super.destroy();
46
+ }
47
+
48
+ /**
49
+ * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
50
+ * design mode class should be set, which influences the size appearance of some controls.
51
+ * @return css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
52
+ */
53
+ public getContentDensityClass(): string {
54
+ if (this.contentDensityClass === undefined) {
55
+ // check whether FLP has already set the content density class; do nothing in this case
56
+ // eslint-disable-next-line sap-no-proprietary-browser-api
57
+ if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
58
+ this.contentDensityClass = "";
59
+ } else if (!support.touch) { // apply "compact" mode if touch is not supported
60
+ this.contentDensityClass = "sapUiSizeCompact";
61
+ } else {
62
+ // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
63
+ this.contentDensityClass = "sapUiSizeCozy";
64
+ }
65
+ }
66
+ return this.contentDensityClass;
67
+ }
68
+ }
@@ -0,0 +1,39 @@
1
+ import JSONModel from "sap/ui/model/json/JSONModel";
2
+ import ODataModel from "sap/ui/model/odata/v2/ODataModel";
3
+ import BaseController from "./BaseController";
4
+
5
+ /**
6
+ * @namespace <%- app.id %>
7
+ */
8
+ export default class App extends BaseController {
9
+
10
+ public onInit(): void {
11
+ const originalBusyDelay = this.getView()!.getBusyIndicatorDelay();
12
+
13
+ const viewModel = new JSONModel({
14
+ busy : true,
15
+ delay : 0,
16
+ layout : "OneColumn",
17
+ previousLayout : "",
18
+ actionButtonsInfo : {
19
+ midColumn : {
20
+ fullScreen : false
21
+ }
22
+ }
23
+ });
24
+ this.setModel(viewModel, "appView");
25
+
26
+ const fnSetAppNotBusy = function() {
27
+ viewModel.setProperty("/busy", false);
28
+ viewModel.setProperty("/delay", originalBusyDelay);
29
+ };
30
+
31
+ // since then() has no "reject"-path attach to the MetadataFailed-Event to disable the busy indicator in case of an error
32
+ const mainModel: ODataModel = this.getUIComponent().getModel() as ODataModel;
33
+ mainModel.metadataLoaded().then(fnSetAppNotBusy);
34
+ mainModel.attachMetadataFailed(fnSetAppNotBusy);
35
+
36
+ // apply content density mode to root view
37
+ this.getView()!.addStyleClass(this.getUIComponent().getContentDensityClass());
38
+ }
39
+ }
@@ -0,0 +1,82 @@
1
+ import ResourceBundle from "sap/base/i18n/ResourceBundle";
2
+ import Model from "sap/ui/model/Model";
3
+ import ResourceModel from "sap/ui/model/resource/ResourceModel";
4
+ import Controller from "sap/ui/core/mvc/Controller";
5
+ import View from "sap/ui/core/mvc/View";
6
+ import History from "sap/ui/core/routing/History";
7
+ import Router from "sap/ui/core/routing/Router";
8
+ import AppComponent from "../Component";
9
+ import { currencyValue } from "../model/formatter";
10
+
11
+ /**
12
+ * @namespace <%- app.id %>
13
+ */
14
+ export default class BaseController extends Controller {
15
+
16
+ public readonly formatter = {
17
+ currencyValue
18
+ };
19
+
20
+ /**
21
+ * Convenience method for accessing the owner component.
22
+ *
23
+ * @returns the owner component
24
+ */
25
+ protected getUIComponent(): AppComponent {
26
+ return super.getOwnerComponent() as AppComponent;
27
+ }
28
+
29
+ /**
30
+ * Convenience method for accessing the router in every controller of the application.
31
+ *
32
+ * @returns the router for this component
33
+ */
34
+ protected getRouter(): Router {
35
+ return this.getUIComponent().getRouter();
36
+ }
37
+
38
+ /**
39
+ * Convenience method for getting the view model by name in every controller of the application.
40
+ *
41
+ * @param name the model name
42
+ * @returns the model instance
43
+ */
44
+ protected getModel<T extends Model>(name?: string): T {
45
+ return this.getView()!.getModel(name) as T;
46
+ }
47
+
48
+ /**
49
+ * Convenience method for setting the view model in every controller of the application.
50
+ *
51
+ * @param model the model instance
52
+ * @param name the model name
53
+ * @returns the view instance
54
+ */
55
+ protected setModel(model: Model, name: string): View {
56
+ return this.getView()!.setModel(model, name);
57
+ }
58
+
59
+ /**
60
+ * Convenience method for getting the resource bundle.
61
+ *
62
+ * @returns the resourceBundle of the component
63
+ */
64
+ protected getResourceBundle(): ResourceBundle {
65
+ return (this.getUIComponent().getModel("i18n") as ResourceModel).getResourceBundle() as ResourceBundle;
66
+ }
67
+
68
+ /**
69
+ * Event handler for navigating back.
70
+ * It there is a history entry we go one step back in the browser history
71
+ * If not, it will replace the current entry of the browser history with the list route.
72
+ *
73
+ */
74
+ protected onNavBack() {
75
+ if (History.getInstance().getPreviousHash() !== undefined) {
76
+ // eslint-disable-next-line sap-no-history-manipulation
77
+ history.go(-1);
78
+ } else {
79
+ this.getRouter().navTo("list", {});
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,184 @@
1
+ import { URLHelper } from "sap/m/library";<%if (template.settings.lineItem.name) {%>
2
+ import Table from "sap/m/Table";<%}%>;
3
+ import Event from "sap/ui/base/Event";
4
+ import JSONModel from "sap/ui/model/json/JSONModel";
5
+ import ListBinding from "sap/ui/model/ListBinding";
6
+ import ODataModel from "sap/ui/model/odata/v2/ODataModel";
7
+ import BaseController from "./BaseController";
8
+
9
+ /**
10
+ * @namespace <%- app.id %>
11
+ */
12
+ export default class Detail extends BaseController {
13
+
14
+ public onInit(): void {
15
+ // Model used to manipulate control states. The chosen values make sure,
16
+ // detail page is busy indication immediately so there is no break in
17
+ // between the busy indication for loading the view's meta data
18
+ const viewModel = new JSONModel({
19
+ busy: false,
20
+ delay: 0<%if (template.settings.lineItem.name) {%>,
21
+ lineItemListTitle: this.getResourceBundle().getText("detailLineItemTableHeading")<%}%>
22
+ });
23
+
24
+ this.getRouter().getRoute("object")!.attachPatternMatched(this.onObjectMatched, this);
25
+
26
+ this.setModel(viewModel, "detailView");
27
+
28
+ (this.getUIComponent().getModel() as ODataModel).metadataLoaded().then(this.onMetadataLoaded.bind(this));
29
+ }
30
+
31
+ /**
32
+ * Event handler when the share by E-Mail button has been clicked
33
+ */
34
+ public onSendEmailPress() {
35
+ const viewModel = this.getModel("detailView");
36
+
37
+ URLHelper.triggerEmail(
38
+ undefined,
39
+ viewModel.getProperty("/shareSendEmailSubject"),
40
+ viewModel.getProperty("/shareSendEmailMessage")
41
+ );
42
+ }
43
+
44
+ <%if (template.settings.lineItem.name) {%>
45
+ /**
46
+ * Updates the item count within the line item table's header
47
+ * @param event an event containing the total number of items in the list
48
+ */
49
+ public onListUpdateFinished(event: Event) {
50
+ const viewModel = this.getModel<JSONModel>("detailView");
51
+ const totalItems = event.getParameter("total") as number;
52
+ let title: string;
53
+ // only update the counter if the length is final
54
+ if ((this.byId("lineItemsList")!.getBinding("items") as ListBinding).isLengthFinal()) {
55
+ if (totalItems) {
56
+ title = this.getResourceBundle().getText("detailLineItemTableHeadingCount", [totalItems]);
57
+ } else {
58
+ //Display 'Line Items' instead of 'Line items (0)'
59
+ title = this.getResourceBundle().getText("detailLineItemTableHeading");
60
+ }
61
+ viewModel.setProperty("/lineItemListTitle", title);
62
+ }
63
+ }<%}%>
64
+
65
+ /**
66
+ * Binds the view to the object path and expands the aggregated line items.
67
+ * @function
68
+ * @param event pattern match event in route 'object'
69
+ * @private
70
+ */
71
+ private onObjectMatched(event: Event) {
72
+ const objectId = event.getParameter("arguments").objectId;
73
+ this.getModel<JSONModel>("appView").setProperty("/layout", "TwoColumnsMidExpanded");
74
+ this.getModel<ODataModel>().metadataLoaded().then(function (this: Detail) {
75
+ const objectPath = this.getModel<ODataModel>().createKey("Suppliers", {
76
+ SupplierID: objectId
77
+ });
78
+ this.bindView("/" + objectPath);
79
+ }.bind(this));
80
+ }
81
+
82
+ /**
83
+ * Binds the view to the object path. Makes sure that detail view displays
84
+ * a busy indicator while data for the corresponding element binding is loaded.
85
+ * @function
86
+ * @param objectPath path to the object to be bound to the view.
87
+ */
88
+ private bindView(objectPath: string) {
89
+ // Set busy indicator during view binding
90
+ const viewModel = this.getModel<JSONModel>("detailView");
91
+
92
+ // If the view was not bound yet its not busy, only if the binding requests data it is set to busy again
93
+ viewModel.setProperty("/busy", false);
94
+
95
+ this.getView()!.bindElement({
96
+ path: objectPath,
97
+ events: {
98
+ change: this.onBindingChange.bind(this),
99
+ dataRequested: function () {
100
+ viewModel.setProperty("/busy", true);
101
+ },
102
+ dataReceived: function () {
103
+ viewModel.setProperty("/busy", false);
104
+ }
105
+ }
106
+ });
107
+ }
108
+
109
+ private onBindingChange() {
110
+ const view = this.getView()!;
111
+ const elementBinding = view.getElementBinding();
112
+
113
+ // No data for the binding
114
+ if (!elementBinding.getBoundContext()) {
115
+ this.getRouter().getTargets()!.display("detailObjectNotFound");
116
+ // if object could not be found, the selection in the list
117
+ // does not make sense anymore.
118
+ this.getUIComponent().listSelector.clearListSelection();
119
+ return;
120
+ }
121
+
122
+ const path = elementBinding.getPath();
123
+ const resourceBundle = this.getResourceBundle();
124
+ const detailObject = view.getModel().getObject(path);
125
+ const viewModel = this.getModel<JSONModel>("detailView");
126
+
127
+ this.getUIComponent().listSelector.selectAListItem(path);
128
+
129
+ viewModel.setProperty("/shareSendEmailSubject",
130
+ resourceBundle.getText("shareSendEmailObjectSubject", [detailObject.<%=template.settings.entity.key%>]));
131
+ viewModel.setProperty("/shareSendEmailMessage",
132
+ resourceBundle.getText("shareSendEmailObjectMessage", [detailObject.<%=template.settings.entity.idProperty%>, detailObject.<%=template.settings.entity.key%>, location.href]));
133
+ }
134
+
135
+ protected onMetadataLoaded() {
136
+ // Store original busy indicator delay for the detail view
137
+ const originalViewBusyDelay = this.getView()!.getBusyIndicatorDelay();
138
+ const viewModel = this.getModel<JSONModel>("detailView");<%if (template.settings.lineItem.name) {%>
139
+ const lineItemTable = this.byId("lineItemsList") as Table;
140
+ const originalLineItemTableBusyDelay = lineItemTable.getBusyIndicatorDelay();<%}%>;
141
+
142
+ // Make sure busy indicator is displayed immediately when
143
+ // detail view is displayed for the first time
144
+ viewModel.setProperty("/delay", 0);<%if (template.settings.lineItem.name) {%>
145
+ viewModel.setProperty("/lineItemTableDelay", 0);
146
+
147
+ lineItemTable.attachEventOnce("updateFinished", function () {
148
+ // Restore original busy indicator delay for line item table
149
+ viewModel.setProperty("/lineItemTableDelay", originalLineItemTableBusyDelay);
150
+ });<%}%>
151
+
152
+ // Binding the view will set it to not busy - so the view is always busy if it is not bound
153
+ viewModel.setProperty("/busy", true);
154
+ // Restore original busy indicator delay for the detail view
155
+ viewModel.setProperty("/delay", originalViewBusyDelay);
156
+ }
157
+
158
+ /**
159
+ * Set the full screen mode to false and navigate to list page
160
+ */
161
+ protected onCloseDetailPress() {
162
+ this.getModel<JSONModel>("appView").setProperty("/actionButtonsInfo/midColumn/fullScreen", false);
163
+ // No item should be selected on list after detail page is closed
164
+ this.getUIComponent().listSelector.clearListSelection();
165
+ this.getRouter().navTo("list");
166
+ }
167
+
168
+ /**
169
+ * Toggle between full and non full screen mode.
170
+ */
171
+ protected toggleFullScreen() {
172
+ const viewModel = this.getModel<JSONModel>("appView");
173
+ const fullScreen = viewModel.getProperty("/actionButtonsInfo/midColumn/fullScreen");
174
+ viewModel.setProperty("/actionButtonsInfo/midColumn/fullScreen", !fullScreen);
175
+ if (!fullScreen) {
176
+ // store current layout and go full screen
177
+ viewModel.setProperty("/previousLayout", viewModel.getProperty("/layout"));
178
+ viewModel.setProperty("/layout", "MidColumnFullScreen");
179
+ } else {
180
+ // reset to previous layout
181
+ viewModel.setProperty("/layout", viewModel.getProperty("/previousLayout"));
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,8 @@
1
+ import BaseController from "./BaseController";
2
+
3
+ /**
4
+ * @namespace <%- app.id %>
5
+ */
6
+ export default class DetailObjectNotFound extends BaseController {
7
+
8
+ }
@@ -0,0 +1,61 @@
1
+ import MessageBox, { Action } from "sap/m/MessageBox";
2
+ import UI5Object from "sap/ui/base/Object";
3
+ import Event from "sap/ui/base/Event";
4
+ import ODataModel from "sap/ui/model/odata/v2/ODataModel";
5
+ import AppComponent from "../Component";
6
+ import ResourceModel from "sap/ui/model/resource/ResourceModel";
7
+ import ResourceBundle from "sap/base/i18n/ResourceBundle";
8
+
9
+ /**
10
+ * @namespace <%- app.id %>
11
+ */
12
+ export default class ErrorHandler extends UI5Object {
13
+
14
+ protected readonly component: AppComponent;
15
+ protected messageOpen = false;
16
+
17
+ /**
18
+ * Handles application errors by automatically attaching to the model events and displaying errors when needed.
19
+ * @param component reference to the app's component
20
+ */
21
+ public constructor(component: AppComponent) {
22
+ super();
23
+ this.component = component;
24
+ const model = component.getModel() as ODataModel;
25
+ model.attachMetadataFailed(this.showServiceError);
26
+ model.attachRequestFailed(function (this: ErrorHandler, event: Event) {
27
+ const params = event.getParameters() as { response: XMLHttpRequest['response'] };
28
+ // An entity that was not found in the service is also throwing a 404 error in oData.
29
+ // We already cover this case with a notFound target so we skip it here.
30
+ // A request that cannot be sent to the server is a technical error that we have to handle though
31
+ if (params.response.statusCode !== "404" || (params.response.statusCode === 404 && params.response.responseText.indexOf("Cannot POST") === 0)) {
32
+ this.showServiceError(event);
33
+ }
34
+ }, this);
35
+ }
36
+
37
+ /**
38
+ * Shows a {@link sap.m.MessageBox} when a service call has failed.
39
+ * Only the first error message will be display.
40
+ * @param {string} sDetails a technical error to be displayed on request
41
+ */
42
+ private showServiceError(event: Event) {
43
+ if (this.messageOpen) {
44
+ return;
45
+ }
46
+ this.messageOpen = true;
47
+ MessageBox.error(
48
+ ((this.component.getModel("i18n") as ResourceModel).getResourceBundle() as ResourceBundle).getText("errorText"),
49
+ {
50
+ id: "serviceErrorMessageBox",
51
+ details: (event.getParameters() as XMLHttpRequest['response']).response,
52
+ styleClass: this.component.getContentDensityClass(),
53
+ actions: [Action.CLOSE],
54
+ onClose: function (this: ErrorHandler) {
55
+ this.messageOpen = false;
56
+ }.bind(this)
57
+ }
58
+ );
59
+ }
60
+
61
+ }