underpost 3.2.8 → 3.2.10
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/.github/workflows/npmpkg.ci.yml +1 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/settings.json +10 -5
- package/CHANGELOG.md +223 -2
- package/CLI-HELP.md +36 -7
- package/README.md +38 -9
- package/bin/build.js +27 -11
- package/bin/deploy.js +20 -21
- package/bin/file.js +32 -13
- package/bin/index.js +2 -1
- package/bin/vs.js +1 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -4
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/package.json +40 -25
- package/scripts/k3s-node-setup.sh +30 -11
- package/scripts/nat-iptables.sh +103 -18
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +20 -11
- package/src/cli/cluster.js +243 -55
- package/src/cli/db.js +106 -62
- package/src/cli/deploy.js +297 -154
- package/src/cli/fs.js +19 -3
- package/src/cli/index.js +37 -9
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +217 -135
- package/src/cli/release.js +289 -131
- package/src/cli/repository.js +91 -34
- package/src/cli/run.js +297 -56
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +19 -5
- package/src/client/components/core/Docs.js +6 -34
- package/src/client/components/core/FileExplorer.js +6 -6
- package/src/client/components/core/Modal.js +65 -2
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Recover.js +4 -4
- package/src/client/components/core/Worker.js +170 -350
- package/src/client/services/default/default.management.js +20 -25
- package/src/client/services/user/guest.service.js +10 -3
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +120 -20
- package/src/db/mongo/MongoBootstrap.js +587 -0
- package/src/db/mongo/MongooseDB.js +126 -22
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +20 -65
- package/src/server/data-query.js +32 -20
- package/src/server/dns.js +22 -0
- package/src/server/process.js +180 -19
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +26 -7
- package/src/server/valkey.js +9 -2
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/typedoc.json +10 -1
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /package/src/client/ssr/{pages → views}/Test.js +0 -0
|
@@ -255,9 +255,11 @@ class Auth {
|
|
|
255
255
|
* @returns {Promise<object>} A promise resolving to the newly created guest session data.
|
|
256
256
|
*/
|
|
257
257
|
async sessionOut() {
|
|
258
|
-
// 1. End User Session
|
|
258
|
+
// 1. End User Session — skip the network call when there is no active user session
|
|
259
|
+
// (avoids a wasted DELETE round-trip for guests / new visitors)
|
|
260
|
+
const hasUserSession = !!GuestService.getUserToken();
|
|
259
261
|
try {
|
|
260
|
-
const result = await UserService.delete({ id: 'logout' });
|
|
262
|
+
const result = hasUserSession ? await UserService.delete({ id: 'logout' }) : null;
|
|
261
263
|
SearchBox.RecentResults.clear();
|
|
262
264
|
this.deleteToken();
|
|
263
265
|
if (this.#refreshTimeout) {
|
|
@@ -267,7 +269,7 @@ class Auth {
|
|
|
267
269
|
this.renderGuestUi();
|
|
268
270
|
// Reset user data in the LogIn state/model
|
|
269
271
|
LogIn.Scope.user.main.model.user = {};
|
|
270
|
-
await LogOut.Trigger(result);
|
|
272
|
+
if (result) await LogOut.Trigger(result);
|
|
271
273
|
} catch (error) {
|
|
272
274
|
logger.error('Error during user logout:', error);
|
|
273
275
|
}
|
|
@@ -279,8 +281,16 @@ class Auth {
|
|
|
279
281
|
|
|
280
282
|
if (result.status === 'success' && result.data.token) {
|
|
281
283
|
this.setGuestToken(result.data.token);
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
+
// Use the POST response data directly — avoids a redundant GET /user/auth
|
|
285
|
+
// round-trip that would otherwise re-verify the token we just received.
|
|
286
|
+
await GuestService.setMeta('guest-session', {
|
|
287
|
+
role: result.data.user?.role,
|
|
288
|
+
userId: result.data.user?._id,
|
|
289
|
+
});
|
|
290
|
+
LogIn.Scope.user.main.model.user = {};
|
|
291
|
+
await LogIn.Trigger(result.data);
|
|
292
|
+
await Account.updateForm(result.data.user);
|
|
293
|
+
return result.data;
|
|
284
294
|
} else {
|
|
285
295
|
logger.error('Failed to get a new guest token.');
|
|
286
296
|
return { user: UserMock.default };
|
|
@@ -309,6 +319,10 @@ class Auth {
|
|
|
309
319
|
// Close any open login/signup modals
|
|
310
320
|
if (s(`.modal-log-in`)) s(`.btn-close-modal-log-in`).click();
|
|
311
321
|
if (s(`.modal-sign-up`)) s(`.btn-close-modal-sign-up`).click();
|
|
322
|
+
if (!s(`.main-body-btn-ui-open`).classList.contains('hide'))
|
|
323
|
+
s(`.main-body-btn-ui-open`).click();
|
|
324
|
+
if (!s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide'))
|
|
325
|
+
s(`.main-body-btn-ui-bar-custom-open`).click();
|
|
312
326
|
});
|
|
313
327
|
}
|
|
314
328
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Css, darkTheme, renderCssAttr, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
|
|
4
|
-
import { buildBadgeToolTipMenuOption, Modal, renderViewTitle } from './Modal.js';
|
|
1
|
+
import { Css, darkTheme, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
|
|
2
|
+
import { Modal, renderViewTitle } from './Modal.js';
|
|
5
3
|
import { listenQueryPathInstance, setQueryPath, closeModalRouteChangeEvent, getProxyPath } from './Router.js';
|
|
6
|
-
import {
|
|
4
|
+
import { s, sIframe } from './VanillaJs.js';
|
|
7
5
|
// https://mintlify.com/docs/quickstart
|
|
8
6
|
class Docs {
|
|
9
7
|
static async RenderModal(type) {
|
|
@@ -289,41 +287,15 @@ class Docs {
|
|
|
289
287
|
},
|
|
290
288
|
});
|
|
291
289
|
});
|
|
292
|
-
|
|
290
|
+
// Register theme events for items that have them (Docs-specific concern)
|
|
293
291
|
for (const docData of Docs.Data) {
|
|
294
292
|
if (docData.themeEvent) {
|
|
295
293
|
ThemeEvents[`doc-icon-${docData.type}`] = docData.themeEvent;
|
|
296
294
|
setTimeout(ThemeEvents[`doc-icon-${docData.type}`]);
|
|
297
295
|
}
|
|
298
|
-
let tabHref, style, labelStyle;
|
|
299
|
-
switch (docData.type) {
|
|
300
|
-
case 'repo':
|
|
301
|
-
case 'coverage-link':
|
|
302
|
-
style = renderCssAttr({ style: { height: '45px' } });
|
|
303
|
-
labelStyle = renderCssAttr({ style: { top: '8px', left: '9px' } });
|
|
304
|
-
break;
|
|
305
|
-
default:
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
tabHref = docData.url();
|
|
309
|
-
const subMenuIcon =
|
|
310
|
-
options.subMenuIcon && typeof options.subMenuIcon === 'function'
|
|
311
|
-
? options.subMenuIcon(docData.type)
|
|
312
|
-
: docData.icon;
|
|
313
|
-
docMenuRender += html`
|
|
314
|
-
${await BtnIcon.instance({
|
|
315
|
-
class: `in wfa main-btn-menu submenu-btn btn-docs btn-docs-${docData.type}`,
|
|
316
|
-
label: html`<span class="inl menu-btn-icon">${subMenuIcon}</span
|
|
317
|
-
><span class="menu-label-text menu-label-text-docs"> ${docData.text} </span>`,
|
|
318
|
-
tabHref,
|
|
319
|
-
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption(docData.text, 'right')),
|
|
320
|
-
useMenuBtn: true,
|
|
321
|
-
})}
|
|
322
|
-
`;
|
|
323
296
|
}
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
htmls('.menu-btn-container-children-docs', docMenuRender);
|
|
297
|
+
// Build submenu items and populate — submenu system is owned by Modal
|
|
298
|
+
Modal.subMenuPopulate('docs', await Modal.buildSubMenuItemsHtml('docs', Docs.Data, options));
|
|
327
299
|
{
|
|
328
300
|
const docsData = [
|
|
329
301
|
{
|
|
@@ -469,12 +469,12 @@ class FileExplorer {
|
|
|
469
469
|
async init(params) {
|
|
470
470
|
console.log('LoadFileActionsRenderer created', params);
|
|
471
471
|
// params.data._id
|
|
472
|
-
|
|
472
|
+
this.eGui = document.createElement('div');
|
|
473
473
|
const isPublic = params.data.isPublic;
|
|
474
474
|
const toggleId = `toggle-public-${params.data._id}`;
|
|
475
475
|
const hasGenericFile = !!params.data.hasGenericFile;
|
|
476
476
|
const hasMdFile = !!params.data.hasMdFile;
|
|
477
|
-
|
|
477
|
+
this.eGui.innerHTML = html`
|
|
478
478
|
<div class="fl">
|
|
479
479
|
${await BtnIcon.instance({
|
|
480
480
|
class: `in fll management-table-btn-mini btn-file-download-${params.data._id}${!hasGenericFile ? ' btn-disabled' : ''}`,
|
|
@@ -981,7 +981,7 @@ class FileExplorer {
|
|
|
981
981
|
});
|
|
982
982
|
}
|
|
983
983
|
getGui() {
|
|
984
|
-
return
|
|
984
|
+
return this.eGui;
|
|
985
985
|
}
|
|
986
986
|
refresh(params) {
|
|
987
987
|
console.log('LoadFileActionsRenderer refreshed', params);
|
|
@@ -994,8 +994,8 @@ class FileExplorer {
|
|
|
994
994
|
console.log('LoadFolderActionsRenderer created', params);
|
|
995
995
|
// params.data._id
|
|
996
996
|
const id = params.data.locationId;
|
|
997
|
-
|
|
998
|
-
|
|
997
|
+
this.eGui = document.createElement('div');
|
|
998
|
+
this.eGui.innerHTML = html`
|
|
999
999
|
<div class="fl">
|
|
1000
1000
|
${await BtnIcon.instance({
|
|
1001
1001
|
class: `in fll management-table-btn-mini btn-folder-delete-${id}`,
|
|
@@ -1058,7 +1058,7 @@ class FileExplorer {
|
|
|
1058
1058
|
});
|
|
1059
1059
|
}
|
|
1060
1060
|
getGui() {
|
|
1061
|
-
return
|
|
1061
|
+
return this.eGui;
|
|
1062
1062
|
}
|
|
1063
1063
|
refresh(params) {
|
|
1064
1064
|
console.log('LoadFolderActionsRenderer refreshed', params);
|
|
@@ -1789,6 +1789,12 @@ class Modal {
|
|
|
1789
1789
|
if (options.onCollapseMenu) options.onCollapseMenu();
|
|
1790
1790
|
s(`.sub-menu-title-container-${'modal-menu'}`).classList.add('hide');
|
|
1791
1791
|
s(`.nav-path-container-${'modal-menu'}`).classList.add('hide');
|
|
1792
|
+
// Shrink any already-open submenu containers to icon-only width
|
|
1793
|
+
Object.keys(Modal.subMenuBtnClass).forEach((subMenuId) => {
|
|
1794
|
+
const container = s(`.menu-btn-container-children-${subMenuId}`);
|
|
1795
|
+
if (container && container.style.height && container.style.height !== '0px')
|
|
1796
|
+
container.style.width = `${collapseSlideMenuWidth}px`;
|
|
1797
|
+
});
|
|
1792
1798
|
Object.keys(this.Data[idModal].onCollapseMenuListener).map((keyListener) =>
|
|
1793
1799
|
this.Data[idModal].onCollapseMenuListener[keyListener](),
|
|
1794
1800
|
);
|
|
@@ -1806,6 +1812,12 @@ class Modal {
|
|
|
1806
1812
|
if (options.onExtendMenu) options.onExtendMenu();
|
|
1807
1813
|
s(`.sub-menu-title-container-${'modal-menu'}`).classList.remove('hide');
|
|
1808
1814
|
s(`.nav-path-container-${'modal-menu'}`).classList.remove('hide');
|
|
1815
|
+
// Expand any already-open submenu containers back to full width
|
|
1816
|
+
Object.keys(Modal.subMenuBtnClass).forEach((subMenuId) => {
|
|
1817
|
+
const container = s(`.menu-btn-container-children-${subMenuId}`);
|
|
1818
|
+
if (container && container.style.height && container.style.height !== '0px')
|
|
1819
|
+
container.style.width = `${originSlideMenuWidth}px`;
|
|
1820
|
+
});
|
|
1809
1821
|
Object.keys(this.Data[idModal].onExtendMenuListener).map((keyListener) =>
|
|
1810
1822
|
this.Data[idModal].onExtendMenuListener[keyListener](),
|
|
1811
1823
|
);
|
|
@@ -2464,6 +2476,52 @@ class Modal {
|
|
|
2464
2476
|
}
|
|
2465
2477
|
};
|
|
2466
2478
|
|
|
2479
|
+
/**
|
|
2480
|
+
* Builds submenu item HTML using the canonical BtnIcon pattern.
|
|
2481
|
+
* Centralises the item-rendering contract so individual submenu owners (e.g. Docs)
|
|
2482
|
+
* only need to supply data — not layout concerns.
|
|
2483
|
+
* @param {string} subMenuId - Submenu identifier (e.g. 'docs')
|
|
2484
|
+
* @param {Array<{type:string, icon:string, text:string, url:function|string}>} items
|
|
2485
|
+
* @param {Object} [options]
|
|
2486
|
+
* @param {function} [options.subMenuIcon] - Optional icon override per item type
|
|
2487
|
+
* @returns {Promise<string>} Rendered HTML string
|
|
2488
|
+
*/
|
|
2489
|
+
static buildSubMenuItemsHtml = async (subMenuId, items = [], options = {}) => {
|
|
2490
|
+
let result = '';
|
|
2491
|
+
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
|
|
2492
|
+
const _tooltipSide = options.tooltipSide || (_menuMode === 'slide-menu-right' ? 'right' : 'left');
|
|
2493
|
+
for (const item of items) {
|
|
2494
|
+
const tabHref = typeof item.url === 'function' ? item.url() : item.url || '';
|
|
2495
|
+
const icon =
|
|
2496
|
+
options.subMenuIcon && typeof options.subMenuIcon === 'function' ? options.subMenuIcon(item.type) : item.icon;
|
|
2497
|
+
result += html`${await BtnIcon.instance({
|
|
2498
|
+
class: `in wfa main-btn-menu submenu-btn btn-${subMenuId} btn-${subMenuId}-${item.type}`,
|
|
2499
|
+
label: html`<span class="inl menu-btn-icon">${icon}</span
|
|
2500
|
+
><span class="menu-label-text menu-label-text-${subMenuId}"> ${item.text} </span>`,
|
|
2501
|
+
tabHref,
|
|
2502
|
+
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption(item.text, _tooltipSide)),
|
|
2503
|
+
useMenuBtn: true,
|
|
2504
|
+
})} `;
|
|
2505
|
+
}
|
|
2506
|
+
return result;
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
/**
|
|
2510
|
+
* Injects pre-built submenu item HTML into the submenu container and syncs
|
|
2511
|
+
* the collapse state so labels are hidden when the menu is icon-only.
|
|
2512
|
+
* @param {string} subMenuId
|
|
2513
|
+
* @param {string} itemsHtml
|
|
2514
|
+
*/
|
|
2515
|
+
static subMenuPopulate = (subMenuId, itemsHtml) => {
|
|
2516
|
+
htmls(`.menu-btn-container-children-${subMenuId}`, itemsHtml);
|
|
2517
|
+
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
|
|
2518
|
+
const _collapseIndicatorClass =
|
|
2519
|
+
_menuMode === 'slide-menu-right' ? '.btn-icon-menu-mode-left' : '.btn-icon-menu-mode-right';
|
|
2520
|
+
if (s(_collapseIndicatorClass) && !s(_collapseIndicatorClass).classList.contains('hide')) {
|
|
2521
|
+
sa(`.menu-label-text-${subMenuId}`).forEach((el) => el.classList.add('hide'));
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2467
2525
|
// Move modal title element into the bar's render container so it aligns with control buttons
|
|
2468
2526
|
/**
|
|
2469
2527
|
* Position a modal relative to an anchor element.
|
|
@@ -2769,9 +2827,14 @@ const subMenuRender = async (subMenuId) => {
|
|
|
2769
2827
|
setTimeout(() => {
|
|
2770
2828
|
Modal.menuTextLabelAnimation('modal-menu', subMenuId);
|
|
2771
2829
|
});
|
|
2772
|
-
// Open animation
|
|
2830
|
+
// Open animation — match the current menu width (collapsed = 50px, extended = 320px)
|
|
2831
|
+
const _menuMode = Modal.Data['modal-menu']?.options?.mode;
|
|
2832
|
+
const _collapseIndicatorClass =
|
|
2833
|
+
_menuMode === 'slide-menu-right' ? '.btn-icon-menu-mode-left' : '.btn-icon-menu-mode-right';
|
|
2834
|
+
const _isMenuCollapsed = s(_collapseIndicatorClass) && !s(_collapseIndicatorClass).classList.contains('hide');
|
|
2835
|
+
const _menuContainerWidth = _isMenuCollapsed ? 50 : 320;
|
|
2773
2836
|
setTimeout(top, 360);
|
|
2774
|
-
menuContainer.style.width =
|
|
2837
|
+
menuContainer.style.width = `${_menuContainerWidth}px`;
|
|
2775
2838
|
menuContainer.style.overflow = null;
|
|
2776
2839
|
menuContainer.style.height = '0px';
|
|
2777
2840
|
menuContainer.style.height = `${_hBtn * 6}px`;
|
|
@@ -73,7 +73,7 @@ class PanelForm {
|
|
|
73
73
|
parentIdModal: undefined,
|
|
74
74
|
route: 'home',
|
|
75
75
|
htmlFormHeader: async () => '',
|
|
76
|
-
firsUpdateEvent: async () => {},
|
|
76
|
+
firsUpdateEvent: async () => { },
|
|
77
77
|
share: {
|
|
78
78
|
copyLink: false,
|
|
79
79
|
copySourceMd: false,
|
|
@@ -196,12 +196,12 @@ class PanelForm {
|
|
|
196
196
|
<img
|
|
197
197
|
class="abs center"
|
|
198
198
|
style="${renderCssAttr({
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
style: {
|
|
200
|
+
width: '100px',
|
|
201
|
+
height: '100px',
|
|
202
|
+
opacity: 0.2,
|
|
203
|
+
},
|
|
204
|
+
})}"
|
|
205
205
|
src="${defaultUrlImage}"
|
|
206
206
|
/>
|
|
207
207
|
`,
|
|
@@ -332,16 +332,10 @@ class PanelForm {
|
|
|
332
332
|
}, 50);
|
|
333
333
|
},
|
|
334
334
|
initEdit: async function ({ data }) {
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
s(`.${fileFormData.id}`).inputFiles = null;
|
|
340
|
-
htmls(
|
|
341
|
-
`.file-name-render-${fileFormData.id}`,
|
|
342
|
-
`<div class="abs center"><i style="font-size: 25px" class="fa-solid fa-cloud"></i></div>`,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
335
|
+
// Do NOT clear the file input here - the file should remain as-is when entering edit mode.
|
|
336
|
+
// If user wants to remove the file, they use the "clean file" button.
|
|
337
|
+
// If user wants to replace the file, they select a new file.
|
|
338
|
+
// Unconditionally clearing the file here would cause the server to receive fileId: null on save.
|
|
345
339
|
setTimeout(() => {
|
|
346
340
|
s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
|
|
347
341
|
}, 50);
|
|
@@ -388,15 +382,15 @@ class PanelForm {
|
|
|
388
382
|
// It will be filtered from the tags array to keep visibility control separate from content tags
|
|
389
383
|
const tags = data.tags
|
|
390
384
|
? uniqueArray(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
385
|
+
data.tags
|
|
386
|
+
.replaceAll('/', ',')
|
|
387
|
+
.replaceAll('-', ',')
|
|
388
|
+
.replaceAll(' ', ',')
|
|
389
|
+
.split(',')
|
|
390
|
+
.map((t) => t.trim())
|
|
391
|
+
.filter((t) => t)
|
|
392
|
+
.concat(prefixTags),
|
|
393
|
+
)
|
|
400
394
|
: prefixTags;
|
|
401
395
|
let originObj, originFileObj, indexOriginObj;
|
|
402
396
|
if (editId) {
|
|
@@ -434,7 +428,17 @@ class PanelForm {
|
|
|
434
428
|
for (const file of inputFiles) {
|
|
435
429
|
indexFormDoc++;
|
|
436
430
|
let fileId = undefined; // Reset for each iteration - only set if user uploaded a file
|
|
431
|
+
// Track whether the file input was explicitly cleared (null) vs never had a file (undefined)
|
|
432
|
+
// In edit mode, null means user cleared the file - we need to tell server to remove it
|
|
433
|
+
const isFileCleared = data.fileId === null && editId;
|
|
437
434
|
await (async () => {
|
|
435
|
+
// When file is null and not the first iteration or not in edit mode, skip upload
|
|
436
|
+
if (!file && !isFileCleared) return;
|
|
437
|
+
// When user cleared file in edit mode, set fileId=null so server removes the reference
|
|
438
|
+
if (isFileCleared) {
|
|
439
|
+
fileId = null;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
438
442
|
const body = new FormData();
|
|
439
443
|
// Only append md file if it was created (has content)
|
|
440
444
|
if (md) body.append('md', md);
|
|
@@ -485,8 +489,8 @@ class PanelForm {
|
|
|
485
489
|
message: documentMessage,
|
|
486
490
|
data: documentData,
|
|
487
491
|
} = originObj && indexFormDoc === 0
|
|
488
|
-
|
|
489
|
-
|
|
492
|
+
? await DocumentService.put({ id: originObj._id, body })
|
|
493
|
+
: await DocumentService.post({
|
|
490
494
|
body,
|
|
491
495
|
});
|
|
492
496
|
const newDoc = {
|
|
@@ -514,12 +518,12 @@ class PanelForm {
|
|
|
514
518
|
fileId: {
|
|
515
519
|
fileBlob: file
|
|
516
520
|
? {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
521
|
+
data: {
|
|
522
|
+
data: await getDataFromInputFile(file),
|
|
523
|
+
},
|
|
524
|
+
mimetype: file.type,
|
|
525
|
+
name: file.name,
|
|
526
|
+
}
|
|
523
527
|
: undefined,
|
|
524
528
|
filePlain: undefined,
|
|
525
529
|
},
|
|
@@ -738,36 +742,36 @@ class PanelForm {
|
|
|
738
742
|
<div
|
|
739
743
|
class="in fll ssr-shimmer-search-box"
|
|
740
744
|
style="${renderCssAttr({
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
745
|
+
style: {
|
|
746
|
+
width: '80%',
|
|
747
|
+
height: '30px',
|
|
748
|
+
top: '-13px',
|
|
749
|
+
left: '10px',
|
|
750
|
+
},
|
|
751
|
+
})}"
|
|
748
752
|
></div>
|
|
749
753
|
</div>`,
|
|
750
754
|
createdAt: html`<div class="fl">
|
|
751
755
|
<div
|
|
752
756
|
class="in fll ssr-shimmer-search-box"
|
|
753
757
|
style="${renderCssAttr({
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
758
|
+
style: {
|
|
759
|
+
width: '50%',
|
|
760
|
+
height: '30px',
|
|
761
|
+
left: '-5px',
|
|
762
|
+
},
|
|
763
|
+
})}"
|
|
760
764
|
></div>
|
|
761
765
|
</div>`,
|
|
762
766
|
mdFileId: html`<div class="fl section-mp">
|
|
763
767
|
<div
|
|
764
768
|
class="in fll ssr-shimmer-search-box"
|
|
765
769
|
style="${renderCssAttr({
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
770
|
+
style: {
|
|
771
|
+
width: '80%',
|
|
772
|
+
height: '30px',
|
|
773
|
+
},
|
|
774
|
+
})}"
|
|
771
775
|
></div>
|
|
772
776
|
</div>`.repeat(random(2, 4)),
|
|
773
777
|
ssr: true,
|
|
@@ -37,7 +37,7 @@ class Recover {
|
|
|
37
37
|
rules: [{ type: 'isEmpty' }, { type: 'isLength', options: { min: 2, max: 20 } }],
|
|
38
38
|
show: () => false,
|
|
39
39
|
disable: function () {
|
|
40
|
-
return !
|
|
40
|
+
return !this.show();
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
'recover-email': {
|
|
@@ -46,7 +46,7 @@ class Recover {
|
|
|
46
46
|
rules: [{ type: 'isEmpty' }, { type: 'isEmail' }],
|
|
47
47
|
show: () => mode === 'recover-verify-email',
|
|
48
48
|
disable: function () {
|
|
49
|
-
return !
|
|
49
|
+
return !this.show();
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
'recover-password': {
|
|
@@ -55,7 +55,7 @@ class Recover {
|
|
|
55
55
|
rules: [{ type: 'isStrongPassword' }],
|
|
56
56
|
show: () => mode === 'change-password',
|
|
57
57
|
disable: function () {
|
|
58
|
-
return !
|
|
58
|
+
return !this.show();
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
61
|
'recover-repeat-password': {
|
|
@@ -63,7 +63,7 @@ class Recover {
|
|
|
63
63
|
rules: [{ type: 'isEmpty' }, { type: 'passwordMismatch', options: `recover-password` }],
|
|
64
64
|
show: () => mode === 'change-password',
|
|
65
65
|
disable: function () {
|
|
66
|
-
return !
|
|
66
|
+
return !this.show();
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
69
|
};
|