create-ui5-freestyle-lr 0.2.0

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 (36) hide show
  1. package/README.md +141 -0
  2. package/package.json +34 -0
  3. package/src/cli.js +74 -0
  4. package/src/metadata/fetch.js +42 -0
  5. package/src/metadata/parse.js +104 -0
  6. package/src/prompts/basic.js +43 -0
  7. package/src/prompts/fields.js +144 -0
  8. package/src/prompts/odata.js +57 -0
  9. package/src/render/buildContext.js +67 -0
  10. package/src/render/controlForType.js +136 -0
  11. package/src/render/engine.js +81 -0
  12. package/src/templates/README.md.ejs +62 -0
  13. package/src/templates/_valueHelp.fragment.xml.ejs +17 -0
  14. package/src/templates/package.json.ejs +14 -0
  15. package/src/templates/ui5.yaml.ejs +28 -0
  16. package/src/templates/webapp/Component.js.ejs +47 -0
  17. package/src/templates/webapp/controller/App.controller.js +29 -0
  18. package/src/templates/webapp/controller/BaseController.js +35 -0
  19. package/src/templates/webapp/controller/ErrorHandler.js +71 -0
  20. package/src/templates/webapp/controller/NotFound.controller.js +12 -0
  21. package/src/templates/webapp/controller/Worklist.controller.js.ejs +1158 -0
  22. package/src/templates/webapp/css/style.css +17 -0
  23. package/src/templates/webapp/i18n/i18n.properties.ejs +83 -0
  24. package/src/templates/webapp/i18n/i18n_de.properties.ejs +83 -0
  25. package/src/templates/webapp/index.html.ejs +52 -0
  26. package/src/templates/webapp/localService/backendCheck.js.ejs +52 -0
  27. package/src/templates/webapp/localService/mockserver.js.ejs +29 -0
  28. package/src/templates/webapp/manifest.json.ejs +106 -0
  29. package/src/templates/webapp/model/formatter.js +148 -0
  30. package/src/templates/webapp/model/models.js +23 -0
  31. package/src/templates/webapp/view/App.view.xml +10 -0
  32. package/src/templates/webapp/view/NotFound.view.xml +12 -0
  33. package/src/templates/webapp/view/Worklist.view.xml.ejs +173 -0
  34. package/src/templates/webapp/view/fragments/GroupDialog.fragment.xml.ejs +11 -0
  35. package/src/templates/webapp/view/fragments/InfoPopover.fragment.xml +38 -0
  36. package/src/templates/webapp/view/fragments/SortDialog.fragment.xml.ejs +11 -0
