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.
- package/README.md +141 -0
- package/package.json +34 -0
- package/src/cli.js +74 -0
- package/src/metadata/fetch.js +42 -0
- package/src/metadata/parse.js +104 -0
- package/src/prompts/basic.js +43 -0
- package/src/prompts/fields.js +144 -0
- package/src/prompts/odata.js +57 -0
- package/src/render/buildContext.js +67 -0
- package/src/render/controlForType.js +136 -0
- package/src/render/engine.js +81 -0
- package/src/templates/README.md.ejs +62 -0
- package/src/templates/_valueHelp.fragment.xml.ejs +17 -0
- package/src/templates/package.json.ejs +14 -0
- package/src/templates/ui5.yaml.ejs +28 -0
- package/src/templates/webapp/Component.js.ejs +47 -0
- package/src/templates/webapp/controller/App.controller.js +29 -0
- package/src/templates/webapp/controller/BaseController.js +35 -0
- package/src/templates/webapp/controller/ErrorHandler.js +71 -0
- package/src/templates/webapp/controller/NotFound.controller.js +12 -0
- package/src/templates/webapp/controller/Worklist.controller.js.ejs +1158 -0
- package/src/templates/webapp/css/style.css +17 -0
- package/src/templates/webapp/i18n/i18n.properties.ejs +83 -0
- package/src/templates/webapp/i18n/i18n_de.properties.ejs +83 -0
- package/src/templates/webapp/index.html.ejs +52 -0
- package/src/templates/webapp/localService/backendCheck.js.ejs +52 -0
- package/src/templates/webapp/localService/mockserver.js.ejs +29 -0
- package/src/templates/webapp/manifest.json.ejs +106 -0
- package/src/templates/webapp/model/formatter.js +148 -0
- package/src/templates/webapp/model/models.js +23 -0
- package/src/templates/webapp/view/App.view.xml +10 -0
- package/src/templates/webapp/view/NotFound.view.xml +12 -0
- package/src/templates/webapp/view/Worklist.view.xml.ejs +173 -0
- package/src/templates/webapp/view/fragments/GroupDialog.fragment.xml.ejs +11 -0
- package/src/templates/webapp/view/fragments/InfoPopover.fragment.xml +38 -0
- 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
|
+
});
|