@underpostnet/underpost 2.96.1 → 2.97.0
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/.dockerignore +1 -2
- package/.env.development +0 -3
- package/.env.production +0 -3
- package/.env.test +0 -3
- package/.prettierignore +1 -2
- package/README.md +31 -31
- package/baremetal/commission-workflows.json +64 -17
- package/cli.md +71 -40
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -4
- package/package.json +3 -2
- package/scripts/disk-clean.sh +128 -187
- package/scripts/ipxe-setup.sh +197 -0
- package/scripts/ports-ls.sh +31 -0
- package/scripts/quick-tftp.sh +19 -0
- package/src/api/document/document.controller.js +15 -0
- package/src/api/document/document.model.js +14 -0
- package/src/api/document/document.router.js +1 -0
- package/src/api/document/document.service.js +61 -3
- package/src/cli/baremetal.js +1610 -432
- package/src/cli/cloud-init.js +354 -231
- package/src/cli/cluster.js +1 -1
- package/src/cli/db.js +22 -0
- package/src/cli/deploy.js +6 -2
- package/src/cli/image.js +1 -0
- package/src/cli/index.js +36 -36
- package/src/cli/run.js +77 -11
- package/src/cli/ssh.js +1 -1
- package/src/client/components/core/Input.js +3 -1
- package/src/client/components/core/Panel.js +161 -15
- package/src/client/components/core/PanelForm.js +198 -35
- package/src/client/components/core/Translate.js +11 -0
- package/src/client/services/document/document.service.js +19 -0
- package/src/index.js +1 -1
- package/src/server/dns.js +8 -2
- package/src/server/start.js +14 -6
|
@@ -3,7 +3,7 @@ import { LoadingAnimation } from '../core/LoadingAnimation.js';
|
|
|
3
3
|
import { Validator } from '../core/Validator.js';
|
|
4
4
|
import { Input } from '../core/Input.js';
|
|
5
5
|
import { darkTheme, ThemeEvents } from './Css.js';
|
|
6
|
-
import { append, getDataFromInputFile, htmls, s } from './VanillaJs.js';
|
|
6
|
+
import { append, copyData, getDataFromInputFile, htmls, s } from './VanillaJs.js';
|
|
7
7
|
import { BtnIcon } from './BtnIcon.js';
|
|
8
8
|
import { Translate } from './Translate.js';
|
|
9
9
|
import { DropDown } from './DropDown.js';
|
|
@@ -14,6 +14,8 @@ import { RichText } from './RichText.js';
|
|
|
14
14
|
import { loggerFactory } from './Logger.js';
|
|
15
15
|
import { Badge } from './Badge.js';
|
|
16
16
|
import { Content } from './Content.js';
|
|
17
|
+
import { DocumentService } from '../../services/document/document.service.js';
|
|
18
|
+
import { NotificationManager } from './NotificationManager.js';
|
|
17
19
|
|
|
18
20
|
const logger = loggerFactory(import.meta);
|
|
19
21
|
|
|
@@ -30,6 +32,9 @@ const Panel = {
|
|
|
30
32
|
originData: () => [],
|
|
31
33
|
filesData: () => [],
|
|
32
34
|
onClick: () => {},
|
|
35
|
+
share: {
|
|
36
|
+
copyLink: false,
|
|
37
|
+
},
|
|
33
38
|
},
|
|
34
39
|
) {
|
|
35
40
|
const idPanel = options?.idPanel ? options.idPanel : getId(this.Tokens, `${idPanel}-`);
|
|
@@ -87,6 +92,62 @@ const Panel = {
|
|
|
87
92
|
htmls(`.${idPanel}-cell-col-a-${id}`, render);
|
|
88
93
|
},
|
|
89
94
|
});
|
|
95
|
+
if (options.share && options.share.copyLink) {
|
|
96
|
+
EventsUI.onClick(
|
|
97
|
+
`.${idPanel}-btn-copy-share-${id}`,
|
|
98
|
+
async (e) => {
|
|
99
|
+
try {
|
|
100
|
+
const shareUrl = `${window.location.origin}${window.location.pathname}?cid=${obj._id}`;
|
|
101
|
+
await copyData(shareUrl);
|
|
102
|
+
await NotificationManager.Push({
|
|
103
|
+
status: 'success',
|
|
104
|
+
html: html`<div>${Translate.Render('link-copied')}</div>`,
|
|
105
|
+
});
|
|
106
|
+
// Track the copy share link event
|
|
107
|
+
await DocumentService.patch({ id: obj._id, action: 'copy-share-link' });
|
|
108
|
+
// Update the count in the UI - read current value from span first
|
|
109
|
+
const countSpan = s(`.${idPanel}-share-count-${id}`);
|
|
110
|
+
if (countSpan) {
|
|
111
|
+
const currentCount = parseInt(countSpan.textContent) || 0;
|
|
112
|
+
const newCount = currentCount + 1;
|
|
113
|
+
htmls(`.${idPanel}-share-count-${id}`, newCount);
|
|
114
|
+
} else {
|
|
115
|
+
// Create count badge if it didn't exist before (was 0)
|
|
116
|
+
const btn = s(`.${idPanel}-btn-copy-share-${id}`);
|
|
117
|
+
if (btn) {
|
|
118
|
+
const countBadge = document.createElement('span');
|
|
119
|
+
countBadge.className = `${idPanel}-share-count-${id}`;
|
|
120
|
+
countBadge.style.cssText =
|
|
121
|
+
'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;';
|
|
122
|
+
countBadge.textContent = '1';
|
|
123
|
+
btn.appendChild(countBadge);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logger.error('Error copying share link:', error);
|
|
128
|
+
await NotificationManager.Push({
|
|
129
|
+
status: 'error',
|
|
130
|
+
html: html`<div>${Translate.Render('error-copying-link')}</div>`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{ context: 'modal' },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Add tooltip hover effect
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
const btn = s(`.${idPanel}-btn-copy-share-${id}`);
|
|
140
|
+
const tooltip = s(`.${idPanel}-share-tooltip-${id}`);
|
|
141
|
+
if (btn && tooltip) {
|
|
142
|
+
btn.addEventListener('mouseenter', () => {
|
|
143
|
+
tooltip.style.opacity = '1';
|
|
144
|
+
});
|
|
145
|
+
btn.addEventListener('mouseleave', () => {
|
|
146
|
+
tooltip.style.opacity = '0';
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
90
151
|
EventsUI.onClick(
|
|
91
152
|
`.${idPanel}-btn-delete-${id}`,
|
|
92
153
|
async (e) => {
|
|
@@ -99,6 +160,8 @@ const Panel = {
|
|
|
99
160
|
);
|
|
100
161
|
EventsUI.onClick(`.${idPanel}-btn-edit-${id}`, async () => {
|
|
101
162
|
logger.warn('edit', obj);
|
|
163
|
+
const searchId = String(obj._id || obj.id);
|
|
164
|
+
|
|
102
165
|
if (obj._id) Panel.Tokens[idPanel].editId = obj._id;
|
|
103
166
|
else if (obj.id) Panel.Tokens[idPanel].editId = obj.id;
|
|
104
167
|
|
|
@@ -106,14 +169,11 @@ const Panel = {
|
|
|
106
169
|
s(`.btn-${idPanel}-label-add`).classList.add('hide');
|
|
107
170
|
|
|
108
171
|
openPanelForm();
|
|
109
|
-
// s(
|
|
110
|
-
s(`.${scrollClassContainer}`).scrollTop = 0;
|
|
111
|
-
|
|
172
|
+
// s(`.${scrollClassContainer}`).scrollTop = 0;
|
|
112
173
|
const originData = options.originData();
|
|
113
174
|
const filesData = options.filesData();
|
|
114
175
|
|
|
115
176
|
// Convert IDs to strings for comparison to handle ObjectId vs string issues
|
|
116
|
-
const searchId = String(obj._id || obj.id);
|
|
117
177
|
const foundOrigin = originData.find((d) => String(d._id || d.id) === searchId);
|
|
118
178
|
const foundFiles = filesData.find((d) => String(d._id || d.id) === searchId);
|
|
119
179
|
|
|
@@ -133,6 +193,8 @@ const Panel = {
|
|
|
133
193
|
);
|
|
134
194
|
}
|
|
135
195
|
|
|
196
|
+
// Clear previous form values then populate with the current item's data
|
|
197
|
+
Input.cleanValues(formData);
|
|
136
198
|
Input.setValues(formData, obj, foundOrigin, foundFiles);
|
|
137
199
|
if (options.on.initEdit) await options.on.initEdit({ data: obj });
|
|
138
200
|
});
|
|
@@ -146,28 +208,28 @@ const Panel = {
|
|
|
146
208
|
};
|
|
147
209
|
});
|
|
148
210
|
if (s(`.${idPanel}-${id}`)) s(`.${idPanel}-${id}`).remove();
|
|
149
|
-
return html` <div class="in box-shadow ${idPanel} ${idPanel}-${id}">
|
|
211
|
+
return html` <div class="in box-shadow ${idPanel} ${idPanel}-${id}" style="position: relative;">
|
|
150
212
|
<div class="fl ${idPanel}-tools session-fl-log-in ${obj.tools ? '' : 'hide'}">
|
|
151
213
|
${await BtnIcon.Render({
|
|
152
|
-
class: `in flr main-btn-menu action-bar-box ${idPanel}-btn-tool ${idPanel}-btn-
|
|
153
|
-
label: html`<div class="abs center"><i class="fas fa-
|
|
214
|
+
class: `in flr main-btn-menu action-bar-box ${idPanel}-btn-tool ${idPanel}-btn-delete-${id}`,
|
|
215
|
+
label: html`<div class="abs center"><i class="fas fa-trash"></i></div>`,
|
|
154
216
|
useVisibilityHover: true,
|
|
155
217
|
tooltipHtml: await Badge.Render({
|
|
156
218
|
id: `tooltip-${idPanel}-${id}`,
|
|
157
|
-
text: `${Translate.Render(`
|
|
219
|
+
text: `${Translate.Render(`delete`)}`,
|
|
158
220
|
classList: '',
|
|
159
|
-
style: { top: `-22px`, left: '-
|
|
221
|
+
style: { top: `-22px`, left: '-13px' },
|
|
160
222
|
}),
|
|
161
223
|
})}
|
|
162
224
|
${await BtnIcon.Render({
|
|
163
|
-
class: `in flr main-btn-menu action-bar-box ${idPanel}-btn-tool ${idPanel}-btn-
|
|
164
|
-
label: html`<div class="abs center"><i class="fas fa-
|
|
225
|
+
class: `in flr main-btn-menu action-bar-box ${idPanel}-btn-tool ${idPanel}-btn-edit-${id}`,
|
|
226
|
+
label: html`<div class="abs center"><i class="fas fa-edit"></i></div>`,
|
|
165
227
|
useVisibilityHover: true,
|
|
166
228
|
tooltipHtml: await Badge.Render({
|
|
167
229
|
id: `tooltip-${idPanel}-${id}`,
|
|
168
|
-
text: `${Translate.Render(`
|
|
230
|
+
text: `${Translate.Render(`edit`)}`,
|
|
169
231
|
classList: '',
|
|
170
|
-
style: { top: `-22px`, left: '-
|
|
232
|
+
style: { top: `-22px`, left: '-5px' },
|
|
171
233
|
}),
|
|
172
234
|
})}
|
|
173
235
|
</div>
|
|
@@ -266,6 +328,32 @@ const Panel = {
|
|
|
266
328
|
</div>
|
|
267
329
|
</div>
|
|
268
330
|
</div>
|
|
331
|
+
${options.share && options.share.copyLink
|
|
332
|
+
? html`<div
|
|
333
|
+
class="${idPanel}-share-btn-container ${idPanel}-share-btn-container-${id}"
|
|
334
|
+
style="position: absolute; bottom: 8px; right: 8px; z-index: 2;"
|
|
335
|
+
>
|
|
336
|
+
<button
|
|
337
|
+
class="btn-icon ${idPanel}-btn-copy-share-${id}"
|
|
338
|
+
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;"
|
|
339
|
+
>
|
|
340
|
+
<i class="fas fa-link" style="font-size: 20px;"></i>
|
|
341
|
+
${obj.totalCopyShareLinkCount && obj.totalCopyShareLinkCount > 0
|
|
342
|
+
? html`<span
|
|
343
|
+
class="${idPanel}-share-count-${id}"
|
|
344
|
+
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;"
|
|
345
|
+
>${obj.totalCopyShareLinkCount}</span
|
|
346
|
+
>`
|
|
347
|
+
: ''}
|
|
348
|
+
</button>
|
|
349
|
+
<div
|
|
350
|
+
class="${idPanel}-share-tooltip-${id}"
|
|
351
|
+
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;"
|
|
352
|
+
>
|
|
353
|
+
${Translate.Render('copy-share-link')}
|
|
354
|
+
</div>
|
|
355
|
+
</div>`
|
|
356
|
+
: ''}
|
|
269
357
|
</div>`;
|
|
270
358
|
};
|
|
271
359
|
|
|
@@ -410,6 +498,11 @@ const Panel = {
|
|
|
410
498
|
</div>`,
|
|
411
499
|
// disabled: true,
|
|
412
500
|
// disabledEye: true,
|
|
501
|
+
})}
|
|
502
|
+
${await BtnIcon.Render({
|
|
503
|
+
class: `inl section-mp btn-custom btn-${idPanel}-clean-file`,
|
|
504
|
+
label: html`<i class="fa-solid fa-file-circle-xmark"></i> ${Translate.Render('clear-file')}`,
|
|
505
|
+
type: 'button',
|
|
413
506
|
})}`;
|
|
414
507
|
break;
|
|
415
508
|
default:
|
|
@@ -476,6 +569,15 @@ const Panel = {
|
|
|
476
569
|
s(`.btn-${idPanel}-clean`).onclick = () => {
|
|
477
570
|
Input.cleanValues(formData);
|
|
478
571
|
};
|
|
572
|
+
s(`.btn-${idPanel}-clean-file`).onclick = () => {
|
|
573
|
+
// Clear file input specifically
|
|
574
|
+
const fileFormData = formData.find((f) => f.inputType === 'file');
|
|
575
|
+
if (fileFormData && s(`.${fileFormData.id}`)) {
|
|
576
|
+
s(`.${fileFormData.id}`).value = '';
|
|
577
|
+
s(`.${fileFormData.id}`).inputFiles = null;
|
|
578
|
+
htmls(`.file-name-render-${fileFormData.id}`, `${fileNameInputExtDefaultContent}`);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
479
581
|
s(`.btn-${idPanel}-close`).onclick = (e) => {
|
|
480
582
|
e.preventDefault();
|
|
481
583
|
s(`.${idPanel}-form-body`).style.opacity = 0;
|
|
@@ -494,10 +596,26 @@ const Panel = {
|
|
|
494
596
|
};
|
|
495
597
|
s(`.btn-${idPanel}-add`).onclick = async (e) => {
|
|
496
598
|
e.preventDefault();
|
|
497
|
-
|
|
599
|
+
|
|
600
|
+
// Clean all form inputs and reset data scope
|
|
601
|
+
Input.cleanValues(formData);
|
|
602
|
+
|
|
603
|
+
// Clean file input specifically
|
|
604
|
+
const fileFormData = formData.find((f) => f.inputType === 'file');
|
|
605
|
+
if (fileFormData && s(`.${fileFormData.id}`)) {
|
|
606
|
+
s(`.${fileFormData.id}`).value = '';
|
|
607
|
+
s(`.${fileFormData.id}`).inputFiles = null;
|
|
608
|
+
htmls(`.file-name-render-${fileFormData.id}`, `${fileNameInputExtDefaultContent}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Reset edit ID to ensure we're in "add" mode
|
|
498
612
|
Panel.Tokens[idPanel].editId = undefined;
|
|
613
|
+
|
|
614
|
+
// Update button labels
|
|
499
615
|
s(`.btn-${idPanel}-label-add`).classList.remove('hide');
|
|
500
616
|
s(`.btn-${idPanel}-label-edit`).classList.add('hide');
|
|
617
|
+
|
|
618
|
+
// Scroll to top
|
|
501
619
|
s(`.${scrollClassContainer}`).scrollTop = 0;
|
|
502
620
|
|
|
503
621
|
openPanelForm();
|
|
@@ -617,6 +735,34 @@ const Panel = {
|
|
|
617
735
|
color: #000000 !important;
|
|
618
736
|
font-size: 17px !important;
|
|
619
737
|
}
|
|
738
|
+
.${idPanel}-share-btn-container button:hover {
|
|
739
|
+
background: transparent !important;
|
|
740
|
+
transform: scale(1.1);
|
|
741
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4) !important;
|
|
742
|
+
}
|
|
743
|
+
.${idPanel}-share-btn-container button:focus {
|
|
744
|
+
outline: none;
|
|
745
|
+
background: transparent !important;
|
|
746
|
+
}
|
|
747
|
+
.${idPanel}-share-btn-container button:focus {
|
|
748
|
+
outline: none;
|
|
749
|
+
background: transparent !important;
|
|
750
|
+
}
|
|
751
|
+
.${idPanel}-share-btn-container button:active {
|
|
752
|
+
transform: scale(0.95);
|
|
753
|
+
}
|
|
754
|
+
.${idPanel}-share-btn-container span[class*='share-count'] {
|
|
755
|
+
animation: ${idPanel}-share-pulse 2s infinite;
|
|
756
|
+
}
|
|
757
|
+
@keyframes ${idPanel}-share-pulse {
|
|
758
|
+
0%,
|
|
759
|
+
100% {
|
|
760
|
+
transform: scale(1);
|
|
761
|
+
}
|
|
762
|
+
50% {
|
|
763
|
+
transform: scale(1.1);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
620
766
|
</style>
|
|
621
767
|
<style class="${idPanel}-styles"></style>
|
|
622
768
|
<div class="${idPanel}-container">
|
|
@@ -27,6 +27,9 @@ const PanelForm = {
|
|
|
27
27
|
route: 'home',
|
|
28
28
|
htmlFormHeader: async () => '',
|
|
29
29
|
firsUpdateEvent: async () => {},
|
|
30
|
+
share: {
|
|
31
|
+
copyLink: false,
|
|
32
|
+
},
|
|
30
33
|
},
|
|
31
34
|
) {
|
|
32
35
|
const { idPanel, defaultUrlImage, Elements } = options;
|
|
@@ -48,7 +51,7 @@ const PanelForm = {
|
|
|
48
51
|
id: 'panel-title',
|
|
49
52
|
model: 'title',
|
|
50
53
|
inputType: 'text',
|
|
51
|
-
rules: [
|
|
54
|
+
rules: [],
|
|
52
55
|
panel: { type: 'title' },
|
|
53
56
|
},
|
|
54
57
|
{
|
|
@@ -80,7 +83,7 @@ const PanelForm = {
|
|
|
80
83
|
// value: html``,
|
|
81
84
|
// },
|
|
82
85
|
// },
|
|
83
|
-
rules: [
|
|
86
|
+
rules: [],
|
|
84
87
|
},
|
|
85
88
|
{
|
|
86
89
|
id: 'panel-mdFileId',
|
|
@@ -108,6 +111,7 @@ const PanelForm = {
|
|
|
108
111
|
titleIcon,
|
|
109
112
|
route: options.route,
|
|
110
113
|
formContainerClass: 'session-in-log-in',
|
|
114
|
+
share: options.share,
|
|
111
115
|
onClick: async function ({ payload }) {
|
|
112
116
|
if (options.route) {
|
|
113
117
|
setQueryPath({ path: options.route, queryPath: payload._id });
|
|
@@ -169,9 +173,113 @@ const PanelForm = {
|
|
|
169
173
|
html: status,
|
|
170
174
|
status,
|
|
171
175
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
|
|
177
|
+
// Handle cid query param update (supports comma-separated list)
|
|
178
|
+
if (status === 'success') {
|
|
179
|
+
const currentCid = getQueryParams().cid;
|
|
180
|
+
|
|
181
|
+
if (currentCid) {
|
|
182
|
+
// Parse cid as comma-separated list
|
|
183
|
+
const cidList = currentCid
|
|
184
|
+
.split(',')
|
|
185
|
+
.map((id) => id.trim())
|
|
186
|
+
.filter((id) => id);
|
|
187
|
+
|
|
188
|
+
// Remove the deleted panel's id from the list
|
|
189
|
+
const updatedCidList = cidList.filter((id) => id !== data.id);
|
|
190
|
+
|
|
191
|
+
if (updatedCidList.length !== cidList.length) {
|
|
192
|
+
// Wait for DOM cleanup before updating query
|
|
193
|
+
|
|
194
|
+
if (updatedCidList.length === 0) {
|
|
195
|
+
// No cids remain, clear query and reload panels with limit
|
|
196
|
+
logger.warn('All cids removed, clearing query');
|
|
197
|
+
setQueryPath({ path: options.route, queryPath: '' });
|
|
198
|
+
|
|
199
|
+
if (options.parentIdModal) Modal.Data[options.parentIdModal].query = window.location.search;
|
|
200
|
+
if (PanelForm.Data[idPanel].updatePanel) await PanelForm.Data[idPanel].updatePanel();
|
|
201
|
+
} else {
|
|
202
|
+
// Update query params with remaining cids only (without ?cid= prefix)
|
|
203
|
+
const cidValue = updatedCidList.join(',');
|
|
204
|
+
setQueryPath({ path: options.route, queryPath: cidValue });
|
|
205
|
+
const actualQuery = window.location.search;
|
|
206
|
+
if (options.parentIdModal) Modal.Data[options.parentIdModal].query = actualQuery;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Return early to skip smart deletion logic when cid is present
|
|
211
|
+
return { status };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Smart deletion: remove from arrays and intelligently load more if needed
|
|
216
|
+
if (status === 'success') {
|
|
217
|
+
const panelData = PanelForm.Data[idPanel];
|
|
218
|
+
|
|
219
|
+
// Remove the deleted item from all data arrays
|
|
220
|
+
const indexInOrigin = panelData.originData.findIndex((d) => d._id === data._id);
|
|
221
|
+
const indexInData = panelData.data.findIndex((d) => d._id === data._id);
|
|
222
|
+
const indexInFiles = panelData.filesData.findIndex((d) => d._id === data._id);
|
|
223
|
+
|
|
224
|
+
if (indexInOrigin > -1) panelData.originData.splice(indexInOrigin, 1);
|
|
225
|
+
if (indexInData > -1) panelData.data.splice(indexInData, 1);
|
|
226
|
+
if (indexInFiles > -1) panelData.filesData.splice(indexInFiles, 1);
|
|
227
|
+
|
|
228
|
+
// Adjust skip count since we removed an item
|
|
229
|
+
if (panelData.skip > 0) panelData.skip--;
|
|
230
|
+
|
|
231
|
+
// If panels are below limit and there might be more, load them
|
|
232
|
+
if (panelData.data.length < panelData.limit && panelData.hasMore && !panelData.loading) {
|
|
233
|
+
const oldDataCount = panelData.data.length;
|
|
234
|
+
const needed = panelData.limit - panelData.data.length; // Calculate exact number needed
|
|
235
|
+
const originalLimit = panelData.limit;
|
|
236
|
+
|
|
237
|
+
// Temporarily set limit to only fetch what's needed (1-to-1 replacement)
|
|
238
|
+
panelData.limit = needed;
|
|
239
|
+
await getPanelData(true); // Load only the needed items
|
|
240
|
+
panelData.limit = originalLimit; // Restore original limit
|
|
241
|
+
|
|
242
|
+
const newItems = panelData.data.slice(oldDataCount);
|
|
243
|
+
|
|
244
|
+
if (oldDataCount === 0) {
|
|
245
|
+
// List was empty, render all panels
|
|
246
|
+
if (panelData.data.length > 0) {
|
|
247
|
+
const containerSelector = `.${options.parentIdModal ? 'html-' + options.parentIdModal : 'main-body'}`;
|
|
248
|
+
htmls(
|
|
249
|
+
containerSelector,
|
|
250
|
+
html`
|
|
251
|
+
<div class="in">${await panelRender({ data: panelData.data })}</div>
|
|
252
|
+
<div class="in panel-placeholder-bottom panel-placeholder-bottom-${idPanel}"></div>
|
|
253
|
+
`,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Show spinner if there's potentially more data
|
|
257
|
+
const lastOriginItem = panelData.originData[panelData.originData.length - 1];
|
|
258
|
+
if (
|
|
259
|
+
!panelData.lasIdAvailable ||
|
|
260
|
+
!lastOriginItem ||
|
|
261
|
+
panelData.lasIdAvailable !== lastOriginItem._id
|
|
262
|
+
)
|
|
263
|
+
LoadingAnimation.spinner.play(`.panel-placeholder-bottom-${idPanel}`, 'dual-ring-mini');
|
|
264
|
+
} else {
|
|
265
|
+
// No more data available, show empty state
|
|
266
|
+
const containerSelector = `.${options.parentIdModal ? 'html-' + options.parentIdModal : 'main-body'}`;
|
|
267
|
+
htmls(
|
|
268
|
+
containerSelector,
|
|
269
|
+
html`
|
|
270
|
+
<div class="in">${await panelRender({ data: [] })}</div>
|
|
271
|
+
<div class="in panel-placeholder-bottom panel-placeholder-bottom-${idPanel}"></div>
|
|
272
|
+
`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// List had some panels, append new ones
|
|
277
|
+
if (newItems.length > 0) {
|
|
278
|
+
for (const item of newItems)
|
|
279
|
+
append(`.${idPanel}-render`, await Panel.Tokens[idPanel].renderPanel(item));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
175
283
|
}
|
|
176
284
|
|
|
177
285
|
return { status };
|
|
@@ -179,30 +287,58 @@ const PanelForm = {
|
|
|
179
287
|
return { status: 'error' };
|
|
180
288
|
},
|
|
181
289
|
initAdd: async function () {
|
|
182
|
-
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
|
|
292
|
+
}, 50);
|
|
183
293
|
},
|
|
184
294
|
initEdit: async function ({ data }) {
|
|
185
|
-
|
|
295
|
+
// Clear file input when entering edit mode
|
|
296
|
+
const fileFormData = formData.find((f) => f.inputType === 'file');
|
|
297
|
+
if (fileFormData && s(`.${fileFormData.id}`)) {
|
|
298
|
+
s(`.${fileFormData.id}`).value = '';
|
|
299
|
+
s(`.${fileFormData.id}`).inputFiles = null;
|
|
300
|
+
htmls(
|
|
301
|
+
`.file-name-render-${fileFormData.id}`,
|
|
302
|
+
`<div class="abs center"><i style="font-size: 25px" class="fa-solid fa-cloud"></i></div>`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
setTimeout(() => {
|
|
306
|
+
s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
|
|
307
|
+
}, 50);
|
|
186
308
|
},
|
|
187
309
|
noResultFound: async function () {
|
|
188
310
|
LoadingAnimation.spinner.stop(`.panel-placeholder-bottom-${idPanel}`);
|
|
189
311
|
},
|
|
190
312
|
add: async function ({ data, editId }) {
|
|
313
|
+
// Validate that either mdFileId has content OR fileId has files
|
|
314
|
+
const hasMdContent = data.mdFileId && data.mdFileId.trim().length > 0;
|
|
315
|
+
const hasFiles = data.fileId && data.fileId.length > 0;
|
|
316
|
+
|
|
317
|
+
if (!data.title || (!hasMdContent && !hasFiles)) {
|
|
318
|
+
NotificationManager.Push({
|
|
319
|
+
html: Translate.Render('require-title-and-content-or-file'),
|
|
320
|
+
status: 'error',
|
|
321
|
+
});
|
|
322
|
+
return { data: [], status: 'error', message: 'Must provide either content or attach a file' };
|
|
323
|
+
}
|
|
324
|
+
|
|
191
325
|
let mdFileId;
|
|
192
326
|
const mdFileName = `${getCapVariableName(data.title)}.md`;
|
|
193
327
|
const location = `${prefixTags.join('/')}`;
|
|
194
328
|
const blob = new Blob([data.mdFileId], { type: 'text/markdown' });
|
|
195
329
|
const md = new File([blob], mdFileName, { type: 'text/markdown' });
|
|
196
|
-
const tags =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
330
|
+
const tags = data.tags
|
|
331
|
+
? uniqueArray(
|
|
332
|
+
data.tags
|
|
333
|
+
.replaceAll('/', ',')
|
|
334
|
+
.replaceAll('-', ',')
|
|
335
|
+
.replaceAll(' ', ',')
|
|
336
|
+
.split(',')
|
|
337
|
+
.map((t) => t.trim())
|
|
338
|
+
.filter((t) => t)
|
|
339
|
+
.concat(prefixTags),
|
|
340
|
+
)
|
|
341
|
+
: prefixTags;
|
|
206
342
|
let originObj, originFileObj, indexOriginObj;
|
|
207
343
|
if (editId) {
|
|
208
344
|
indexOriginObj = PanelForm.Data[idPanel].originData.findIndex((d) => d._id === editId);
|
|
@@ -341,10 +477,16 @@ const PanelForm = {
|
|
|
341
477
|
const panelData = PanelForm.Data[idPanel];
|
|
342
478
|
logger.warn('getPanelData called, isLoadMore:', isLoadMore);
|
|
343
479
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
480
|
+
const cidQuery = getQueryParams().cid;
|
|
481
|
+
|
|
482
|
+
// When cid query exists, bypass pagination and loading checks
|
|
483
|
+
if (!cidQuery) {
|
|
484
|
+
if (panelData.loading || !panelData.hasMore) {
|
|
485
|
+
logger.warn('getPanelData early return - loading:', panelData.loading, 'hasMore:', panelData.hasMore);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
347
488
|
}
|
|
489
|
+
|
|
348
490
|
panelData.loading = true;
|
|
349
491
|
|
|
350
492
|
if (!isLoadMore) {
|
|
@@ -353,13 +495,20 @@ const PanelForm = {
|
|
|
353
495
|
panelData.hasMore = true;
|
|
354
496
|
}
|
|
355
497
|
|
|
498
|
+
// When cid query exists, don't apply skip/limit pagination
|
|
499
|
+
const params = {
|
|
500
|
+
tags: prefixTags.join(','),
|
|
501
|
+
...(cidQuery && { cid: cidQuery }),
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Only apply pagination when there's no cid query
|
|
505
|
+
if (!cidQuery) {
|
|
506
|
+
params.skip = panelData.skip;
|
|
507
|
+
params.limit = panelData.limit;
|
|
508
|
+
}
|
|
509
|
+
|
|
356
510
|
const result = await DocumentService.get({
|
|
357
|
-
params
|
|
358
|
-
tags: prefixTags.join(','),
|
|
359
|
-
...(getQueryParams().cid && { cid: getQueryParams().cid }),
|
|
360
|
-
skip: panelData.skip,
|
|
361
|
-
limit: panelData.limit,
|
|
362
|
-
},
|
|
511
|
+
params,
|
|
363
512
|
id: 'public/',
|
|
364
513
|
});
|
|
365
514
|
|
|
@@ -417,6 +566,7 @@ const PanelForm = {
|
|
|
417
566
|
fileId,
|
|
418
567
|
tools: Elements.Data.user.main.model.user._id === documentObject.userId._id,
|
|
419
568
|
_id: documentObject._id,
|
|
569
|
+
totalCopyShareLinkCount: documentObject.totalCopyShareLinkCount || 0,
|
|
420
570
|
});
|
|
421
571
|
} catch (fileError) {
|
|
422
572
|
logger.error('Error fetching files for document:', documentObject._id, fileError);
|
|
@@ -425,9 +575,17 @@ const PanelForm = {
|
|
|
425
575
|
}
|
|
426
576
|
}
|
|
427
577
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
578
|
+
// Only update pagination when not using cid query
|
|
579
|
+
if (!cidQuery) {
|
|
580
|
+
panelData.skip += result.data.data.length;
|
|
581
|
+
panelData.hasMore = result.data.data.length === panelData.limit;
|
|
582
|
+
} else {
|
|
583
|
+
// When cid query is used, disable infinite scroll
|
|
584
|
+
panelData.hasMore = false;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const lastItem = result.data.data[result.data.data.length - 1];
|
|
588
|
+
if (result.data.data.length === 0 || (lastItem && lastItem._id === panelData.lasIdAvailable)) {
|
|
431
589
|
LoadingAnimation.spinner.stop(`.panel-placeholder-bottom-${idPanel}`);
|
|
432
590
|
panelData.hasMore = false;
|
|
433
591
|
}
|
|
@@ -538,7 +696,8 @@ const PanelForm = {
|
|
|
538
696
|
loading: false,
|
|
539
697
|
};
|
|
540
698
|
|
|
541
|
-
|
|
699
|
+
// Always reset skip to 0 when reloading (whether cid exists or not)
|
|
700
|
+
this.Data[idPanel].skip = 0;
|
|
542
701
|
|
|
543
702
|
const containerSelector = `.${options.parentIdModal ? 'html-' + options.parentIdModal : 'main-body'}`;
|
|
544
703
|
htmls(containerSelector, await renderSrrPanelData());
|
|
@@ -553,22 +712,26 @@ const PanelForm = {
|
|
|
553
712
|
`,
|
|
554
713
|
);
|
|
555
714
|
|
|
715
|
+
const lastOriginItem = this.Data[idPanel].originData[this.Data[idPanel].originData.length - 1];
|
|
556
716
|
if (
|
|
557
717
|
!this.Data[idPanel].lasIdAvailable ||
|
|
558
|
-
|
|
559
|
-
|
|
718
|
+
!lastOriginItem ||
|
|
719
|
+
this.Data[idPanel].lasIdAvailable !== lastOriginItem._id
|
|
560
720
|
)
|
|
561
721
|
LoadingAnimation.spinner.play(`.panel-placeholder-bottom-${idPanel}`, 'dual-ring-mini');
|
|
562
722
|
|
|
563
723
|
const scrollContainerSelector = `.modal-${options.route}`;
|
|
564
724
|
|
|
725
|
+
// Always remove old scroll event before setting new one
|
|
726
|
+
if (this.Data[idPanel].removeScrollEvent) {
|
|
727
|
+
this.Data[idPanel].removeScrollEvent();
|
|
728
|
+
}
|
|
729
|
+
|
|
565
730
|
if (cid) {
|
|
566
731
|
LoadingAnimation.spinner.stop(`.panel-placeholder-bottom-${idPanel}`);
|
|
567
732
|
return;
|
|
568
733
|
}
|
|
569
|
-
|
|
570
|
-
this.Data[idPanel].removeScrollEvent();
|
|
571
|
-
}
|
|
734
|
+
|
|
572
735
|
const { removeEvent } = Scroll.setEvent(scrollContainerSelector, async (payload) => {
|
|
573
736
|
const panelData = PanelForm.Data[idPanel];
|
|
574
737
|
if (!panelData) return;
|
|
@@ -188,6 +188,9 @@ const TranslateCore = {
|
|
|
188
188
|
Translate.Data['error-update-user'] = { en: 'error update user', es: 'error al actualizar el usuario' };
|
|
189
189
|
|
|
190
190
|
Translate.Data['edit'] = { en: 'Edit', es: 'Editar' };
|
|
191
|
+
Translate.Data['copy-share-link'] = { en: 'Copy share link', es: 'Copiar enlace compartido' };
|
|
192
|
+
Translate.Data['link-copied'] = { en: 'Link copied to clipboard', es: 'Enlace copiado al portapapeles' };
|
|
193
|
+
Translate.Data['error-copying-link'] = { en: 'Error copying link', es: 'Error al copiar enlace' };
|
|
191
194
|
Translate.Data['unconfirmed'] = { en: 'unconfirmed', es: 'No confirmado' };
|
|
192
195
|
Translate.Data['confirmed'] = { en: 'confirmed', es: 'Confirmado' };
|
|
193
196
|
Translate.Data['confirm'] = { en: 'confirm', es: 'confirmar' };
|
|
@@ -520,6 +523,14 @@ const TranslateCore = {
|
|
|
520
523
|
en: 'Data reloaded successfully.',
|
|
521
524
|
es: 'Datos recargados con éxito.',
|
|
522
525
|
};
|
|
526
|
+
Translate.Data['clear-file'] = {
|
|
527
|
+
en: 'Clear File',
|
|
528
|
+
es: 'Limpiar Archivos',
|
|
529
|
+
};
|
|
530
|
+
Translate.Data['require-title-and-content-or-file'] = {
|
|
531
|
+
en: 'Require title and content or file',
|
|
532
|
+
es: 'Requiere título y contenido o archivo',
|
|
533
|
+
};
|
|
523
534
|
},
|
|
524
535
|
};
|
|
525
536
|
|
|
@@ -92,6 +92,25 @@ const DocumentService = {
|
|
|
92
92
|
return reject(error);
|
|
93
93
|
}),
|
|
94
94
|
),
|
|
95
|
+
patch: (options = { id: '', action: '' }) =>
|
|
96
|
+
new Promise((resolve, reject) =>
|
|
97
|
+
fetch(getApiBaseUrl({ id: `${options.id}/${options.action}`, endpoint }), {
|
|
98
|
+
method: 'PATCH',
|
|
99
|
+
headers: headersFactory(),
|
|
100
|
+
credentials: 'include',
|
|
101
|
+
})
|
|
102
|
+
.then(async (res) => {
|
|
103
|
+
return await res.json();
|
|
104
|
+
})
|
|
105
|
+
.then((res) => {
|
|
106
|
+
logger.info(res);
|
|
107
|
+
return resolve(res);
|
|
108
|
+
})
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
logger.error(error);
|
|
111
|
+
return reject(error);
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
95
114
|
};
|
|
96
115
|
|
|
97
116
|
export { DocumentService };
|