underpost 2.8.878 → 2.8.881

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 (49) hide show
  1. package/.env.development +35 -3
  2. package/.env.production +40 -3
  3. package/.env.test +35 -3
  4. package/.github/workflows/release.cd.yml +2 -1
  5. package/README.md +44 -36
  6. package/bin/deploy.js +40 -0
  7. package/cli.md +88 -86
  8. package/conf.js +28 -3
  9. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  10. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  11. package/package.json +1 -2
  12. package/src/api/document/document.controller.js +66 -0
  13. package/src/api/document/document.model.js +51 -0
  14. package/src/api/document/document.router.js +24 -0
  15. package/src/api/document/document.service.js +125 -0
  16. package/src/cli/deploy.js +1 -1
  17. package/src/cli/index.js +2 -0
  18. package/src/cli/run.js +27 -1
  19. package/src/client/Default.index.js +46 -1
  20. package/src/client/components/core/Account.js +8 -1
  21. package/src/client/components/core/AgGrid.js +18 -9
  22. package/src/client/components/core/BtnIcon.js +3 -2
  23. package/src/client/components/core/Content.js +2 -1
  24. package/src/client/components/core/CssCore.js +4 -0
  25. package/src/client/components/core/Docs.js +0 -3
  26. package/src/client/components/core/Input.js +34 -19
  27. package/src/client/components/core/Modal.js +29 -7
  28. package/src/client/components/core/ObjectLayerEngine.js +370 -0
  29. package/src/client/components/core/ObjectLayerEngineModal.js +1 -0
  30. package/src/client/components/core/Panel.js +7 -2
  31. package/src/client/components/core/PanelForm.js +187 -63
  32. package/src/client/components/core/VanillaJs.js +3 -0
  33. package/src/client/components/default/MenuDefault.js +94 -41
  34. package/src/client/components/default/RoutesDefault.js +2 -0
  35. package/src/client/services/default/default.management.js +1 -0
  36. package/src/client/services/document/document.service.js +97 -0
  37. package/src/client/services/file/file.service.js +2 -0
  38. package/src/client/ssr/Render.js +1 -1
  39. package/src/client/ssr/head/DefaultScripts.js +2 -0
  40. package/src/client/ssr/head/Seo.js +1 -0
  41. package/src/index.js +1 -1
  42. package/src/mailer/EmailRender.js +1 -1
  43. package/src/server/client-build.js +2 -3
  44. package/src/server/client-formatted.js +40 -12
  45. package/src/server/conf.js +5 -1
  46. package/src/server/object-layer.js +196 -0
  47. package/src/server/runtime.js +10 -13
  48. package/src/server/ssr.js +52 -10
  49. package/src/server/valkey.js +89 -1