@@ -0,0 +1,67 @@
1
+ import {
2
+ cellControlFor,
3
+ columnHAlignFor,
4
+ columnImportanceFor,
5
+ columnWidthFor,
6
+ exportTypeFor,
7
+ filterControlFor,
8
+ filterOperatorFor,
9
+ isStringType,
10
+ } from "./controlForType.js";
11
+
12
+ function slugify(s) {
13
+ return s.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
14
+ }
15
+
16
+ function sortDelegateFor(field) {
17
+ if (!field) return null;
18
+ return field;
19
+ }
20
+
21
+ function detectHighlightField(columns) {
22
+ // Prefer plain "Status" / "TechStatus" over *Txt fields
23
+ for (const col of columns) {
24
+ if (/^(tech)?status$/i.test(col.name)) return col.name;
25
+ }
26
+ for (const col of columns) {
27
+ if (/status/i.test(col.name) && !/txt$/i.test(col.name)) return col.name;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ export function buildContext(answers) {
33
+ const namespacePath = answers.appId.replace(/\./g, "/");
34
+ const appName = answers.appId.split(".").pop();
35
+ const primaryKey = answers.keyFields[0] || null;
36
+
37
+ const i18nLabels = new Map();
38
+ for (const col of answers.columns) {
39
+ i18nLabels.set(`col.${col.name}`, col.label || col.name);
40
+ }
41
+ for (const f of answers.filters) {
42
+ i18nLabels.set(`filter.${f.fieldName}`, f.label || f.fieldName);
43
+ }
44
+
45
+ const highlightField = detectHighlightField(answers.columns);
46
+
47
+ return {
48
+ ...answers,
49
+ namespacePath,
50
+ appName,
51
+ primaryKey,
52
+ highlightField,
53
+ i18nLabels,
54
+ helpers: {
55
+ cellControlFor,
56
+ columnHAlignFor,
57
+ columnImportanceFor,
58
+ columnWidthFor,
59
+ exportTypeFor,
60
+ filterControlFor,
61
+ filterOperatorFor,
62
+ isStringType,
63
+ slugify,
64
+ sortDelegateFor,
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,136 @@
1
+ const DATE_TYPES = new Set(["Edm.DateTime", "Edm.DateTimeOffset"]);
2
+ const NUMBER_TYPES = new Set([
3
+ "Edm.Decimal",
4
+ "Edm.Double",
5
+ "Edm.Single",
6
+ "Edm.Int16",
7
+ "Edm.Int32",
8
+ "Edm.Int64",
9
+ "Edm.Byte",
10
+ "Edm.SByte",
11
+ ]);
12
+ const STATUS_RE = /Status$/i;
13
+ const GUID_RE = /GUID/i;
14
+ const ID_SUFFIX_RE = /Id$/;
15
+ const KEY_UUID_RE = /(Uuid|Key$)/i;
16
+ const MODEL = "worklistView";
17
+
18
+ function p(field) {
19
+ return "{" + MODEL + ">" + field + "}";
20
+ }
21
+
22
+ function pf(field, formatter) {
23
+ return `{path:'${MODEL}>${field}', formatter:'.formatter.${formatter}'}`;
24
+ }
25
+
26
+ export function cellControlFor(prop, { isFirstColumn }) {
27
+ const name = prop.name;
28
+
29
+ // First key column → ObjectIdentifier (most prominent)
30
+ if (isFirstColumn && prop.isKey) {
31
+ return `<m:ObjectIdentifier title="${p(name)}"/>`;
32
+ }
33
+
34
+ // Date / DateTime → ObjectIdentifier with date as title and time as text
35
+ if (DATE_TYPES.has(prop.type)) {
36
+ return `<m:ObjectIdentifier title="${pf(name, "formatDateOnly")}" text="${pf(name, "formatTimeOnly")}"/>`;
37
+ }
38
+
39
+ // Edm.Time → just time
40
+ if (prop.type === "Edm.Time") {
41
+ return `<m:Text text="${pf(name, "formatTimeOnly")}"/>`;
42
+ }
43
+
44
+ // Boolean → accept/decline icon
45
+ if (prop.type === "Edm.Boolean") {
46
+ return `<core:Icon src="{= \${${MODEL}>${name}} ? 'sap-icon://accept' : 'sap-icon://decline'}"/>`;
47
+ }
48
+
49
+ // Numeric → right-aligned with 2-decimal formatter
50
+ if (NUMBER_TYPES.has(prop.type)) {
51
+ return `<m:Text text="${pf(name, "numberUnit")}"/>`;
52
+ }
53
+
54
+ // Status-looking field → GenericTag with state mapped from value
55
+ if (STATUS_RE.test(name)) {
56
+ return `<m:GenericTag text="${p(name)}" status="${pf(name, "formatStatusState")}"/>`;
57
+ }
58
+
59
+ // ID-like fields → FormattedText with different styles per pattern
60
+ // for visual variety between primary / secondary / reference IDs
61
+ if (GUID_RE.test(name)) {
62
+ // Primary identifiers: bold monospace
63
+ return `<m:FormattedText htmlText="${pf(name, "formatStrongCode")}"/>`;
64
+ }
65
+ if (ID_SUFFIX_RE.test(name)) {
66
+ // Secondary identifiers: plain monospace
67
+ return `<m:FormattedText htmlText="${pf(name, "formatCode")}"/>`;
68
+ }
69
+ if (KEY_UUID_RE.test(name)) {
70
+ // References / UUIDs: italic
71
+ return `<m:FormattedText htmlText="${pf(name, "formatCite")}"/>`;
72
+ }
73
+
74
+ // Default → plain Text
75
+ return `<m:Text text="${p(name)}"/>`;
76
+ }
77
+
78
+ export function columnHAlignFor(prop) {
79
+ if (NUMBER_TYPES.has(prop.type)) return "End";
80
+ if (prop.type === "Edm.Boolean") return "Center";
81
+ if (DATE_TYPES.has(prop.type)) return "Center";
82
+ return "Begin";
83
+ }
84
+
85
+ export function columnImportanceFor(prop, idx, total) {
86
+ if (idx === 0) return "High";
87
+ if (idx >= total - 3) return "Low";
88
+ return "Medium";
89
+ }
90
+
91
+ /**
92
+ * Default per-column width based on the Edm type and field name. These keep
93
+ * the table readable without forcing the user to set widths manually. The
94
+ * ColumnResizer plugin (added in the view) lets the user drag-resize at runtime.
95
+ */
96
+ export function columnWidthFor(prop, isFirstColumn) {
97
+ if (isFirstColumn && prop.isKey) return "12rem";
98
+ if (DATE_TYPES.has(prop.type)) return "10rem";
99
+ if (prop.type === "Edm.Time") return "6rem";
100
+ if (prop.type === "Edm.Boolean") return "4rem";
101
+ if (NUMBER_TYPES.has(prop.type)) return "7rem";
102
+ if (STATUS_RE.test(prop.name)) return "8rem";
103
+ if (GUID_RE.test(prop.name)) return "10rem";
104
+ if (ID_SUFFIX_RE.test(prop.name) || KEY_UUID_RE.test(prop.name)) return "10rem";
105
+ return "10rem";
106
+ }
107
+
108
+ export function filterControlFor(filter) {
109
+ const id = filter.fieldName;
110
+ if (filter.inputType === "dateRange") {
111
+ return `<m:DateRangeSelection id="${id}" displayFormat="dd.MM.yyyy"/>`;
112
+ }
113
+ if (filter.inputType === "valueHelp") {
114
+ return `<m:MultiInput id="${id}" width="100%" showValueHelp="true" valueHelpIconSrc="sap-icon://multiselect-all" valueHelpOnly="true" valueHelpRequest=".on${id}VH"/>`;
115
+ }
116
+ return `<m:MultiInput id="${id}" width="100%"/>`;
117
+ }
118
+
119
+ export function filterOperatorFor(filter) {
120
+ if (filter.inputType === "dateRange") return "BT";
121
+ if (filter.inputType === "valueHelp") return "EQ";
122
+ return "Contains";
123
+ }
124
+
125
+ export function isStringType(type) {
126
+ return type === "Edm.String" || type === "Edm.Guid";
127
+ }
128
+
129
+ export function exportTypeFor(prop) {
130
+ if (DATE_TYPES.has(prop.type)) return "DateTime";
131
+ if (prop.type === "Edm.Date") return "Date";
132
+ if (prop.type === "Edm.Time") return "Time";
133
+ if (prop.type === "Edm.Boolean") return "Boolean";
134
+ if (NUMBER_TYPES.has(prop.type)) return "Number";
135
+ return "String";
136
+ }
@@ -0,0 +1,81 @@
1
+ import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import ejs from "ejs";
5
+ import { buildContext } from "./buildContext.js";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const TEMPLATES_DIR = path.resolve(__dirname, "../templates");
10
+ const PER_FILTER_VH_TEMPLATE = path.resolve(TEMPLATES_DIR, "_valueHelp.fragment.xml.ejs");
11
+
12
+ async function* walk(dir) {
13
+ const entries = await readdir(dir, { withFileTypes: true });
14
+ for (const entry of entries) {
15
+ const full = path.join(dir, entry.name);
16
+ if (entry.isDirectory()) {
17
+ yield* walk(full);
18
+ } else if (entry.isFile()) {
19
+ yield full;
20
+ }
21
+ }
22
+ }
23
+
24
+ function shouldSkip(relPath, ctx) {
25
+ const normalized = relPath.replace(/\\/g, "/");
26
+ const basename = path.basename(normalized);
27
+ if (basename.startsWith("_")) return true;
28
+ if (!ctx.hasGerman && normalized.includes("i18n_de.")) return true;
29
+ return false;
30
+ }
31
+
32
+ function outputPathFor(relPath, targetDir) {
33
+ const stripped = relPath.endsWith(".ejs") ? relPath.slice(0, -4) : relPath;
34
+ return path.join(targetDir, stripped);
35
+ }
36
+
37
+ async function renderEjsTo(tmplPath, outPath, ctx) {
38
+ const tmpl = await readFile(tmplPath, "utf8");
39
+ const rendered = ejs.render(tmpl, ctx, { filename: tmplPath });
40
+ await mkdir(path.dirname(outPath), { recursive: true });
41
+ await writeFile(outPath, rendered, "utf8");
42
+ }
43
+
44
+ async function renderStaticTo(tmplPath, outPath, ctx) {
45
+ const raw = await readFile(tmplPath, "utf8");
46
+ const replaced = raw
47
+ .replace(/__APP_ID__/g, ctx.appId)
48
+ .replace(/__NAMESPACE_PATH__/g, ctx.namespacePath)
49
+ .replace(/__APP_NAME__/g, ctx.appName);
50
+ await mkdir(path.dirname(outPath), { recursive: true });
51
+ await writeFile(outPath, replaced, "utf8");
52
+ }
53
+
54
+ export async function render(answers, targetDir) {
55
+ const ctx = buildContext(answers);
56
+
57
+ for await (const tmplPath of walk(TEMPLATES_DIR)) {
58
+ const relPath = path.relative(TEMPLATES_DIR, tmplPath);
59
+ if (shouldSkip(relPath, ctx)) continue;
60
+
61
+ const outPath = outputPathFor(relPath, targetDir);
62
+ if (tmplPath.endsWith(".ejs")) {
63
+ await renderEjsTo(tmplPath, outPath, ctx);
64
+ } else {
65
+ await renderStaticTo(tmplPath, outPath, ctx);
66
+ }
67
+ }
68
+
69
+ for (const filter of ctx.filters.filter((f) => f.valueHelp)) {
70
+ const subCtx = { ...ctx, filter };
71
+ const outName = `${filter.fieldName}VH.fragment.xml`;
72
+ const outPath = path.join(targetDir, "webapp", "view", "fragments", outName);
73
+ await renderEjsTo(PER_FILTER_VH_TEMPLATE, outPath, subCtx);
74
+ }
75
+
76
+ if (answers.rawMetadataXml) {
77
+ const metaPath = path.join(targetDir, "webapp", "localService", "metadata.xml");
78
+ await mkdir(path.dirname(metaPath), { recursive: true });
79
+ await writeFile(metaPath, answers.rawMetadataXml, "utf8");
80
+ }
81
+ }
@@ -0,0 +1,62 @@
1
+ # <%= appTitle %>
2
+
3
+ <% if (appDescription) { %><%= appDescription %><% } else { %>Freestyle SAPUI5 List Report app.<% } %>
4
+
5
+ Generated with [create-ui5-freestyle-lr](https://www.npmjs.com/package/create-ui5-freestyle-lr).
6
+
7
+ ## Develop
8
+
9
+ npm install
10
+ npm start
11
+
12
+ The app opens in your browser. `ui5 serve` proxies OData requests via `<%= serviceUrl %>`.
13
+ If the backend requires auth, the browser will prompt (basic auth).
14
+
15
+ ## Build
16
+
17
+ npm run build # dev build into dist/
18
+ npm run build:prod # prod build with cachebuster
19
+
20
+ ## Layout
21
+
22
+ webapp/
23
+ ├── Component.js, manifest.json, index.html
24
+ ├── view/
25
+ │ ├── App.view.xml
26
+ │ ├── Worklist.view.xml Main list view with FilterBar + Table + Footer pagination
27
+ │ ├── NotFound.view.xml
28
+ │ └── fragments/ Value-help dialogs, sort/group dialogs, info popover
29
+ ├── controller/
30
+ │ ├── BaseController.js Shared helpers (getRouter, getModel, ...)
31
+ │ ├── Worklist.controller.js Main list controller (pagination, filters, sorting, grouping)
32
+ │ ├── ErrorHandler.js Central OData error handling
33
+ │ └── App.controller.js, NotFound.controller.js
34
+ ├── model/
35
+ │ ├── formatter.js Value formatters used in bindings
36
+ │ └── models.js Device + FLP model factories
37
+ ├── i18n/ Translations (<% if (hasGerman) { %>en + de<% } else { %>en<% } %>)
38
+ ├── css/style.css
39
+ └── localService/metadata.xml Snapshot of the OData $metadata
40
+
41
+ ## Adding a filter field manually
42
+
43
+ 1. Open `webapp/view/Worklist.view.xml` → find the `<fb:FilterBar>` block.
44
+ 2. Copy an existing `<fb:FilterGroupItem>`, change `name` / `label` / control `id`.
45
+ 3. (If it needs a value help) Copy an existing `*VH.fragment.xml` in `webapp/view/fragments/`
46
+ and rename. Add its handlers (`on<Field>VH`, `on<Field>VHOkPress`, `on<Field>VHCancelPress`,
47
+ `on<Field>VHAfterClose`) to `Worklist.controller.js` by copying an existing block.
48
+ 4. Add the i18n key `filter.<Field>=<label>` in `i18n/i18n.properties`
49
+ (and `i18n_de.properties` if German is enabled).
50
+ 5. In `Worklist.controller.js` → `onSearch`, add a block that reads tokens from the new
51
+ MultiInput and builds a `Filter` — copy an existing block.
52
+
53
+ ## Adding a column
54
+
55
+ 1. In `Worklist.view.xml` → `<m:columns>` add a new `<m:Column>` with `<m:Text text="{i18n>col.<Field>}"/>`.
56
+ 2. In `<m:items>` → `<m:ColumnListItem>` → `<m:cells>` add a cell control bound to the new field.
57
+ 3. Add `col.<Field>=<label>` in the i18n files.
58
+
59
+ ## OData metadata snapshot
60
+
61
+ `webapp/localService/metadata.xml` is a snapshot taken at scaffold time. If the service schema changes,
62
+ replace this file (or refetch via `ui5 serve`'s proxy).
@@ -0,0 +1,17 @@
1
+ <core:FragmentDefinition
2
+ xmlns="sap.m"
3
+ xmlns:core="sap.ui.core">
4
+ <SelectDialog
5
+ id="<%= filter.fieldName %>VHDialog"
6
+ title="{i18n>filter.<%= filter.fieldName %>}"
7
+ multiSelect="true"
8
+ rememberSelections="true"
9
+ confirm=".on<%= filter.fieldName %>VHOkPress"
10
+ cancel=".on<%= filter.fieldName %>VHCancelPress"
11
+ search=".on<%= filter.fieldName %>VHSearch"
12
+ items="{worklistView>/<%= filter.valueHelp.entitySet %>}">
13
+ <StandardListItem
14
+ title="{worklistView><%= filter.valueHelp.textField %>}"
15
+ description="{worklistView><%= filter.valueHelp.keyField %>}"/>
16
+ </SelectDialog>
17
+ </core:FragmentDefinition>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "<%= appName %>",
3
+ "version": "1.0.0",
4
+ "description": "<%= appDescription || appTitle %>",
5
+ "private": true,
6
+ "scripts": {
7
+ "start": "ui5 serve --open index.html",
8
+ "build": "ui5 build --clean-dest --include-task=generateManifestBundle",
9
+ "build:prod": "ui5 build --clean-dest --include-task=generateManifestBundle --include-task=generateCachebusterInfo"
10
+ },
11
+ "devDependencies": {
12
+ "@ui5/cli": "^4.0.0"
13
+ }
14
+ }
@@ -0,0 +1,28 @@
1
+ specVersion: "4.0"
2
+ metadata:
3
+ name: <%= appName %>
4
+ type: application
5
+ framework:
6
+ name: SAPUI5
7
+ version: "<%= minUI5Version %>"
8
+ libraries:
9
+ - name: sap.ui.core
10
+ - name: sap.m
11
+ - name: sap.f
12
+ - name: sap.ui.comp
13
+ - name: sap.ui.layout
14
+ - name: sap.uxap
15
+ - name: sap.ui.export
16
+ - name: sap.ui.fl
17
+ - name: themelib_sap_horizon
18
+ #
19
+ # To call a real backend during development, uncomment the proxy block below
20
+ # after running: npm install ui5-middleware-simpleproxy --save-dev
21
+ #
22
+ # server:
23
+ # customMiddleware:
24
+ # - name: ui5-middleware-simpleproxy
25
+ # mountPath: <%= serviceUrl.replace(/\/+$/, '') %>
26
+ # afterMiddleware: compression
27
+ # configuration:
28
+ # baseUri: https://your-backend-host:port<%= serviceUrl %>
@@ -0,0 +1,47 @@
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("<%= appId %>.Component", {
10
+
11
+ metadata: {
12
+ manifest: "json"
13
+ },
14
+
15
+ init: function () {
16
+ UIComponent.prototype.init.apply(this, arguments);
17
+
18
+ this._oErrorHandler = new ErrorHandler(this);
19
+
20
+ this.setModel(models.createDeviceModel(), "device");
21
+ this.setModel(models.createFLPModel(), "FLP");
22
+
23
+ this.getRouter().initialize();
24
+ },
25
+
26
+ destroy: function () {
27
+ if (this._oErrorHandler && this._oErrorHandler.destroy) {
28
+ this._oErrorHandler.destroy();
29
+ }
30
+ UIComponent.prototype.destroy.apply(this, arguments);
31
+ },
32
+
33
+ getContentDensityClass: function () {
34
+ if (this._sContentDensityClass === undefined) {
35
+ if (document.body.classList.contains("sapUiSizeCozy") ||
36
+ document.body.classList.contains("sapUiSizeCompact")) {
37
+ this._sContentDensityClass = "";
38
+ } else if (!Device.support.touch) {
39
+ this._sContentDensityClass = "sapUiSizeCompact";
40
+ } else {
41
+ this._sContentDensityClass = "sapUiSizeCozy";
42
+ }
43
+ }
44
+ return this._sContentDensityClass;
45
+ }
46
+ });
47
+ });
@@ -0,0 +1,29 @@
1
+ sap.ui.define([
2
+ "./BaseController",
3
+ "sap/ui/model/json/JSONModel"
4
+ ], function (BaseController, JSONModel) {
5
+ "use strict";
6
+
7
+ return BaseController.extend("__APP_ID__.controller.App", {
8
+
9
+ onInit: function () {
10
+ const oViewModel = new JSONModel({
11
+ busy: true,
12
+ delay: 0
13
+ });
14
+ this.setModel(oViewModel, "appView");
15
+
16
+ const iOriginalBusyDelay = this.getView().getBusyIndicatorDelay();
17
+ const fnNotBusy = function () {
18
+ oViewModel.setProperty("/busy", false);
19
+ oViewModel.setProperty("/delay", iOriginalBusyDelay);
20
+ };
21
+
22
+ const oModel = this.getOwnerComponent().getModel();
23
+ oModel.metadataLoaded().then(fnNotBusy);
24
+ oModel.attachMetadataFailed(fnNotBusy);
25
+
26
+ this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
27
+ }
28
+ });
29
+ });
@@ -0,0 +1,35 @@
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("__APP_ID__.controller.BaseController", {
8
+
9
+ getRouter: function () {
10
+ return UIComponent.getRouterFor(this);
11
+ },
12
+
13
+ getModel: function (sName) {
14
+ return this.getView().getModel(sName);
15
+ },
16
+
17
+ setModel: function (oModel, sName) {
18
+ return this.getView().setModel(oModel, sName);
19
+ },
20
+
21
+ getResourceBundle: function () {
22
+ return this.getOwnerComponent().getModel("i18n").getResourceBundle();
23
+ },
24
+
25
+ onNavBack: function () {
26
+ const oHistory = sap.ui.core.routing.History.getInstance();
27
+ const sPreviousHash = oHistory.getPreviousHash();
28
+ if (sPreviousHash !== undefined) {
29
+ window.history.go(-1);
30
+ } else {
31
+ this.getRouter().navTo("worklist", {}, true);
32
+ }
33
+ }
34
+ });
35
+ });
@@ -0,0 +1,71 @@
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("__APP_ID__.controller.ErrorHandler", {
8
+
9
+ constructor: function (oComponent) {
10
+ this._oResourceBundle = oComponent.getModel("i18n").getResourceBundle();
11
+ this._oComponent = oComponent;
12
+ this._oModel = oComponent.getModel();
13
+ this._bMessageOpen = false;
14
+ this._sErrorText = this._oResourceBundle.getText("errorText");
15
+
16
+ this._oModel.attachMetadataFailed(function (oEvent) {
17
+ this._showServiceError(oEvent.getParameters().response);
18
+ }, this);
19
+
20
+ this._oModel.attachRequestFailed(function (oEvent) {
21
+ const oParams = oEvent.getParameters();
22
+ const oResponse = oParams.response;
23
+ const sCode = String(oResponse.statusCode);
24
+
25
+ // Ignore typical transient 404s that libraries retry (e.g. "Cannot POST" pre-flight)
26
+ if (sCode === "404" && oResponse.responseText && oResponse.responseText.indexOf("Cannot POST") === 0) {
27
+ return;
28
+ }
29
+ if (this._bMessageOpen) {
30
+ return;
31
+ }
32
+ this._bMessageOpen = true;
33
+ MessageBox.error(this._extractMessage(oResponse), {
34
+ onClose: function () {
35
+ this._bMessageOpen = false;
36
+ }.bind(this)
37
+ });
38
+ }, this);
39
+ },
40
+
41
+ _extractMessage: function (oResponse) {
42
+ try {
43
+ if (oResponse && oResponse.responseText) {
44
+ const o = JSON.parse(oResponse.responseText);
45
+ if (o.error && o.error.message && o.error.message.value) {
46
+ return o.error.message.value;
47
+ }
48
+ }
49
+ } catch (e) {
50
+ // fall through to default
51
+ }
52
+ return this._sErrorText;
53
+ },
54
+
55
+ _showServiceError: function (sDetails) {
56
+ if (this._bMessageOpen) {
57
+ return;
58
+ }
59
+ this._bMessageOpen = true;
60
+ MessageBox.error(this._sErrorText, {
61
+ id: "serviceErrorMessageBox",
62
+ details: sDetails,
63
+ styleClass: this._oComponent.getContentDensityClass(),
64
+ actions: [MessageBox.Action.CLOSE],
65
+ onClose: function () {
66
+ this._bMessageOpen = false;
67
+ }.bind(this)
68
+ });
69
+ }
70
+ });
71
+ });
@@ -0,0 +1,12 @@
1
+ sap.ui.define([
2
+ "./BaseController"
3
+ ], function (BaseController) {
4
+ "use strict";
5
+
6
+ return BaseController.extend("__APP_ID__.controller.NotFound", {
7
+
8
+ onNavBack: function () {
9
+ this.getRouter().navTo("worklist", {}, true);
10
+ }
11
+ });
12
+ });