underpost 2.8.871 → 2.8.873
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/.env.development +2 -1
- package/.env.production +2 -1
- package/.env.test +2 -1
- package/.github/workflows/ghpkg.ci.yml +1 -1
- package/.github/workflows/npmpkg.ci.yml +1 -1
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +2 -2
- package/README.md +66 -36
- package/bin/build.js +4 -0
- package/bin/deploy.js +4 -0
- package/cli.md +88 -87
- package/conf.js +2 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +138 -0
- package/manifests/deployment/dd-test-development/proxy.yaml +26 -0
- package/package.json +6 -3
- package/src/api/core/core.router.js +2 -1
- package/src/api/default/default.controller.js +6 -1
- package/src/api/default/default.router.js +6 -2
- package/src/api/default/default.service.js +10 -1
- package/src/api/file/file.router.js +2 -1
- package/src/api/test/test.router.js +1 -1
- package/src/api/user/postman_collection.json +216 -0
- package/src/api/user/user.controller.js +25 -60
- package/src/api/user/user.model.js +29 -7
- package/src/api/user/user.router.js +6 -3
- package/src/api/user/user.service.js +80 -32
- package/src/cli/baremetal.js +33 -3
- package/src/cli/cloud-init.js +11 -0
- package/src/cli/deploy.js +5 -2
- package/src/cli/index.js +1 -0
- package/src/cli/lxd.js +7 -0
- package/src/cli/repository.js +1 -0
- package/src/cli/run.js +18 -5
- package/src/cli/ssh.js +20 -6
- package/src/client/components/core/Account.js +2 -1
- package/src/client/components/core/AgGrid.js +30 -8
- package/src/client/components/core/Auth.js +98 -55
- package/src/client/components/core/CalendarCore.js +3 -4
- package/src/client/components/core/CommonJs.js +1 -2
- package/src/client/components/core/Content.js +2 -1
- package/src/client/components/core/Css.js +2 -1
- package/src/client/components/core/CssCore.js +2 -1
- package/src/client/components/core/Docs.js +4 -4
- package/src/client/components/core/FileExplorer.js +3 -3
- package/src/client/components/core/JoyStick.js +2 -2
- package/src/client/components/core/LoadingAnimation.js +2 -2
- package/src/client/components/core/LogIn.js +16 -23
- package/src/client/components/core/LogOut.js +5 -1
- package/src/client/components/core/Logger.js +4 -1
- package/src/client/components/core/Modal.js +17 -27
- package/src/client/components/core/ObjectLayerEngineModal.js +2 -1
- package/src/client/components/core/Pagination.js +207 -0
- package/src/client/components/core/Panel.js +3 -11
- package/src/client/components/core/PanelForm.js +6 -15
- package/src/client/components/core/Recover.js +2 -2
- package/src/client/components/core/Router.js +205 -33
- package/src/client/components/core/SignUp.js +1 -2
- package/src/client/components/core/Stream.js +1 -1
- package/src/client/components/core/VanillaJs.js +0 -83
- package/src/client/components/core/Worker.js +2 -2
- package/src/client/components/default/LogInDefault.js +0 -6
- package/src/client/components/default/LogOutDefault.js +0 -16
- package/src/client/components/default/MenuDefault.js +4 -3
- package/src/client/components/default/RoutesDefault.js +3 -2
- package/src/client/services/core/core.service.js +6 -2
- package/src/client/services/default/default.management.js +115 -18
- package/src/client/services/default/default.service.js +9 -4
- package/src/client/services/user/user.management.js +6 -0
- package/src/client/services/user/user.service.js +11 -4
- package/src/client/ssr/head/DefaultScripts.js +1 -0
- package/src/index.js +24 -2
- package/src/runtime/lampp/Lampp.js +89 -2
- package/src/runtime/xampp/Xampp.js +48 -1
- package/src/server/auth.js +518 -155
- package/src/server/conf.js +19 -1
- package/src/server/runtime.js +62 -221
- package/src/server/ssl.js +1 -2
- package/src/server/ssr.js +85 -0
- package/src/server/valkey.js +2 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { getQueryParams, setQueryParams } from './Router.js';
|
|
2
|
+
|
|
3
|
+
class AgPagination extends HTMLElement {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this.attachShadow({ mode: 'open' });
|
|
7
|
+
this._gridId = null;
|
|
8
|
+
const queryParams = getQueryParams();
|
|
9
|
+
this._currentPage = parseInt(queryParams.page, 10) || 1;
|
|
10
|
+
this._limit = parseInt(queryParams.limit, 10) || 10;
|
|
11
|
+
this._totalPages = 1;
|
|
12
|
+
this._totalItems = 0;
|
|
13
|
+
this.handlePageChange = this.handlePageChange.bind(this);
|
|
14
|
+
this.handleLimitChange = this.handleLimitChange.bind(this);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static get observedAttributes() {
|
|
18
|
+
return ['grid-id', 'current-page', 'total-pages', 'total-items', 'limit'];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
22
|
+
switch (name) {
|
|
23
|
+
case 'grid-id':
|
|
24
|
+
this._gridId = newValue;
|
|
25
|
+
break;
|
|
26
|
+
case 'current-page':
|
|
27
|
+
this._currentPage = parseInt(newValue, 10) || this._currentPage;
|
|
28
|
+
break;
|
|
29
|
+
case 'total-pages':
|
|
30
|
+
this._totalPages = parseInt(newValue, 10) || 1;
|
|
31
|
+
break;
|
|
32
|
+
case 'total-items':
|
|
33
|
+
this._totalItems = parseInt(newValue, 10) || 0;
|
|
34
|
+
break;
|
|
35
|
+
case 'limit':
|
|
36
|
+
this._limit = parseInt(newValue, 10) || this._limit;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
this.update();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
connectedCallback() {
|
|
43
|
+
this.render();
|
|
44
|
+
this.addEventListeners();
|
|
45
|
+
this.update();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
disconnectedCallback() {
|
|
49
|
+
// Event listeners on shadow DOM are garbage collected with the component
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
handlePageChange(newPage) {
|
|
53
|
+
if (newPage < 1 || newPage > this._totalPages || newPage === this._currentPage) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this._currentPage = newPage;
|
|
57
|
+
setQueryParams({ page: newPage, limit: this._limit });
|
|
58
|
+
this.dispatchEvent(new CustomEvent('page-change', { detail: { page: newPage } }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
handleLimitChange(event) {
|
|
62
|
+
const newLimit = parseInt(event.target.value, 10);
|
|
63
|
+
if (newLimit === this._limit) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this._limit = newLimit;
|
|
67
|
+
this._currentPage = 1; // Reset to first page on limit change
|
|
68
|
+
setQueryParams({ page: 1, limit: newLimit });
|
|
69
|
+
this.dispatchEvent(new CustomEvent('limit-change', { detail: { limit: newLimit } }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
update() {
|
|
73
|
+
if (!this.shadowRoot.querySelector('#page-info')) return;
|
|
74
|
+
|
|
75
|
+
const isFirstPage = this._currentPage === 1;
|
|
76
|
+
const isLastPage = this._currentPage === this._totalPages;
|
|
77
|
+
|
|
78
|
+
this.shadowRoot.querySelector('#first-page').disabled = isFirstPage;
|
|
79
|
+
this.shadowRoot.querySelector('#prev-page').disabled = isFirstPage;
|
|
80
|
+
this.shadowRoot.querySelector('#next-page').disabled = isLastPage;
|
|
81
|
+
this.shadowRoot.querySelector('#last-page').disabled = isLastPage;
|
|
82
|
+
|
|
83
|
+
const startItem = this._totalItems > 0 ? (this._currentPage - 1) * this._limit + 1 : 0;
|
|
84
|
+
const endItem = Math.min(this._currentPage * this._limit, this._totalItems);
|
|
85
|
+
|
|
86
|
+
this.shadowRoot.querySelector('#summary-info').textContent = `${startItem} - ${endItem} of ${this._totalItems}`;
|
|
87
|
+
this.shadowRoot.querySelector('#page-info').textContent = `Page ${this._currentPage} of ${this._totalPages}`;
|
|
88
|
+
|
|
89
|
+
const limitSelector = this.shadowRoot.querySelector('#limit-selector');
|
|
90
|
+
if (limitSelector.value != this._limit) {
|
|
91
|
+
limitSelector.value = this._limit;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.renderPageButtons();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
renderPageButtons() {
|
|
98
|
+
const pageButtonsContainer = this.shadowRoot.querySelector('#page-buttons');
|
|
99
|
+
pageButtonsContainer.innerHTML = '';
|
|
100
|
+
|
|
101
|
+
const maxButtons = 5;
|
|
102
|
+
let startPage = Math.max(1, this._currentPage - Math.floor(maxButtons / 2));
|
|
103
|
+
let endPage = Math.min(this._totalPages, startPage + maxButtons - 1);
|
|
104
|
+
|
|
105
|
+
if (endPage - startPage + 1 < maxButtons) {
|
|
106
|
+
startPage = Math.max(1, endPage - maxButtons + 1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
110
|
+
const button = document.createElement('button');
|
|
111
|
+
button.textContent = i;
|
|
112
|
+
button.disabled = i === this._currentPage;
|
|
113
|
+
if (i === this._currentPage) {
|
|
114
|
+
button.classList.add('active');
|
|
115
|
+
}
|
|
116
|
+
button.addEventListener('click', () => this.handlePageChange(i));
|
|
117
|
+
pageButtonsContainer.appendChild(button);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
addEventListeners() {
|
|
122
|
+
this.shadowRoot.querySelector('#first-page').addEventListener('click', () => this.handlePageChange(1));
|
|
123
|
+
this.shadowRoot
|
|
124
|
+
.querySelector('#prev-page')
|
|
125
|
+
.addEventListener('click', () => this.handlePageChange(this._currentPage - 1));
|
|
126
|
+
this.shadowRoot
|
|
127
|
+
.querySelector('#next-page')
|
|
128
|
+
.addEventListener('click', () => this.handlePageChange(this._currentPage + 1));
|
|
129
|
+
this.shadowRoot
|
|
130
|
+
.querySelector('#last-page')
|
|
131
|
+
.addEventListener('click', () => this.handlePageChange(this._totalPages));
|
|
132
|
+
this.shadowRoot.querySelector('#limit-selector').addEventListener('change', this.handleLimitChange);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render() {
|
|
136
|
+
this.shadowRoot.innerHTML = html`
|
|
137
|
+
<style>
|
|
138
|
+
:host {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
justify-content: center;
|
|
142
|
+
padding: 8px;
|
|
143
|
+
font-family: sans-serif;
|
|
144
|
+
font-size: 14px;
|
|
145
|
+
gap: 8px;
|
|
146
|
+
}
|
|
147
|
+
button {
|
|
148
|
+
border: 1px solid #ccc;
|
|
149
|
+
background-color: #f0f0f0;
|
|
150
|
+
padding: 6px 12px;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
border-radius: 4px;
|
|
153
|
+
}
|
|
154
|
+
button:disabled {
|
|
155
|
+
cursor: not-allowed;
|
|
156
|
+
opacity: 0.5;
|
|
157
|
+
}
|
|
158
|
+
button.active {
|
|
159
|
+
border-color: #007bff;
|
|
160
|
+
background-color: #007bff;
|
|
161
|
+
color: white;
|
|
162
|
+
}
|
|
163
|
+
#page-info {
|
|
164
|
+
min-width: 80px;
|
|
165
|
+
text-align: center;
|
|
166
|
+
}
|
|
167
|
+
#page-buttons {
|
|
168
|
+
display: flex;
|
|
169
|
+
gap: 4px;
|
|
170
|
+
}
|
|
171
|
+
.summary-panel,
|
|
172
|
+
.page-summary-panel {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 8px;
|
|
176
|
+
}
|
|
177
|
+
#limit-selector {
|
|
178
|
+
border: 1px solid #ccc;
|
|
179
|
+
background-color: #f0f0f0;
|
|
180
|
+
padding: 6px 12px;
|
|
181
|
+
border-radius: 4px;
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
184
|
+
<div class="summary-panel">
|
|
185
|
+
<span id="summary-info"></span>
|
|
186
|
+
</div>
|
|
187
|
+
<button id="first-page">First</button>
|
|
188
|
+
<button id="prev-page">Previous</button>
|
|
189
|
+
<div class="page-summary-panel">
|
|
190
|
+
<div id="page-buttons"></div>
|
|
191
|
+
<span id="page-info"></span>
|
|
192
|
+
</div>
|
|
193
|
+
<button id="next-page">Next</button>
|
|
194
|
+
<button id="last-page">Last</button>
|
|
195
|
+
<select id="limit-selector">
|
|
196
|
+
<option value="10">10</option>
|
|
197
|
+
<option value="20">20</option>
|
|
198
|
+
<option value="50">50</option>
|
|
199
|
+
<option value="100">100</option>
|
|
200
|
+
</select>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
customElements.define('ag-pagination', AgPagination);
|
|
206
|
+
|
|
207
|
+
export { AgPagination };
|
|
@@ -2,17 +2,14 @@ import { getId, isValidDate, newInstance } from './CommonJs.js';
|
|
|
2
2
|
import { LoadingAnimation } from '../core/LoadingAnimation.js';
|
|
3
3
|
import { Validator } from '../core/Validator.js';
|
|
4
4
|
import { Input } from '../core/Input.js';
|
|
5
|
-
import { Responsive } from '../core/Responsive.js';
|
|
6
5
|
import { darkTheme, ThemeEvents } from './Css.js';
|
|
7
|
-
import { append, getDataFromInputFile,
|
|
6
|
+
import { append, getDataFromInputFile, htmls, s } from './VanillaJs.js';
|
|
8
7
|
import { BtnIcon } from './BtnIcon.js';
|
|
9
8
|
import { Translate } from './Translate.js';
|
|
10
9
|
import { DropDown } from './DropDown.js';
|
|
11
|
-
import { dynamicCol
|
|
10
|
+
import { dynamicCol } from './Css.js';
|
|
12
11
|
import { EventsUI } from './EventsUI.js';
|
|
13
12
|
import { ToggleSwitch } from './ToggleSwitch.js';
|
|
14
|
-
import { Modal } from './Modal.js';
|
|
15
|
-
import { RouterEvents, setQueryPath } from './Router.js';
|
|
16
13
|
import { RichText } from './RichText.js';
|
|
17
14
|
import { loggerFactory } from './Logger.js';
|
|
18
15
|
import { Badge } from './Badge.js';
|
|
@@ -470,12 +467,7 @@ const Panel = {
|
|
|
470
467
|
s(`.${btnSelector}`).classList.remove('hide');
|
|
471
468
|
}
|
|
472
469
|
}
|
|
473
|
-
|
|
474
|
-
s(`.${idPanel}-form-body`).classList.add('hide');
|
|
475
|
-
});
|
|
476
|
-
if (options.route && getQueryParams().cid) {
|
|
477
|
-
setQueryPath({ path: options.route, queryPath: '', replace: true });
|
|
478
|
-
}
|
|
470
|
+
s(`.${idPanel}-form-body`).classList.add('hide');
|
|
479
471
|
};
|
|
480
472
|
s(`.btn-${idPanel}-add`).onclick = (e) => {
|
|
481
473
|
e.preventDefault();
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { getCapVariableName, newInstance, random, range, uniqueArray } from './CommonJs.js';
|
|
2
2
|
import { marked } from 'marked';
|
|
3
|
-
import {
|
|
4
|
-
getBlobFromUint8ArrayFile,
|
|
5
|
-
getDataFromInputFile,
|
|
6
|
-
getQueryParams,
|
|
7
|
-
getRawContentFile,
|
|
8
|
-
htmls,
|
|
9
|
-
} from './VanillaJs.js';
|
|
3
|
+
import { getBlobFromUint8ArrayFile, getDataFromInputFile, getRawContentFile, htmls } from './VanillaJs.js';
|
|
10
4
|
import { Panel } from './Panel.js';
|
|
11
5
|
import { NotificationManager } from './NotificationManager.js';
|
|
12
6
|
import { DocumentService } from '../../services/document/document.service.js';
|
|
@@ -15,7 +9,7 @@ import { getSrcFromFileData } from './Input.js';
|
|
|
15
9
|
import { imageShimmer, renderCssAttr } from './Css.js';
|
|
16
10
|
import { Translate } from './Translate.js';
|
|
17
11
|
import { Modal } from './Modal.js';
|
|
18
|
-
import { closeModalRouteChangeEvents, listenQueryPathInstance, setQueryPath } from './Router.js';
|
|
12
|
+
import { closeModalRouteChangeEvents, listenQueryPathInstance, setQueryPath, getQueryParams } from './Router.js';
|
|
19
13
|
|
|
20
14
|
const PanelForm = {
|
|
21
15
|
Data: {},
|
|
@@ -170,7 +164,7 @@ const PanelForm = {
|
|
|
170
164
|
status,
|
|
171
165
|
});
|
|
172
166
|
if (getQueryParams().cid === data.id) {
|
|
173
|
-
setQueryPath({ path: options.route, queryPath: ''
|
|
167
|
+
setQueryPath({ path: options.route, queryPath: '' });
|
|
174
168
|
if (PanelForm.Data[idPanel].updatePanel) await PanelForm.Data[idPanel].updatePanel();
|
|
175
169
|
}
|
|
176
170
|
|
|
@@ -426,10 +420,8 @@ const PanelForm = {
|
|
|
426
420
|
let firsUpdateEvent = false;
|
|
427
421
|
let lastCid;
|
|
428
422
|
let lastUserId;
|
|
429
|
-
closeModalRouteChangeEvents[idPanel] = (
|
|
430
|
-
|
|
431
|
-
this.Data[idPanel].updatePanel();
|
|
432
|
-
}
|
|
423
|
+
closeModalRouteChangeEvents[idPanel] = () => {
|
|
424
|
+
this.Data[idPanel].updatePanel();
|
|
433
425
|
};
|
|
434
426
|
this.Data[idPanel].updatePanel = async () => {
|
|
435
427
|
const cid = getQueryParams().cid ? getQueryParams().cid : '';
|
|
@@ -437,7 +429,6 @@ const PanelForm = {
|
|
|
437
429
|
if (lastCid === cid && !forceUpdate) return;
|
|
438
430
|
lastUserId = newInstance(Elements.Data.user.main.model.user._id);
|
|
439
431
|
lastCid = cid;
|
|
440
|
-
if (options.route === 'home') Modal.homeCid = newInstance(cid);
|
|
441
432
|
htmls(`.${options.parentIdModal ? 'html-' + options.parentIdModal : 'main-body'}`, await renderSrrPanelData());
|
|
442
433
|
await getPanelData();
|
|
443
434
|
htmls(
|
|
@@ -461,7 +452,7 @@ const PanelForm = {
|
|
|
461
452
|
Modal.Data['modal-menu'].onHome[idPanel] = async () => {
|
|
462
453
|
lastCid = undefined;
|
|
463
454
|
lastUserId = undefined;
|
|
464
|
-
setQueryPath({ path: options.route, queryPath: '' });
|
|
455
|
+
setQueryPath({ path: options.route, queryPath: options.route === 'home' ? '?' : '' });
|
|
465
456
|
await this.Data[idPanel].updatePanel();
|
|
466
457
|
};
|
|
467
458
|
}
|
|
@@ -3,11 +3,11 @@ import { Auth } from './Auth.js';
|
|
|
3
3
|
import { BtnIcon } from './BtnIcon.js';
|
|
4
4
|
import { EventsUI } from './EventsUI.js';
|
|
5
5
|
import { Input } from './Input.js';
|
|
6
|
-
import { LogIn } from './LogIn.js';
|
|
7
6
|
import { NotificationManager } from './NotificationManager.js';
|
|
8
7
|
import { Translate } from './Translate.js';
|
|
9
8
|
import { Validator } from './Validator.js';
|
|
10
|
-
import {
|
|
9
|
+
import { s } from './VanillaJs.js';
|
|
10
|
+
import { getProxyPath, getQueryParams } from './Router.js';
|
|
11
11
|
|
|
12
12
|
const Recover = {
|
|
13
13
|
Event: {},
|
|
@@ -1,21 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router module for handling routing in a PWA application.
|
|
3
|
+
* @module src/client/components/core/Router.js
|
|
4
|
+
* @namespace PwaRouter
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import { titleFormatted } from './CommonJs.js';
|
|
2
8
|
import { loggerFactory } from './Logger.js';
|
|
3
|
-
import {
|
|
9
|
+
import { htmls, s } from './VanillaJs.js';
|
|
4
10
|
import { Modal } from './Modal.js';
|
|
5
11
|
import { Worker } from './Worker.js';
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
const logger = loggerFactory(import.meta, { trace: true });
|
|
8
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @type {Object.<string, function>}
|
|
17
|
+
* @description Holds event listeners for router changes.
|
|
18
|
+
* @memberof PwaRouter
|
|
19
|
+
*/
|
|
9
20
|
const RouterEvents = {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @type {string[]}
|
|
24
|
+
* @description Array of core UI component IDs that should not trigger modal close route changes.
|
|
25
|
+
* @memberof PwaRouter
|
|
26
|
+
*/
|
|
27
|
+
const coreUI = ['modal-menu', 'main-body', 'main-body-top', 'bottom-bar', 'board-notification'];
|
|
28
|
+
/**
|
|
29
|
+
* @type {Object.<string, function>}
|
|
30
|
+
* @description Holds event listeners for route changes that should close a modal.
|
|
31
|
+
* @memberof PwaRouter
|
|
32
|
+
*/
|
|
10
33
|
const closeModalRouteChangeEvents = {};
|
|
11
34
|
|
|
12
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Determines the base path for the application, often used for routing within a sub-directory.
|
|
37
|
+
* It checks the current URL's pathname and `window.Routes` to return the appropriate proxy path.
|
|
38
|
+
*
|
|
39
|
+
* @returns {string} The calculated proxy path. Returns `/<first-segment>/` if a segment exists,
|
|
40
|
+
* otherwise `/`. If `window.Routes` indicates the path is a root route, it returns `/`.
|
|
41
|
+
* @memberof PwaRouter
|
|
42
|
+
*/
|
|
43
|
+
const getProxyPath = () => {
|
|
44
|
+
let path = location.pathname.split('/')[1] ? `/${location.pathname.split('/')[1]}/` : '/';
|
|
45
|
+
if (window.Routes && path !== '/' && path.slice(0, -1) in window.Routes()) path = '/';
|
|
46
|
+
return path;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sets the browser's path using the History API. It sanitizes the path, handles query strings and hashes,
|
|
51
|
+
* and prevents pushing the same state twice.
|
|
52
|
+
* @param {string} [path='/'] - The new path to set. Can include query strings and hashes.
|
|
53
|
+
* @param {object} [options={ removeSearch: false, removeHash: false }] - Options for path manipulation.
|
|
54
|
+
* @param {boolean} [options.removeSearch=false] - If true, removes the search part of the URL.
|
|
55
|
+
* @param {boolean} [options.removeHash=false] - If true, removes the hash part of the URL. Defaults to `false`.
|
|
56
|
+
* @param {object} [stateStorage={}] - State object to associate with the history entry.
|
|
57
|
+
* @param {string} [title=''] - The title for the new history entry.
|
|
58
|
+
* @memberof PwaRouter
|
|
59
|
+
* @returns {void | undefined} Returns `undefined` if the new path is the same as the current path, otherwise `void` (result of `history.pushState`).
|
|
60
|
+
*/
|
|
61
|
+
const setPath = (path = '/', options = { removeSearch: false, removeHash: false }, stateStorage = {}, title = '') => {
|
|
62
|
+
logger.warn(`Set path input`, `${path}`);
|
|
63
|
+
if (!path) path = '/';
|
|
64
|
+
|
|
65
|
+
let [inputPath, inputSearchHash] = `${path}`.split('?');
|
|
66
|
+
let [inputSearch, inputHash] = inputSearchHash ? inputSearchHash.split('#') : [];
|
|
67
|
+
|
|
68
|
+
let sanitizedPath = (inputPath[0] !== '/' ? `/${inputPath}` : inputPath)
|
|
69
|
+
.trim()
|
|
70
|
+
.replaceAll('//', '/')
|
|
71
|
+
.replaceAll(`\\`, '/');
|
|
72
|
+
|
|
73
|
+
if (sanitizedPath.length > 1 && sanitizedPath[sanitizedPath.length - 1] === '/')
|
|
74
|
+
sanitizedPath = sanitizedPath.slice(0, -1);
|
|
75
|
+
|
|
76
|
+
const newFullPath = `${sanitizedPath}${inputSearch && !options.removeSearch ? `?${inputSearch}` : ''}${
|
|
77
|
+
inputHash && !options.removeHash ? `#${inputHash}` : ''
|
|
78
|
+
}`;
|
|
79
|
+
const currentFullPath = `${window.location.pathname}${location.search}${location.hash}`;
|
|
80
|
+
logger.warn(`Set path output`, {
|
|
81
|
+
inputPath: inputPath,
|
|
82
|
+
inputSearch: inputSearch,
|
|
83
|
+
inputHash: inputHash,
|
|
84
|
+
sanitizedPath: sanitizedPath,
|
|
85
|
+
currentLocationSearch: location.search,
|
|
86
|
+
currentLocationHash: location.hash,
|
|
87
|
+
currentFullPath,
|
|
88
|
+
newFullPath,
|
|
89
|
+
});
|
|
90
|
+
if (currentFullPath === newFullPath) {
|
|
91
|
+
logger.warn('Prevent overwriting same path', { currentFullPath, newFullPath });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
return history.pushState.call(history, stateStorage, title, newFullPath);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extracts query parameters from the current URL's search string and returns them as an object.
|
|
99
|
+
* @returns An object containing the query parameters from the current URL is being returned.
|
|
100
|
+
* @memberof PwaRouter
|
|
101
|
+
*/
|
|
102
|
+
const getQueryParams = () => {
|
|
103
|
+
const params = new URLSearchParams(window.location.search);
|
|
104
|
+
let queries = {};
|
|
105
|
+
for (const param of params) {
|
|
106
|
+
queries[param[0]] = param[1];
|
|
107
|
+
}
|
|
108
|
+
return queries;
|
|
109
|
+
};
|
|
13
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Sanitizes a route string for use in CSS classes or other identifiers.
|
|
113
|
+
* Defaults to 'home' for empty, '/', or '\' routes.
|
|
114
|
+
* @param {string} route - The route string to sanitize.
|
|
115
|
+
* @returns {string} The sanitized route string.
|
|
116
|
+
* @memberof PwaRouter
|
|
117
|
+
*/
|
|
14
118
|
const sanitizeRoute = (route) =>
|
|
15
119
|
!route || route === '/' || route === `\\`
|
|
16
120
|
? 'home'
|
|
17
121
|
: route.toLowerCase().replaceAll('/', '').replaceAll(`\\`, '').replaceAll(' ', '-');
|
|
18
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Sets the document title and updates the active state of the main menu button corresponding to the route.
|
|
125
|
+
* The title is formatted and appended with the main application title from `Worker.title` if it's not already present.
|
|
126
|
+
* @param {string} route - The current route string.
|
|
127
|
+
* @memberof PwaRouter
|
|
128
|
+
*/
|
|
19
129
|
const setDocTitle = (route) => {
|
|
20
130
|
const _route = sanitizeRoute(route);
|
|
21
131
|
// logger.warn('setDocTitle', _route);
|
|
@@ -27,6 +137,14 @@ const setDocTitle = (route) => {
|
|
|
27
137
|
}
|
|
28
138
|
};
|
|
29
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Main router function. It matches the current URL path against the provided routes configuration
|
|
142
|
+
* and renders the corresponding component. It also fires registered router events.
|
|
143
|
+
* @param {object} [options={ Routes: () => {}, e: new PopStateEvent() }] - The router options.
|
|
144
|
+
* @param {function} options.Routes - A function that returns the routes object.
|
|
145
|
+
* @param {PopStateEvent} options.e - The popstate event object.
|
|
146
|
+
* @memberof PwaRouter
|
|
147
|
+
*/
|
|
30
148
|
const Router = function (options = { Routes: () => {}, e: new PopStateEvent() }) {
|
|
31
149
|
const { e, Routes } = options;
|
|
32
150
|
const proxyPath = getProxyPath();
|
|
@@ -50,20 +168,43 @@ const Router = function (options = { Routes: () => {}, e: new PopStateEvent() })
|
|
|
50
168
|
}
|
|
51
169
|
};
|
|
52
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Initializes the router and sets up the `onpopstate` event listener to handle browser
|
|
173
|
+
* back/forward navigation.
|
|
174
|
+
* @param {object} RouterInstance - The router instance configuration, including the `Routes` function.
|
|
175
|
+
* @memberof PwaRouter
|
|
176
|
+
*/
|
|
53
177
|
const LoadRouter = function (RouterInstance) {
|
|
54
178
|
Router(RouterInstance);
|
|
55
179
|
window.onpopstate = (e) => Router({ ...RouterInstance, e });
|
|
56
180
|
};
|
|
57
181
|
|
|
58
|
-
|
|
59
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Sets the URL path with a specific query parameter, commonly used for content IDs.
|
|
184
|
+
* This function constructs a new URI based on the proxy path, a given path, and an optional query parameter.
|
|
185
|
+
* @param {object} [options={ path: '', queryPath: '' }] - The path options.
|
|
186
|
+
* @param {string} [options.path=''] - The base path segment.
|
|
187
|
+
* @memberof PwaRouter
|
|
188
|
+
*/
|
|
189
|
+
const setQueryPath = (options = { path: '', queryPath: '' }, queryKey = 'cid') => {
|
|
190
|
+
const { queryPath, path } = options;
|
|
60
191
|
const newUri = `${getProxyPath()}${path === 'home' ? '' : `${path}/`}${
|
|
61
192
|
typeof queryPath === 'string' && queryPath ? `?${queryKey}=${queryPath}` : ''
|
|
62
193
|
}`;
|
|
63
194
|
const currentUri = `${window.location.pathname}${location.search}`;
|
|
64
|
-
if (currentUri !== newUri && currentUri !== `${newUri}/`) setPath(newUri, {}, ''
|
|
195
|
+
if (currentUri !== newUri && currentUri !== `${newUri}/`) setPath(newUri, {}, '');
|
|
65
196
|
};
|
|
66
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Registers a listener for route changes that specifically watches for a `queryKey` parameter
|
|
200
|
+
* on a matching `routeId`. The provided event callback is triggered with the query parameter's value.
|
|
201
|
+
* @param {object} options - The listener options.
|
|
202
|
+
* @param {string} options.id - A unique ID for the listener.
|
|
203
|
+
* @param {string} options.routeId - The route ID to listen for.
|
|
204
|
+
* @param {function(string): void} options.event - The callback function to execute with the query path value (or an empty string if not found).
|
|
205
|
+
* @param {string} [queryKey='cid'] - The query parameter key to look for.
|
|
206
|
+
* @memberof PwaRouter
|
|
207
|
+
*/
|
|
67
208
|
const listenQueryPathInstance = ({ id, routeId, event }, queryKey = 'cid') => {
|
|
68
209
|
RouterEvents[id] = ({ path, pushPath, proxyPath, route }) => {
|
|
69
210
|
if ((route === '' && routeId === 'home') || (route && routeId && route === routeId)) {
|
|
@@ -80,14 +221,21 @@ const listenQueryPathInstance = ({ id, routeId, event }, queryKey = 'cid') => {
|
|
|
80
221
|
});
|
|
81
222
|
};
|
|
82
223
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Handles the logic for changing the route when a modal is closed. It determines the next URL
|
|
226
|
+
* based on the remaining open modals or falls back to a home URL.
|
|
227
|
+
* @param {object} [options={}] - Options for the modal close event.
|
|
228
|
+
* @param {string} options.closedId - The ID of the modal that was just closed.
|
|
229
|
+
* @memberof PwaRouter
|
|
230
|
+
*/
|
|
88
231
|
const closeModalRouteChangeEvent = (options = {}) => {
|
|
89
|
-
|
|
232
|
+
logger.warn('closeModalRouteChangeEvent', options);
|
|
233
|
+
const { closedId } = options;
|
|
90
234
|
if (!closedId) return;
|
|
235
|
+
if (coreUI.find((id) => closedId.startsWith(id))) {
|
|
236
|
+
logger.warn('prevent core ui component close');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
91
239
|
|
|
92
240
|
const remainingModals = Object.keys(Modal.Data).filter(
|
|
93
241
|
(id) => id !== closedId && (Modal.Data[id]?.options?.route || Modal.Data[id]?.options?.query),
|
|
@@ -95,27 +243,21 @@ const closeModalRouteChangeEvent = (options = {}) => {
|
|
|
95
243
|
|
|
96
244
|
const topModalId = remainingModals.reverse().find((id) => Modal.Data[id]);
|
|
97
245
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const path = route ? `${getProxyPath()}${route}` : location.pathname;
|
|
103
|
-
const newUrl = `${path}${query || ''}`;
|
|
104
|
-
|
|
105
|
-
triggerCloseModalRouteChangeEvents(newUrl);
|
|
106
|
-
setPath(newUrl, {}, '', { replace: true });
|
|
107
|
-
setDocTitle(route || path);
|
|
108
|
-
Modal.setTopModalCallback(topModalId);
|
|
109
|
-
} else {
|
|
110
|
-
const homeUrl = `${getProxyPath()}${homeCid ? `?cid=${homeCid}` : ''}`;
|
|
111
|
-
triggerCloseModalRouteChangeEvents(homeUrl);
|
|
112
|
-
setPath(homeUrl, {}, '', { replace: true });
|
|
113
|
-
setDocTitle('home');
|
|
114
|
-
}
|
|
246
|
+
for (const event of Object.keys(closeModalRouteChangeEvents)) closeModalRouteChangeEvents[event]();
|
|
247
|
+
if (topModalId) Modal.setTopModalCallback(topModalId);
|
|
248
|
+
setPath(`${getProxyPath()}${Modal.Data[topModalId]?.options?.route ?? ''}`);
|
|
249
|
+
setDocTitle(Modal.Data[topModalId]?.options?.route ?? '');
|
|
115
250
|
};
|
|
116
251
|
|
|
117
|
-
|
|
118
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Handles routing for modals that are meant to be displayed as a "view" (e.g., a full-page modal).
|
|
254
|
+
* It updates the URL to reflect the modal's route.
|
|
255
|
+
* @param {object} [options={ route: 'home' }] - The options for handling the modal view route.
|
|
256
|
+
* @param {string} options.route - The route associated with the modal view.
|
|
257
|
+
* @memberof PwaRouter
|
|
258
|
+
*/
|
|
259
|
+
const handleModalViewRoute = (options = { route: '' }) => {
|
|
260
|
+
const { route } = options;
|
|
119
261
|
if (!route) return;
|
|
120
262
|
|
|
121
263
|
let path = window.location.pathname;
|
|
@@ -129,14 +271,44 @@ const handleModalViewRoute = (options = {}) => {
|
|
|
129
271
|
}
|
|
130
272
|
};
|
|
131
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Sets or updates query parameters in the URL.
|
|
276
|
+
* It preserves the existing path, hash, and other query parameters.
|
|
277
|
+
*
|
|
278
|
+
* @param {Object.<string, string|number>} newParams - An object of query parameters to set or update.
|
|
279
|
+
* If a value is `null` or `undefined`, the parameter will be removed.
|
|
280
|
+
* @param {object} [options={ replace: true }] - Options for history manipulation.
|
|
281
|
+
* @param {boolean} [options.replace=true] - If true, uses `history.replaceState` instead of `history.pushState`.
|
|
282
|
+
* @memberof PwaRouter
|
|
283
|
+
*/
|
|
284
|
+
const setQueryParams = (newParams, options = { replace: true }) => {
|
|
285
|
+
const url = new URL(window.location.href);
|
|
286
|
+
Object.entries(newParams).forEach(([key, value]) => {
|
|
287
|
+
if (value === null || value === undefined) {
|
|
288
|
+
url.searchParams.delete(key);
|
|
289
|
+
} else {
|
|
290
|
+
url.searchParams.set(key, value);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const newPath = url.pathname + url.search + url.hash;
|
|
295
|
+
|
|
296
|
+
history.pushState(history.state, '', newPath);
|
|
297
|
+
};
|
|
298
|
+
|
|
132
299
|
export {
|
|
300
|
+
RouterEvents,
|
|
301
|
+
closeModalRouteChangeEvents,
|
|
302
|
+
coreUI,
|
|
133
303
|
Router,
|
|
134
304
|
setDocTitle,
|
|
135
305
|
LoadRouter,
|
|
136
|
-
RouterEvents,
|
|
137
306
|
setQueryPath,
|
|
138
307
|
listenQueryPathInstance,
|
|
139
308
|
closeModalRouteChangeEvent,
|
|
140
309
|
handleModalViewRoute,
|
|
141
|
-
|
|
310
|
+
getQueryParams,
|
|
311
|
+
getProxyPath,
|
|
312
|
+
setPath,
|
|
313
|
+
setQueryParams,
|
|
142
314
|
};
|