@underpostnet/underpost 2.97.1 → 2.97.5

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 (59) hide show
  1. package/README.md +2 -2
  2. package/cli.md +3 -1
  3. package/conf.js +2 -0
  4. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  5. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  6. package/package.json +1 -1
  7. package/src/api/core/core.service.js +0 -5
  8. package/src/api/default/default.service.js +7 -5
  9. package/src/api/document/document.model.js +1 -1
  10. package/src/api/document/document.router.js +5 -0
  11. package/src/api/document/document.service.js +105 -47
  12. package/src/api/file/file.model.js +112 -4
  13. package/src/api/file/file.ref.json +42 -0
  14. package/src/api/file/file.service.js +380 -32
  15. package/src/api/user/user.model.js +38 -1
  16. package/src/api/user/user.router.js +96 -63
  17. package/src/api/user/user.service.js +81 -48
  18. package/src/cli/db.js +424 -166
  19. package/src/cli/index.js +8 -0
  20. package/src/cli/repository.js +1 -1
  21. package/src/cli/run.js +1 -0
  22. package/src/cli/ssh.js +10 -10
  23. package/src/client/components/core/Account.js +327 -36
  24. package/src/client/components/core/AgGrid.js +3 -0
  25. package/src/client/components/core/Auth.js +9 -3
  26. package/src/client/components/core/Chat.js +2 -2
  27. package/src/client/components/core/Content.js +159 -78
  28. package/src/client/components/core/CssCore.js +16 -12
  29. package/src/client/components/core/FileExplorer.js +115 -8
  30. package/src/client/components/core/Input.js +204 -11
  31. package/src/client/components/core/LogIn.js +42 -20
  32. package/src/client/components/core/Modal.js +138 -24
  33. package/src/client/components/core/Panel.js +69 -31
  34. package/src/client/components/core/PanelForm.js +262 -77
  35. package/src/client/components/core/PublicProfile.js +888 -0
  36. package/src/client/components/core/Router.js +117 -15
  37. package/src/client/components/core/SearchBox.js +329 -13
  38. package/src/client/components/core/SignUp.js +26 -7
  39. package/src/client/components/core/SocketIo.js +6 -3
  40. package/src/client/components/core/Translate.js +98 -0
  41. package/src/client/components/core/Validator.js +15 -0
  42. package/src/client/components/core/windowGetDimensions.js +6 -6
  43. package/src/client/components/default/MenuDefault.js +59 -12
  44. package/src/client/components/default/RoutesDefault.js +1 -0
  45. package/src/client/services/core/core.service.js +163 -1
  46. package/src/client/services/default/default.management.js +451 -64
  47. package/src/client/services/default/default.service.js +13 -6
  48. package/src/client/services/file/file.service.js +43 -16
  49. package/src/client/services/user/user.service.js +13 -9
  50. package/src/db/DataBaseProvider.js +1 -1
  51. package/src/db/mongo/MongooseDB.js +1 -1
  52. package/src/index.js +1 -1
  53. package/src/mailer/MailerProvider.js +4 -4
  54. package/src/runtime/express/Express.js +2 -1
  55. package/src/runtime/lampp/Lampp.js +2 -2
  56. package/src/server/auth.js +3 -6
  57. package/src/server/data-query.js +449 -0
  58. package/src/server/object-layer.js +0 -3
  59. package/src/ws/IoInterface.js +2 -2
@@ -17,6 +17,8 @@ import { Content } 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';
20
+ import { getProxyPath, setQueryPath, navigateToProfile } from './Router.js';
21
+ import { PublicProfile } from './PublicProfile.js';
20
22
 
21
23
  const logger = loggerFactory(import.meta);
22
24
 
@@ -197,7 +199,7 @@ const Panel = {
197
199
 
198
200
  // Clear previous form values then populate with the current item's data
199
201
  Input.cleanValues(formData);
200
- Input.setValues(formData, obj, foundOrigin, foundFiles);
202
+ await Input.setValues(formData, obj, foundOrigin, foundFiles);
201
203
  if (options.on.initEdit) await options.on.initEdit({ data: obj });
202
204
  });
