cyberia 3.2.9 → 3.2.12

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 (169) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +6 -0
  2. package/.github/workflows/npmpkg.ci.yml +1 -0
  3. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  4. package/.github/workflows/release.cd.yml +1 -0
  5. package/.vscode/extensions.json +9 -9
  6. package/.vscode/settings.json +20 -4
  7. package/CHANGELOG.md +213 -1
  8. package/CLI-HELP.md +92 -23
  9. package/README.md +190 -348
  10. package/bin/build.js +24 -8
  11. package/bin/build.template.js +187 -0
  12. package/bin/cyberia.js +229 -52
  13. package/bin/deploy.js +12 -2
  14. package/bin/index.js +229 -52
  15. package/bump.config.js +26 -0
  16. package/conf.js +130 -24
  17. package/deployment.yaml +4 -2
  18. package/hardhat/package-lock.json +113 -144
  19. package/hardhat/package.json +4 -3
  20. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  21. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  22. package/manifests/deployment/dd-cyberia-development/deployment.yaml +4 -2
  23. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  24. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  25. package/manifests/kind-config-dev.yaml +8 -0
  26. package/manifests/lxd/lxd-admin-profile.yaml +12 -3
  27. package/manifests/mongodb/pv-pvc.yaml +44 -8
  28. package/manifests/mongodb/statefulset.yaml +55 -68
  29. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  30. package/manifests/mongodb-4.4/kustomization.yaml +3 -1
  31. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  32. package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
  33. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  34. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  35. package/manifests/valkey/statefulset.yaml +1 -1
  36. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  37. package/package.json +27 -15
  38. package/scripts/ipxe-setup.sh +52 -49
  39. package/scripts/k3s-node-setup.sh +81 -46
  40. package/scripts/lxd-vm-setup.sh +193 -8
  41. package/scripts/maas-nat-firewalld.sh +145 -0
  42. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +38 -33
  43. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +16 -16
  44. package/src/api/core/core.router.js +19 -14
  45. package/src/api/core/core.service.js +5 -5
  46. package/src/api/crypto/crypto.router.js +18 -12
  47. package/src/api/crypto/crypto.service.js +3 -3
  48. package/src/api/cyberia-action/cyberia-action.model.js +1 -1
  49. package/src/api/cyberia-action/cyberia-action.router.js +22 -18
  50. package/src/api/cyberia-action/cyberia-action.service.js +5 -5
  51. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +74 -0
  52. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +99 -0
  53. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +98 -0
  54. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
  55. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +25 -20
  56. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +6 -6
  57. package/src/api/cyberia-entity/cyberia-entity.router.js +22 -18
  58. package/src/api/cyberia-entity/cyberia-entity.service.js +5 -5
  59. package/src/api/cyberia-instance/cyberia-fallback-world.js +79 -4
  60. package/src/api/cyberia-instance/cyberia-instance.router.js +57 -52
  61. package/src/api/cyberia-instance/cyberia-instance.service.js +10 -10
  62. package/src/api/cyberia-instance/cyberia-world-generator.js +3 -3
  63. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +14 -48
  64. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +22 -18
  65. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +5 -5
  66. package/src/api/cyberia-map/cyberia-map.router.js +35 -30
  67. package/src/api/cyberia-map/cyberia-map.service.js +7 -7
  68. package/src/api/cyberia-quest/cyberia-quest.model.js +1 -1
  69. package/src/api/cyberia-quest/cyberia-quest.router.js +22 -18
  70. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  71. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +22 -18
  72. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +5 -5
  73. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +451 -0
  74. package/src/api/default/default.router.js +22 -18
  75. package/src/api/default/default.service.js +5 -5
  76. package/src/api/document/document.router.js +28 -23
  77. package/src/api/document/document.service.js +100 -23
  78. package/src/api/file/file.router.js +19 -13
  79. package/src/api/file/file.service.js +9 -7
  80. package/src/api/instance/instance.router.js +29 -24
  81. package/src/api/instance/instance.service.js +6 -6
  82. package/src/api/ipfs/ipfs.router.js +21 -16
  83. package/src/api/ipfs/ipfs.service.js +8 -8
  84. package/src/api/object-layer/object-layer.router.js +512 -507
  85. package/src/api/object-layer/object-layer.service.js +17 -14
  86. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +22 -18
  87. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +5 -5
  88. package/src/api/test/test.router.js +17 -12
  89. package/src/api/types.js +24 -0
  90. package/src/api/user/guest.service.js +5 -4
  91. package/src/api/user/user.router.js +297 -288
  92. package/src/api/user/user.service.js +100 -35
  93. package/src/cli/baremetal.js +132 -101
  94. package/src/cli/cluster.js +700 -232
  95. package/src/cli/db.js +59 -60
  96. package/src/cli/deploy.js +216 -137
  97. package/src/cli/fs.js +13 -3
  98. package/src/cli/index.js +80 -15
  99. package/src/cli/ipfs.js +4 -6
  100. package/src/cli/kubectl.js +4 -1
  101. package/src/cli/lxd.js +1099 -223
  102. package/src/cli/monitor.js +9 -3
  103. package/src/cli/release.js +334 -140
  104. package/src/cli/repository.js +68 -23
  105. package/src/cli/run.js +193 -49
  106. package/src/cli/secrets.js +11 -2
  107. package/src/cli/test.js +9 -3
  108. package/src/client/Default.index.js +9 -3
  109. package/src/client/components/core/Auth.js +5 -0
  110. package/src/client/components/core/ClientEvents.js +76 -0
  111. package/src/client/components/core/EventBus.js +4 -0
  112. package/src/client/components/core/Modal.js +82 -41
  113. package/src/client/components/core/PanelForm.js +56 -52
  114. package/src/client/components/core/Worker.js +162 -363
  115. package/src/client/components/cyberia/MapEngineCyberia.js +1 -1
  116. package/src/client/components/cyberia/SharedDefaultsCyberia.js +330 -0
  117. package/src/client/public/cyberia-docs/ARCHITECTURE.md +50 -410
  118. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +114 -327
  119. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +200 -222
  120. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +203 -185
  121. package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
  122. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +2 -2
  123. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  124. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
  125. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  126. package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
  127. package/src/client/ssr/views/CyberiaServerMetrics.js +982 -0
  128. package/src/client/sw/core.sw.js +174 -112
  129. package/src/db/DataBaseProvider.js +115 -15
  130. package/src/db/mariadb/MariaDB.js +2 -1
  131. package/src/db/mongo/MongoBootstrap.js +657 -0
  132. package/src/db/mongo/MongooseDB.js +129 -21
  133. package/src/grpc/cyberia/grpc-server.js +25 -57
  134. package/src/index.js +1 -1
  135. package/src/runtime/cyberia-client/Dockerfile +24 -3
  136. package/src/runtime/cyberia-client/Dockerfile.dev +82 -0
  137. package/src/runtime/cyberia-server/Dockerfile +29 -4
  138. package/src/runtime/cyberia-server/Dockerfile.dev +71 -0
  139. package/src/runtime/express/Express.js +2 -2
  140. package/src/runtime/wp/Wp.js +8 -5
  141. package/src/server/auth.js +2 -2
  142. package/src/server/client-build-docs.js +1 -1
  143. package/src/server/client-build.js +94 -129
  144. package/src/server/conf.js +86 -83
  145. package/src/server/process.js +180 -19
  146. package/src/server/proxy.js +9 -2
  147. package/src/server/runtime.js +1 -1
  148. package/src/server/start.js +17 -5
  149. package/src/server/valkey.js +2 -0
  150. package/src/ws/IoInterface.js +16 -16
  151. package/src/ws/core/channels/core.ws.chat.js +11 -11
  152. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  153. package/src/ws/core/channels/core.ws.stream.js +19 -19
  154. package/src/ws/core/core.ws.connection.js +8 -8
  155. package/src/ws/core/core.ws.server.js +6 -5
  156. package/src/ws/default/channels/default.ws.main.js +10 -10
  157. package/src/ws/default/default.ws.connection.js +4 -4
  158. package/src/ws/default/default.ws.server.js +4 -3
  159. package/bin/file.js +0 -202
  160. package/bin/vs.js +0 -74
  161. package/bin/zed.js +0 -84
  162. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +0 -574
  163. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +0 -467
  164. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  165. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  166. package/src/client/ssr/pages/CyberiaServerMetrics.js +0 -461
  167. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  168. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  169. /package/src/client/ssr/{pages → views}/Test.js +0 -0
