underpost 3.2.9 → 3.2.11
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/extensions.json +9 -9
- package/.vscode/settings.json +20 -4
- package/CHANGELOG.md +195 -1
- package/CLI-HELP.md +92 -23
- package/README.md +38 -9
- package/bin/build.js +27 -7
- package/bin/build.template.js +187 -0
- package/bin/deploy.js +12 -2
- package/bin/index.js +2 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -7
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- 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/lxd/lxd-admin-profile.yaml +12 -3
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/manifests/mongodb-4.4/headless-service.yaml +10 -0
- package/manifests/mongodb-4.4/kustomization.yaml +3 -1
- package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
- package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
- package/manifests/mongodb-4.4/statefulset.yaml +79 -0
- package/manifests/mongodb-4.4/storage-class.yaml +9 -0
- package/manifests/valkey/statefulset.yaml +1 -1
- package/manifests/valkey/valkey-nodeport.yaml +17 -0
- package/package.json +27 -12
- package/scripts/ipxe-setup.sh +52 -49
- package/scripts/k3s-node-setup.sh +81 -46
- package/scripts/lxd-vm-setup.sh +193 -8
- package/scripts/maas-nat-firewalld.sh +145 -0
- 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 +132 -101
- package/src/cli/cluster.js +700 -232
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +216 -137
- package/src/cli/fs.js +13 -3
- package/src/cli/index.js +80 -15
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +1099 -223
- package/src/cli/monitor.js +9 -3
- package/src/cli/release.js +334 -140
- package/src/cli/repository.js +68 -23
- package/src/cli/run.js +191 -47
- package/src/cli/secrets.js +11 -2
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +5 -0
- package/src/client/components/core/ClientEvents.js +76 -0
- package/src/client/components/core/EventBus.js +4 -0
- package/src/client/components/core/Modal.js +82 -41
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Worker.js +162 -363
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +115 -15
- package/src/db/mariadb/MariaDB.js +2 -1
- package/src/db/mongo/MongoBootstrap.js +657 -0
- package/src/db/mongo/MongooseDB.js +129 -21
- 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 +81 -79
- package/src/server/process.js +180 -19
- package/src/server/proxy.js +9 -2
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +16 -4
- package/src/server/valkey.js +2 -0
- 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/bin/file.js +0 -202
- package/bin/vs.js +0 -74
- package/bin/zed.js +0 -84
- 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
package/src/cli/secrets.js
CHANGED
|
@@ -26,17 +26,26 @@ class UnderpostSecret {
|
|
|
26
26
|
* @memberof UnderpostSecret
|
|
27
27
|
*/
|
|
28
28
|
underpost: {
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @method createFromEnvFile
|
|
31
|
+
* @description Reads application secrets from a .env file and writes them to the underpost .env file. Used for local development and testing.
|
|
32
|
+
* @param {string} envPath - The path to the .env file to read secrets from. Defaults to './.env'.
|
|
33
|
+
* @memberof UnderpostSecret
|
|
34
|
+
*/
|
|
35
|
+
createFromEnvFile(envPath = './.env') {
|
|
30
36
|
Underpost.env.clean();
|
|
31
37
|
const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
32
38
|
for (const key of Object.keys(envObj)) {
|
|
33
39
|
Underpost.env.set(key, envObj[key]);
|
|
34
40
|
}
|
|
35
41
|
},
|
|
36
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* @method createFromContainerEnv
|
|
44
|
+
* @description Reads application secrets from process.env (injected via envFrom: secretRef)
|
|
37
45
|
* and writes them to the underpost .env file, filtering out known system and
|
|
38
46
|
* Kubernetes-injected environment variables. Replaces the fragile shell-based
|
|
39
47
|
* `printenv | grep -vE` pattern with a maintainable Node.js blocklist.
|
|
48
|
+
* @memberof UnderpostSecret
|
|
40
49
|
*/
|
|
41
50
|
createFromContainerEnv() {
|
|
42
51
|
Underpost.env.clean();
|
package/src/cli/test.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @namespace UnderpostTest
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import fs from 'fs-extra';
|
|
7
8
|
import { timer } from '../client/components/core/CommonJs.js';
|
|
8
9
|
import { MariaDB } from '../db/mariadb/MariaDB.js';
|
|
9
10
|
import { getNpmRootPath } from '../server/conf.js';
|
|
@@ -42,7 +43,13 @@ class UnderpostTest {
|
|
|
42
43
|
*/
|
|
43
44
|
run() {
|
|
44
45
|
actionInitLog();
|
|
45
|
-
|
|
46
|
+
const underpostTestPath = `${getNpmRootPath()}/underpost`;
|
|
47
|
+
if (fs.existsSync(underpostTestPath)) {
|
|
48
|
+
shellExec(`cd ${underpostTestPath} && npm run test`);
|
|
49
|
+
} else {
|
|
50
|
+
logger.warn(`Global underpost not found at ${underpostTestPath}, running local npm test instead`);
|
|
51
|
+
shellExec('npm test');
|
|
52
|
+
}
|
|
46
53
|
},
|
|
47
54
|
/**
|
|
48
55
|
* @method callback
|
|
@@ -148,8 +155,7 @@ class UnderpostTest {
|
|
|
148
155
|
const pods = Underpost.kubectl.get(podName, kindType);
|
|
149
156
|
let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
|
|
150
157
|
logger.info(
|
|
151
|
-
`Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
|
|
152
|
-
index + 1
|
|
158
|
+
`Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${index + 1
|
|
153
159
|
}/${maxAttempts}`,
|
|
154
160
|
pods[0] ? pods[0].STATUS : 'Not found kind object',
|
|
155
161
|
);
|
|
@@ -19,6 +19,12 @@ const DefaultTemplate = async () => {
|
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
21
|
return html`
|
|
22
|
+
<style>
|
|
23
|
+
.feature-icon {
|
|
24
|
+
font-size: 2.5rem;
|
|
25
|
+
margin-bottom: 1rem;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
22
28
|
<div class="landing-container">
|
|
23
29
|
<div class="content-wrapper">
|
|
24
30
|
<h1 class="animated-text">
|
|
@@ -27,17 +33,17 @@ const DefaultTemplate = async () => {
|
|
|
27
33
|
</h1>
|
|
28
34
|
<div class="features">
|
|
29
35
|
<div class="feature-card">
|
|
30
|
-
<i class="icon"
|
|
36
|
+
<i class="fas fa-rocket feature-icon"></i>
|
|
31
37
|
<h3>Fast & Reliable</h3>
|
|
32
38
|
<p>Lightning-fast performance with 99.9% uptime</p>
|
|
33
39
|
</div>
|
|
34
40
|
<div class="feature-card">
|
|
35
|
-
<i class="icon"
|
|
41
|
+
<i class="fas fa-palette feature-icon"></i>
|
|
36
42
|
<h3>Beautiful UI</h3>
|
|
37
43
|
<p>Modern and intuitive user interface</p>
|
|
38
44
|
</div>
|
|
39
45
|
<div class="feature-card">
|
|
40
|
-
<i class="icon"
|
|
46
|
+
<i class="fas fa-bolt feature-icon"></i>
|
|
41
47
|
<h3>Powerful Features</h3>
|
|
42
48
|
<p>Everything you need in one place</p>
|
|
43
49
|
</div>
|
|
@@ -319,6 +319,11 @@ class Auth {
|
|
|
319
319
|
// Close any open login/signup modals
|
|
320
320
|
if (s(`.modal-log-in`)) s(`.btn-close-modal-log-in`).click();
|
|
321
321
|
if (s(`.modal-sign-up`)) s(`.btn-close-modal-sign-up`).click();
|
|
322
|
+
if (!s(`.main-body-btn-ui-open`).classList.contains('hide')) s(`.main-body-btn-ui-open`).click();
|
|
323
|
+
if (!s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide')) {
|
|
324
|
+
SearchBox.Data.skipOpen = true;
|
|
325
|
+
s(`.main-body-btn-ui-bar-custom-open`).click();
|
|
326
|
+
}
|
|
322
327
|
});
|
|
323
328
|
}
|
|
324
329
|
|
|
@@ -45,6 +45,79 @@ const AppointmentEventType = {
|
|
|
45
45
|
submitted: 'appointment:submitted',
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
const ModalEventType = {
|
|
49
|
+
close: 'modal:close',
|
|
50
|
+
menu: 'modal:menu',
|
|
51
|
+
collapseMenu: 'modal:collapse-menu',
|
|
52
|
+
extendMenu: 'modal:extend-menu',
|
|
53
|
+
dragEnd: 'modal:drag-end',
|
|
54
|
+
observer: 'modal:observer',
|
|
55
|
+
click: 'modal:click',
|
|
56
|
+
expandUi: 'modal:expand-ui',
|
|
57
|
+
barUiOpen: 'modal:bar-ui-open',
|
|
58
|
+
barUiClose: 'modal:bar-ui-close',
|
|
59
|
+
reload: 'modal:reload',
|
|
60
|
+
home: 'modal:home',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const ModalListenerChannels = {
|
|
64
|
+
onCloseListener: ModalEventType.close,
|
|
65
|
+
onMenuListener: ModalEventType.menu,
|
|
66
|
+
onCollapseMenuListener: ModalEventType.collapseMenu,
|
|
67
|
+
onExtendMenuListener: ModalEventType.extendMenu,
|
|
68
|
+
onDragEndListener: ModalEventType.dragEnd,
|
|
69
|
+
onObserverListener: ModalEventType.observer,
|
|
70
|
+
onClickListener: ModalEventType.click,
|
|
71
|
+
onExpandUiListener: ModalEventType.expandUi,
|
|
72
|
+
onBarUiOpen: ModalEventType.barUiOpen,
|
|
73
|
+
onBarUiClose: ModalEventType.barUiClose,
|
|
74
|
+
onReloadModalListener: ModalEventType.reload,
|
|
75
|
+
onHome: ModalEventType.home,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const createModalEventChannel = (bus, type) => {
|
|
79
|
+
const busKey = (key) => `${type}::${key}`;
|
|
80
|
+
return new Proxy(
|
|
81
|
+
{},
|
|
82
|
+
{
|
|
83
|
+
get(_target, prop) {
|
|
84
|
+
if (typeof prop === 'symbol') return undefined;
|
|
85
|
+
const key = busKey(prop);
|
|
86
|
+
if (!bus.has(key)) return undefined;
|
|
87
|
+
return (detail) => bus.emitKey(key, detail);
|
|
88
|
+
},
|
|
89
|
+
set(_target, prop, value) {
|
|
90
|
+
if (typeof prop !== 'symbol' && typeof value === 'function') bus.on(type, value, { key: busKey(prop) });
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
deleteProperty(_target, prop) {
|
|
94
|
+
if (typeof prop !== 'symbol') bus.off(busKey(prop));
|
|
95
|
+
return true;
|
|
96
|
+
},
|
|
97
|
+
has(_target, prop) {
|
|
98
|
+
return typeof prop !== 'symbol' && bus.has(busKey(prop));
|
|
99
|
+
},
|
|
100
|
+
ownKeys() {
|
|
101
|
+
const prefix = `${type}::`;
|
|
102
|
+
return bus.keysOf(type).map((key) => String(key).slice(prefix.length));
|
|
103
|
+
},
|
|
104
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
105
|
+
if (typeof prop !== 'symbol' && bus.has(busKey(prop)))
|
|
106
|
+
return { enumerable: true, configurable: true, writable: true, value: undefined };
|
|
107
|
+
return undefined;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// One EventBus per modal id, surfaced through the legacy channel names.
|
|
114
|
+
const createModalEvents = () => {
|
|
115
|
+
const bus = new EventBus();
|
|
116
|
+
const channels = {};
|
|
117
|
+
for (const [name, type] of Object.entries(ModalListenerChannels)) channels[name] = createModalEventChannel(bus, type);
|
|
118
|
+
return { bus, channels };
|
|
119
|
+
};
|
|
120
|
+
|
|
48
121
|
const authLoginEvents = new EventBus();
|
|
49
122
|
const authLogoutEvents = new EventBus();
|
|
50
123
|
const authSignupEvents = new EventBus();
|
|
@@ -70,6 +143,9 @@ export {
|
|
|
70
143
|
KeyboardEventType,
|
|
71
144
|
AccountEventType,
|
|
72
145
|
AppointmentEventType,
|
|
146
|
+
ModalEventType,
|
|
147
|
+
ModalListenerChannels,
|
|
148
|
+
createModalEvents,
|
|
73
149
|
authLoginEvents,
|
|
74
150
|
authLogoutEvents,
|
|
75
151
|
authSignupEvents,
|
|
@@ -36,6 +36,7 @@ import { Worker } from './Worker.js';
|
|
|
36
36
|
import { Scroll } from './Scroll.js';
|
|
37
37
|
import { windowGetH, windowGetW } from './windowGetDimensions.js';
|
|
38
38
|
import { SearchBox } from './SearchBox.js';
|
|
39
|
+
import { createModalEvents } from './ClientEvents.js';
|
|
39
40
|
|
|
40
41
|
const logger = loggerFactory(import.meta, { trace: true });
|
|
41
42
|
|
|
@@ -93,6 +94,11 @@ const logger = loggerFactory(import.meta, { trace: true });
|
|
|
93
94
|
/**
|
|
94
95
|
* @typedef {object} ModalDataEntry
|
|
95
96
|
* @property {ModalRenderOptions} options - Original render options.
|
|
97
|
+
* @property {import('./EventBus.js').EventBus} events - Per-modal event bus backing the listener channels below.
|
|
98
|
+
*
|
|
99
|
+
* The `onX` channels are EventBus-backed proxies that preserve the historical
|
|
100
|
+
* `{ [key]: listener }` map API: assign to register, read+call to invoke a single
|
|
101
|
+
* listener, `delete` to remove, and `Object.keys` to enumerate registered keys.
|
|
96
102
|
* @property {Object.<string, Function>} onCloseListener - Close event listeners keyed by id.
|
|
97
103
|
* @property {Object.<string, Function>} onMenuListener - Menu button event listeners.
|
|
98
104
|
* @property {Object.<string, Function>} onCollapseMenuListener - Collapse menu listeners.
|
|
@@ -123,6 +129,69 @@ class Modal {
|
|
|
123
129
|
/** @type {Object.<string, ModalDataEntry>} */
|
|
124
130
|
static Data = {};
|
|
125
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Bottom offset of the slide-menu area for the given options.
|
|
134
|
+
* @param {ModalRenderOptions} options
|
|
135
|
+
* @param {number} [heightDefaultBottomBar=0]
|
|
136
|
+
* @returns {number}
|
|
137
|
+
*/
|
|
138
|
+
static getModalTop(options, heightDefaultBottomBar = 0) {
|
|
139
|
+
return windowGetH() - (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Available modal height, accounting for the top/bottom bars when the UI is expanded.
|
|
144
|
+
* @param {ModalRenderOptions} options
|
|
145
|
+
* @param {number} [heightDefaultTopBar=50]
|
|
146
|
+
* @param {number} [heightDefaultBottomBar=0]
|
|
147
|
+
* @returns {number}
|
|
148
|
+
*/
|
|
149
|
+
static getModalHeight(options, heightDefaultTopBar = 50, heightDefaultBottomBar = 0) {
|
|
150
|
+
const barsVisible = s(`.main-body-btn-ui-close`) && !s(`.main-body-btn-ui-close`).classList.contains('hide');
|
|
151
|
+
return (
|
|
152
|
+
windowGetH() -
|
|
153
|
+
(barsVisible
|
|
154
|
+
? (options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) +
|
|
155
|
+
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
156
|
+
: 0)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Left CSS value for a slide menu in its open/closed state.
|
|
162
|
+
* @param {ModalRenderOptions} options
|
|
163
|
+
* @param {{ originSlideMenuWidth: number, collapseSlideMenuWidth: number }} widths
|
|
164
|
+
* @param {{ open: boolean }} [ops={ open: false }]
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
static getModalMenuLeftStyle(options, { originSlideMenuWidth, collapseSlideMenuWidth }, ops = { open: false }) {
|
|
168
|
+
return `${
|
|
169
|
+
options.mode === 'slide-menu-right'
|
|
170
|
+
? `${
|
|
171
|
+
windowGetW() +
|
|
172
|
+
(ops?.open
|
|
173
|
+
? -1 * originSlideMenuWidth +
|
|
174
|
+
(options.mode === 'slide-menu-right' && s(`.btn-icon-menu-mode-right`).classList.contains('hide')
|
|
175
|
+
? originSlideMenuWidth - collapseSlideMenuWidth
|
|
176
|
+
: 0)
|
|
177
|
+
: originSlideMenuWidth)
|
|
178
|
+
}px`
|
|
179
|
+
: `-${ops?.open ? '0px' : originSlideMenuWidth}px`
|
|
180
|
+
}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Viewport-centered top/left CSS values for a modal of the given size.
|
|
185
|
+
* @param {{ width: number, height: number }} param0
|
|
186
|
+
* @returns {{ top: string, left: string }}
|
|
187
|
+
*/
|
|
188
|
+
static getModalCenter({ width, height }) {
|
|
189
|
+
return {
|
|
190
|
+
top: `${windowGetH() / 2 - height / 2}px`,
|
|
191
|
+
left: `${windowGetW() / 2 - width / 2}px`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
126
195
|
/**
|
|
127
196
|
* Create or reload a modal. When the modal already exists in the DOM the
|
|
128
197
|
* existing instance is reloaded via its onReloadModalListener callbacks.
|
|
@@ -175,52 +244,21 @@ class Modal {
|
|
|
175
244
|
const heightDefaultTopBar = 50;
|
|
176
245
|
const heightDefaultBottomBar = 0;
|
|
177
246
|
const idModal = options.id ? options.id : getId(this.Data, 'modal-');
|
|
247
|
+
const { bus: eventBus, channels: eventChannels } = createModalEvents();
|
|
178
248
|
this.Data[idModal] = {
|
|
179
249
|
options,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
onCollapseMenuListener: {},
|
|
183
|
-
onExtendMenuListener: {},
|
|
184
|
-
onDragEndListener: {},
|
|
185
|
-
onObserverListener: {},
|
|
186
|
-
onClickListener: {},
|
|
187
|
-
onExpandUiListener: {},
|
|
188
|
-
onBarUiOpen: {},
|
|
189
|
-
onBarUiClose: {},
|
|
190
|
-
onReloadModalListener: {},
|
|
191
|
-
onHome: {},
|
|
250
|
+
events: eventBus,
|
|
251
|
+
...eventChannels,
|
|
192
252
|
homeModals: options.homeModals ? options.homeModals : [],
|
|
193
253
|
query: options.query ? `${window.location.search}` : undefined,
|
|
194
|
-
getTop: () =>
|
|
195
|
-
|
|
196
|
-
return result;
|
|
197
|
-
},
|
|
198
|
-
getHeight: () => {
|
|
199
|
-
return (
|
|
200
|
-
windowGetH() -
|
|
201
|
-
(s(`.main-body-btn-ui-close`) && !s(`.main-body-btn-ui-close`).classList.contains('hide')
|
|
202
|
-
? (options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) +
|
|
203
|
-
(options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
|
|
204
|
-
: 0)
|
|
205
|
-
);
|
|
206
|
-
},
|
|
254
|
+
getTop: () => Modal.getModalTop(options, heightDefaultBottomBar),
|
|
255
|
+
getHeight: () => Modal.getModalHeight(options, heightDefaultTopBar, heightDefaultBottomBar),
|
|
207
256
|
getMenuLeftStyle: (ops = { open: false }) =>
|
|
208
|
-
|
|
209
|
-
options.mode === 'slide-menu-right'
|
|
210
|
-
? `${
|
|
211
|
-
windowGetW() +
|
|
212
|
-
(ops?.open
|
|
213
|
-
? -1 * originSlideMenuWidth +
|
|
214
|
-
(options.mode === 'slide-menu-right' && s(`.btn-icon-menu-mode-right`).classList.contains('hide')
|
|
215
|
-
? originSlideMenuWidth - collapseSlideMenuWidth
|
|
216
|
-
: 0)
|
|
217
|
-
: originSlideMenuWidth)
|
|
218
|
-
}px`
|
|
219
|
-
: `-${ops?.open ? '0px' : originSlideMenuWidth}px`
|
|
220
|
-
}`,
|
|
257
|
+
Modal.getModalMenuLeftStyle(options, { originSlideMenuWidth, collapseSlideMenuWidth }, ops),
|
|
221
258
|
center: () => {
|
|
222
|
-
top =
|
|
223
|
-
|
|
259
|
+
const { top: centeredTop, left: centeredLeft } = Modal.getModalCenter({ width, height });
|
|
260
|
+
top = centeredTop;
|
|
261
|
+
left = centeredLeft;
|
|
224
262
|
},
|
|
225
263
|
...this.Data[idModal],
|
|
226
264
|
};
|
|
@@ -1029,6 +1067,10 @@ class Modal {
|
|
|
1029
1067
|
hoverFocusCtl.checkDismiss();
|
|
1030
1068
|
};
|
|
1031
1069
|
EventsUI.onClick(`.top-bar-search-box-container`, () => {
|
|
1070
|
+
if (SearchBox.Data.skipOpen) {
|
|
1071
|
+
SearchBox.Data.skipOpen = false;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1032
1074
|
searchBoxHistoryOpen();
|
|
1033
1075
|
searchBoxCallBack(formDataInfoNode[0]);
|
|
1034
1076
|
const inputEl = s(`.${inputSearchBoxId}`);
|
|
@@ -2138,7 +2180,6 @@ class Modal {
|
|
|
2138
2180
|
dragInstance = setDragInstance();
|
|
2139
2181
|
if (options && options.maximize) s(`.btn-maximize-${idModal}`).click();
|
|
2140
2182
|
if (options.observer) {
|
|
2141
|
-
this.Data[idModal].onObserverListener = {};
|
|
2142
2183
|
this.Data[idModal].observerCallBack = () => {
|
|
2143
2184
|
// logger.info('ResizeObserver', `.${idModal}`, s(`.${idModal}`).offsetWidth, s(`.${idModal}`).offsetHeight);
|
|
2144
2185
|
if (this.Data[idModal] && this.Data[idModal].onObserverListener)
|
|
@@ -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,
|