203
205
  s(`.a-${payload._id}`).onclick = async (e) => {
@@ -240,6 +242,32 @@ const Panel = {
240
242
  // Register theme change handler
241
243
  const profileThemeHandlerId = `${id}-creator-profile-theme`;
242
244
  ThemeEvents[profileThemeHandlerId] = updateCreatorProfileTheme;
245
+
246
+ // Add click handlers for public profile links
247
+ setTimeout(() => {
248
+ const links = sa(`.creator-profile-link-${id}`);
249
+ links.forEach((link) => {
250
+ link.onclick = async (e) => {
251
+ e.preventDefault();
252
+ const username = link.getAttribute('data-id');
253
+ // Check if public profile modal is already open
254
+ const currentModal = s('.modal-public-profile');
255
+ if (currentModal) {
256
+ // Modal is already open, update the profile content dynamically
257
+ // Navigate to clean URL without intermediate ?cid= in history
258
+ navigateToProfile(username, { replace: false });
259
+ await PublicProfile.Update({
260
+ idModal: 'modal-public-profile',
261
+ user: { username },
262
+ });
263
+ } else {
264
+ // Modal is not open, navigate to clean URL and open modal
265
+ navigateToProfile(username, { replace: false });
266
+ if (s('.main-btn-public-profile')) s('.main-btn-public-profile').click();
267
+ }
268
+ };
269
+ });
270
+ });
243
271
  }
244
272
  });
245
273
  if (s(`.${idPanel}-${id}`)) s(`.${idPanel}-${id}`).remove();
@@ -287,37 +315,46 @@ const Panel = {
287
315
  ? 'rgba(255,255,255,0.02)'
288
316
  : 'rgba(0,0,0,0.02)'}; border-radius: 4px 4px 0 0;"
289
317
  >
290
- ${obj.userInfo.profileImageId && obj.userInfo.profileImageId._id
291
- ? html`<img
292
- class="creator-avatar"
293
- src="${getApiBaseUrl({ id: obj.userInfo.profileImageId._id, endpoint: 'file/blob' })}"
294
- alt="${obj.userInfo.username || obj.userInfo.email}"
295
- style="width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid ${darkTheme
296
- ? 'rgba(102, 126, 234, 0.5)'
297
- : 'rgba(102, 126, 234, 0.3)'}; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15);"
298
- title="${obj.userInfo.email || obj.userInfo.username}"
299
- />`
300
- : html`<div
301
- class="creator-avatar"
302
- style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15);"
303
- title="${obj.userInfo.email || obj.userInfo.username}"
304
- >
305
- ${(obj.userInfo.username || obj.userInfo.email || 'U').charAt(0).toUpperCase()}
306
- </div>`}
318
+ <a
319
+ href="${getProxyPath()}u/${obj.userInfo.username}"
320
+ class="creator-profile-link-${id}"
321
+ data-id="${obj.userInfo.username}"
322
+ style="display: flex;"
323
+ >
324
+ ${obj.userInfo.profileImageId && obj.userInfo.profileImageId._id
325
+ ? html`<img
326
+ class="creator-avatar"
327
+ src="${getApiBaseUrl({ id: obj.userInfo.profileImageId._id, endpoint: 'file/blob' })}"
328
+ alt="${obj.userInfo.username}"
329
+ style="width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid ${darkTheme
330
+ ? 'rgba(102, 126, 234, 0.5)'
331
+ : 'rgba(102, 126, 234, 0.3)'}; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15);"
332
+ title="${obj.userInfo.username}"
333
+ />`
334
+ : html`<div
335
+ class="creator-avatar"
336
+ style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15);"
337
+ title="${obj.userInfo.username}"
338
+ >
339
+ ${(obj.userInfo.username || 'U').charAt(0).toUpperCase()}
340
+ </div>`}
341
+ </a>
307
342
  <div style="display: flex; flex-direction: column; min-width: 0; flex: 1;">
308
- <span
309
- class="creator-username"
343
+ <a
344
+ href="${getProxyPath()}u/${obj.userInfo.username}"
345
+ class="creator-username creator-profile-link-${id}"
346
+ data-id="${obj.userInfo.username}"
310
347
  style="font-size: 14px; font-weight: 600; color: ${darkTheme
311
348
  ? 'rgba(255,255,255,0.9)'
312
349
  : 'rgba(0,0,0,0.85)'}; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
313
350
  >
314
351
  ${obj.userInfo.username || obj.userInfo.email || 'Unknown'}
315
- </span>
352
+ </a>
316
353
  <span
317
354
  style="font-size: 11px; color: ${darkTheme
318
355
  ? 'rgba(255,255,255,0.5)'