@@ -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,
@@ -69,6 +69,10 @@ class EventBus {
69
69
  return this.listeners.has(key);
70
70
  }
71
71
 
72
+ keysOf(type) {
73
+ return [...(this.typeKeys.get(type) ?? [])];
74
+ }
75
+
72
76
  async emit(type, detail) {
73
77
  if (!(this.typeKeys.get(type)?.size > 0)) return;
74
78
 
@@ -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
- onCloseListener: {},
181
- onMenuListener: {},
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
- const result = windowGetH() - (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar);
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 = `${windowGetH() / 2 - height / 2}px`;
223
- left = `${windowGetW() / 2 - width / 2}px`;
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
- style: {
200
- width: '100px',
201
- height: '100px',
202
- opacity: 0.2,
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
- // Clear file input when entering edit mode
336
- const fileFormData = formData.find((f) => f.inputType === 'file');
337
- if (fileFormData && s(`.${fileFormData.id}`)) {
338
- s(`.${fileFormData.id}`).value = '';
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
- data.tags
392
- .replaceAll('/', ',')
393
- .replaceAll('-', ',')
394
- .replaceAll(' ', ',')
395
- .split(',')
396
- .map((t) => t.trim())
397
- .filter((t) => t)
398
- .concat(prefixTags),
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
- ? await DocumentService.put({ id: originObj._id, body })
489
- : await DocumentService.post({
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
- data: {
518
- data: await getDataFromInputFile(file),
519
- },
520
- mimetype: file.type,
521
- name: file.name,
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
- style: {
742
- width: '80%',
743
- height: '30px',
744
- top: '-13px',
745
- left: '10px',
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
- style: {
755
- width: '50%',
756
- height: '30px',
757
- left: '-5px',
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
- style: {
767
- width: '80%',
768
- height: '30px',
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,