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,1158 @@
|
|
|
1
|
+
sap.ui.define([
|
|
2
|
+
"./BaseController",
|
|
3
|
+
"sap/ui/model/json/JSONModel",
|
|
4
|
+
"sap/ui/model/Filter",
|
|
5
|
+
"sap/ui/model/FilterOperator",
|
|
6
|
+
"sap/ui/model/Sorter",
|
|
7
|
+
"sap/ui/core/Fragment",
|
|
8
|
+
"sap/m/MessageToast",
|
|
9
|
+
"sap/m/Dialog",
|
|
10
|
+
"sap/m/VBox",
|
|
11
|
+
"sap/m/CheckBox",
|
|
12
|
+
"sap/m/Button",
|
|
13
|
+
"sap/m/Token",
|
|
14
|
+
"sap/m/SearchField",
|
|
15
|
+
"sap/ui/Device",
|
|
16
|
+
"sap/ui/core/routing/HashChanger",
|
|
17
|
+
"../model/formatter"
|
|
18
|
+
], function (BaseController, JSONModel, Filter, FilterOperator, Sorter, Fragment,
|
|
19
|
+
MessageToast, Dialog, VBox, CheckBox, Button, Token, SearchField, Device, HashChanger, formatter) {
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
const PAGE_SIZE_MIN = 10;
|
|
23
|
+
const PAGE_SIZE_MAX = 100;
|
|
24
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
25
|
+
const AUTO_REFRESH_INTERVAL_MS = 30000;
|
|
26
|
+
|
|
27
|
+
return BaseController.extend("<%= appId %>.controller.Worklist", {
|
|
28
|
+
|
|
29
|
+
formatter: formatter,
|
|
30
|
+
|
|
31
|
+
onInit: function () {
|
|
32
|
+
const oRB = this.getResourceBundle();
|
|
33
|
+
const oViewModel = new JSONModel({
|
|
34
|
+
rows: [],
|
|
35
|
+
page: 1,
|
|
36
|
+
top: DEFAULT_PAGE_SIZE,
|
|
37
|
+
totalCount: 0,
|
|
38
|
+
totalPages: 1,
|
|
39
|
+
pageList: [{ key: "1", text: "1" }],
|
|
40
|
+
prevEnabled: false,
|
|
41
|
+
nextEnabled: false,
|
|
42
|
+
filterInfo: "",
|
|
43
|
+
sortInfo: "",
|
|
44
|
+
groupInfo: "",
|
|
45
|
+
searchInfo: "",
|
|
46
|
+
infoBarVisible: false,
|
|
47
|
+
infoBarText: "",
|
|
48
|
+
tableTitleCount: oRB.getText("tableTitleNoCount"),
|
|
49
|
+
noDataText: oRB.getText("noDataText"),
|
|
50
|
+
infoPopoverData: {},
|
|
51
|
+
isCompact: !sap.ui.Device.support.touch,
|
|
52
|
+
selectedCount: 0,
|
|
53
|
+
autoRefreshOn: false,
|
|
54
|
+
lastRefreshedText: ""
|
|
55
|
+
<% if (filters.filter(function (f) { return f.valueHelp; }).length) { -%>
|
|
56
|
+
,
|
|
57
|
+
<% filters.filter(function (f) { return f.valueHelp; }).forEach(function (f, idx, arr) { -%>
|
|
58
|
+
"<%= f.valueHelp.entitySet %>": []<%= idx < arr.length - 1 ? "," : "" %>
|
|
59
|
+
<% }) -%>
|
|
60
|
+
<% } -%>
|
|
61
|
+
});
|
|
62
|
+
this.setModel(oViewModel, "worklistView");
|
|
63
|
+
|
|
64
|
+
this._rtState = { filters: [], sorter: null, groupSorter: null };
|
|
65
|
+
|
|
66
|
+
<% if (defaultSortField) { -%>
|
|
67
|
+
this._rtState.sorter = new Sorter("<%= defaultSortField %>", <%= defaultSortDescending %>);
|
|
68
|
+
<% } -%>
|
|
69
|
+
|
|
70
|
+
<% if (searchFields.length) { -%>
|
|
71
|
+
// basicSearch is a hidden FilterBar aggregation — must be set programmatically
|
|
72
|
+
this.byId("filterbar").setBasicSearch(new SearchField({
|
|
73
|
+
id: this.createId("searchField"),
|
|
74
|
+
width: "18rem",
|
|
75
|
+
placeholder: oRB.getText("searchField.placeholder"),
|
|
76
|
+
search: this.onSearch.bind(this)
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
<% } -%>
|
|
80
|
+
this._applyDensityClass(oViewModel.getProperty("/isCompact"));
|
|
81
|
+
|
|
82
|
+
this._loadValueHelpData();
|
|
83
|
+
// Restore filter / sort / group / page from URL query (deep link, refresh-proof)
|
|
84
|
+
this._restoreStateFromUrl();
|
|
85
|
+
this._updateCount();
|
|
86
|
+
this._loadData();
|
|
87
|
+
|
|
88
|
+
this._wireVariantManagement();
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// ============================================================
|
|
92
|
+
// VARIANT MANAGEMENT WIRING
|
|
93
|
+
// (1) On flex `initialized`, apply the currently selected
|
|
94
|
+
// variant's state — covers the default-variant flow on page
|
|
95
|
+
// reload (VM auto-selects the default but does not always
|
|
96
|
+
// re-fire `select`). URL state takes precedence if present.
|
|
97
|
+
// (2) Capture-phase click listener forces the variant popover
|
|
98
|
+
// to always anchor on the chevron button. Title text clicks
|
|
99
|
+
// are routed to the button, so the popup origin is consistent.
|
|
100
|
+
// ============================================================
|
|
101
|
+
|
|
102
|
+
_wireVariantManagement: function () {
|
|
103
|
+
const oVMCtrl = this.byId("variantMgmt");
|
|
104
|
+
if (!oVMCtrl) return;
|
|
105
|
+
|
|
106
|
+
const fnApplyDefault = () => {
|
|
107
|
+
if (this._variantInitDone) return;
|
|
108
|
+
this._variantInitDone = true;
|
|
109
|
+
const oStore = this._readVariantStore();
|
|
110
|
+
// Prefer our own __default tracking over flex's auto-select,
|
|
111
|
+
// because flex's setDefault changes don't always survive a
|
|
112
|
+
// LocalStorageConnector round-trip cleanly across reloads.
|
|
113
|
+
// (URL state is intentionally NOT given precedence — the
|
|
114
|
+
// app writes routine sort/page state into the URL on every
|
|
115
|
+
// action, which would otherwise mask the real default.)
|
|
116
|
+
const sVMKey = oVMCtrl.getCurrentVariantKey();
|
|
117
|
+
const sKey = oStore.__default || sVMKey;
|
|
118
|
+
if (sKey && oStore[sKey]) {
|
|
119
|
+
if (sVMKey !== sKey) {
|
|
120
|
+
oVMCtrl.setCurrentVariantKey(sKey);
|
|
121
|
+
}
|
|
122
|
+
this._applyVariantState(oStore[sKey]);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
oVMCtrl.attachInitialized(fnApplyDefault);
|
|
127
|
+
// Safety net in case `initialized` does not fire as expected
|
|
128
|
+
// (e.g. attached after the event has already happened).
|
|
129
|
+
setTimeout(fnApplyDefault, 1500);
|
|
130
|
+
|
|
131
|
+
oVMCtrl.addEventDelegate({
|
|
132
|
+
onAfterRendering: () => {
|
|
133
|
+
const oDom = oVMCtrl.getDomRef();
|
|
134
|
+
if (!oDom || oDom.dataset.lrAnchorPatched) return;
|
|
135
|
+
oDom.dataset.lrAnchorPatched = "1";
|
|
136
|
+
const oBtn = oDom.querySelector("button");
|
|
137
|
+
if (!oBtn) return;
|
|
138
|
+
oDom.addEventListener("click", (e) => {
|
|
139
|
+
if (oBtn.contains(e.target)) return;
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
oBtn.click();
|
|
143
|
+
}, true);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// ============================================================
|
|
149
|
+
// DATA LOADING
|
|
150
|
+
// ============================================================
|
|
151
|
+
|
|
152
|
+
_loadValueHelpData: function () {
|
|
153
|
+
const oModel = this.getOwnerComponent().getModel();
|
|
154
|
+
const oVM = this.getModel("worklistView");
|
|
155
|
+
<% filters.filter(function (f) { return f.valueHelp; }).forEach(function (f) { -%>
|
|
156
|
+
oModel.read("/<%= f.valueHelp.entitySet %>", {
|
|
157
|
+
success: function (oData) { oVM.setProperty("/<%= f.valueHelp.entitySet %>", oData.results || []); },
|
|
158
|
+
error: function () { oVM.setProperty("/<%= f.valueHelp.entitySet %>", []); }
|
|
159
|
+
});
|
|
160
|
+
<% }) -%>
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
_loadData: function (bShowToast) {
|
|
164
|
+
const oView = this.getView();
|
|
165
|
+
const oVM = this.getModel("worklistView");
|
|
166
|
+
const oModel = this.getOwnerComponent().getModel();
|
|
167
|
+
|
|
168
|
+
const iTop = oVM.getProperty("/top");
|
|
169
|
+
const iPage = oVM.getProperty("/page") || 1;
|
|
170
|
+
const iSkip = (iPage - 1) * iTop;
|
|
171
|
+
|
|
172
|
+
oView.setBusy(true);
|
|
173
|
+
oModel.read("/<%= entitySet %>", {
|
|
174
|
+
filters: this._buildBackendFilters(),
|
|
175
|
+
urlParameters: { "$top": iTop, "$skip": iSkip },
|
|
176
|
+
success: (oData) => {
|
|
177
|
+
oVM.setProperty("/rows", oData.results || []);
|
|
178
|
+
oVM.setProperty("/lastRefreshedText",
|
|
179
|
+
"<cite>" + new Date().toLocaleTimeString() + "</cite>");
|
|
180
|
+
oView.setBusy(false);
|
|
181
|
+
this._scrollTableToTop();
|
|
182
|
+
if (bShowToast) MessageToast.show(this.getResourceBundle().getText("tableRefreshed"));
|
|
183
|
+
},
|
|
184
|
+
error: () => {
|
|
185
|
+
oVM.setProperty("/rows", []);
|
|
186
|
+
oView.setBusy(false);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
_updateCount: function () {
|
|
192
|
+
const oVM = this.getModel("worklistView");
|
|
193
|
+
this.getOwnerComponent().getModel().read("/<%= entitySet %>/$count", {
|
|
194
|
+
filters: this._buildBackendFilters(),
|
|
195
|
+
success: (count) => {
|
|
196
|
+
const iTotal = parseInt(count, 10) || 0;
|
|
197
|
+
const iTop = oVM.getProperty("/top");
|
|
198
|
+
const iTotalPages = Math.max(1, Math.ceil(iTotal / iTop));
|
|
199
|
+
oVM.setProperty("/totalCount", iTotal);
|
|
200
|
+
oVM.setProperty("/totalPages", iTotalPages);
|
|
201
|
+
|
|
202
|
+
const aPages = [];
|
|
203
|
+
for (let i = 1; i <= iTotalPages; i++) {
|
|
204
|
+
aPages.push({ key: String(i), text: String(i) });
|
|
205
|
+
}
|
|
206
|
+
oVM.setProperty("/pageList", aPages);
|
|
207
|
+
this._updatePaginationButtons();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
_buildBackendFilters: function () {
|
|
213
|
+
const aFilters = [...this._rtState.filters];
|
|
214
|
+
const oSearchFilter = this._buildSearchFilter();
|
|
215
|
+
if (oSearchFilter) aFilters.push(oSearchFilter);
|
|
216
|
+
return aFilters;
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
_buildSearchFilter: function () {
|
|
220
|
+
<% if (searchFields.length) { -%>
|
|
221
|
+
const oSearchField = this.byId("searchField");
|
|
222
|
+
if (!oSearchField) return null;
|
|
223
|
+
const sQuery = oSearchField.getValue();
|
|
224
|
+
if (!sQuery) return null;
|
|
225
|
+
return new Filter({
|
|
226
|
+
filters: [
|
|
227
|
+
<% searchFields.forEach(function (f, i, arr) { -%>
|
|
228
|
+
new Filter("<%= f %>", FilterOperator.Contains, sQuery)<%= i < arr.length - 1 ? "," : "" %>
|
|
229
|
+
<% }) -%>
|
|
230
|
+
],
|
|
231
|
+
and: false
|
|
232
|
+
});
|
|
233
|
+
<% } else { -%>
|
|
234
|
+
return null;
|
|
235
|
+
<% } -%>
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
// ============================================================
|
|
239
|
+
// FILTER BAR SEARCH
|
|
240
|
+
// ============================================================
|
|
241
|
+
|
|
242
|
+
onSearch: function () {
|
|
243
|
+
const aFilters = [];
|
|
244
|
+
const aLabels = [];
|
|
245
|
+
const oRB = this.getResourceBundle();
|
|
246
|
+
|
|
247
|
+
<% filters.forEach(function (f) { -%>
|
|
248
|
+
<% if (f.inputType === 'dateRange') { -%>
|
|
249
|
+
{
|
|
250
|
+
const oDR = this.byId("<%= f.fieldName %>");
|
|
251
|
+
if (oDR && oDR.getDateValue() && oDR.getSecondDateValue()) {
|
|
252
|
+
const start = new Date(oDR.getDateValue()); start.setHours(0, 0, 0, 0);
|
|
253
|
+
const end = new Date(oDR.getSecondDateValue()); end.setHours(23, 59, 59, 999);
|
|
254
|
+
aFilters.push(new Filter("<%= f.fieldName %>", FilterOperator.BT, start, end));
|
|
255
|
+
aLabels.push(oRB.getText("filter.<%= f.fieldName %>"));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
<% } else if (f.inputType === 'valueHelp') { -%>
|
|
259
|
+
{
|
|
260
|
+
const oMI = this.byId("<%= f.fieldName %>");
|
|
261
|
+
if (oMI && oMI.getTokens().length) {
|
|
262
|
+
const aOr = oMI.getTokens().map(t => new Filter("<%= f.fieldName %>", FilterOperator.EQ, t.getKey()));
|
|
263
|
+
aFilters.push(new Filter(aOr, false));
|
|
264
|
+
aLabels.push(oRB.getText("filter.<%= f.fieldName %>"));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
<% } else { -%>
|
|
268
|
+
{
|
|
269
|
+
const oMI = this.byId("<%= f.fieldName %>");
|
|
270
|
+
if (oMI && oMI.getTokens().length) {
|
|
271
|
+
const aOr = oMI.getTokens().map(t => new Filter("<%= f.fieldName %>", FilterOperator.Contains, t.getText()));
|
|
272
|
+
aFilters.push(new Filter(aOr, false));
|
|
273
|
+
aLabels.push(oRB.getText("filter.<%= f.fieldName %>"));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
<% } -%>
|
|
277
|
+
<% }) -%>
|
|
278
|
+
|
|
279
|
+
this._rtState.filters = aFilters;
|
|
280
|
+
this.getModel("worklistView").setProperty("/filterInfo",
|
|
281
|
+
aLabels.length ? oRB.getText("info.filteredBy", [aLabels.join(", ")]) : "");
|
|
282
|
+
this._updateInfoBar();
|
|
283
|
+
|
|
284
|
+
this.getModel("worklistView").setProperty("/page", 1);
|
|
285
|
+
this._writeStateToUrl();
|
|
286
|
+
this._updateCount();
|
|
287
|
+
this._loadData();
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// ============================================================
|
|
291
|
+
// PAGINATION
|
|
292
|
+
// ============================================================
|
|
293
|
+
|
|
294
|
+
onPrevPage: function () {
|
|
295
|
+
const oVM = this.getModel("worklistView");
|
|
296
|
+
const iPage = oVM.getProperty("/page");
|
|
297
|
+
if (iPage > 1) { oVM.setProperty("/page", iPage - 1); this._loadData(); this._writeStateToUrl(); }
|
|
298
|
+
this._updatePaginationButtons();
|
|
299
|
+
},
|
|
300
|
+
onNextPage: function () {
|
|
301
|
+
const oVM = this.getModel("worklistView");
|
|
302
|
+
const iPage = oVM.getProperty("/page");
|
|
303
|
+
if (iPage < oVM.getProperty("/totalPages")) { oVM.setProperty("/page", iPage + 1); this._loadData(); this._writeStateToUrl(); }
|
|
304
|
+
this._updatePaginationButtons();
|
|
305
|
+
},
|
|
306
|
+
onFirstPage: function () {
|
|
307
|
+
this.getModel("worklistView").setProperty("/page", 1);
|
|
308
|
+
this._loadData();
|
|
309
|
+
this._writeStateToUrl();
|
|
310
|
+
this._updatePaginationButtons();
|
|
311
|
+
},
|
|
312
|
+
onLastPage: function () {
|
|
313
|
+
const oVM = this.getModel("worklistView");
|
|
314
|
+
oVM.setProperty("/page", oVM.getProperty("/totalPages"));
|
|
315
|
+
this._loadData();
|
|
316
|
+
this._writeStateToUrl();
|
|
317
|
+
this._updatePaginationButtons();
|
|
318
|
+
},
|
|
319
|
+
onPageSelect: function (oEvt) {
|
|
320
|
+
const sKey = oEvt.getSource().getSelectedKey();
|
|
321
|
+
const iPage = parseInt(sKey, 10);
|
|
322
|
+
if (isNaN(iPage)) return;
|
|
323
|
+
const oVM = this.getModel("worklistView");
|
|
324
|
+
if (oVM.getProperty("/page") === iPage) return;
|
|
325
|
+
oVM.setProperty("/page", iPage);
|
|
326
|
+
this._loadData();
|
|
327
|
+
this._writeStateToUrl();
|
|
328
|
+
this._updatePaginationButtons();
|
|
329
|
+
},
|
|
330
|
+
onStepChange: function (oEvt) {
|
|
331
|
+
const iValue = parseInt(oEvt.getParameter("value"), 10);
|
|
332
|
+
if (isNaN(iValue) || iValue < PAGE_SIZE_MIN || iValue > PAGE_SIZE_MAX) return;
|
|
333
|
+
const oVM = this.getModel("worklistView");
|
|
334
|
+
oVM.setProperty("/top", iValue);
|
|
335
|
+
oVM.setProperty("/page", 1);
|
|
336
|
+
this._updateCount();
|
|
337
|
+
this._loadData();
|
|
338
|
+
},
|
|
339
|
+
_updatePaginationButtons: function () {
|
|
340
|
+
const oVM = this.getModel("worklistView");
|
|
341
|
+
const iPage = oVM.getProperty("/page") || 1;
|
|
342
|
+
const iTotalPages = oVM.getProperty("/totalPages") || 1;
|
|
343
|
+
oVM.setProperty("/prevEnabled", iPage > 1);
|
|
344
|
+
oVM.setProperty("/nextEnabled", iPage < iTotalPages);
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// ============================================================
|
|
348
|
+
// SORT / GROUP DIALOGS
|
|
349
|
+
// ============================================================
|
|
350
|
+
|
|
351
|
+
onSortDialog: function () {
|
|
352
|
+
if (!this._pSortDialog) {
|
|
353
|
+
this._pSortDialog = Fragment.load({
|
|
354
|
+
id: this.getView().getId(),
|
|
355
|
+
name: "<%= appId %>.view.fragments.SortDialog",
|
|
356
|
+
controller: this
|
|
357
|
+
}).then((oDialog) => {
|
|
358
|
+
this.getView().addDependent(oDialog);
|
|
359
|
+
return oDialog;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
this._pSortDialog.then((oDialog) => oDialog.open());
|
|
363
|
+
},
|
|
364
|
+
onSortDialogConfirm: function (oEvt) {
|
|
365
|
+
const sKey = oEvt.getParameter("sortItem").getKey();
|
|
366
|
+
const bDesc = oEvt.getParameter("sortDescending");
|
|
367
|
+
this._rtState.sorter = new Sorter(sKey, bDesc);
|
|
368
|
+
this._applyRTState();
|
|
369
|
+
const oRB = this.getResourceBundle();
|
|
370
|
+
this.getModel("worklistView").setProperty("/sortInfo",
|
|
371
|
+
oRB.getText("info.sortedBy", [
|
|
372
|
+
oRB.getText("col." + sKey) || sKey,
|
|
373
|
+
oRB.getText(bDesc ? "sort.desc" : "sort.asc")
|
|
374
|
+
]));
|
|
375
|
+
this._updateInfoBar();
|
|
376
|
+
this._writeStateToUrl();
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
onGroupDialog: function () {
|
|
380
|
+
if (!this._pGroupDialog) {
|
|
381
|
+
this._pGroupDialog = Fragment.load({
|
|
382
|
+
id: this.getView().getId(),
|
|
383
|
+
name: "<%= appId %>.view.fragments.GroupDialog",
|
|
384
|
+
controller: this
|
|
385
|
+
}).then((oDialog) => {
|
|
386
|
+
this.getView().addDependent(oDialog);
|
|
387
|
+
return oDialog;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
this._pGroupDialog.then((oDialog) => oDialog.open());
|
|
391
|
+
},
|
|
392
|
+
onGroupDialogConfirm: function (oEvt) {
|
|
393
|
+
const oGroupItem = oEvt.getParameter("groupItem");
|
|
394
|
+
const oRB = this.getResourceBundle();
|
|
395
|
+
const oVM = this.getModel("worklistView");
|
|
396
|
+
|
|
397
|
+
if (!oGroupItem) {
|
|
398
|
+
this._rtState.groupSorter = null;
|
|
399
|
+
oVM.setProperty("/groupInfo", "");
|
|
400
|
+
} else {
|
|
401
|
+
const sKey = oGroupItem.getKey();
|
|
402
|
+
const bDesc = oEvt.getParameter("groupDescending");
|
|
403
|
+
this._rtState.groupSorter = new Sorter(sKey, bDesc, (oCtx) => {
|
|
404
|
+
const v = oCtx.getProperty(sKey);
|
|
405
|
+
const t = v != null && v !== "" ? v : oRB.getText("group.empty");
|
|
406
|
+
return { key: t, text: t };
|
|
407
|
+
});
|
|
408
|
+
oVM.setProperty("/groupInfo",
|
|
409
|
+
oRB.getText("info.groupedBy", [oRB.getText("col." + sKey) || sKey]));
|
|
410
|
+
}
|
|
411
|
+
this._applyRTState();
|
|
412
|
+
this._updateInfoBar();
|
|
413
|
+
this._writeStateToUrl();
|
|
414
|
+
},
|
|
415
|
+
onGroupDialogReset: function () {
|
|
416
|
+
this._rtState.groupSorter = null;
|
|
417
|
+
this.getModel("worklistView").setProperty("/groupInfo", "");
|
|
418
|
+
this._applyRTState();
|
|
419
|
+
this._updateInfoBar();
|
|
420
|
+
this._writeStateToUrl();
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
_applyRTState: function () {
|
|
424
|
+
const oTable = this.byId("worklistTable");
|
|
425
|
+
const oBinding = oTable && oTable.getBinding("items");
|
|
426
|
+
if (!oBinding) return;
|
|
427
|
+
const aSorters = [this._rtState.groupSorter, this._rtState.sorter].filter(Boolean);
|
|
428
|
+
oBinding.sort(aSorters);
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
// ============================================================
|
|
432
|
+
// QUICK SORT MENU (column header click)
|
|
433
|
+
// ============================================================
|
|
434
|
+
|
|
435
|
+
onColumnMenuBeforeOpen: function (oEvt) {
|
|
436
|
+
const oColumn = oEvt.getParameter("openBy");
|
|
437
|
+
if (!oColumn) return;
|
|
438
|
+
const sFieldName = oColumn.data("fieldName");
|
|
439
|
+
if (!sFieldName) return;
|
|
440
|
+
|
|
441
|
+
const oMenu = this.byId("columnHeaderMenu");
|
|
442
|
+
const oQuickSort = oMenu.getQuickActions()[0];
|
|
443
|
+
const oSortItem = oQuickSort.getItems()[0];
|
|
444
|
+
|
|
445
|
+
oSortItem.setKey(sFieldName);
|
|
446
|
+
oSortItem.setLabel(oColumn.getHeader().getText());
|
|
447
|
+
|
|
448
|
+
// Reflect current sort state in the menu
|
|
449
|
+
const oCurrent = this._rtState.sorter;
|
|
450
|
+
if (oCurrent && oCurrent.sPath === sFieldName) {
|
|
451
|
+
oSortItem.setSortOrder(oCurrent.bDescending ? "Descending" : "Ascending");
|
|
452
|
+
} else {
|
|
453
|
+
oSortItem.setSortOrder("None");
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
onColumnQuickSort: function (oEvt) {
|
|
458
|
+
const oItem = oEvt.getParameter("item");
|
|
459
|
+
const sKey = oItem.getKey();
|
|
460
|
+
const sOrder = oItem.getSortOrder();
|
|
461
|
+
|
|
462
|
+
const oRB = this.getResourceBundle();
|
|
463
|
+
const oVM = this.getModel("worklistView");
|
|
464
|
+
|
|
465
|
+
if (sOrder === "None") {
|
|
466
|
+
this._rtState.sorter = null;
|
|
467
|
+
oVM.setProperty("/sortInfo", "");
|
|
468
|
+
} else {
|
|
469
|
+
const bDesc = sOrder === "Descending";
|
|
470
|
+
this._rtState.sorter = new Sorter(sKey, bDesc);
|
|
471
|
+
oVM.setProperty("/sortInfo",
|
|
472
|
+
oRB.getText("info.sortedBy", [
|
|
473
|
+
oRB.getText("col." + sKey) || sKey,
|
|
474
|
+
oRB.getText(bDesc ? "sort.desc" : "sort.asc")
|
|
475
|
+
]));
|
|
476
|
+
}
|
|
477
|
+
this._applyRTState();
|
|
478
|
+
this._updateInfoBar();
|
|
479
|
+
this._writeStateToUrl();
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
// ============================================================
|
|
483
|
+
// DENSITY (compact on desktop, cozy on touch — set once at init)
|
|
484
|
+
// ============================================================
|
|
485
|
+
|
|
486
|
+
_applyDensityClass: function (bCompact) {
|
|
487
|
+
const oView = this.getView();
|
|
488
|
+
if (bCompact) {
|
|
489
|
+
oView.removeStyleClass("sapUiSizeCozy").addStyleClass("sapUiSizeCompact");
|
|
490
|
+
} else {
|
|
491
|
+
oView.removeStyleClass("sapUiSizeCompact").addStyleClass("sapUiSizeCozy");
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
// ============================================================
|
|
496
|
+
// COLUMN SETTINGS
|
|
497
|
+
// ============================================================
|
|
498
|
+
|
|
499
|
+
onColumnDialog: function () {
|
|
500
|
+
const oTable = this.byId("worklistTable");
|
|
501
|
+
const oRB = this.getResourceBundle();
|
|
502
|
+
|
|
503
|
+
const oDialog = new Dialog({
|
|
504
|
+
title: oRB.getText("columnSettings"),
|
|
505
|
+
contentWidth: "20rem",
|
|
506
|
+
draggable: false,
|
|
507
|
+
resizable: false
|
|
508
|
+
});
|
|
509
|
+
const oVBox = new VBox({ width: "100%" });
|
|
510
|
+
oTable.getColumns().forEach((oCol) => {
|
|
511
|
+
const oHeader = oCol.getHeader();
|
|
512
|
+
if (!oHeader || !oHeader.getText || !oHeader.getText()) return;
|
|
513
|
+
oVBox.addItem(new CheckBox({
|
|
514
|
+
text: oHeader.getText(),
|
|
515
|
+
selected: oCol.getVisible(),
|
|
516
|
+
select: (e) => oCol.setVisible(e.getParameter("selected"))
|
|
517
|
+
}));
|
|
518
|
+
});
|
|
519
|
+
oDialog.addContent(oVBox);
|
|
520
|
+
oDialog.addButton(new Button({
|
|
521
|
+
text: oRB.getText("close"),
|
|
522
|
+
press: () => { oDialog.close(); oDialog.destroy(); }
|
|
523
|
+
}));
|
|
524
|
+
this.getView().addDependent(oDialog);
|
|
525
|
+
oDialog.open();
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
// ============================================================
|
|
529
|
+
// REFRESH / RESET
|
|
530
|
+
// ============================================================
|
|
531
|
+
|
|
532
|
+
onRefresh: function () {
|
|
533
|
+
const oVM = this.getModel("worklistView");
|
|
534
|
+
this._rtState = { filters: [], sorter: null, groupSorter: null };
|
|
535
|
+
|
|
536
|
+
<% filters.forEach(function (f) { -%>
|
|
537
|
+
{
|
|
538
|
+
const ctrl = this.byId("<%= f.fieldName %>");
|
|
539
|
+
if (ctrl) {
|
|
540
|
+
<% if (f.inputType === 'dateRange') { -%>
|
|
541
|
+
ctrl.setDateValue(null);
|
|
542
|
+
ctrl.setSecondDateValue(null);
|
|
543
|
+
ctrl.setValue("");
|
|
544
|
+
<% } else { -%>
|
|
545
|
+
ctrl.removeAllTokens();
|
|
546
|
+
ctrl.setValue("");
|
|
547
|
+
<% } -%>
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
<% }) -%>
|
|
551
|
+
<% if (searchFields.length) { -%>
|
|
552
|
+
const oSearchField = this.byId("searchField");
|
|
553
|
+
if (oSearchField) oSearchField.setValue("");
|
|
554
|
+
<% } -%>
|
|
555
|
+
|
|
556
|
+
oVM.setProperty("/page", 1);
|
|
557
|
+
oVM.setProperty("/filterInfo", "");
|
|
558
|
+
oVM.setProperty("/sortInfo", "");
|
|
559
|
+
oVM.setProperty("/groupInfo", "");
|
|
560
|
+
oVM.setProperty("/searchInfo", "");
|
|
561
|
+
this._updateInfoBar();
|
|
562
|
+
this._writeStateToUrl();
|
|
563
|
+
this._updateCount();
|
|
564
|
+
this._loadData();
|
|
565
|
+
MessageToast.show(this.getResourceBundle().getText("tableRefreshed"));
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
// ============================================================
|
|
569
|
+
// INFO BAR
|
|
570
|
+
// ============================================================
|
|
571
|
+
|
|
572
|
+
_updateInfoBar: function () {
|
|
573
|
+
const oVM = this.getModel("worklistView");
|
|
574
|
+
const aParts = [
|
|
575
|
+
oVM.getProperty("/filterInfo"),
|
|
576
|
+
oVM.getProperty("/searchInfo"),
|
|
577
|
+
oVM.getProperty("/groupInfo"),
|
|
578
|
+
oVM.getProperty("/sortInfo")
|
|
579
|
+
].filter(Boolean);
|
|
580
|
+
oVM.setProperty("/infoBarVisible", aParts.length > 0);
|
|
581
|
+
oVM.setProperty("/infoBarText", aParts.join(" • "));
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
onTableUpdateFinished: function () {
|
|
585
|
+
const oTable = this.byId("worklistTable");
|
|
586
|
+
const oBinding = oTable.getBinding("items");
|
|
587
|
+
const iVisible = oBinding ? oBinding.getLength() : 0;
|
|
588
|
+
const iTotal = this.getModel("worklistView").getProperty("/totalCount");
|
|
589
|
+
const oRB = this.getResourceBundle();
|
|
590
|
+
const sTitle = iVisible
|
|
591
|
+
? oRB.getText("tableTitleCount", [iVisible, iTotal])
|
|
592
|
+
: oRB.getText("tableTitleNoCount");
|
|
593
|
+
this.getModel("worklistView").setProperty("/tableTitleCount", sTitle);
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
// ============================================================
|
|
597
|
+
// SCROLL-TO-TOP ANIMATION (nice touch after page change)
|
|
598
|
+
// ============================================================
|
|
599
|
+
|
|
600
|
+
_scrollTableToTop: function () {
|
|
601
|
+
setTimeout(() => {
|
|
602
|
+
const oTable = this.byId("worklistTable");
|
|
603
|
+
const oDom = oTable && oTable.getDomRef();
|
|
604
|
+
if (!oDom) return;
|
|
605
|
+
let oScroll = oDom.parentElement;
|
|
606
|
+
while (oScroll && oScroll !== document.body) {
|
|
607
|
+
const oStyle = window.getComputedStyle(oScroll);
|
|
608
|
+
const bScrollable = (oStyle.overflowY === "auto" || oStyle.overflowY === "scroll");
|
|
609
|
+
if (bScrollable && oScroll.scrollHeight > oScroll.clientHeight) break;
|
|
610
|
+
oScroll = oScroll.parentElement;
|
|
611
|
+
}
|
|
612
|
+
if (oScroll) this._animateScroll(oScroll, 0, 600);
|
|
613
|
+
}, 100);
|
|
614
|
+
},
|
|
615
|
+
_animateScroll: function (oEl, iTarget, iDuration) {
|
|
616
|
+
const iStart = oEl.scrollTop;
|
|
617
|
+
const iDiff = iTarget - iStart;
|
|
618
|
+
const iStartTime = performance.now();
|
|
619
|
+
const ease = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
620
|
+
const step = (iNow) => {
|
|
621
|
+
const iProgress = Math.min((iNow - iStartTime) / iDuration, 1);
|
|
622
|
+
oEl.scrollTop = iStart + iDiff * ease(iProgress);
|
|
623
|
+
if (iProgress < 1) requestAnimationFrame(step);
|
|
624
|
+
};
|
|
625
|
+
requestAnimationFrame(step);
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
// ============================================================
|
|
629
|
+
// ROW PRESS / INFO POPOVER
|
|
630
|
+
// Whole-row click opens the InfoPopover with row details. There's
|
|
631
|
+
// no detail page in this template; if you need one, add a route
|
|
632
|
+
// and replace the body of onItemPress with router.navTo(...).
|
|
633
|
+
// ============================================================
|
|
634
|
+
|
|
635
|
+
onItemPress: function (oEvt) {
|
|
636
|
+
this._openInfoPopover(oEvt.getSource(), oEvt.getSource());
|
|
637
|
+
},
|
|
638
|
+
onInfoLinkPress: function (oEvt) {
|
|
639
|
+
// Cell-level link click. UI5 suppresses the parent row-press when an
|
|
640
|
+
// interactive child is clicked, so this fires alone. We anchor the
|
|
641
|
+
// popover on the link itself for a tighter visual.
|
|
642
|
+
const oLink = oEvt.getSource();
|
|
643
|
+
const oItem = oLink.getParent(); // cells are direct children of ColumnListItem
|
|
644
|
+
this._openInfoPopover(oItem, oLink);
|
|
645
|
+
},
|
|
646
|
+
_openInfoPopover: function (oItem, oOpener) {
|
|
647
|
+
const oRow = oItem.getBindingContext("worklistView").getObject();
|
|
648
|
+
const oVM = this.getModel("worklistView");
|
|
649
|
+
|
|
650
|
+
oVM.setProperty("/infoPopoverData", {
|
|
651
|
+
key: oRow.<%= primaryKey || columns[0].name %>,
|
|
652
|
+
detail: "Replace this with any field(s) from your row object.",
|
|
653
|
+
timestamp: new Date()
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
if (!this._pInfoPopover) {
|
|
657
|
+
this._pInfoPopover = Fragment.load({
|
|
658
|
+
id: this.getView().getId(),
|
|
659
|
+
name: "<%= appId %>.view.fragments.InfoPopover",
|
|
660
|
+
controller: this
|
|
661
|
+
}).then((oPopover) => {
|
|
662
|
+
this.getView().addDependent(oPopover);
|
|
663
|
+
return oPopover;
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
this._pInfoPopover.then((oPopover) => oPopover.openBy(oOpener));
|
|
667
|
+
},
|
|
668
|
+
onInfoPopoverClose: function () {
|
|
669
|
+
if (this._pInfoPopover) {
|
|
670
|
+
this._pInfoPopover.then((oPopover) => oPopover.close());
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
// ============================================================
|
|
675
|
+
// MULTI-SELECT + BULK ACTION
|
|
676
|
+
// ============================================================
|
|
677
|
+
|
|
678
|
+
onSelectionChange: function () {
|
|
679
|
+
const oTable = this.byId("worklistTable");
|
|
680
|
+
this.getModel("worklistView").setProperty("/selectedCount", oTable.getSelectedItems().length);
|
|
681
|
+
},
|
|
682
|
+
onClearSelection: function () {
|
|
683
|
+
this.byId("worklistTable").removeSelections(true);
|
|
684
|
+
this.getModel("worklistView").setProperty("/selectedCount", 0);
|
|
685
|
+
},
|
|
686
|
+
onBulkAction: function () {
|
|
687
|
+
const aItems = this.byId("worklistTable").getSelectedItems();
|
|
688
|
+
const aRows = aItems.map((oItem) => oItem.getBindingContext("worklistView").getObject());
|
|
689
|
+
// Replace this body with your real bulk action — call an OData
|
|
690
|
+
// function import, MessageBox confirm, etc. The selected rows are
|
|
691
|
+
// in `aRows` (full row objects) and can be filtered by key field.
|
|
692
|
+
MessageToast.show(this.getResourceBundle().getText("bulkActionDemo", [aRows.length]));
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
// ============================================================
|
|
696
|
+
// AUTO-REFRESH
|
|
697
|
+
// ToggleButton drives a setInterval that calls _updateCount +
|
|
698
|
+
// _loadData on a fixed cadence. Manual refresh works alongside.
|
|
699
|
+
// The timer is cleared on toggle-off and on view destroy.
|
|
700
|
+
// ============================================================
|
|
701
|
+
|
|
702
|
+
onToggleAutoRefresh: function () {
|
|
703
|
+
const oRB = this.getResourceBundle();
|
|
704
|
+
const bOn = this.getModel("worklistView").getProperty("/autoRefreshOn");
|
|
705
|
+
if (bOn) {
|
|
706
|
+
// Immediate fetch so the user sees feedback instantly,
|
|
707
|
+
// then continue on the fixed cadence.
|
|
708
|
+
this._updateCount();
|
|
709
|
+
this._loadData();
|
|
710
|
+
this._iAutoRefreshTimer = setInterval(() => {
|
|
711
|
+
this._updateCount();
|
|
712
|
+
this._loadData(true);
|
|
713
|
+
}, AUTO_REFRESH_INTERVAL_MS);
|
|
714
|
+
MessageToast.show(oRB.getText("autoRefresh.on"));
|
|
715
|
+
} else if (this._iAutoRefreshTimer) {
|
|
716
|
+
clearInterval(this._iAutoRefreshTimer);
|
|
717
|
+
this._iAutoRefreshTimer = null;
|
|
718
|
+
MessageToast.show(oRB.getText("autoRefresh.off"));
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
onExit: function () {
|
|
723
|
+
if (this._iAutoRefreshTimer) clearInterval(this._iAutoRefreshTimer);
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
// ============================================================
|
|
727
|
+
// EXPORT TO EXCEL
|
|
728
|
+
// Uses sap.ui.export.Spreadsheet. Re-fetches all matching rows
|
|
729
|
+
// (current filters + sort) so the export matches what the user
|
|
730
|
+
// sees, not just the current page. A confirm dialog appears for
|
|
731
|
+
// large result sets to avoid accidental heavy backend calls.
|
|
732
|
+
// ============================================================
|
|
733
|
+
|
|
734
|
+
onExportToExcel: function () {
|
|
735
|
+
const oVM = this.getModel("worklistView");
|
|
736
|
+
const iTotal = oVM.getProperty("/totalCount") || 0;
|
|
737
|
+
const oRB = this.getResourceBundle();
|
|
738
|
+
if (iTotal === 0) {
|
|
739
|
+
MessageToast.show(oRB.getText("export.noData"));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const LARGE_THRESHOLD = 1000;
|
|
743
|
+
if (iTotal > LARGE_THRESHOLD) {
|
|
744
|
+
sap.ui.require(["sap/m/MessageBox"], (MessageBox) => {
|
|
745
|
+
MessageBox.confirm(oRB.getText("export.largeConfirm", [iTotal]), {
|
|
746
|
+
onClose: (sAction) => {
|
|
747
|
+
if (sAction === MessageBox.Action.OK) this._fetchAllAndExport();
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
} else {
|
|
752
|
+
this._fetchAllAndExport();
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
_fetchAllAndExport: function () {
|
|
757
|
+
const oView = this.getView();
|
|
758
|
+
const oVM = this.getModel("worklistView");
|
|
759
|
+
const oModel = this.getOwnerComponent().getModel();
|
|
760
|
+
const iTotal = oVM.getProperty("/totalCount") || 0;
|
|
761
|
+
const aSorters = [this._rtState.groupSorter, this._rtState.sorter].filter(Boolean);
|
|
762
|
+
|
|
763
|
+
oView.setBusy(true);
|
|
764
|
+
oModel.read("/<%= entitySet %>", {
|
|
765
|
+
filters: this._buildBackendFilters(),
|
|
766
|
+
sorters: aSorters,
|
|
767
|
+
urlParameters: { "$top": String(iTotal), "$skip": "0" },
|
|
768
|
+
success: (oData) => {
|
|
769
|
+
oView.setBusy(false);
|
|
770
|
+
this._doExport(oData.results || []);
|
|
771
|
+
},
|
|
772
|
+
error: () => {
|
|
773
|
+
oView.setBusy(false);
|
|
774
|
+
MessageToast.show(this.getResourceBundle().getText("export.error"));
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
_doExport: function (aRows) {
|
|
780
|
+
const oRB = this.getResourceBundle();
|
|
781
|
+
const aColumns = this._buildExportColumns();
|
|
782
|
+
sap.ui.require([
|
|
783
|
+
"sap/ui/export/Spreadsheet",
|
|
784
|
+
"sap/ui/export/library",
|
|
785
|
+
"sap/m/Dialog",
|
|
786
|
+
"sap/m/ProgressIndicator",
|
|
787
|
+
"sap/m/VBox",
|
|
788
|
+
"sap/m/Text"
|
|
789
|
+
], (Spreadsheet, exportLib, Dialog, ProgressIndicator, VBox, Text) => {
|
|
790
|
+
const EdmType = exportLib.EdmType;
|
|
791
|
+
aColumns.forEach((c) => { c.type = EdmType[c.type] || EdmType.String; });
|
|
792
|
+
const sStamp = new Date().toISOString().slice(0, 10);
|
|
793
|
+
|
|
794
|
+
const oProgress = new ProgressIndicator({
|
|
795
|
+
percentValue: 0,
|
|
796
|
+
displayValue: "0%",
|
|
797
|
+
showValue: true,
|
|
798
|
+
state: "Success",
|
|
799
|
+
width: "100%",
|
|
800
|
+
height: "1.75rem"
|
|
801
|
+
});
|
|
802
|
+
const oDialog = new Dialog({
|
|
803
|
+
title: oRB.getText("export.dialogTitle"),
|
|
804
|
+
contentWidth: "24rem",
|
|
805
|
+
draggable: false,
|
|
806
|
+
resizable: false,
|
|
807
|
+
content: new VBox({
|
|
808
|
+
items: [
|
|
809
|
+
new Text({ text: oRB.getText("export.dialogText") }).addStyleClass("sapUiSmallMarginBottom"),
|
|
810
|
+
oProgress
|
|
811
|
+
]
|
|
812
|
+
}).addStyleClass("sapUiMediumMargin")
|
|
813
|
+
});
|
|
814
|
+
this.getView().addDependent(oDialog);
|
|
815
|
+
oDialog.open();
|
|
816
|
+
|
|
817
|
+
const oSheet = new Spreadsheet({
|
|
818
|
+
workbook: {
|
|
819
|
+
columns: aColumns,
|
|
820
|
+
context: {
|
|
821
|
+
title: oRB.getText("appTitle"),
|
|
822
|
+
sheetName: oRB.getText("appTitle")
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
dataSource: aRows,
|
|
826
|
+
fileName: oRB.getText("appTitle") + "_" + sStamp + ".xlsx",
|
|
827
|
+
showProgress: false
|
|
828
|
+
});
|
|
829
|
+
const pBuild = oSheet.build();
|
|
830
|
+
|
|
831
|
+
// ProgressIndicator animates width via its own CSS transition
|
|
832
|
+
// when setPercentValue is called. We feed a few target values
|
|
833
|
+
// and the control fills smoothly between them — same pattern
|
|
834
|
+
// as SAP's own ProgressIndicator demo.
|
|
835
|
+
const setPct = (v) => { oProgress.setPercentValue(v); oProgress.setDisplayValue(v + "%"); };
|
|
836
|
+
const aTicks = [
|
|
837
|
+
{ v: 30, delay: 50 },
|
|
838
|
+
{ v: 65, delay: 550 },
|
|
839
|
+
{ v: 90, delay: 1100 }
|
|
840
|
+
];
|
|
841
|
+
const aTimers = aTicks.map((t) => setTimeout(() => setPct(t.v), t.delay));
|
|
842
|
+
const pMinShow = new Promise((r) => setTimeout(r, 1500));
|
|
843
|
+
|
|
844
|
+
return Promise.all([pBuild, pMinShow])
|
|
845
|
+
.then(() => {
|
|
846
|
+
aTimers.forEach(clearTimeout);
|
|
847
|
+
setPct(100);
|
|
848
|
+
return new Promise((r) => setTimeout(r, 500));
|
|
849
|
+
})
|
|
850
|
+
.then(() => MessageToast.show(oRB.getText("export.success")))
|
|
851
|
+
.catch(() => MessageToast.show(oRB.getText("export.error")))
|
|
852
|
+
.finally(() => {
|
|
853
|
+
aTimers.forEach(clearTimeout);
|
|
854
|
+
oDialog.close();
|
|
855
|
+
oDialog.destroy();
|
|
856
|
+
oSheet.destroy();
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
_buildExportColumns: function () {
|
|
862
|
+
const oRB = this.getResourceBundle();
|
|
863
|
+
const oTable = this.byId("worklistTable");
|
|
864
|
+
const aResult = [];
|
|
865
|
+
oTable.getColumns().forEach((oCol) => {
|
|
866
|
+
if (!oCol.getVisible()) return;
|
|
867
|
+
let sField = null, sType = "String";
|
|
868
|
+
oCol.getCustomData().forEach((d) => {
|
|
869
|
+
if (d.getKey() === "fieldName") sField = d.getValue();
|
|
870
|
+
else if (d.getKey() === "exportType") sType = d.getValue();
|
|
871
|
+
});
|
|
872
|
+
if (!sField) return;
|
|
873
|
+
aResult.push({
|
|
874
|
+
label: oRB.getText("col." + sField) || sField,
|
|
875
|
+
property: sField,
|
|
876
|
+
type: sType
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
return aResult;
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
// ============================================================
|
|
883
|
+
// VARIANT MANAGEMENT
|
|
884
|
+
// sap.ui.fl.variants.VariantManagement renders the dropdown and
|
|
885
|
+
// persists variant METADATA (list + default flag) via the flex
|
|
886
|
+
// LocalStorageConnector configured in index.html. Our APP state
|
|
887
|
+
// (filters/sort/group/columns/top/search) is stored separately
|
|
888
|
+
// in localStorage, keyed by the variant key from the save event.
|
|
889
|
+
// Both stores live in localStorage and stay in sync via the key.
|
|
890
|
+
// ============================================================
|
|
891
|
+
|
|
892
|
+
_variantStoreKey: function () {
|
|
893
|
+
return "lrVariants_" + this.getView().getId();
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
_readVariantStore: function () {
|
|
897
|
+
try {
|
|
898
|
+
const sRaw = localStorage.getItem(this._variantStoreKey());
|
|
899
|
+
return sRaw ? JSON.parse(sRaw) : {};
|
|
900
|
+
} catch (e) { return {}; }
|
|
901
|
+
},
|
|
902
|
+
|
|
903
|
+
_writeVariantStore: function (oStore) {
|
|
904
|
+
try { localStorage.setItem(this._variantStoreKey(), JSON.stringify(oStore)); } catch (e) { /* quota */ }
|
|
905
|
+
},
|
|
906
|
+
|
|
907
|
+
onVariantSelect: function (oEvent) {
|
|
908
|
+
const sKey = oEvent.getParameter("key");
|
|
909
|
+
const oStore = this._readVariantStore();
|
|
910
|
+
this._applyVariantState(oStore[sKey] || null);
|
|
911
|
+
},
|
|
912
|
+
|
|
913
|
+
onVariantSave: function (oEvent) {
|
|
914
|
+
// Capture state synchronously (matches user intent at click time).
|
|
915
|
+
const oState = this._collectVariantState();
|
|
916
|
+
const sEventKey = oEvent.getParameter("key");
|
|
917
|
+
const bDefault = !!oEvent.getParameter("def");
|
|
918
|
+
const oRB = this.getResourceBundle();
|
|
919
|
+
|
|
920
|
+
// Write under the event's key first.
|
|
921
|
+
const oStore = this._readVariantStore();
|
|
922
|
+
if (sEventKey) {
|
|
923
|
+
oStore[sEventKey] = oState;
|
|
924
|
+
if (bDefault) oStore.__default = sEventKey;
|
|
925
|
+
}
|
|
926
|
+
this._writeVariantStore(oStore);
|
|
927
|
+
|
|
928
|
+
// The save event can fire BEFORE flex finalizes the variant's
|
|
929
|
+
// permanent key. Belt-and-suspenders: also write under the
|
|
930
|
+
// post-commit current key. Whichever key is later returned by
|
|
931
|
+
// VM on select / init, we have state under it.
|
|
932
|
+
setTimeout(() => {
|
|
933
|
+
const oVMCtrl = this.byId("variantMgmt");
|
|
934
|
+
const sCurrentKey = oVMCtrl.getCurrentVariantKey();
|
|
935
|
+
if (sCurrentKey) {
|
|
936
|
+
const oStore2 = this._readVariantStore();
|
|
937
|
+
oStore2[sCurrentKey] = oState;
|
|
938
|
+
if (bDefault) oStore2.__default = sCurrentKey;
|
|
939
|
+
this._writeVariantStore(oStore2);
|
|
940
|
+
}
|
|
941
|
+
}, 100);
|
|
942
|
+
|
|
943
|
+
MessageToast.show(oRB.getText("variant.saved"));
|
|
944
|
+
},
|
|
945
|
+
|
|
946
|
+
onVariantManage: function (oEvent) {
|
|
947
|
+
const aDeleted = oEvent.getParameter("deleted") || [];
|
|
948
|
+
const sNewDef = oEvent.getParameter("def");
|
|
949
|
+
const oStore = this._readVariantStore();
|
|
950
|
+
aDeleted.forEach((k) => {
|
|
951
|
+
delete oStore[k];
|
|
952
|
+
if (oStore.__default === k) delete oStore.__default;
|
|
953
|
+
});
|
|
954
|
+
if (sNewDef !== undefined) oStore.__default = sNewDef;
|
|
955
|
+
this._writeVariantStore(oStore);
|
|
956
|
+
},
|
|
957
|
+
|
|
958
|
+
_collectVariantState: function () {
|
|
959
|
+
const oVM = this.getModel("worklistView");
|
|
960
|
+
const oTable = this.byId("worklistTable");
|
|
961
|
+
const oState = {
|
|
962
|
+
filters: {}, sort: null, group: null, columns: {},
|
|
963
|
+
top: oVM.getProperty("/top"), search: ""
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
<% filters.forEach(function (f) { -%>
|
|
967
|
+
{
|
|
968
|
+
const oCtrl = this.byId("<%= f.fieldName %>");
|
|
969
|
+
if (oCtrl) {
|
|
970
|
+
<% if (f.inputType === 'dateRange') { -%>
|
|
971
|
+
if (oCtrl.getDateValue() && oCtrl.getSecondDateValue()) {
|
|
972
|
+
oState.filters["<%= f.fieldName %>"] = {
|
|
973
|
+
type: "dateRange",
|
|
974
|
+
start: oCtrl.getDateValue().toISOString(),
|
|
975
|
+
end: oCtrl.getSecondDateValue().toISOString()
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
<% } else { -%>
|
|
979
|
+
const aTokens = oCtrl.getTokens().map((t) => ({ key: t.getKey(), text: t.getText() }));
|
|
980
|
+
if (aTokens.length) oState.filters["<%= f.fieldName %>"] = { type: "tokens", tokens: aTokens };
|
|
981
|
+
<% } -%>
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
<% }) -%>
|
|
985
|
+
|
|
986
|
+
if (this._rtState.sorter) oState.sort = { path: this._rtState.sorter.sPath, descending: this._rtState.sorter.bDescending };
|
|
987
|
+
if (this._rtState.groupSorter) oState.group = { path: this._rtState.groupSorter.sPath };
|
|
988
|
+
|
|
989
|
+
oTable.getColumns().forEach((oCol) => {
|
|
990
|
+
const oField = oCol.getCustomData().find((d) => d.getKey() === "fieldName");
|
|
991
|
+
if (oField) oState.columns[oField.getValue()] = oCol.getVisible();
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
<% if (searchFields.length) { -%>
|
|
995
|
+
const oSF = this.byId("searchField");
|
|
996
|
+
oState.search = oSF ? oSF.getValue() : "";
|
|
997
|
+
<% } -%>
|
|
998
|
+
return oState;
|
|
999
|
+
},
|
|
1000
|
+
|
|
1001
|
+
_applyVariantState: function (oState) {
|
|
1002
|
+
oState = oState || {};
|
|
1003
|
+
const oVM = this.getModel("worklistView");
|
|
1004
|
+
const oRB = this.getResourceBundle();
|
|
1005
|
+
const oTable = this.byId("worklistTable");
|
|
1006
|
+
|
|
1007
|
+
this._rtState = { filters: [], sorter: null, groupSorter: null };
|
|
1008
|
+
|
|
1009
|
+
<% filters.forEach(function (f) { -%>
|
|
1010
|
+
{
|
|
1011
|
+
const oCtrl = this.byId("<%= f.fieldName %>");
|
|
1012
|
+
if (oCtrl) {
|
|
1013
|
+
<% if (f.inputType === 'dateRange') { -%>
|
|
1014
|
+
const fState = oState.filters && oState.filters["<%= f.fieldName %>"];
|
|
1015
|
+
if (fState && fState.start && fState.end) {
|
|
1016
|
+
oCtrl.setDateValue(new Date(fState.start));
|
|
1017
|
+
oCtrl.setSecondDateValue(new Date(fState.end));
|
|
1018
|
+
} else {
|
|
1019
|
+
oCtrl.setDateValue(null);
|
|
1020
|
+
oCtrl.setSecondDateValue(null);
|
|
1021
|
+
oCtrl.setValue("");
|
|
1022
|
+
}
|
|
1023
|
+
<% } else { -%>
|
|
1024
|
+
oCtrl.removeAllTokens();
|
|
1025
|
+
const fState = oState.filters && oState.filters["<%= f.fieldName %>"];
|
|
1026
|
+
if (fState && fState.tokens) {
|
|
1027
|
+
fState.tokens.forEach((t) => oCtrl.addToken(new Token({ key: t.key, text: t.text })));
|
|
1028
|
+
}
|
|
1029
|
+
<% } -%>
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
<% }) -%>
|
|
1033
|
+
|
|
1034
|
+
if (oState.sort) this._rtState.sorter = new Sorter(oState.sort.path, oState.sort.descending);
|
|
1035
|
+
if (oState.group) {
|
|
1036
|
+
const sKey = oState.group.path;
|
|
1037
|
+
this._rtState.groupSorter = new Sorter(sKey, false, (oCtx) => {
|
|
1038
|
+
const v = oCtx.getProperty(sKey);
|
|
1039
|
+
const t = v != null && v !== "" ? v : oRB.getText("group.empty");
|
|
1040
|
+
return { key: t, text: t };
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
oTable.getColumns().forEach((oCol) => {
|
|
1045
|
+
const oField = oCol.getCustomData().find((d) => d.getKey() === "fieldName");
|
|
1046
|
+
if (!oField) return;
|
|
1047
|
+
const sName = oField.getValue();
|
|
1048
|
+
const bVisible = oState.columns && oState.columns[sName] !== undefined ? oState.columns[sName] : true;
|
|
1049
|
+
oCol.setVisible(bVisible);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
oVM.setProperty("/top", oState.top || DEFAULT_PAGE_SIZE);
|
|
1053
|
+
oVM.setProperty("/page", 1);
|
|
1054
|
+
|
|
1055
|
+
<% if (searchFields.length) { -%>
|
|
1056
|
+
const oSF = this.byId("searchField");
|
|
1057
|
+
if (oSF) oSF.setValue(oState.search || "");
|
|
1058
|
+
<% } -%>
|
|
1059
|
+
|
|
1060
|
+
this.onSearch();
|
|
1061
|
+
this._applyRTState();
|
|
1062
|
+
},
|
|
1063
|
+
|
|
1064
|
+
// ============================================================
|
|
1065
|
+
// URL STATE PERSISTENCE
|
|
1066
|
+
// Filter / sort / group / page are mirrored into the URL hash so
|
|
1067
|
+
// the page survives reload and the link is shareable.
|
|
1068
|
+
// ============================================================
|
|
1069
|
+
|
|
1070
|
+
_writeStateToUrl: function () {
|
|
1071
|
+
const oVM = this.getModel("worklistView");
|
|
1072
|
+
const aParts = [];
|
|
1073
|
+
|
|
1074
|
+
const oSearchField = this.byId("searchField");
|
|
1075
|
+
if (oSearchField && oSearchField.getValue()) {
|
|
1076
|
+
aParts.push("q=" + encodeURIComponent(oSearchField.getValue()));
|
|
1077
|
+
}
|
|
1078
|
+
if (this._rtState.sorter) {
|
|
1079
|
+
aParts.push("s=" + encodeURIComponent(this._rtState.sorter.sPath + ":" + (this._rtState.sorter.bDescending ? "desc" : "asc")));
|
|
1080
|
+
}
|
|
1081
|
+
if (this._rtState.groupSorter) {
|
|
1082
|
+
aParts.push("g=" + encodeURIComponent(this._rtState.groupSorter.sPath));
|
|
1083
|
+
}
|
|
1084
|
+
const iPage = oVM.getProperty("/page");
|
|
1085
|
+
if (iPage > 1) aParts.push("p=" + iPage);
|
|
1086
|
+
|
|
1087
|
+
HashChanger.getInstance().replaceHash(aParts.length ? "?" + aParts.join("&") : "");
|
|
1088
|
+
},
|
|
1089
|
+
|
|
1090
|
+
_restoreStateFromUrl: function () {
|
|
1091
|
+
const sHash = HashChanger.getInstance().getHash() || "";
|
|
1092
|
+
const sQuery = sHash.replace(/^\?/, "");
|
|
1093
|
+
if (!sQuery) return;
|
|
1094
|
+
const oQuery = {};
|
|
1095
|
+
sQuery.split("&").forEach((sPair) => {
|
|
1096
|
+
const [sK, sV] = sPair.split("=");
|
|
1097
|
+
if (sK && sV !== undefined) oQuery[sK] = decodeURIComponent(sV);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
const oVM = this.getModel("worklistView");
|
|
1101
|
+
if (oQuery.q) {
|
|
1102
|
+
const oSearchField = this.byId("searchField");
|
|
1103
|
+
if (oSearchField) oSearchField.setValue(oQuery.q);
|
|
1104
|
+
}
|
|
1105
|
+
if (oQuery.s) {
|
|
1106
|
+
const [sPath, sDir] = oQuery.s.split(":");
|
|
1107
|
+
this._rtState.sorter = new Sorter(sPath, sDir === "desc");
|
|
1108
|
+
}
|
|
1109
|
+
if (oQuery.g) {
|
|
1110
|
+
this._rtState.groupSorter = new Sorter(oQuery.g, false);
|
|
1111
|
+
}
|
|
1112
|
+
if (oQuery.p) {
|
|
1113
|
+
oVM.setProperty("/page", parseInt(oQuery.p, 10));
|
|
1114
|
+
}
|
|
1115
|
+
}<% if (filters.filter(function (f) { return f.valueHelp; }).length) { %>,
|
|
1116
|
+
|
|
1117
|
+
// ============================================================
|
|
1118
|
+
// VALUE HELPS — one block per VH filter
|
|
1119
|
+
// ============================================================
|
|
1120
|
+
<% filters.filter(function (f) { return f.valueHelp; }).forEach(function (f, idx, arr) { -%>
|
|
1121
|
+
|
|
1122
|
+
on<%= f.fieldName %>VH: function () {
|
|
1123
|
+
if (!this._p<%= f.fieldName %>VH) {
|
|
1124
|
+
this._p<%= f.fieldName %>VH = this.loadFragment({
|
|
1125
|
+
name: "<%= appId %>.view.fragments.<%= f.fieldName %>VH"
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
this._p<%= f.fieldName %>VH.then((oDialog) => oDialog.open());
|
|
1129
|
+
},
|
|
1130
|
+
on<%= f.fieldName %>VHOkPress: function (oEvt) {
|
|
1131
|
+
const oMI = this.byId("<%= f.fieldName %>");
|
|
1132
|
+
oMI.removeAllTokens();
|
|
1133
|
+
(oEvt.getParameter("selectedItems") || []).forEach((oItem) => {
|
|
1134
|
+
oMI.addToken(new Token({
|
|
1135
|
+
text: oItem.getTitle(),
|
|
1136
|
+
key: oItem.getDescription()
|
|
1137
|
+
}));
|
|
1138
|
+
});
|
|
1139
|
+
},
|
|
1140
|
+
on<%= f.fieldName %>VHCancelPress: function () { /* no-op */ },
|
|
1141
|
+
on<%= f.fieldName %>VHSearch: function (oEvt) {
|
|
1142
|
+
const sValue = oEvt.getParameter("value");
|
|
1143
|
+
const oBinding = oEvt.getParameter("itemsBinding");
|
|
1144
|
+
if (!oBinding) return;
|
|
1145
|
+
if (!sValue) { oBinding.filter([]); return; }
|
|
1146
|
+
oBinding.filter(new Filter({
|
|
1147
|
+
filters: [
|
|
1148
|
+
new Filter("<%= f.valueHelp.keyField %>", FilterOperator.Contains, sValue),
|
|
1149
|
+
new Filter("<%= f.valueHelp.textField %>", FilterOperator.Contains, sValue)
|
|
1150
|
+
],
|
|
1151
|
+
and: false
|
|
1152
|
+
}));
|
|
1153
|
+
}<%= idx < arr.length - 1 ? "," : "" %>
|
|
1154
|
+
<% }) -%>
|
|
1155
|
+
<% } %>
|
|
1156
|
+
|
|
1157
|
+
});
|
|
1158
|
+
});
|