@underpostnet/underpost 2.98.0 → 2.98.1

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/src/cli/run.js CHANGED
@@ -146,111 +146,6 @@ class UnderpostRun {
146
146
  * @memberof UnderpostRun
147
147
  */
148
148
  static RUNNERS = {
149
- /**
150
- * @method spark-template
151
- * @description Creates a new Spark template project using `sbt new` in `/home/dd/spark-template`, initializes a Git repository, and runs `replace_params.sh` and `build.sh`.
152
- * @param {string} path - The input value, identifier, or path for the operation.
153
- * @param {Object} options - The default underpost runner options for customizing workflow
154
- * @memberof UnderpostRun
155
- */
156
- 'spark-template': (path, options = UnderpostRun.DEFAULT_OPTION) => {
157
- const dir = '/home/dd/spark-template';
158
- shellExec(`sudo rm -rf ${dir}`);
159
- shellCd('/home/dd');
160
-
161
- // pbcopy(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8`);
162
- // await read({ prompt: 'Command copy to clipboard, press enter to continue.\n' });
163
- shellExec(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8 '--name=spark-template'`);
164
-
165
- shellCd(dir);
166
-
167
- shellExec(`git init && git add . && git commit -m "Base implementation"`);
168
- shellExec(`chmod +x ./replace_params.sh`);
169
- shellExec(`chmod +x ./build.sh`);
170
-
171
- shellExec(`./replace_params.sh`);
172
- shellExec(`./build.sh`);
173
-
174
- shellCd('/home/dd/engine');
175
- },
176
- /**
177
- * @method rmi
178
- * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
179
- * @param {string} path - The input value, identifier, or path for the operation.
180
- * @param {Object} options - The default underpost runner options for customizing workflow
181
- * @memberof UnderpostRun
182
- */
183
- rmi: (path, options = UnderpostRun.DEFAULT_OPTION) => {
184
- shellExec(`podman rmi $(podman images -qa) --force`);
185
- },
186
- /**
187
- * @method kill
188
- * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
189
- * @param {string} path - The input value, identifier, or path for the operation (used as the port number).
190
- * @param {Object} options - The default underpost runner options for customizing workflow
191
- * @memberof UnderpostRun
192
- */
193
- kill: (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
194
- if (options.pid) return shellExec(`sudo kill -9 ${options.pid}`);
195
- for (const _path of path.split(',')) {
196
- if (_path.split('+')[1]) {
197
- let [port, sumPortOffSet] = _path.split('+');
198
- port = parseInt(port);
199
- sumPortOffSet = parseInt(sumPortOffSet);
200
- for (const sumPort of range(0, sumPortOffSet))
201
- shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})`);
202
- } else shellExec(`sudo kill -9 $(lsof -t -i:${_path})`);
203
- }
204
- },
205
- /**
206
- * @method secret
207
- * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
208
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
209
- * @param {Object} options - The default underpost runner options for customizing workflow
210
- * @memberof UnderpostRun
211
- */
212
- secret: (path, options = UnderpostRun.DEFAULT_OPTION) => {
213
- shellExec(
214
- `underpost secret underpost --create-from-file ${
215
- path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`
216
- }`,
217
- );
218
- },
219
- /**
220
- * @method underpost-config
221
- * @description Calls `UnderpostDeploy.API.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
222
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
223
- * @param {Object} options - The default underpost runner options for customizing workflow
224
- * @memberof UnderpostRun
225
- */
226
- 'underpost-config': (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
227
- UnderpostDeploy.API.configMap(path ? path : 'production', options.namespace);
228
- },
229
- /**
230
- * @method gpu-env
231
- * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
232
- * @param {string} path - The input value, identifier, or path for the operation.
233
- * @param {Object} options - The default underpost runner options for customizing workflow
234
- * @memberof UnderpostRun
235
- */
236
- 'gpu-env': (path, options = UnderpostRun.DEFAULT_OPTION) => {
237
- shellExec(
238
- `node bin cluster --dev --reset && node bin cluster --dev --dedicated-gpu --kubeadm && kubectl get pods --all-namespaces -o wide -w`,
239
- );
240
- },
241
- /**
242
- * @method tf-gpu-test
243
- * @description Deletes existing `tf-gpu-test-script` ConfigMap and `tf-gpu-test-pod`, and applies the test manifest from `manifests/deployment/tensorflow/tf-gpu-test.yaml`.
244
- * @param {string} path - The input value, identifier, or path for the operation.
245
- * @param {Object} options - The default underpost runner options for customizing workflow
246
- * @memberof UnderpostRun
247
- */
248
- 'tf-gpu-test': (path, options = UnderpostRun.DEFAULT_OPTION) => {
249
- const { underpostRoot, namespace } = options;
250
- shellExec(`kubectl delete configmap tf-gpu-test-script -n ${namespace} --ignore-not-found`);
251
- shellExec(`kubectl delete pod tf-gpu-test-pod -n ${namespace} --ignore-not-found`);
252
- shellExec(`kubectl apply -f ${underpostRoot}/manifests/deployment/tensorflow/tf-gpu-test.yaml -n ${namespace}`);
253
- },
254
149
  /**
255
150
  * @method dev-cluster
256
151
  * @description Resets and deploys a full development cluster including MongoDB, Valkey, exposes services, and updates `/etc/hosts` for local access.
@@ -899,7 +794,7 @@ EOF
899
794
  'host-update': async (path, options = UnderpostRun.DEFAULT_OPTION) => {
900
795
  // const baseCommand = options.dev ? 'node bin' : 'underpost';
901
796
  shellExec(`chmod +x ${options.underpostRoot}/scripts/rocky-setup.sh`);
902
- shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh --yes${options.dev ? ' --install-dev' : ``}`);
797
+ shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
903
798
  },
904
799
 
905
800
  /**
@@ -1600,6 +1495,113 @@ EOF
1600
1495
  ],
1601
1496
  });
1602
1497
  },
1498
+
1499
+ /**
1500
+ * @method spark-template
1501
+ * @description Creates a new Spark template project using `sbt new` in `/home/dd/spark-template`, initializes a Git repository, and runs `replace_params.sh` and `build.sh`.
1502
+ * @param {string} path - The input value, identifier, or path for the operation.
1503
+ * @param {Object} options - The default underpost runner options for customizing workflow
1504
+ * @memberof UnderpostRun
1505
+ */
1506
+ 'spark-template': (path, options = UnderpostRun.DEFAULT_OPTION) => {
1507
+ const dir = '/home/dd/spark-template';
1508
+ shellExec(`sudo rm -rf ${dir}`);
1509
+ shellCd('/home/dd');
1510
+
1511
+ // pbcopy(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8`);
1512
+ // await read({ prompt: 'Command copy to clipboard, press enter to continue.\n' });
1513
+ shellExec(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8 '--name=spark-template'`);
1514
+
1515
+ shellCd(dir);
1516
+
1517
+ shellExec(`git init && git add . && git commit -m "Base implementation"`);
1518
+ shellExec(`chmod +x ./replace_params.sh`);
1519
+ shellExec(`chmod +x ./build.sh`);
1520
+
1521
+ shellExec(`./replace_params.sh`);
1522
+ shellExec(`./build.sh`);
1523
+
1524
+ shellCd('/home/dd/engine');
1525
+ },
1526
+ /**
1527
+ * @method rmi
1528
+ * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
1529
+ * @param {string} path - The input value, identifier, or path for the operation.
1530
+ * @param {Object} options - The default underpost runner options for customizing workflow
1531
+ * @memberof UnderpostRun
1532
+ */
1533
+ rmi: (path, options = UnderpostRun.DEFAULT_OPTION) => {
1534
+ shellExec(`podman rmi $(podman images -qa) --force`);
1535
+ },
1536
+ /**
1537
+ * @method kill
1538
+ * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
1539
+ * @param {string} path - The input value, identifier, or path for the operation (used as the port number).
1540
+ * @param {Object} options - The default underpost runner options for customizing workflow
1541
+ * @memberof UnderpostRun
1542
+ */
1543
+ kill: (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
1544
+ if (options.pid) return shellExec(`sudo kill -9 ${options.pid}`);
1545
+ for (const _path of path.split(',')) {
1546
+ if (_path.split('+')[1]) {
1547
+ let [port, sumPortOffSet] = _path.split('+');
1548
+ port = parseInt(port);
1549
+ sumPortOffSet = parseInt(sumPortOffSet);
1550
+ for (const sumPort of range(0, sumPortOffSet))
1551
+ shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})`);
1552
+ } else shellExec(`sudo kill -9 $(lsof -t -i:${_path})`);
1553
+ }
1554
+ },
1555
+ /**
1556
+ * @method secret
1557
+ * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
1558
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
1559
+ * @param {Object} options - The default underpost runner options for customizing workflow
1560
+ * @memberof UnderpostRun
1561
+ */
1562
+ secret: (path, options = UnderpostRun.DEFAULT_OPTION) => {
1563
+ const secretPath = path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`;
1564
+ const command = options.dev
1565
+ ? `node bin secret underpost --create-from-file ${secretPath}`
1566
+ : `underpost secret underpost --create-from-file ${secretPath}`;
1567
+ shellExec(command);
1568
+ },
1569
+ /**
1570
+ * @method underpost-config
1571
+ * @description Calls `UnderpostDeploy.API.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
1572
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
1573
+ * @param {Object} options - The default underpost runner options for customizing workflow
1574
+ * @memberof UnderpostRun
1575
+ */
1576
+ 'underpost-config': (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
1577
+ UnderpostDeploy.API.configMap(path ? path : 'production', options.namespace);
1578
+ },
1579
+ /**
1580
+ * @method gpu-env
1581
+ * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
1582
+ * @param {string} path - The input value, identifier, or path for the operation.
1583
+ * @param {Object} options - The default underpost runner options for customizing workflow
1584
+ * @memberof UnderpostRun
1585
+ */
1586
+ 'gpu-env': (path, options = UnderpostRun.DEFAULT_OPTION) => {
1587
+ shellExec(
1588
+ `node bin cluster --dev --reset && node bin cluster --dev --dedicated-gpu --kubeadm && kubectl get pods --all-namespaces -o wide -w`,
1589
+ );
1590
+ },
1591
+ /**
1592
+ * @method tf-gpu-test
1593
+ * @description Deletes existing `tf-gpu-test-script` ConfigMap and `tf-gpu-test-pod`, and applies the test manifest from `manifests/deployment/tensorflow/tf-gpu-test.yaml`.
1594
+ * @param {string} path - The input value, identifier, or path for the operation.
1595
+ * @param {Object} options - The default underpost runner options for customizing workflow
1596
+ * @memberof UnderpostRun
1597
+ */
1598
+ 'tf-gpu-test': (path, options = UnderpostRun.DEFAULT_OPTION) => {
1599
+ const { underpostRoot, namespace } = options;
1600
+ shellExec(`kubectl delete configmap tf-gpu-test-script -n ${namespace} --ignore-not-found`);
1601
+ shellExec(`kubectl delete pod tf-gpu-test-pod -n ${namespace} --ignore-not-found`);
1602
+ shellExec(`kubectl apply -f ${underpostRoot}/manifests/deployment/tensorflow/tf-gpu-test.yaml -n ${namespace}`);
1603
+ },
1604
+
1603
1605
  /**
1604
1606
  * @method deploy-job
1605
1607
  * @description Creates and applies a custom Kubernetes Pod manifest (Job) for running arbitrary commands inside a container image (defaulting to a TensorFlow/NVIDIA image).
@@ -1,6 +1,6 @@
1
1
  import { marked } from 'marked';
2
2
  import { FileService } from '../../services/file/file.service.js';
3
- import { append, getBlobFromUint8ArrayFile, getRawContentFile, htmls, s } from './VanillaJs.js';
3
+ import { append, getBlobFromUint8ArrayFile, getRawContentFile, htmls, s, sa } from './VanillaJs.js';
4
4
  import { s4 } from './CommonJs.js';
5
5
  import { Translate } from './Translate.js';
6
6
  import { Modal, renderViewTitle } from './Modal.js';
@@ -12,6 +12,49 @@ import { getQueryParams } from './Router.js';
12
12
 
13
13
  const logger = loggerFactory(import.meta);
14
14
 
15
+ const attachMarkdownLinkHandlers = (containerSelector) => {
16
+ const links = sa(`${containerSelector} a[href]`);
17
+ links.forEach((link) => {
18
+ link.addEventListener('click', async (e) => {
19
+ e.preventDefault();
20
+ const href = link.getAttribute('href');
21
+
22
+ // Check if link is external
23
+ const isExternal = href.startsWith('http://') || href.startsWith('https://');
24
+
25
+ if (isExternal) {
26
+ // Show warning modal for external links
27
+ const result = await Modal.RenderConfirm({
28
+ id: `external-link-${s4()}`,
29
+ html: async () => html`
30
+ <div class="in section-mp" style="text-align: center; padding: 20px;">
31
+ <p>${Translate.Render('external-link-warning')}</p>
32
+ <p style="word-break: break-all; margin-top: 10px;"><strong>${href}</strong></p>
33
+ </div>
34
+ `,
35
+ icon: html`<i class="fas fa-external-link-alt"></i>`,
36
+ style: {
37
+ width: '350px',
38
+ height: '500px',
39
+ overflow: 'auto',
40
+ 'z-index': '11',
41
+ resize: 'none',
42
+ },
43
+ });
44
+
45
+ // Only open link if user confirmed (not cancelled or closed)
46
+ if (result && result.status === 'confirm') {
47
+ window.open(href, '_blank', 'noopener,noreferrer');
48
+ }
49
+ // If cancelled, do nothing - don't navigate
50
+ } else {
51
+ // Internal link - navigate normally
52
+ window.location.href = href;
53
+ }
54
+ });
55
+ });
56
+ };
57
+
15
58
  const Content = {
16
59
  Render: async function (options = { idModal: '' }) {
17
60
  const { idModal } = options;
@@ -239,6 +282,11 @@ ${JSON.stringify(JSON.parse(content), null, 4)}</pre
239
282
 
240
283
  if (options.raw) return render;
241
284
  append(container, render);
285
+
286
+ // Scrape and handle markdown links after DOM insertion
287
+ if (ext === 'md') {
288
+ attachMarkdownLinkHandlers(container);
289
+ }
242
290
  },
243
291
 
244
292
  /**
@@ -274,4 +322,4 @@ ${JSON.stringify(JSON.parse(content), null, 4)}</pre
274
322
  },
275
323
  };
276
324
 
277
- export { Content };
325
+ export { Content, attachMarkdownLinkHandlers };
@@ -13,7 +13,7 @@ import { ToggleSwitch } from './ToggleSwitch.js';
13
13
  import { RichText } from './RichText.js';
14
14
  import { loggerFactory } from './Logger.js';
15
15
  import { Badge } from './Badge.js';
16
- import { Content } from './Content.js';
16
+ import { Content, attachMarkdownLinkHandlers } from './Content.js';
17
17
  import { DocumentService } from '../../services/document/document.service.js';
18
18
  import { NotificationManager } from './NotificationManager.js';
19
19
  import { getApiBaseUrl } from '../../services/core/core.service.js';
@@ -37,6 +37,7 @@ const Panel = {
37
37
  onClick: () => {},
38
38
  share: {
39
39
  copyLink: false,
40
+ copySourceMd: false,
40
41
  },
41
42
  showCreatorProfile: false,
42
43
  },
@@ -152,6 +153,51 @@ const Panel = {
152
153
  }
153
154
  });
154
155
  }
156
+ if (options.share && options.share.copySourceMd) {
157
+ EventsUI.onClick(
158
+ `.${idPanel}-btn-copy-source-md-${id}`,
159
+ async (e) => {
160
+ try {
161
+ const filesData = options.filesData();
162
+ const foundFiles = filesData.find((d) => String(d._id || d.id) === String(obj._id || obj.id));
163
+
164
+ if (foundFiles && foundFiles.mdFileId && foundFiles.mdFileId.mdPlain) {
165
+ await copyData(foundFiles.mdFileId.mdPlain);
166
+ await NotificationManager.Push({
167
+ status: 'success',
168
+ html: html`<div>${Translate.Render('markdown-source-copied')}</div>`,
169
+ });
170
+ } else {
171
+ await NotificationManager.Push({
172
+ status: 'warning',
173
+ html: html`<div>No markdown source available</div>`,
174
+ });
175
+ }
176
+ } catch (error) {
177
+ logger.error('Error copying markdown source:', error);
178
+ await NotificationManager.Push({
179
+ status: 'error',
180
+ html: html`<div>${Translate.Render('error-copying-markdown')}</div>`,
181
+ });
182
+ }
183
+ },
184
+ { context: 'modal' },
185
+ );
186
+
187
+ // Add tooltip hover effect
188
+ setTimeout(() => {
189
+ const btn = s(`.${idPanel}-btn-copy-source-md-${id}`);
190
+ const tooltip = s(`.${idPanel}-source-md-tooltip-${id}`);
191
+ if (btn && tooltip) {
192
+ btn.addEventListener('mouseenter', () => {
193
+ tooltip.style.opacity = '1';
194
+ });
195
+ btn.addEventListener('mouseleave', () => {
196
+ tooltip.style.opacity = '0';
197
+ });
198
+ }
199
+ });
200
+ }
155
201
  EventsUI.onClick(
156
202
  `.${idPanel}-btn-delete-${id}`,
157
203
  async (e) => {
@@ -514,30 +560,50 @@ const Panel = {
514
560
  </div>
515
561
  </div>
516
562
  </div>
517
- ${options.share && options.share.copyLink
563
+ ${options.share && (options.share.copyLink || options.share.copySourceMd)
518
564
  ? html`<div
519
565
  class="${idPanel}-share-btn-container ${idPanel}-share-btn-container-${id}"
520
- style="position: absolute; bottom: 8px; right: 8px; z-index: 2;"
566
+ style="position: absolute; bottom: 8px; right: 8px; z-index: 2; display: flex; gap: 8px;"
521
567
  >
522
- <button
523
- class="btn-icon ${idPanel}-btn-copy-share-${id}"
524
- style="background: transparent; color: #888; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; position: relative; transition: all 0.3s ease;"
525
- >
526
- <i class="fas fa-link" style="font-size: 20px;"></i>
527
- ${obj.totalCopyShareLinkCount && obj.totalCopyShareLinkCount > 0
528
- ? html`<span
529
- class="${idPanel}-share-count-${id}"
530
- style="position: absolute; top: -4px; right: -4px; background: #666; color: white; border-radius: 10px; padding: 1px 5px; font-size: 10px; font-weight: bold; min-width: 16px; text-align: center;"
531
- >${obj.totalCopyShareLinkCount}</span
532
- >`
533
- : ''}
534
- </button>
535
- <div
536
- class="${idPanel}-share-tooltip-${id}"
537
- style="position: absolute; bottom: 50px; right: 0; background: rgba(0,0,0,0.8); color: white; padding: 6px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.3s ease;"
538
- >
539
- ${Translate.Render('copy-share-link')}
540
- </div>
568
+ ${options.share.copyLink
569
+ ? html`<div style="position: relative;">
570
+ <button
571
+ class="btn-icon ${idPanel}-btn-copy-share-${id}"
572
+ style="background: transparent; color: #888; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; position: relative; transition: all 0.3s ease;"
573
+ >
574
+ <i class="fas fa-link" style="font-size: 20px;"></i>
575
+ ${obj.totalCopyShareLinkCount && obj.totalCopyShareLinkCount > 0
576
+ ? html`<span
577
+ class="${idPanel}-share-count-${id}"
578
+ style="position: absolute; top: -4px; right: -4px; background: #666; color: white; border-radius: 10px; padding: 1px 5px; font-size: 10px; font-weight: bold; min-width: 16px; text-align: center;"
579
+ >${obj.totalCopyShareLinkCount}</span
580
+ >`
581
+ : ''}
582
+ </button>
583
+ <div
584
+ class="${idPanel}-share-tooltip-${id}"
585
+ style="position: absolute; bottom: 50px; right: 0; background: rgba(0,0,0,0.8); color: white; padding: 6px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.3s ease;"
586
+ >
587
+ ${Translate.Render('copy-share-link')}
588
+ </div>
589
+ </div>`
590
+ : ''}
591
+ ${options.share.copySourceMd
592
+ ? html`<div style="position: relative;">
593
+ <button
594
+ class="btn-icon ${idPanel}-btn-copy-source-md-${id}"
595
+ style="background: transparent; color: #888; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; position: relative; transition: all 0.3s ease;"
596
+ >
597
+ <i class="fas fa-code" style="font-size: 20px;"></i>
598
+ </button>
599
+ <div
600
+ class="${idPanel}-source-md-tooltip-${id}"
601
+ style="position: absolute; bottom: 50px; right: 0; background: rgba(0,0,0,0.8); color: white; padding: 6px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.3s ease;"
602
+ >
603
+ Copy Source MD
604
+ </div>
605
+ </div>`
606
+ : ''}
541
607
  </div>`
542
608
  : ''}
543
609
  </div>`;
@@ -748,6 +814,8 @@ const Panel = {
748
814
  append(`.${idPanel}-render`, await renderPanel(doc));
749
815
  }
750
816
  } else htmls(`.${idPanel}-render`, await renderPanel({ ...obj, ...documents }));
817
+
818
+ attachMarkdownLinkHandlers(`.${idPanel}-render`);
751
819
  Input.cleanValues(formData);
752
820
  s(`.btn-${idPanel}-close`).click();
753
821
  s(`.${scrollClassContainer}`).scrollTop = 0;
@@ -810,6 +878,7 @@ const Panel = {
810
878
  if (options.on.initAdd) await options.on.initAdd();
811
879
  };
812
880
  if (s(`.${scrollClassContainer}`)) s(`.${scrollClassContainer}`).style.overflow = 'auto';
881
+ attachMarkdownLinkHandlers(`.${idPanel}-render`);
813
882
  });
814
883
 
815
884
  if (data.length > 0) for (const obj of data) render += await renderPanel(obj);
@@ -92,6 +92,7 @@ const PanelForm = {
92
92
  firsUpdateEvent: async () => {},
93
93
  share: {
94
94
  copyLink: false,
95
+ copySourceMd: false,
95
96
  },
96
97
  showCreatorProfile: false,
97
98
  },
@@ -679,6 +679,14 @@ const TranslateCore = {
679
679
  en: 'Public Profile',
680
680
  es: 'Perfil Público',
681
681
  };
682
+ Translate.Data['external-link-warning'] = {
683
+ en: 'You are about to open an external link. Do you want to continue?',
684
+ es: 'Está a punto de abrir un enlace externo. ¿Desea continuar?',
685
+ };
686
+ Translate.Data['markdown-source-copied'] = {
687
+ en: 'Markdown source copied to clipboard',
688
+ es: 'Fuente de Markdown copiada al portapapeles',
689
+ };
682
690
  },
683
691
  };
684
692