319
356
  : 'rgba(0,0,0,0.45)'}; line-height: 1.3; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500;"
320
- >Creator</span
357
+ >${obj.userInfo.briefDescription || 'Uploader'}</span
321
358
  >
322
359
  </div>
323
360
  </div>`
@@ -718,15 +755,16 @@ const Panel = {
718
755
  s(`.btn-${idPanel}-clean`).onclick = () => {
719
756
  Input.cleanValues(formData);
720
757
  };
721
- s(`.btn-${idPanel}-clean-file`).onclick = () => {
722
- // Clear file input specifically
723
- const fileFormData = formData.find((f) => f.inputType === 'file');
724
- if (fileFormData && s(`.${fileFormData.id}`)) {
725
- s(`.${fileFormData.id}`).value = '';
726
- s(`.${fileFormData.id}`).inputFiles = null;
727
- htmls(`.file-name-render-${fileFormData.id}`, `${fileNameInputExtDefaultContent}`);
728
- }
729
- };
758
+ if (s(`.btn-${idPanel}-clean-file`))
759
+ s(`.btn-${idPanel}-clean-file`).onclick = () => {
760
+ // Clear file input specifically
761
+ const fileFormData = formData.find((f) => f.inputType === 'file');
762
+ if (fileFormData && s(`.${fileFormData.id}`)) {
763
+ s(`.${fileFormData.id}`).value = '';
764
+ s(`.${fileFormData.id}`).inputFiles = null;
765
+ htmls(`.file-name-render-${fileFormData.id}`, `${fileNameInputExtDefaultContent}`);
766
+ }
767
+ };
730
768
  s(`.btn-${idPanel}-close`).onclick = (e) => {
731
769
  e.preventDefault();
732
770
  s(`.${idPanel}-form-body`).style.opacity = 0;
@@ -13,9 +13,72 @@ import { closeModalRouteChangeEvents, listenQueryPathInstance, setQueryPath, get
13
13
  import { Scroll } from './Scroll.js';
14
14
  import { LoadingAnimation } from './LoadingAnimation.js';
15
15
  import { loggerFactory } from './Logger.js';
16
+ import { getApiBaseUrl } from '../../services/core/core.service.js';
16
17
 
17
18
  const logger = loggerFactory(import.meta, { trace: true });
18
19
 
20
+ function sanitizeFilename(title, options = {}) {
21
+ const { replacement = '-', maxLength = 255, preserveExtension = true } = options;
22
+
23
+ if (typeof title !== 'string' || title.trim() === '') {
24
+ return 'untitled';
25
+ }
26
+
27
+ // 1) Extract extension (optional)
28
+ let name = title;
29
+ let ext = '';
30
+ if (preserveExtension) {
31
+ const match = title.match(/(\.[^.\s]{1,10})$/u);
32
+ if (match) {
33
+ ext = match[1];
34
+ name = title.slice(0, -ext.length);
35
+ }
36
+ }
37
+
38
+ // 2) Normalize Unicode and remove diacritics
39
+ name = name.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
40
+
41
+ // 3) Remove control characters and null bytes
42
+ name = name.replace(/[\x00-\x1f\x7f]/g, '');
43
+
44
+ // 4) Remove forbidden filename characters (Windows / POSIX)
45
+ name = name.replace(/[<>:"/\\|?*\u0000]/g, '');
46
+
47
+ // 5) Collapse whitespace and replace with separator
48
+ name = name.replace(/\s+/g, replacement);
49
+
50
+ // 6) Collapse multiple separators
51
+ const escaped = replacement.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
52
+ name = name.replace(new RegExp(`${escaped}{2,}`, 'g'), replacement);
53
+
54
+ // 7) Trim dots and separators from edges
55
+ name = name.replace(new RegExp(`^[\\.${escaped}]+|[\\.${escaped}]+$`, 'g'), '');
56
+
57
+ // 8) Protect against Windows reserved names
58
+ if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(name)) {
59
+ name = '_' + name;
60
+ }
61
+
62
+ // 9) Enforce max length
63
+ const maxNameLength = Math.max(1, maxLength - ext.length);
64
+ if (name.length > maxNameLength) {
65
+ name = name.slice(0, maxNameLength);
66
+ }
67
+
68
+ // 10) Fallback
69
+ if (!name) name = 'untitled';
70
+
71
+ return name + ext;
72
+ }
73
+
74
+ const userInfoFactory = (userDoc) => ({
75
+ username: userDoc.userId.username,
76
+ email: userDoc.userId.email,
77
+ _id: userDoc.userId._id,
78
+ profileImageId: userDoc.userId.profileImageId,
79
+ briefDescription: userDoc.userId.briefDescription,
80
+ });
81
+
19
82
  const PanelForm = {
20
83
  Data: {},
21
84
  instance: async function (
@@ -129,29 +192,40 @@ const PanelForm = {
129
192
  render: imageShimmer(),
130
193
  });
131
194
  }
132
- if (!options.data.fileId)
133
- return await options.htmlRender({
134
- render: html`
135
- <img
136
- class="abs center"
137
- style="${renderCssAttr({
138
- style: {
139
- width: '100px',
140
- height: '100px',
141
- opacity: 0.2,
142
- },
143
- })}"
144
- src="${defaultUrlImage}"
145
- />
146
- `,
195
+
196
+ // Get the filesData for this item
197
+ const filesDataItem = PanelForm.Data[idPanel].filesData.find((f) => f._id === options.data._id);
198
+
199
+ // Priority 1: Check if there's an actual file (not markdown content)
200
+ // fileId array defaults to [null] for batch upload logic
201
+ const fileBlob = filesDataItem?.fileId?.fileBlob;
202
+ if (fileBlob) {
203
+ return await options.fileRender({
204
+ file: fileBlob,
205
+ style: {
206
+ overflow: 'auto',
207
+ width: '100%',
208
+ height: 'auto',
209
+ },
147
210
  });
148
- return await options.fileRender({
149
- file: PanelForm.Data[idPanel].filesData.find((f) => f._id === options.data._id)?.fileId?.fileBlob,
150
- style: {
151
- overflow: 'auto',
152
- width: '100%',
153
- height: 'auto',
154
- },
211
+ }
212
+
213
+ // Priority 2: If no actual file, show default image
214
+ // (Don't show markdown content in file area - mdFileId stays in content area)
215
+ return await options.htmlRender({
216
+ render: html`
217
+ <img
218
+ class="abs center"
219
+ style="${renderCssAttr({
220
+ style: {
221
+ width: '100px',
222
+ height: '100px',
223
+ opacity: 0.2,
224
+ },
225
+ })}"
226
+ src="${defaultUrlImage}"
227
+ />
228
+ `,
155
229
  });
156
230
  },
157
231
  on: {
@@ -326,11 +400,34 @@ const PanelForm = {
326
400
  return { data: [], status: 'error', message: 'Must provide either content or attach a file' };
327
401
  }
328
402
 
403
+ // Sanitize title for filename - normalize UTF-8 string
404
+ // In browser, strings are already UTF-16, just ensure valid characters
405
+ const sanitizedTitle = sanitizeFilename(data.title);
406
+
329
407
  let mdFileId;
330
- const mdFileName = `${getCapVariableName(data.title)}.md`;
408
+ const mdFileName = `${getCapVariableName(sanitizedTitle)}.md`;
331
409
  const location = `${prefixTags.join('/')}`;
332
- const blob = new Blob([data.mdFileId], { type: 'text/markdown' });
333
- const md = new File([blob], mdFileName, { type: 'text/markdown' });
410
+
411
+ // Only create markdown file if there's actual content
412
+ let md = null;
413
+ let mdBlob = null;
414
+ let mdPlain = null;
415
+
416
+ if (hasMdContent) {
417
+ // Markdown content is already UTF-16 in browser, use as-is
418
+ const blob = new Blob([data.mdFileId], { type: 'text/markdown' });
419
+ md = new File([blob], mdFileName, { type: 'text/markdown' });
420
+
421
+ mdBlob = {
422
+ data: {
423
+ data: await getDataFromInputFile(md),
424
+ },
425
+ mimetype: md.type,
426
+ name: md.name,
427
+ };
428
+ mdPlain = await getRawContentFile(getBlobFromUint8ArrayFile(mdBlob.data.data, mdBlob.mimetype));
429
+ }
430
+
334
431
  // Parse and normalize tags
335
432
  // Note: 'public' tag is automatically extracted by the backend and converted to isPublic field
336
433
  // It will be filtered from the tags array to keep visibility control separate from content tags
@@ -355,18 +452,25 @@ const PanelForm = {
355
452
  }
356
453
  }
357
454
 
358
- const mdBlob = {
359
- data: {
360
- data: await getDataFromInputFile(md),
361
- },
362
- mimetype: md.type,
363
- name: md.name,
364
- };
365
- const mdPlain = await getRawContentFile(getBlobFromUint8ArrayFile(mdBlob.data.data, mdBlob.mimetype));
366
455
  const baseNewDoc = newInstance(data);
367
456
  baseNewDoc.tags = tags.filter((t) => !prefixTags.includes(t));
368
- baseNewDoc.mdFileId = marked.parse(data.mdFileId);
369
- baseNewDoc.userId = Elements.Data.user.main.model.user._id;
457
+ baseNewDoc.mdFileId = hasMdContent ? marked.parse(data.mdFileId) : null;
458
+ baseNewDoc.userId = Elements.Data.user?.main?.model?.user?._id;
459
+
460
+ // Ensure profileImageId is properly formatted as object with _id property
461
+ const profileImageIdValue = Elements.Data.user?.main?.model?.user?.profileImageId;
462
+ const formattedProfileImageId = profileImageIdValue
463
+ ? typeof profileImageIdValue === 'string'
464
+ ? { _id: profileImageIdValue }
465
+ : profileImageIdValue
466
+ : null;
467
+
468
+ baseNewDoc.userInfo = {
469
+ username: Elements.Data.user?.main?.model?.user?.username,
470
+ email: Elements.Data.user?.main?.model?.user?.email,
471
+ _id: Elements.Data.user?.main?.model?.user?._id,
472
+ profileImageId: formattedProfileImageId,
473
+ };
370
474
  baseNewDoc.tools = true;
371
475
 
372
476
  const documents = [];
@@ -378,30 +482,46 @@ const PanelForm = {
378
482
 
379
483
  for (const file of inputFiles) {
380
484
  indexFormDoc++;
381
- let fileId;
485
+ let fileId = undefined; // Reset for each iteration - only set if user uploaded a file
382
486
 
383
487
  await (async () => {
384
488
  const body = new FormData();
385
- body.append('md', md);
489
+ // Only append md file if it was created (has content)
490
+ if (md) body.append('md', md);
386
491
  if (file) body.append('file', file);
387
- const { status, data } = await FileService.post({ body });
492
+ const { status, data: uploadedFiles } = await FileService.post({ body });
388
493
  // await timer(3000);
389
494
  NotificationManager.Push({
390
495
  html: Translate.Render(`${status}-upload-file`),
391
496
  status,
392
497
  });
393
- if (status === 'success') {
394
- // Identify files by comparing filename instead of just mimetype
395
- // This handles the case where an .md file is uploaded as the optional file
396
- // - mdFileId: matches the generated mdFileName from the title
397
- // - fileId: any other file (including other .md files)
398
- for (const uploadedFile of data) {
399
- if (uploadedFile.name === mdFileName) {
498
+ if (status === 'success' && uploadedFiles && Array.isArray(uploadedFiles)) {
499
+ // CRITICAL DIFFERENTIATION:
500
+ // - mdFileId: markdown file GENERATED FROM rich text editor content
501
+ // - fileId: file UPLOADED BY USER (could be .md, .pdf, image, etc.)
502
+ //
503
+ // Both can be markdown files, but we must distinguish:
504
+ // Rich text editor content → mdFileId
505
+ // User-uploaded file → fileId
506
+
507
+ for (const uploadedFile of uploadedFiles) {
508
+ if (hasMdContent && uploadedFile.name === mdFileName) {
509
+ // This is the markdown file created FROM rich text editor
400
510
  mdFileId = uploadedFile._id;
401
- } else {
511
+ logger.info(`Assigned rich text markdown to mdFileId: ${mdFileName}`);
512
+ } else if (!hasMdContent || uploadedFile.name !== mdFileName) {
513
+ // This is a file uploaded by user (even if it's an .md file)
402
514
  fileId = uploadedFile._id;
515
+ logger.info(`Assigned user-uploaded file to fileId: ${uploadedFile.name}`);
403
516
  }
404
517
  }
518
+
519
+ // Validation: mdFileId should exist only if rich text content was provided
520
+ if (hasMdContent && !mdFileId) {
521
+ logger.error(
522
+ `ERROR: No markdown content file found. Expected: ${mdFileName}, Got: ${uploadedFiles.map((f) => f.name).join(', ')}`,
523
+ );
524
+ }
405
525
  }
406
526
  })();
407
527
  // Backend will automatically extract 'public' from tags and set isPublic field
@@ -431,6 +551,12 @@ const PanelForm = {
431
551
  // Use server response data - backend has already processed tags and isPublic
432
552
  isPublic: documentData.isPublic || false,
433
553
  tags: (documentData.tags || []).filter((t) => !prefixTags.includes(t)),
554
+ // Ensure userInfo is present for profile header rendering
555
+ userInfo:
556
+ baseNewDoc.userInfo ||
557
+ (documentData.userId && typeof documentData.userId === 'object'
558
+ ? userInfoFactory(documentData)
559
+ : null),
434
560
  };
435
561
 
436
562
  if (documentStatus === 'error') status = 'error';
@@ -439,7 +565,7 @@ const PanelForm = {
439
565
  const filesData = {
440
566
  id: documentData._id,
441
567
  _id: documentData._id,
442
- mdFileId: { mdBlob, mdPlain },
568
+ mdFileId: mdBlob && mdPlain ? { mdBlob, mdPlain } : null,
443
569
  fileId: {
444
570
  fileBlob: file
445
571
  ? {
@@ -537,29 +663,50 @@ const PanelForm = {
537
663
  let mdFileId, fileId;
538
664
  let mdBlob, fileBlob;
539
665
  let mdPlain, filePlain;
666
+ let parsedMarkdown = '';
540
667
 
541
668
  try {
542
- {
543
- const {
544
- data: [file],
545
- } = await FileService.get({ id: documentObject.mdFileId._id });
546
-
547
- // const ext = file.name.split('.')[file.name.split('.').length - 1];
548
- mdBlob = file;
549
- mdPlain = await getRawContentFile(getBlobFromUint8ArrayFile(file.data.data, file.mimetype));
550
- mdFileId = newInstance(mdPlain);
669
+ // Fetch markdown content if mdFileId exists
670
+ if (documentObject.mdFileId) {
671
+ const mdFileIdValue = documentObject.mdFileId._id || documentObject.mdFileId;
672
+ try {
673
+ // Get markdown content from blob endpoint using FileService
674
+ const { data: blobArray, status } = await FileService.get({ id: `blob/${mdFileIdValue}` });
675
+ if (status === 'success' && blobArray && blobArray[0]) {
676
+ mdPlain = await blobArray[0].text();
677
+ // Parse markdown with proper error handling
678
+ try {
679
+ parsedMarkdown = mdPlain ? marked.parse(mdPlain) : '';
680
+ } catch (parseError) {
681
+ logger.error('Error parsing markdown for document:', documentObject._id, parseError);
682
+ parsedMarkdown = `<p><strong>Error rendering markdown:</strong> ${parseError.message}</p>`;
683
+ }
684
+ } else {
685
+ logger.warn('Failed to fetch markdown blob content');
686
+ parsedMarkdown = '';
687
+ }
688
+ } catch (fetchError) {
689
+ logger.error('Error fetching markdown content:', mdFileIdValue, fetchError);
690
+ parsedMarkdown = '';
691
+ }
551
692
  }
693
+
694
+ // Handle optional fileId
552
695
  if (documentObject.fileId) {
553
- const {
554
- data: [file],
555
- } = await FileService.get({ id: documentObject.fileId._id });
556
-
557
- // const ext = file.name.split('.')[file.name.split('.').length - 1];
558
- fileBlob = file;
559
- filePlain = undefined;
560
- fileId = getSrcFromFileData(file);
696
+ const fileIdValue = documentObject.fileId._id || documentObject.fileId;
697
+ try {
698
+ // Get file metadata for display
699
+ const { data: fileArray } = await FileService.get({ id: fileIdValue });
700
+ if (fileArray && fileArray[0]) {
701
+ fileBlob = fileArray[0];
702
+ fileId = getSrcFromFileData(fileArray[0]);
703
+ }
704
+ } catch (fetchError) {
705
+ logger.error('Error fetching file metadata:', fileIdValue, fetchError);
706
+ }
561
707
  }
562
708
 
709
+ // Store file metadata and references
563
710
  panelData.filesData.push({
564
711
  id: documentObject._id,
565
712
  _id: documentObject._id,
@@ -567,33 +714,67 @@ const PanelForm = {
567
714
  fileId: { fileBlob, filePlain },
568
715
  });
569
716
 
717
+ // Add to data array for display - use pre-parsed markdown
570
718
  panelData.data.push({
571
719
  id: documentObject._id,
572
720
  title: documentObject.title,
573
721
  createdAt: documentObject.createdAt,
574
722
  // Backend filters 'public' tag automatically - it's converted to isPublic field
575
723
  tags: documentObject.tags.filter((t) => !prefixTags.includes(t)),
576
- mdFileId: marked.parse(mdFileId),
577
- userId: documentObject.userId._id,
724
+ mdFileId: parsedMarkdown,
725
+ userId:
726
+ documentObject.userId && typeof documentObject.userId === 'object'
727
+ ? documentObject.userId._id
728
+ : documentObject.userId,
578
729
  userInfo:
579
730
  documentObject.userId && typeof documentObject.userId === 'object'
580
- ? {
581
- username: documentObject.userId.username,
582
- email: documentObject.userId.email,
583
- _id: documentObject.userId._id,
584
- profileImageId: documentObject.userId.profileImageId,
585
- }
731
+ ? userInfoFactory(documentObject)
586
732
  : null,
587
733
  fileId,
588
- tools: Elements.Data.user.main.model.user._id === documentObject.userId._id,
734
+ tools:
735
+ documentObject.userId &&
736
+ typeof documentObject.userId === 'object' &&
737
+ Elements.Data.user?.main?.model?.user?._id &&
738
+ documentObject.userId._id === Elements.Data.user.main.model.user._id,
589
739
  _id: documentObject._id,
590
740
  totalCopyShareLinkCount: documentObject.totalCopyShareLinkCount || 0,
591
741
  isPublic: documentObject.isPublic || false,
592
742
  });
593
743
  } catch (fileError) {
594
- logger.error('Error fetching files for document:', documentObject._id, fileError);
744
+ logger.error('Error processing files for document:', documentObject._id, fileError);
595
745
  // Still add the document to originData even if file fetching fails
596
- // but skip adding to data and filesData arrays
746
+ // Add minimal data without file references
747
+ panelData.filesData.push({
748
+ id: documentObject._id,
749
+ _id: documentObject._id,
750
+ mdFileId: { mdBlob: null, mdPlain: '' },
751
+ fileId: { fileBlob: null, filePlain: undefined },
752
+ });
753
+
754
+ panelData.data.push({
755
+ id: documentObject._id,
756
+ title: documentObject.title,
757
+ createdAt: documentObject.createdAt,
758
+ tags: documentObject.tags.filter((t) => !prefixTags.includes(t)),
759
+ mdFileId: '',
760
+ userId:
761
+ documentObject.userId && typeof documentObject.userId === 'object'
762
+ ? documentObject.userId._id
763
+ : documentObject.userId,
764
+ userInfo:
765
+ documentObject.userId && typeof documentObject.userId === 'object'
766
+ ? userInfoFactory(documentObject)
767
+ : null,
768
+ fileId: null,
769
+ tools:
770
+ documentObject.userId &&
771
+ typeof documentObject.userId === 'object' &&
772
+ Elements.Data.user?.main?.model?.user?._id &&
773
+ documentObject.userId._id === Elements.Data.user.main.model.user._id,
774
+ _id: documentObject._id,
775
+ totalCopyShareLinkCount: documentObject.totalCopyShareLinkCount || 0,
776
+ isPublic: documentObject.isPublic || false,
777
+ });
597
778
  }
598
779
  }
599
780
 
@@ -694,7 +875,9 @@ const PanelForm = {
694
875
  cid,
695
876
  forceUpdate,
696
877
  },
697
- JSON.stringify(Elements.Data.user.main.model.user, null, 4),
878
+ Elements.Data.user?.main?.model?.user
879
+ ? JSON.stringify(Elements.Data.user.main.model.user, null, 4)
880
+ : 'No user data',
698
881
  );
699
882
 
700
883
  // Normalize empty values for comparison (undefined, null, '' should all be treated as empty)
@@ -703,7 +886,9 @@ const PanelForm = {
703
886
 
704
887
  if (loadingGetData || (normalizedLastCid === normalizedCid && !forceUpdate)) return;
705
888
  loadingGetData = true;
706
- lastUserId = newInstance(Elements.Data.user.main.model.user._id);
889
+ lastUserId = Elements.Data.user?.main?.model?.user?._id
890
+ ? newInstance(Elements.Data.user.main.model.user._id)
891
+ : null;
707
892
  lastCid = cid;
708
893
 
709
894
  logger.warn('Init render panel data');