@underpostnet/underpost 2.97.1 → 2.98.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 (63) hide show
  1. package/README.md +2 -2
  2. package/cli.md +3 -1
  3. package/conf.js +2 -0
  4. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  5. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  6. package/package.json +1 -1
  7. package/scripts/rocky-pwa.sh +200 -0
  8. package/src/api/core/core.service.js +0 -5
  9. package/src/api/default/default.service.js +7 -5
  10. package/src/api/document/document.model.js +1 -1
  11. package/src/api/document/document.router.js +5 -0
  12. package/src/api/document/document.service.js +176 -128
  13. package/src/api/file/file.model.js +112 -4
  14. package/src/api/file/file.ref.json +42 -0
  15. package/src/api/file/file.service.js +380 -32
  16. package/src/api/user/user.model.js +38 -1
  17. package/src/api/user/user.router.js +96 -63
  18. package/src/api/user/user.service.js +81 -48
  19. package/src/cli/db.js +424 -166
  20. package/src/cli/index.js +8 -0
  21. package/src/cli/repository.js +1 -1
  22. package/src/cli/run.js +1 -0
  23. package/src/cli/ssh.js +10 -10
  24. package/src/client/components/core/Account.js +327 -36
  25. package/src/client/components/core/AgGrid.js +3 -0
  26. package/src/client/components/core/Auth.js +11 -3
  27. package/src/client/components/core/Chat.js +2 -2
  28. package/src/client/components/core/Content.js +161 -80
  29. package/src/client/components/core/Css.js +30 -0
  30. package/src/client/components/core/CssCore.js +16 -12
  31. package/src/client/components/core/FileExplorer.js +813 -49
  32. package/src/client/components/core/Input.js +207 -12
  33. package/src/client/components/core/LogIn.js +42 -20
  34. package/src/client/components/core/Modal.js +138 -24
  35. package/src/client/components/core/Panel.js +71 -32
  36. package/src/client/components/core/PanelForm.js +262 -77
  37. package/src/client/components/core/PublicProfile.js +888 -0
  38. package/src/client/components/core/Responsive.js +15 -7
  39. package/src/client/components/core/Router.js +117 -15
  40. package/src/client/components/core/SearchBox.js +322 -116
  41. package/src/client/components/core/SignUp.js +26 -7
  42. package/src/client/components/core/SocketIo.js +6 -3
  43. package/src/client/components/core/Translate.js +148 -0
  44. package/src/client/components/core/Validator.js +15 -0
  45. package/src/client/components/core/windowGetDimensions.js +6 -6
  46. package/src/client/components/default/MenuDefault.js +59 -12
  47. package/src/client/components/default/RoutesDefault.js +1 -0
  48. package/src/client/services/core/core.service.js +163 -1
  49. package/src/client/services/default/default.management.js +454 -76
  50. package/src/client/services/default/default.service.js +13 -6
  51. package/src/client/services/file/file.service.js +43 -16
  52. package/src/client/services/user/user.service.js +13 -9
  53. package/src/client/sw/default.sw.js +107 -184
  54. package/src/db/DataBaseProvider.js +1 -1
  55. package/src/db/mongo/MongooseDB.js +1 -1
  56. package/src/index.js +1 -1
  57. package/src/mailer/MailerProvider.js +4 -4
  58. package/src/runtime/express/Express.js +2 -1
  59. package/src/runtime/lampp/Lampp.js +2 -2
  60. package/src/server/auth.js +3 -6
  61. package/src/server/data-query.js +449 -0
  62. package/src/server/object-layer.js +0 -3
  63. package/src/ws/IoInterface.js +2 -2