@@ -0,0 +1,125 @@
1
+ import { loggerFactory } from '../../server/logger.js';
2
+ import { DataBaseProvider } from '../../db/DataBaseProvider.js';
3
+ import { DocumentDto } from './document.model.js';
4
+ import { uniqueArray } from '../../client/components/core/CommonJs.js';
5
+ import { getBearerToken, verifyJWT } from '../../server/auth.js';
6
+ import { isValidObjectId } from 'mongoose';
7
+
8
+ const logger = loggerFactory(import.meta);
9
+
10
+ const DocumentService = {
11
+ post: async (req, res, options) => {
12
+ /** @type {import('./document.model.js').DocumentModel} */
13
+ const Document = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Document;
14
+
15
+ switch (req.params.id) {
16
+ default:
17
+ req.body.userId = req.auth.user._id;
18
+ return await new Document(req.body).save();
19
+ }
20
+ },
21
+ get: async (req, res, options) => {
22
+ /** @type {import('./document.model.js').DocumentModel} */
23
+ const Document = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Document;
24
+ /** @type {import('../user/user.model.js').UserModel} */
25
+ const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
26
+
27
+ if (req.path.startsWith('/public') && req.query['tags']) {
28
+ const publisherUsers = await User.find({ $or: [{ role: 'admin' }, { role: 'moderator' }] });
29
+
30
+ const token = getBearerToken(req);
31
+ let user;
32
+ if (token) user = verifyJWT(token, options);
33
+
34
+ const queryPayload = {
35
+ userId: {
36
+ $in: publisherUsers.map((p) => p._id).concat(user?.role && user.role !== 'guest' ? [user._id] : []),
37
+ },
38
+ tags: {
39
+ // $in: uniqueArray(['public'].concat(req.query['tags'].split(','))),
40
+ $all: uniqueArray(['public'].concat(req.query['tags'].split(','))),
41
+ },
42
+ ...(req.query.cid
43
+ ? {
44
+ _id: {
45
+ $in: req.query.cid.split(',').filter((cid) => isValidObjectId(cid)),
46
+ },
47
+ }
48
+ : undefined),
49
+ };
50
+ logger.info('queryPayload', queryPayload);
51
+ // sort in descending (-1) order by length
52
+ const sort = { createdAt: -1 };
53
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 6;
54
+ const skip = req.query.skip ? parseInt(req.query.skip, 10) : 0;
55
+
56
+ return await Document.find(queryPayload)
57
+ .sort(sort)
58
+ .limit(limit)
59
+ .skip(skip)
60
+ .populate(DocumentDto.populate.file())
61
+ .populate(user && user.role !== 'guest' ? DocumentDto.populate.user() : null);
62
+ }
63
+
64
+ switch (req.params.id) {
65
+ default:
66
+ return await Document.find({
67
+ userId: req.auth.user._id,
68
+ ...(req.params.id ? { _id: req.params.id } : undefined),
69
+ }).populate(DocumentDto.populate.file());
70
+ }
71
+ },
72
+ delete: async (req, res, options) => {
73
+ /** @type {import('./document.model.js').DocumentModel} */
74
+ const Document = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Document;
75
+ /** @type {import('../file/file.model.js').FileModel} */
76
+ const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
77
+
78
+ switch (req.params.id) {
79
+ default: {
80
+ const document = await Document.findOne({ _id: req.params.id });
81
+ if (!document) throw new Error('document not found');
82
+
83
+ if (document.userId.toString() !== req.auth.user._id) throw new Error('invalid user');
84
+
85
+ if (document.mdFileId) {
86
+ const file = await File.findOne({ _id: document.mdFileId });
87
+ if (file) await File.findByIdAndDelete(document.mdFileId);
88
+ }
89
+
90
+ if (document.fileId) {
91
+ const file = await File.findOne({ _id: document.fileId });
92
+ if (file) await File.findByIdAndDelete(document.fileId);
93
+ }
94
+
95
+ return await Document.findByIdAndDelete(req.params.id);
96
+ }
97
+ }
98
+ },
99
+ put: async (req, res, options) => {
100
+ /** @type {import('./document.model.js').DocumentModel} */
101
+ const Document = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Document;
102
+ /** @type {import('../file/file.model.js').FileModel} */
103
+ const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
104
+
105
+ switch (req.params.id) {
106
+ default: {
107
+ const document = await Document.findOne({ _id: req.params.id });
108
+ if (!document) throw new Error(`Document not found`);
109
+
110
+ if (document.mdFileId) {
111
+ const file = await File.findOne({ _id: document.mdFileId });
112
+ if (file) await File.findByIdAndDelete(document.mdFileId);
113
+ }
114
+
115
+ if (document.fileId) {
116
+ const file = await File.findOne({ _id: document.fileId });
117
+ if (file) await File.findByIdAndDelete(document.fileId);
118
+ }
119
+ return await Document.findByIdAndUpdate(req.params.id, req.body);
120
+ }
121
+ }
122
+ },
123
+ };
124
+
125
+ export { DocumentService };
package/src/cli/deploy.js CHANGED
@@ -334,7 +334,7 @@ EOF`);
334
334
  if (!options.replicas) options.replicas = 1;
335
335
  if (options.sync) UnderpostDeploy.API.sync(deployList, options);
336
336
  if (options.buildManifest === true) await UnderpostDeploy.API.buildManifest(deployList, env, options);
337
- if (options.infoRouter === true) {
337
+ if (options.infoRouter === true || options.buildManifest === true) {
338
338
  logger.info('router', await UnderpostDeploy.API.routerFactory(deployList, env));
339
339
  return;
340
340
  }
package/src/cli/index.js CHANGED
@@ -360,6 +360,8 @@ program
360
360
  .option('--image-name <image-name>', 'Optional: Specifies the image name for test execution.')
361
361
  .option('--container-name <container-name>', 'Optional: Specifies the container name for test execution.')
362
362
  .option('--namespace <namespace>', 'Optional: Specifies the namespace for test execution.')
363
+ .option('--kubeadm', 'Flag to indicate Kubeadm cluster type context')
364
+ .option('--k3s', 'Flag to indicate K3s cluster type context')
363
365
  .description('Runs a script from the specified path.')
364
366
  .action(UnderpostRun.API.callback);
365
367
 
package/src/cli/run.js CHANGED
@@ -23,6 +23,8 @@ class UnderpostRun {
23
23
  namespace: '',
24
24
  build: false,
25
25
  replicas: 1,
26
+ k3s: false,
27
+ kubeadm: false,
26
28
  };
27
29
  static RUNNERS = {
28
30
  'spark-template': (path, options = UnderpostRun.DEFAULT_OPTION) => {
@@ -253,7 +255,24 @@ class UnderpostRun {
253
255
  },
254
256
  'db-client': async (path, options = UnderpostRun.DEFAULT_OPTION) => {
255
257
  const { underpostRoot } = options;
258
+
259
+ const image = 'adminer:4.7.6-standalone';
260
+
261
+ if (!options.kubeadm && !options.k3s) {
262
+ // Only load if not kubeadm/k3s (Kind needs it)
263
+ shellExec(`docker pull ${image}`);
264
+ shellExec(`sudo kind load docker-image ${image}`);
265
+ } else if (options.kubeadm || options.k3s)
266
+ // For kubeadm/k3s, ensure it's available for containerd
267
+ shellExec(`sudo crictl pull ${image}`);
268
+
269
+ shellExec(`kubectl delete deployment adminer`);
256
270
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/deployment/adminer/.`);
271
+ const successInstance = await UnderpostTest.API.statusMonitor('adminer', 'Running', 'pods', 1000, 60 * 10);
272
+
273
+ if (successInstance) {
274
+ shellExec(`underpost deploy --expose adminer`);
275
+ }
257
276
  },
