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,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
+ });