@@ -46,13 +46,21 @@ const Responsive = {
46
46
  // alternative option
47
47
  // this.Observer = new ResizeObserver(this.resizeCallback);
48
48
  // this.Observer.observe(document.documentElement);
49
- screen.orientation.addEventListener('change', (event) => {
50
- const type = event.target.type; // landscape-primary | portrait-primary
51
- const angle = event.target.angle; // 90 degrees.
52
- logger.info(`ScreenOrientation change: ${type}, ${angle} degrees.`);
53
- setTimeout(() => window.onresize({}, true));
54
- Responsive.triggerEventsOrientation();
55
- });
49
+
50
+ // Check if screen.orientation is available before adding event listener
51
+ if (
52
+ typeof screen !== 'undefined' &&
53
+ screen.orientation &&
54
+ typeof screen.orientation.addEventListener === 'function'
55
+ ) {
56
+ screen.orientation.addEventListener('change', (event) => {
57
+ const type = event.target.type; // landscape-primary | portrait-primary
58
+ const angle = event.target.angle; // 90 degrees.
59
+ logger.info(`ScreenOrientation change: ${type}, ${angle} degrees.`);
60
+ setTimeout(() => window.onresize({}, true));
61
+ Responsive.triggerEventsOrientation();
62
+ });
63
+ }
56
64
  Responsive.matchMediaOrientationInstance = matchMedia('screen and (orientation:portrait)');
57
65
 
58
66
  Responsive.matchMediaOrientationInstance.onchange = (e) => {
@@ -55,17 +55,24 @@ const getProxyPath = () => {
55
55
 
56
56
  /**
57
57
  * Sets the browser's path using the History API. It sanitizes the path, handles query strings and hashes,
58
- * and prevents pushing the same state twice.
58
+ * and prevents pushing the same state twice unless forced or using replace mode.
59
59
  * @param {string} [path='/'] - The new path to set. Can include query strings and hashes.
60
- * @param {object} [options={ removeSearch: false, removeHash: false }] - Options for path manipulation.
60
+ * @param {object} [options={ removeSearch: false, removeHash: false, replace: false, force: false }] - Options for path manipulation.
61
61
  * @param {boolean} [options.removeSearch=false] - If true, removes the search part of the URL.
62
62
  * @param {boolean} [options.removeHash=false] - If true, removes the hash part of the URL. Defaults to `false`.
63
+ * @param {boolean} [options.replace=false] - If true, uses replaceState instead of pushState.
64
+ * @param {boolean} [options.force=false] - If true, allows navigation to the same path (useful for query param changes).
63
65
  * @param {object} [stateStorage={}] - State object to associate with the history entry.
64
66
  * @param {string} [title=''] - The title for the new history entry.
65
67
  * @memberof PwaRouter
66
- * @returns {void | undefined} Returns `undefined` if the new path is the same as the current path, otherwise `void` (result of `history.pushState`).
68
+ * @returns {void | undefined} Returns `undefined` if the new path is the same as the current path and not forced, otherwise `void` (result of `history.pushState` or `history.replaceState`).
67
69
  */
68
- const setPath = (path = '/', options = { removeSearch: false, removeHash: false }, stateStorage = {}, title = '') => {
70
+ const setPath = (
71
+ path = '/',
72
+ options = { removeSearch: false, removeHash: false, replace: false, force: false },
73
+ stateStorage = {},
74
+ title = '',
75
+ ) => {
69
76
  // logger.warn(`Set path input`, `${path}`);
70
77
  if (!path) path = '/';
71
78
 
@@ -94,11 +101,16 @@ const setPath = (path = '/', options = { removeSearch: false, removeHash: false
94
101
  // currentFullPath,
95
102
  // newFullPath,
96
103
  // });
97
- if (currentFullPath === newFullPath) {
104
+ if (currentFullPath === newFullPath && !options.force) {
98
105
  // logger.warn('Prevent overwriting same path', { currentFullPath, newFullPath });
99
106
  return;
100
107
  }
101
- return history.pushState.call(history, stateStorage, title, newFullPath);
108
+
109
+ if (options.replace) {
110
+ return history.replaceState.call(history, stateStorage, title, newFullPath);
111
+ } else {
112
+ return history.pushState.call(history, stateStorage, title, newFullPath);
113
+ }
102
114
  };
103
115
 
104
116
  /**
@@ -139,13 +151,15 @@ const sanitizeRoute = (route) =>
139
151
  * @memberof PwaRouter
140
152
  */
141
153
  const setDocTitle = (route) => {
142
- const _route = sanitizeRoute(route);
154
+ let _route = sanitizeRoute(route);
143
155
  // logger.warn('setDocTitle', _route);
144
156
  const title = titleFormatted(_route);
145
157
  htmls('title', html`${title}${title.match(Worker.title.toLowerCase()) ? '' : ` | ${Worker.title}`}`);
146
- if (s(`.main-btn-${_route}`)) {
158
+
159
+ const btnSelector = _route === 'u' ? 'public-profile' : _route;
160
+ if (s(`.main-btn-${btnSelector}`)) {
147
161
  if (s(`.main-btn-menu-active`)) s(`.main-btn-menu-active`).classList.remove(`main-btn-menu-active`);
148
- if (s(`.main-btn-${_route}`)) s(`.main-btn-${_route}`).classList.add(`main-btn-menu-active`);
162
+ if (s(`.main-btn-${btnSelector}`)) s(`.main-btn-${btnSelector}`).classList.add(`main-btn-menu-active`);
149
163
  }
150
164
  };
151
165
 
@@ -168,11 +182,19 @@ const Router = function (options = { Routes: () => {}, e: new PopStateEvent() })
168
182
  let pushPath = `${proxyPath}${route}`;
169
183
 
170
184
  if (path[path.length - 1] !== '/') path = `${path}/`;
185
+ // Handle clean profile URLs: match /u/username with /u route
186
+ let matchPath = path;
187
+ if (route === 'u' && path.startsWith(`${proxyPath}u/`) && path !== `${proxyPath}u/`) {
188
+ handleCleanProfileUrl(path);
189
+ matchPath = `${proxyPath}u/`;
190
+ }
191
+
192
+ if (matchPath[matchPath.length - 1] !== '/') matchPath = `${matchPath}/`;
171
193
  if (pushPath[pushPath.length - 1] !== '/') pushPath = `${pushPath}/`;
172
194
 
173
- const routerEvent = { path, pushPath, route };
195
+ const routerEvent = { path: matchPath, pushPath, route };
174
196
 
175
- if (path === pushPath) {
197
+ if (matchPath === pushPath) {
176
198
  for (const event of Object.keys(RouterEvents)) RouterEvents[event](routerEvent);
177
199
  subMenuHandler(Object.keys(Routes()), route);
178
200
  setDocTitle(route);
@@ -206,15 +228,81 @@ const LoadRouter = function (RouterInstance) {
206
228
  * This function constructs a new URI based on the proxy path, a given path, and an optional query parameter.
207
229
  * @param {object} [options={ path: '', queryPath: '' }] - The path options.
208
230
  * @param {string} [options.path=''] - The base path segment.
231
+ * @param {string} [options.queryPath=''] - The query parameter value.
232
+ * @param {string} [queryKey='cid'] - The query parameter key.
209
233
  * @memberof PwaRouter
210
234
  */
211
- const setQueryPath = (options = { path: '', queryPath: '' }, queryKey = 'cid') => {
235
+ const setQueryPath = (options = { path: '', queryPath: '' }, queryKey = 'cid', navOptions = {}) => {
212
236
  const { queryPath, path } = options;
213
- const newUri = `${getProxyPath()}${path === 'home' ? '' : `${path}/`}${
237
+ const { replace = false } = navOptions;
238
+ const newUri = `${getProxyPath()}${path === 'home' ? '' : `${path}`}${
214
239
  typeof queryPath === 'string' && queryPath ? `?${queryKey}=${queryPath}` : ''
215
240
  }`;
216
241
  const currentUri = `${window.location.pathname}${location.search}`;
217
- if (currentUri !== newUri && currentUri !== `${newUri}/`) setPath(newUri, {}, '');
242
+
243
+ // For query parameter changes on the same path, force the navigation to ensure proper history
244
+ const isSamePath = window.location.pathname === new URL(newUri, window.location.origin).pathname;
245
+ const isDifferentQuery = window.location.search !== new URL(newUri, window.location.origin).search;
246
+ const shouldForce = isSamePath && isDifferentQuery;
247
+
248
+ if (currentUri !== newUri && currentUri !== `${newUri}/`) {
249
+ setPath(newUri, { force: shouldForce, replace }, '');
250
+ }
251
+ };
252
+
253
+ /**
254
+ * Extracts username from clean public profile URLs like /u/username.
255
+ * @param {string} [pathname] - The pathname to extract from (defaults to current pathname).
256
+ * @returns {string|null} The username if found, null otherwise.
257
+ * @memberof PwaRouter
258
+ */
259
+ const extractUsernameFromPath = (pathname = window.location.pathname) => {
260
+ const proxyPath = getProxyPath();
261
+ const cleanPathPrefix = `${proxyPath}u/`.replace(/\/+/g, '/');
262
+
263
+ if (pathname.startsWith(cleanPathPrefix)) {
264
+ const username = pathname.slice(cleanPathPrefix.length).split('/')[0];
265
+ return username || null;
266
+ }
267
+ return null;
268
+ };
269
+
270
+ /**
271
+ * Handles direct navigation to clean public profile URLs.
272
+ * Converts clean URLs like /u/username to internal query format for SPA.
273
+ * @param {string} [pathname] - The pathname to handle (defaults to current pathname).
274
+ * @returns {boolean} True if this was a public profile URL that was handled.
275
+ * @memberof PwaRouter
276
+ */
277
+ const handleCleanProfileUrl = (pathname = window.location.pathname) => {
278
+ const username = extractUsernameFromPath(pathname);
279
+ if (username) {
280
+ // Convert clean URL to internal query format for data fetching
281
+ // Don't modify history - just return the username for the caller to use
282
+ return username;
283
+ }
284
+ return null;
285
+ };
286
+
287
+ /**
288
+ * Navigates to a public profile URL without adding intermediate query URLs to history.
289
+ * This ensures clean back/forward navigation between profiles.
290
+ * @param {string} username - The username to navigate to.
291
+ * @param {object} [options={}] - Navigation options.
292
+ * @param {boolean} [options.replace=false] - If true, replaces current history entry instead of pushing.
293
+ * @memberof PwaRouter
294
+ */
295
+ const navigateToProfile = (username, options = {}) => {
296
+ const { replace = false } = options;
297
+ if (!username) return;
298
+
299
+ const cleanPath = `${getProxyPath()}u/${username}`;
300
+ const currentPath = window.location.pathname;
301
+
302
+ // If we're already on this profile's clean URL, no navigation needed
303
+ if (currentPath === cleanPath) return;
304
+ // Navigate directly to clean URL, avoiding intermediate ?cid= URLs in history
305
+ setPath(cleanPath, { replace });
218
306
  };
219
307
 
220
308
  /**
@@ -304,7 +392,12 @@ const handleModalViewRoute = (options = { RouterInstance: { Routes: () => {} },
304
392
  const newPath = `${proxyPath}${route}`;
305
393
  if (RouterInstance && RouterInstance.Routes) subMenuHandler(Object.keys(RouterInstance.Routes()), route);
306
394
 
307
- if (path !== newPath) {
395
+ // Check if we're already on this route or a sub-path of it (e.g., /u/username for route 'u')
396
+ // Don't push to history if already on the route or its sub-path
397
+ const routeBasePath = `${proxyPath}${route}`;
398
+ const isOnRouteOrSubPath = path === newPath || path.startsWith(`${routeBasePath}/`);
399
+
400
+ if (!isOnRouteOrSubPath) {
308
401
  setPath(newPath);
309
402
  setDocTitle(newPath);
310
403
  }
@@ -331,6 +424,12 @@ const setQueryParams = (newParams, options = { replace: true }) => {
331
424
  });
332
425
 
333
426
  const newPath = url.pathname + url.search + url.hash;
427
+ const currentPath = window.location.pathname + window.location.search + window.location.hash;
428
+
429
+ // Only update history and trigger listeners if the URL actually changed
430
+ if (newPath === currentPath) {
431
+ return;
432
+ }
334
433
 
335
434
  if (options.replace) {
336
435
  history.replaceState(history.state, '', newPath);
@@ -348,12 +447,15 @@ const setQueryParams = (newParams, options = { replace: true }) => {
348
447
 
349
448
  export {
350
449
  RouterEvents,
450
+ navigateToProfile,
351
451
  closeModalRouteChangeEvents,
352
452
  coreUI,
353
453
  Router,
354
454
  setDocTitle,
355
455
  LoadRouter,
356
456
  setQueryPath,
457
+ extractUsernameFromPath,
458
+ handleCleanProfileUrl,
357
459
  listenQueryPathInstance,
358
460
  closeModalRouteChangeEvent,
359
461
  handleModalViewRoute,