258
277
  promote: async (path, options = UnderpostRun.DEFAULT_OPTION) => {
259
278
  let [inputDeployId, inputEnv, inputReplicas] = path.split(',');
@@ -332,8 +351,15 @@ class UnderpostRun {
332
351
  const checkStatusIterationMsDelay = 1000;
333
352
  const iteratorTag = `[${deployId}-${env}-${targetTraffic}]`;
334
353
  logger.info('Deployment init', { deployId, env, targetTraffic, checkStatusIterationMsDelay });
354
+ const minReadyOk = 3;
355
+ let readyOk = 0;
335
356
 
336
- while (!UnderpostDeploy.API.checkDeploymentReadyStatus(deployId, env, targetTraffic, ignorePods).ready) {
357
+ while (readyOk < minReadyOk) {
358
+ const ready = UnderpostDeploy.API.checkDeploymentReadyStatus(deployId, env, targetTraffic, ignorePods).ready;
359
+ if (ready === true) {
360
+ readyOk++;
361
+ logger.info(`${iteratorTag} | Deployment ready. Verification number: ${readyOk}`);
362
+ }
337
363
  await timer(checkStatusIterationMsDelay);
338
364
  checkStatusIteration++;
339
365
  logger.info(
@@ -16,6 +16,50 @@ import { SocketIo } from './components/core/SocketIo.js';
16
16
  import { SocketIoDefault } from './components/default/SocketIoDefault.js';
17
17
  import { ElementsDefault } from './components/default/ElementsDefault.js';
18
18
  import { CssDefaultDark, CssDefaultLight } from './components/default/CssDefault.js';
19
+ import { EventsUI } from './components/core/EventsUI.js';
20
+ import { Modal } from './components/core/Modal.js';
21
+
22
+ const htmlMainBody = async () => {
23
+ setTimeout(() => {
24
+ EventsUI.onClick('.get-started-button', (e) => {
25
+ e.preventDefault();
26
+ location.href = `https://www.nexodev.org/docs/?cid=src`;
27
+ });
28
+ });
29
+ return html`
30
+ <div class="landing-container">
31
+ <div class="content-wrapper">
32
+ <h1 class="animated-text">
33
+ <span class="greeting">Hello, World!</span>
34
+ <span class="subtitle">Welcome to Our Platform</span>
35
+ </h1>
36
+
37
+ <div class="features">
38
+ <div class="feature-card">
39
+ <i class="icon">🚀</i>
40
+ <h3>Fast & Reliable</h3>
41
+ <p>Lightning-fast performance with 99.9% uptime</p>
42
+ </div>
43
+ <div class="feature-card">
44
+ <i class="icon">🎨</i>
45
+ <h3>Beautiful UI</h3>
46
+ <p>Modern and intuitive user interface</p>
47
+ </div>
48
+ <div class="feature-card">
49
+ <i class="icon">⚡</i>
50
+ <h3>Powerful Features</h3>
51
+ <p>Everything you need in one place</p>
52
+ </div>
53
+ </div>
54
+
55
+ <button class="cta-button get-started-button">
56
+ Get Started
57
+ <span class="button-icon">→</span>
58
+ </button>
59
+ </div>
60
+ </div>
61
+ `;
62
+ };
19
63
 
20
64
  window.onload = () =>
21
65
  Worker.instance({
@@ -25,7 +69,7 @@ window.onload = () =>
25
69
  await TranslateCore.Init();
26
70
  await TranslateDefault.Init();
27
71
  await Responsive.Init();
28
- await MenuDefault.Render();
72
+ await MenuDefault.Render({ htmlMainBody });
29
73
  await SocketIo.Init({
30
74
  channels: ElementsDefault.Data,
31
75
  path: `/`,
@@ -35,5 +79,6 @@ window.onload = () =>
35
79
  await LogOutDefault();
36
80
  await SignUpDefault();
37
81
  await Keyboard.Init({ callBackTime: DefaultParams.EVENT_CALLBACK_TIME });
82
+ await Modal.RenderSeoSanitizer();
38
83
  },
39
84
  });
@@ -195,7 +195,14 @@ const Account = {
195
195
  await this.instanceModalUiEvents({ user });
196
196
  });
197
197
  return html`
198
- <input type="file" accept="${profileFileAccept.join(', ')}" class="account-profile-image-input hide" />
198
+ <label for="account-profile-image-input-label"
199
+ ><span class="hide">Profile Image</span>
200
+ <input
201
+ type="file"
202
+ accept="${profileFileAccept.join(', ')}"
203
+ class="account-profile-image-input hide"
204
+ id="account-profile-image-input-label"
205
+ /></label>
199
206
  ${renderWave({ id: waveAnimationId })}
200
207
 
201
208
  <form class="in">
@@ -5,6 +5,7 @@ import { ThemeEvents, darkTheme } from './Css.js';
5
5
  import { append, htmls, s } from './VanillaJs.js';
6
6
  import { getProxyPath } from './Router.js';
7
7
  import './Pagination.js';
8
+ import { Modal } from './Modal.js';
8
9
 
9
10
  const AgGrid = {
10
11
  grids: {},
@@ -57,18 +58,26 @@ const AgGrid = {
57
58
  delete ThemeEvents[id];
58
59
  }
59
60
  };
61
+
62
+ if (!options.style || !options.style.height) {
63
+ if (options.parentModal && Modal.Data[options.parentModal].options.observer) {
64
+ Modal.Data[options.parentModal].onObserverListener[id + '-observer'] = ({ width, height }) => {
65
+ if (s(`.${id}`)) s(`.${id}`).style.height = `${height - 180}px`;
66
+ else delete Modal.Data[options.parentModal].onObserverListener[id + '-observer'];
67
+ };
68
+ s(`.${id}`).style.height = `${s(`.${options.parentModal}`).offsetHeight - 180}px`;
69
+ } else s(`.${id}`).style.height = '500px';
70
+ }
60
71
  });
61
72
  const usePagination = options?.usePagination;
62
73
  return html`
63
- <div>
64
- <div
65
- class="${id} ${this.theme}${options?.darkTheme ? `-dark` : ''}"
66
- style="${options?.style
67
- ? Object.keys(options.style).map((styleKey) => `${styleKey}: ${options.style[styleKey]}; `)
68
- : 'height: 500px'}"
69
- ></div>
70
- ${usePagination ? `<ag-pagination id="ag-pagination-${id}"></ag-pagination>` : ''}
71
- </div>
74
+ <div
75
+ class="${id} ${this.theme}${options?.darkTheme ? `-dark` : ''}"
76
+ style="${options?.style
77
+ ? Object.keys(options.style).map((styleKey) => `${styleKey}: ${options.style[styleKey]}; `)
78
+ : ''}"
79
+ ></div>
80
+ ${usePagination ? `<ag-pagination id="ag-pagination-${id}"></ag-pagination>` : ''}
72
81
  `;
73
82
  },
74
83
  RenderStyle: async function (
@@ -1,7 +1,7 @@
1
1
  import { getId, s4 } from './CommonJs.js';
2
2
  import { renderCssAttr } from './Css.js';
3
3
  import { ToolTip } from './ToolTip.js';
4
- import { getAllChildNodes, s } from './VanillaJs.js';
4
+ import { getAllChildNodes, htmlStrSanitize, s } from './VanillaJs.js';
5
5
 
6
6
  const BtnIcon = {
7
7
  Tokens: {},
@@ -32,7 +32,7 @@ const BtnIcon = {
32
32
  : ''}`;
33
33
  let render = html`<button
34
34
  ${options?.class ? `class="${options.class} ${tokenId}"` : ''}
35
- ${options?.type ? `type="${options.type}"` : ''}
35
+ ${options?.type ? `type="${options.type}"` : `type="button"`}
36
36
  ${options?.style ? `style="${options.style}"` : ''}
37
37
  ${options?.attrs ? `${options.attrs}` : ''}
38
38
  >
@@ -49,6 +49,7 @@ const BtnIcon = {
49
49
  >`
50
50
  : label}
51
51
  </div>
52
+ <label class="hide">${htmlStrSanitize(options.label) ? htmlStrSanitize(options.label) : tokenId}</label>
52
53
  </button>`;
53
54
  if (options.tooltipHtml)
54
55
  setTimeout(() => {
@@ -1,7 +1,7 @@
1
1
  import { marked } from 'marked';
2
2
  import { FileService } from '../../services/file/file.service.js';
3
3
  import { append, getBlobFromUint8ArrayFile, getRawContentFile, htmls, s } from './VanillaJs.js';
4
- import { titleFormatted } from './CommonJs.js';
4
+ import { s4 } from './CommonJs.js';
5
5
  import { Translate } from './Translate.js';
6
6
  import { Modal, renderViewTitle } from './Modal.js';
7
7
  import { DocumentService } from '../../services/document/document.service.js';
@@ -139,6 +139,7 @@ const Content = {
139
139
  case 'png': {
140
140
  const url = Content.urlFactory(options);
141
141
  const imgRender = html`<img
142
+ alt="${file.name ? file.name : `file ${s4()}`}"
142
143
  class="in ${options.class}"
143
144
  ${styleFactory(options.style, `${renderChessPattern(50)}`)}
144
145
  src="${url}"
@@ -215,6 +215,10 @@ const CssCommonCore = async () => {
215
215
  .bar-default-modal {
216
216
  overflow: hidden;
217
217
  }
218
+ .panel-placeholder-bottom {
219
+ height: 100px;
220
+ color: gray;
221
+ }
218
222
  </style>
219
223
  ${boxShadow({ selector: '.account-profile-image' })}
220
224
  <div class="ag-grid-style"></div>`;
@@ -129,19 +129,16 @@ const Docs = {
129
129
  s(`.btn-docs-src`).onclick = async () => {
130
130
  setQueryPath({ path: 'docs', queryPath: 'src' });
131
131
  cleanActive();
132
- s(`.btn-docs-src`).classList.add('main-btn-menu-active');
133
132
  await this.RenderModal('src', options.modalOptions);
134
133
  };
135
134
  s(`.btn-docs-api`).onclick = async () => {
136
135
  setQueryPath({ path: 'docs', queryPath: 'api' });
137
136
  cleanActive();
138
- s(`.btn-docs-api`).classList.add('main-btn-menu-active');
139
137
  await this.RenderModal('api', options.modalOptions);
140
138
  };
141
139
  s(`.btn-docs-coverage`).onclick = async () => {
142
140
  setQueryPath({ path: 'docs', queryPath: 'coverage' });
143
141
  cleanActive();
144
- s(`.btn-docs-coverage`).classList.add('main-btn-menu-active');
145
142
  await this.RenderModal('coverage', options.modalOptions);
146
143
  };
147
144
 
@@ -7,7 +7,7 @@ import { loggerFactory } from './Logger.js';
7
7
  import { RichText } from './RichText.js';
8
8
  import { ToggleSwitch } from './ToggleSwitch.js';
9
9
  import { Translate } from './Translate.js';
10
- import { htmls, s } from './VanillaJs.js';
10
+ import { htmls, htmlStrSanitize, s } from './VanillaJs.js';
11
11
  const logger = loggerFactory(import.meta);
12
12
 
13
13
  const fileFormDataFactory = (e, extensions) => {
@@ -61,22 +61,27 @@ const Input = {
61
61
  };
62
62
  });
63
63
 
64
- const inputElement = html` <input
65
- type="${options?.type ? options.type : 'text'}"
66
- class="${options.inputClass ? options.inputClass : 'in wfa'} ${id}"
67
- ${options?.min !== undefined ? `min="${options.min}"` : ''}
68
- ${options?.max !== undefined ? `max="${options.max}"` : ''}
69
- placeholder${options?.placeholder ? `="${options.placeholder}"` : ''}
70
- ${options?.value !== undefined ? `value="${options.value}"` : ''}
71
- ${options?.autocomplete ? `autocomplete="${options.autocomplete}"` : ''}
72
- ${options?.disabled ? `disabled` : ''}
73
- ${options?.name !== undefined ? `name="${options.name}"` : ''}
74
- ${options?.pattern !== undefined ? `pattern="${options.pattern}"` : ''}
75
- ${options?.pattern === undefined && options.type === 'tel' ? `pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"` : ''}
76
- ${options?.required ? ` required ` : ''}
77
- ${options?.accept ? `accept="${options.accept}"` : ''}
78
- ${options?.multiple ? `multiple="multiple"` : ''}
79
- />
64
+ const labelValue = htmlStrSanitize(options.label) ? htmlStrSanitize(options.label) : id;
65
+
66
+ const inputElement = html` <label for="${id}-name">
67
+ <span class="hide">${labelValue}</span>
68
+ <input
69
+ type="${options?.type ? options.type : 'text'}"
70
+ class="${options.inputClass ? options.inputClass : 'in wfa'} ${id}"
71
+ ${options?.min !== undefined ? `min="${options.min}"` : ''}
72
+ ${options?.max !== undefined ? `max="${options.max}"` : ''}
73
+ placeholder${options?.placeholder ? `="${options.placeholder}"` : ''}
74
+ ${options?.value !== undefined ? `value="${options.value}"` : ''}
75
+ ${options?.autocomplete ? `autocomplete="${options.autocomplete}"` : ''}
76
+ ${options?.disabled ? `disabled` : ''}
77
+ ${options?.name !== undefined ? `name="${options.name}"` : `name='${id}-name'`}
78
+ ${options?.pattern !== undefined ? `pattern="${options.pattern}"` : ''}
79
+ ${options?.pattern === undefined && options.type === 'tel' ? `pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"` : ''}
80
+ ${options?.required ? ` required ` : ''}
81
+ ${options?.accept ? `accept="${options.accept}"` : ''}
82
+ ${options?.multiple ? `multiple="multiple"` : ''}
83
+ id="${id}-name"
84
+ /></label>
80
85
  <div class="${id}-input-extension input-info input-extension ${options?.extension ? '' : 'hide'}">
81
86
  ${options?.extension ? await options.extension() : ''}
82
87
  </div>`;
@@ -244,7 +249,12 @@ const Input = {
244
249
  };
245
250
 
246
251
  const InputFile = {
247
- Render: async function ({ id, multiple }, on = { change: () => {}, clear: () => {} }) {
252
+ Render: async function (
253
+ options = { id: '', multiple: false, extensionsAccept: [] },
254
+ on = { change: () => {}, clear: () => {} },
255
+ ) {
256
+ let { id, multiple, extensionsAccept } = options;
257
+ if (!extensionsAccept) extensionsAccept = [];
248
258
  // drag drop multi file
249
259
  const gridId = `ag-grid-input-file-${id}`;
250
260
  setTimeout(() => {
@@ -322,7 +332,12 @@ const InputFile = {
322
332
  return html` <div class="fl">
323
333
  <div class="in fll input-file-col">
324
334
  <div class="in section-mp input-file-sub-col">
325
- <input class="wfa ${id}" type="file" ${multiple ? `multiple="multiple"` : ''} />
335
+ <input
336
+ class="wfa ${id}"
337
+ type="file"
338
+ ${multiple ? `multiple="multiple"` : ''}
339
+ ${extensionsAccept.length > 0 ? `accept="${extensionsAccept.join(', ')}"` : ''}
340
+ />
326
341
  <div class="in">
327
342
  ${await BtnIcon.Render({
328
343
  class: `wfa btn-clear-input-file-${id}`,
@@ -1,4 +1,4 @@
1
- import { getId, newInstance } from './CommonJs.js';
1
+ import { getId, newInstance, s4 } from './CommonJs.js';
2
2
  import { Draggable } from '@neodrag/vanilla';
3
3
  import { append, s, prepend, htmls, sa, getAllChildNodes, isActiveElement } from './VanillaJs.js';
4
4
  import { BtnIcon } from './BtnIcon.js';
@@ -30,8 +30,9 @@ import { DropDown } from './DropDown.js';
30
30
  import { Keyboard } from './Keyboard.js';
31
31
  import { Badge } from './Badge.js';
32
32
  import { Worker } from './Worker.js';
33
+ import { Scroll } from './Scroll.js';
33
34
 
34
- const logger = loggerFactory(import.meta);
35
+ const logger = loggerFactory(import.meta, { trace: true });
35
36
 
36
37
  const Modal = {
37
38
  Data: {},
@@ -87,10 +88,15 @@ const Modal = {
87
88
  homeModals: options.homeModals ? options.homeModals : [],
88
89
  query: options.query ? `${window.location.search}` : undefined,
89
90
  getTop: () => window.innerHeight - (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar),
90
- getHeight: () =>
91
- window.innerHeight -
92
- (options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) -
93
- (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar),
91
+ getHeight: () => {
92
+ return (
93
+ window.innerHeight -
94
+ (s(`.main-body-btn-ui-close`) && !s(`.main-body-btn-ui-close`).classList.contains('hide')
95
+ ? (options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) +
96
+ (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
97
+ : 0)
98
+ );
99
+ },
94
100
  };
95
101
 
96
102
  if (idModal !== 'main-body' && options.mode !== 'view') {
@@ -367,6 +373,7 @@ const Modal = {
367
373
  s(`.modal-menu`).style.top = '0px';
368
374
  s(`.main-body-btn-container`).style.top = '50px';
369
375
  s(`.main-body`).style.top = '0px';
376
+ s(`.main-body`).style.height = `${window.innerHeight}px`;
370
377
  for (const event of Object.keys(Modal.Data[idModal].onBarUiClose))
371
378
  Modal.Data[idModal].onBarUiClose[event]();
372
379
  } else {
@@ -379,6 +386,7 @@ const Modal = {
379
386
  s(`.slide-menu-top-bar`).classList.remove('hide');
380
387
  s(`.bottom-bar`).classList.remove('hide');
381
388
  s(`.main-body`).style.top = `${options.heightTopBar}px`;
389
+ s(`.main-body`).style.height = `${window.innerHeight - options.heightTopBar}px`;
382
390
  for (const event of Object.keys(Modal.Data[idModal].onBarUiOpen))
383
391
  Modal.Data[idModal].onBarUiOpen[event]();
384
392
  }
@@ -1291,6 +1299,15 @@ const Modal = {
1291
1299
  }
1292
1300
 
1293
1301
  await NotificationManager.RenderBoard(options);
1302
+
1303
+ const { removeEvent } = Scroll.setEvent('.main-body', async (payload) => {
1304
+ console.warn('scroll', payload);
1305
+ if (payload.scrollTop > 100) {
1306
+ if (!s(`.main-body-btn-ui-close`).classList.contains('hide')) s(`.main-body-btn-ui-close`).click();
1307
+
1308
+ removeEvent();
1309
+ }
1310
+ });
1294
1311
  });
1295
1312
  })();
1296
1313
  break;
@@ -1385,7 +1402,7 @@ const Modal = {
1385
1402
  <div
1386
1403
  class="fix ${options && options.class ? options.class : ''} modal ${options.disableBoxShadow
1387
1404
  ? ''
1388
- : 'box-shadow'} ${idModal}"
1405
+ : 'box-shadow'} ${idModal === 'main-body' ? `${idModal} modal-home` : idModal}"
1389
1406
  >
1390
1407
  <div class="abs modal-handle-${idModal}"></div>
1391
1408
  <div class="in modal-html-${idModal}">
@@ -2246,6 +2263,11 @@ const Modal = {
2246
2263
  s(`.bottom-bar`).classList.remove('hide');
2247
2264
  s(`.modal-menu`).classList.remove('hide');
2248
2265
  },
2266
+ RenderSeoSanitizer: async () => {
2267
+ sa('img').forEach((img) => {
2268
+ if (!img.getAttribute('alt')) img.setAttribute('alt', 'image ' + Worker.title + ' ' + s4());
2269
+ });
2270
+ },
2249
2271
  };
2250
2272
 
2251
2273
  const renderMenuLabel = ({ img, text, icon }) => {