@underpostnet/underpost 2.97.1 → 2.98.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.
Files changed (63) 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/scripts/rocky-pwa.sh +200 -0
  8. package/src/api/core/core.service.js +0 -5
  9. package/src/api/default/default.service.js +7 -5
  10. package/src/api/document/document.model.js +1 -1
  11. package/src/api/document/document.router.js +5 -0
  12. package/src/api/document/document.service.js +176 -128
  13. package/src/api/file/file.model.js +112 -4
  14. package/src/api/file/file.ref.json +42 -0
  15. package/src/api/file/file.service.js +380 -32
  16. package/src/api/user/user.model.js +38 -1
  17. package/src/api/user/user.router.js +96 -63
  18. package/src/api/user/user.service.js +81 -48
  19. package/src/cli/db.js +424 -166
  20. package/src/cli/index.js +8 -0
  21. package/src/cli/repository.js +1 -1
  22. package/src/cli/run.js +1 -0
  23. package/src/cli/ssh.js +10 -10
  24. package/src/client/components/core/Account.js +327 -36
  25. package/src/client/components/core/AgGrid.js +3 -0
  26. package/src/client/components/core/Auth.js +11 -3
  27. package/src/client/components/core/Chat.js +2 -2
  28. package/src/client/components/core/Content.js +161 -80
  29. package/src/client/components/core/Css.js +30 -0
  30. package/src/client/components/core/CssCore.js +16 -12
  31. package/src/client/components/core/FileExplorer.js +813 -49
  32. package/src/client/components/core/Input.js +207 -12
  33. package/src/client/components/core/LogIn.js +42 -20
  34. package/src/client/components/core/Modal.js +138 -24
  35. package/src/client/components/core/Panel.js +71 -32
  36. package/src/client/components/core/PanelForm.js +262 -77
  37. package/src/client/components/core/PublicProfile.js +888 -0
  38. package/src/client/components/core/Responsive.js +15 -7
  39. package/src/client/components/core/Router.js +117 -15
  40. package/src/client/components/core/SearchBox.js +322 -116
  41. package/src/client/components/core/SignUp.js +26 -7
  42. package/src/client/components/core/SocketIo.js +6 -3
  43. package/src/client/components/core/Translate.js +148 -0
  44. package/src/client/components/core/Validator.js +15 -0
  45. package/src/client/components/core/windowGetDimensions.js +6 -6
  46. package/src/client/components/default/MenuDefault.js +59 -12
  47. package/src/client/components/default/RoutesDefault.js +1 -0
  48. package/src/client/services/core/core.service.js +163 -1
  49. package/src/client/services/default/default.management.js +454 -76
  50. package/src/client/services/default/default.service.js +13 -6
  51. package/src/client/services/file/file.service.js +43 -16
  52. package/src/client/services/user/user.service.js +13 -9
  53. package/src/client/sw/default.sw.js +107 -184
  54. package/src/db/DataBaseProvider.js +1 -1
  55. package/src/db/mongo/MongooseDB.js +1 -1
  56. package/src/index.js +1 -1
  57. package/src/mailer/MailerProvider.js +4 -4
  58. package/src/runtime/express/Express.js +2 -1
  59. package/src/runtime/lampp/Lampp.js +2 -2
  60. package/src/server/auth.js +3 -6
  61. package/src/server/data-query.js +449 -0
  62. package/src/server/object-layer.js +0 -3
  63. package/src/ws/IoInterface.js +2 -2
@@ -5,17 +5,17 @@ import { AgGrid } from './AgGrid.js';
5
5
  import { Auth } from './Auth.js';
6
6
  import { BtnIcon } from './BtnIcon.js';
7
7
  import { getSubpaths, uniqueArray } from './CommonJs.js';
8
- import { darkTheme, renderCssAttr } from './Css.js';
8
+ import { Css, darkTheme, dynamicCol, renderCssAttr, Themes } from './Css.js';
9
9
  import { EventsUI } from './EventsUI.js';
10
10
  import { fileFormDataFactory, Input, InputFile } from './Input.js';
11
11
  import { loggerFactory } from './Logger.js';
12
- import { Modal } from './Modal.js';
12
+ import { Modal, renderViewTitle } from './Modal.js';
13
13
  import { NotificationManager } from './NotificationManager.js';
14
14
  import { RouterEvents } from './Router.js';
15
15
  import { Translate } from './Translate.js';
16
16
  import { Validator } from './Validator.js';
17
17
  import { copyData, downloadFile, s } from './VanillaJs.js';
18
- import { getProxyPath, getQueryParams, setPath } from './Router.js';
18
+ import { getProxyPath, getQueryParams, setPath, setQueryParams, listenQueryParamsChange } from './Router.js';
19
19
 
20
20
  const logger = loggerFactory(import.meta);
21
21
 
@@ -94,6 +94,22 @@ const FileExplorer = {
94
94
  const query = getQueryParams();
95
95
  let location = query?.location ? this.locationFormat({ f: query }) : '/';
96
96
  let files, folders, documentId, documentInstance;
97
+
98
+ // Simple pagination state
99
+ const PAGE_SIZE = 5;
100
+ let currentPage = query?.page ? parseInt(query.page) - 1 : 0;
101
+ if (currentPage < 0) currentPage = 0;
102
+ let displayedFiles = [];
103
+
104
+ // Search filter state - initialize from URL query param
105
+ let searchFilters = {
106
+ title: query?.title || '',
107
+ mdFile: query?.mdFile || '',
108
+ file: query?.file || '',
109
+ };
110
+ let filteredFiles = [];
111
+ let isProcessingQueryChange = false; // Prevent recursion during URL sync
112
+ const queryParamsListenerId = `file-explorer-${idModal}`;
97
113
  const cleanData = () => {
98
114
  files = [];
99
115
  folders = [];
@@ -101,27 +117,89 @@ const FileExplorer = {
101
117
  documentInstance = [];
102
118
  };
103
119
  cleanData();
120
+ const applySearchFilter = () => {
121
+ filteredFiles = files;
122
+ };
123
+
124
+ const updatePaginationUI = () => {
125
+ const paginationInfo = s(`.file-explorer-pagination-info`);
126
+ const prevBtn = s(`.file-explorer-prev-btn`);
127
+ const nextBtn = s(`.file-explorer-next-btn`);
128
+ if (paginationInfo) {
129
+ const totalPages = Math.ceil(filteredFiles.length / PAGE_SIZE);
130
+ const showing =
131
+ searchFilters.title || searchFilters.mdFile || searchFilters.file
132
+ ? `${filteredFiles.length}/${files.length}`
133
+ : `${files.length}`;
134
+ paginationInfo.textContent = `${currentPage + 1} / ${totalPages || 1} (${showing} files)`;
135
+ }
136
+ if (prevBtn) {
137
+ prevBtn.disabled = currentPage === 0;
138
+ prevBtn.style.opacity = currentPage === 0 ? '0.5' : '1';
139
+ prevBtn.style.cursor = currentPage === 0 ? 'not-allowed' : 'pointer';
140
+ }
141
+ if (nextBtn) {
142
+ const isDisabled = (currentPage + 1) * PAGE_SIZE >= filteredFiles.length;
143
+ nextBtn.disabled = isDisabled;
144
+ nextBtn.style.opacity = isDisabled ? '0.5' : '1';
145
+ nextBtn.style.cursor = isDisabled ? 'not-allowed' : 'pointer';
146
+ }
147
+ };
148
+
149
+ const getPagedFiles = () => {
150
+ const start = currentPage * PAGE_SIZE;
151
+ const end = start + PAGE_SIZE;
152
+ return filteredFiles.slice(start, end);
153
+ };
154
+
104
155
  FileExplorer.Api[idModal].displayList = async () => {
105
156
  if (!s(`.${idModal}`)) return;
106
157
  const query = getQueryParams();
107
158
  location = query?.location ? this.locationFormat({ f: query }) : '/';
108
159
  s(`.file-explorer-query-nav`).value = location;
109
- const format = this.documentDataFormat({ document: documentInstance, location });
160
+
161
+ // Sync search filters from URL
162
+ searchFilters = {
163
+ title: query?.title || '',
164
+ mdFile: query?.mdFile || '',
165
+ file: query?.file || '',
166
+ };
167
+
168
+ if (s(`.file-explorer-search-title`)) s(`.file-explorer-search-title`).value = searchFilters.title;
169
+ if (s(`.file-explorer-search-md-file`)) s(`.file-explorer-search-md-file`).value = searchFilters.mdFile;
170
+ if (s(`.file-explorer-search-file`)) s(`.file-explorer-search-file`).value = searchFilters.file;
171
+
172
+ const format = this.documentDataFormat({ document: documentInstance, location, searchFilters });
110
173
  files = format.files;
111
174
  folders = format.folders;
112
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
175
+ applySearchFilter();
176
+ const queryPage = query?.page ? parseInt(query.page) - 1 : 0;
177
+ currentPage = queryPage >= 0 ? queryPage : 0;
178
+ displayedFiles = getPagedFiles();
179
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
113
180
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
181
+ updatePaginationUI();
114
182
  };
115
183
  FileExplorer.Api[idModal].updateData = async (optionsUpdate = { display: false }) => {
116
184
  if (!s(`.${idModal}`)) return;
117
185
  if (Auth.getToken()) {
118
186
  try {
119
- const { status, data: document } = await DocumentService.get();
120
- const format = this.documentDataFormat({ document, location });
187
+ const { status, data: responseData } = await DocumentService.get({
188
+ params: {
189
+ searchTitle: searchFilters.title,
190
+ searchMdFile: searchFilters.mdFile,
191
+ searchFile: searchFilters.file,
192
+ location,
193
+ },
194
+ });
195
+ // Handle both old format (array) and new format with pagination
196
+ const document = Array.isArray(responseData) ? responseData : responseData.data || [];
197
+ const format = this.documentDataFormat({ document, location, searchFilters });
121
198
  files = format.files;
122
199
  documentId = format.documentId;
123
200
  folders = format.folders;
124
201
  documentInstance = document;
202
+ applySearchFilter();
125
203
  } catch (error) {
126
204
  logger.error(error);
127
205
  NotificationManager.Push({
@@ -143,6 +221,180 @@ const FileExplorer = {
143
221
  });
144
222
  };
145
223
 
224
+ // Listen for query param changes (browser back/forward navigation)
225
+ listenQueryParamsChange({
226
+ id: queryParamsListenerId,
227
+ event: async (queryParams) => {
228
+ if (!s(`.${idModal}`)) return;
229
+ if (isProcessingQueryChange) return;
230
+
231
+ const tab = queryParams?.tab || '';
232
+ if (tab === 'upload') {
233
+ s(`.file-explorer-nav`).style.display = 'none';
234
+ s(`.file-explorer-uploader`).style.display = 'block';
235
+ } else {
236
+ s(`.file-explorer-nav`).style.display = 'block';
237
+ s(`.file-explorer-uploader`).style.display = 'none';
238
+ }
239
+
240
+ const page = queryParams?.page ? parseInt(queryParams.page) - 1 : 0;
241
+ if (page !== currentPage) {
242
+ currentPage = page >= 0 ? page : 0;
243
+ displayedFiles = getPagedFiles();
244
+ if (AgGrid.grids[gridFileId]) {
245
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
246
+ }
247
+ updatePaginationUI();
248
+ }
249
+
250
+ const newFilters = {
251
+ title: queryParams?.title || '',
252
+ mdFile: queryParams?.mdFile || '',
253
+ file: queryParams?.file || '',
254
+ };
255
+
256
+ if (
257
+ newFilters.title !== searchFilters.title ||
258
+ newFilters.mdFile !== searchFilters.mdFile ||
259
+ newFilters.file !== searchFilters.file
260
+ ) {
261
+ isProcessingQueryChange = true;
262
+ searchFilters = newFilters;
263
+
264
+ if (s(`.file-explorer-search-title`)) s(`.file-explorer-search-title`).value = searchFilters.title;
265
+ if (s(`.file-explorer-search-md-file`)) s(`.file-explorer-search-md-file`).value = searchFilters.mdFile;
266
+ if (s(`.file-explorer-search-file`)) s(`.file-explorer-search-file`).value = searchFilters.file;
267
+
268
+ await FileExplorer.Api[idModal].updateData({ display: true });
269
+
270
+ setTimeout(() => {
271
+ isProcessingQueryChange = false;
272
+ }, 100);
273
+ }
274
+ },
275
+ });
276
+
277
+ // Pagination button event handlers
278
+ setTimeout(() => {
279
+ EventsUI.onClick(`.file-explorer-prev-btn`, (e) => {
280
+ e.preventDefault();
281
+ if (currentPage > 0) {
282
+ setQueryParams({ page: currentPage }, { replace: false });
283
+ }
284
+ });
285
+
286
+ EventsUI.onClick(`.file-explorer-next-btn`, (e) => {
287
+ e.preventDefault();
288
+ if ((currentPage + 1) * PAGE_SIZE < filteredFiles.length) {
289
+ setQueryParams({ page: currentPage + 2 }, { replace: false });
290
+ }
291
+ });
292
+
293
+ // Search input handlers
294
+ let searchTimeout;
295
+
296
+ const setupSearchInput = (selector, key) => {
297
+ const el = s(selector);
298
+ if (el) {
299
+ if (searchFilters[key]) el.value = searchFilters[key];
300
+ el.addEventListener('input', (e) => {
301
+ clearTimeout(searchTimeout);
302
+ searchTimeout = setTimeout(async () => {
303
+ const val = e.target.value.trim();
304
+ if (val === searchFilters[key]) return;
305
+
306
+ isProcessingQueryChange = true;
307
+ searchFilters[key] = val;
308
+ await FileExplorer.Api[idModal].updateData({ display: true });
309
+
310
+ const queryParams = {};
311
+ if (searchFilters.title) queryParams.title = searchFilters.title;
312
+ if (searchFilters.mdFile) queryParams.mdFile = searchFilters.mdFile;
313
+ if (searchFilters.file) queryParams.file = searchFilters.file;
314
+
315
+ if (!searchFilters.title) queryParams.title = null;
316
+ if (!searchFilters.mdFile) queryParams.mdFile = null;
317
+ if (!searchFilters.file) queryParams.file = null;
318
+
319
+ queryParams.page = 1;
320
+
321
+ setQueryParams(queryParams, { replace: false });
322
+
323
+ setTimeout(() => {
324
+ isProcessingQueryChange = false;
325
+ }, 100);
326
+ }, 300);
327
+ });
328
+ el.addEventListener('keydown', (e) => {
329
+ if (e.key === 'Enter') e.preventDefault();
330
+ });
331
+ }
332
+ };
333
+
334
+ setupSearchInput(`.file-explorer-search-title`, 'title');
335
+ setupSearchInput(`.file-explorer-search-md-file`, 'mdFile');
336
+ setupSearchInput(`.file-explorer-search-file`, 'file');
337
+
338
+ // Submit search button
339
+ EventsUI.onClick(`.file-explorer-search-submit`, async (e) => {
340
+ e.preventDefault();
341
+ clearTimeout(searchTimeout);
342
+
343
+ const titleVal = s(`.file-explorer-search-title`)?.value.trim() || '';
344
+ const mdFileVal = s(`.file-explorer-search-md-file`)?.value.trim() || '';
345
+ const fileVal = s(`.file-explorer-search-file`)?.value.trim() || '';
346
+
347
+ searchFilters = { title: titleVal, mdFile: mdFileVal, file: fileVal };
348
+
349
+ isProcessingQueryChange = true;
350
+
351
+ const queryParams = {};
352
+ if (searchFilters.title) queryParams.title = searchFilters.title;
353
+ if (searchFilters.mdFile) queryParams.mdFile = searchFilters.mdFile;
354
+ if (searchFilters.file) queryParams.file = searchFilters.file;
355
+
356
+ if (!searchFilters.title) queryParams.title = null;
357
+ if (!searchFilters.mdFile) queryParams.mdFile = null;
358
+ if (!searchFilters.file) queryParams.file = null;
359
+
360
+ queryParams.page = 1;
361
+
362
+ setQueryParams(queryParams, { replace: false });
363
+
364
+ await FileExplorer.Api[idModal].updateData({ display: true });
365
+
366
+ setTimeout(() => {
367
+ isProcessingQueryChange = false;
368
+ }, 100);
369
+ });
370
+
371
+ // Clear search button
372
+ EventsUI.onClick(`.file-explorer-search-clear`, (e) => {
373
+ e.preventDefault();
374
+
375
+ if (!searchFilters.title && !searchFilters.mdFile && !searchFilters.file) return;
376
+
377
+ isProcessingQueryChange = true;
378
+ if (s(`.file-explorer-search-title`)) s(`.file-explorer-search-title`).value = '';
379
+ if (s(`.file-explorer-search-md-file`)) s(`.file-explorer-search-md-file`).value = '';
380
+ if (s(`.file-explorer-search-file`)) s(`.file-explorer-search-file`).value = '';
381
+
382
+ searchFilters = { title: '', mdFile: '', file: '' };
383
+
384
+ applySearchFilter();
385
+ currentPage = 0;
386
+ displayedFiles = getPagedFiles();
387
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
388
+ updatePaginationUI();
389
+
390
+ setQueryParams({ title: null, mdFile: null, file: null, page: 1 }, { replace: false });
391
+
392
+ setTimeout(() => {
393
+ isProcessingQueryChange = false;
394
+ }, 100);
395
+ });
396
+ });
397
+
146
398
  setTimeout(async () => {
147
399
  FileExplorer.Api[idModal].updateData({ display: true });
148
400
  const formData = [
@@ -156,6 +408,15 @@ const FileExplorer = {
156
408
 
157
409
  EventsUI.onClick(`.btn-input-file-explorer`, async (e) => {
158
410
  e.preventDefault();
411
+
412
+ // Check authentication before upload
413
+ if (!Auth.getToken()) {
414
+ return NotificationManager.Push({
415
+ html: Translate.Render(`error-user-not-authenticated`),
416
+ status: 'error',
417
+ });
418
+ }
419
+
159
420
  const { errorMessage } = await validators();
160
421
  if (!formBodyFiles)
161
422
  return NotificationManager.Push({
@@ -166,6 +427,12 @@ const FileExplorer = {
166
427
  let fileData;
167
428
  {
168
429
  const { status, data } = await FileService.post({ body: formBodyFiles });
430
+ if (status === 'error' || !data) {
431
+ return NotificationManager.Push({
432
+ html: Translate.Render(`error-upload-file`),
433
+ status: 'error',
434
+ });
435
+ }
169
436
  fileData = data;
170
437
  }
171
438
  {
@@ -189,15 +456,25 @@ const FileExplorer = {
189
456
  const format = this.documentDataFormat({ document: documentInstance, location });
190
457
  files = format.files;
191
458
  folders = format.folders;
192
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
459
+ applySearchFilter();
460
+ currentPage = 0;
461
+ displayedFiles = getPagedFiles();
462
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
193
463
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
464
+ updatePaginationUI();
194
465
  NotificationManager.Push({
195
466
  html: Translate.Render(`${status}-upload-file`),
196
467
  status,
197
468
  });
198
469
  if (status === 'success') {
199
- s(`.btn-input-home-directory`).click();
470
+ // Clear the file input
200
471
  s(`.btn-clear-input-file-${idDropFileInput}`).click();
472
+ // Switch to explorer view with the uploaded location
473
+ setQueryParams({ tab: null, location: location }, { replace: false });
474
+ // Show explorer view, hide upload view
475
+ s(`.file-explorer-nav`).style.display = 'block';
476
+ s(`.file-explorer-uploader`).style.display = 'none';
477
+ s(`.file-explorer-query-nav`).value = location;
201
478
  }
202
479
  }
203
480
  });
@@ -214,19 +491,26 @@ const FileExplorer = {
214
491
  });
215
492
  EventsUI.onClick(`.btn-input-home-directory`, async (e) => {
216
493
  e.preventDefault();
494
+
495
+ if (getQueryParams()?.tab === 'upload') {
496
+ setQueryParams({ tab: null }, { replace: false });
497
+ return;
498
+ }
499
+
217
500
  let newLocation = '/';
218
- if (s(`.file-explorer-uploader`).style.display !== 'none') {
219
- s(`.file-explorer-uploader`).style.display = 'none';
220
- s(`.file-explorer-nav`).style.display = 'block';
221
- } else if (newLocation === location) return;
222
- else location = newLocation;
501
+ if (newLocation === location) return;
502
+ location = newLocation;
223
503
  setPath(`${window.location.pathname}?location=${location}`);
224
504
  s(`.file-explorer-query-nav`).value = location;
225
505
  const format = this.documentDataFormat({ document: documentInstance, location });
226
506
  files = format.files;
227
507
  folders = format.folders;
228
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
508
+ applySearchFilter();
509
+ currentPage = 0;
510
+ displayedFiles = getPagedFiles();
511
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
229
512
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
513
+ updatePaginationUI();
230
514
  });
231
515
  EventsUI.onClick(`.btn-input-copy-directory`, async (e) => {
232
516
  e.preventDefault();
@@ -246,8 +530,7 @@ const FileExplorer = {
246
530
  });
247
531
  EventsUI.onClick(`.btn-input-upload-file`, async (e) => {
248
532
  e.preventDefault();
249
- s(`.file-explorer-nav`).style.display = 'none';
250
- s(`.file-explorer-uploader`).style.display = 'block';
533
+ setQueryParams({ tab: 'upload' }, { replace: false });
251
534
  });
252
535
  });
253
536
 
@@ -259,6 +542,8 @@ const FileExplorer = {
259
542
  // params.data._id
260
543
 
261
544
  this.eGui = document.createElement('div');
545
+ const isPublic = params.data.isPublic;
546
+ const toggleId = `toggle-public-${params.data._id}`;
262
547
  this.eGui.innerHTML = html`
263
548
  <div class="fl">
264
549
  ${await BtnIcon.Render({
@@ -281,6 +566,18 @@ const FileExplorer = {
281
566
  label: html`<i class="fas fa-copy"></i>`,
282
567
  type: 'button',
283
568
  })}
569
+ ${await BtnIcon.Render({
570
+ class: `in fll management-table-btn-mini btn-file-edit-${params.data._id}`,
571
+ label: html`<i class="fas fa-edit"></i>`,
572
+ type: 'button',
573
+ })}
574
+ ${await BtnIcon.Render({
575
+ class: `in fll management-table-btn-mini ${toggleId}`,
576
+ label: isPublic
577
+ ? html`<i class="fas fa-globe" style="color: #4caf50;"></i>`
578
+ : html`<i class="fas fa-lock" style="color: #9e9e9e;"></i>`,
579
+ type: 'button',
580
+ })}
284
581
  </div>
285
582
  `;
286
583
 
@@ -289,7 +586,10 @@ const FileExplorer = {
289
586
  const url = `${window.location.origin}${uri}`;
290
587
 
291
588
  const originObj = documentInstance.find((d) => d._id === params.data._id);
292
- const blobUri = originObj ? getApiBaseUrl({ id: originObj.fileId._id, endpoint: 'file/blob' }) : undefined;
589
+ const blobUri =
590
+ originObj && originObj.fileId
591
+ ? getApiBaseUrl({ id: originObj.fileId._id, endpoint: 'file/blob' })
592
+ : undefined;
293
593
 
294
594
  if (!originObj) {
295
595
  s(`.btn-file-view-${params.data._id}`).classList.add('hide');
@@ -315,13 +615,21 @@ const FileExplorer = {
315
615
 
316
616
  EventsUI.onClick(`.btn-file-download-${params.data._id}`, async (e) => {
317
617
  e.preventDefault();
318
- console.log(params);
319
- const {
320
- data: [file],
321
- status,
322
- } = await FileService.get({ id: params.data.fileId });
323
-
324
- downloadFile(new Blob([new Uint8Array(file.data.data)], { type: params.data.mimetype }), params.data.name);
618
+ try {
619
+ // Use FileService with blob/ prefix for centralized blob fetching
620
+ const { data: blobArray, status } = await FileService.get({ id: `blob/${params.data.fileId}` });
621
+ if (status === 'success' && blobArray && blobArray[0]) {
622
+ downloadFile(blobArray[0], params.data.name);
623
+ } else {
624
+ throw new Error('Failed to fetch file blob');
625
+ }
626
+ } catch (error) {
627
+ logger.error('Download failed:', error);
628
+ NotificationManager.Push({
629
+ html: 'Download failed',
630
+ status: 'error',
631
+ });
632
+ }
325
633
  });
326
634
  EventsUI.onClick(
327
635
  `.btn-file-delete-${params.data._id}`,
@@ -364,10 +672,367 @@ const FileExplorer = {
364
672
  const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
365
673
  files = format.files;
366
674
  folders = format.folders;
675
+ applySearchFilter();
367
676
  // AgGrid.grids[gridFileId].setGridOption('rowData', files);
368
677
  // const selectedData = gridApi.getSelectedRows();
369
678
  AgGrid.grids[gridFileId].applyTransaction({ remove: [params.data] });
370
679
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
680
+ updatePaginationUI();
681
+ },
682
+ { context: 'modal' },
683
+ );
684
+
685
+ // Toggle public/private status
686
+ EventsUI.onClick(
687
+ `.${toggleId}`,
688
+ async (e) => {
689
+ e.preventDefault();
690
+
691
+ // If document is currently private, show confirmation before making public
692
+ if (!params.data.isPublic) {
693
+ const confirmResult = await Modal.RenderConfirm({
694
+ html: async () => {
695
+ return html`
696
+ <div class="in section-mp" style="text-align: center">
697
+ ${Translate.Render('confirm-make-public')}
698
+ <br />
699
+ "${params.data.title}"
700
+ </div>
701
+ `;
702
+ },
703
+ id: `confirm-toggle-public-${params.data._id}`,
704
+ });
705
+ if (confirmResult.status !== 'confirm') return;
706
+ }
707
+
708
+ try {
709
+ const { data, status } = await DocumentService.patch({
710
+ id: params.data._id,
711
+ action: 'toggle-public',
712
+ });
713
+
714
+ if (status === 'success') {
715
+ // Update local data
716
+ params.data.isPublic = data.isPublic;
717
+
718
+ // Update documentInstance
719
+ const docIndex = documentInstance.findIndex((d) => d._id === params.data._id);
720
+ if (docIndex !== -1) {
721
+ documentInstance[docIndex].isPublic = data.isPublic;
722
+ }
723
+
724
+ // Update button icon
725
+ const btnElement = s(`.${toggleId}`);
726
+ if (btnElement) {
727
+ const iconElement = btnElement.querySelector('i');
728
+ if (iconElement) {
729
+ if (data.isPublic) {
730
+ iconElement.className = 'fas fa-globe';
731
+ iconElement.style.color = '#4caf50';
732
+ } else {
733
+ iconElement.className = 'fas fa-lock';
734
+ iconElement.style.color = '#9e9e9e';
735
+ }
736
+ }
737
+ }
738
+
739
+ NotificationManager.Push({
740
+ html: data.isPublic
741
+ ? Translate.Render('document-now-public')
742
+ : Translate.Render('document-now-private'),
743
+ status: 'success',
744
+ });
745
+ } else {
746
+ throw new Error('Failed to toggle public status');
747
+ }
748
+ } catch (error) {
749
+ logger.error('Toggle public failed:', error);
750
+ NotificationManager.Push({
751
+ html: Translate.Render('error-toggle-public'),
752
+ status: 'error',
753
+ });
754
+ }
755
+ },
756
+ { context: 'modal' },
757
+ );
758
+
759
+ // Edit document button
760
+ EventsUI.onClick(
761
+ `.btn-file-edit-${params.data._id}`,
762
+ async (e) => {
763
+ e.preventDefault();
764
+
765
+ // Get the original document data from documentInstance
766
+ const originDoc = documentInstance.find((d) => d._id === params.data._id);
767
+ const editModalId = `edit-doc-${params.data._id}`;
768
+
769
+ // Check file existence for proper UX
770
+ const hasMdFile = !!(originDoc && originDoc.mdFileId);
771
+ const hasGenericFile = !!(originDoc && originDoc.fileId);
772
+
773
+ const mdFileMimetype = hasMdFile ? originDoc.mdFileId.mimetype : '';
774
+ const genericFileMimetype = hasGenericFile ? originDoc.fileId.mimetype : '';
775
+
776
+ const editFormHtml = async () => {
777
+ return html`
778
+ <div class="in edit-document-form" style="max-width: 600px; margin: 0 auto; padding: 20px;">
779
+ <!-- Header -->
780
+ <div
781
+ class="in"
782
+ style="text-align: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid rgba(128,128,128,0.3);"
783
+ >
784
+ <p style="color: #888; font-size: 14px; margin: 0;">
785
+ ${Translate.Render('editing')}: <strong style="color: inherit;">${params.data.title}</strong>
786
+ </p>
787
+ </div>
788
+
789
+ <!-- Document Title -->
790
+ <div class="in section-mp" style="margin-bottom: 20px;">
791
+ ${await Input.Render({
792
+ id: `edit-doc-title-${params.data._id}`,
793
+ type: 'text',
794
+ label: html`<i class="fas fa-heading"></i> ${Translate.Render('doc-title')}`,
795
+ containerClass: 'in section-mp input-container-width',
796
+ placeholder: true,
797
+ value: params.data.title || '',
798
+ })}
799
+ </div>
800
+
801
+ <!-- MD File Name -->
802
+ <div class="in section-mp" style="margin-bottom: 20px;">
803
+ ${hasMdFile
804
+ ? await Input.Render({
805
+ id: `edit-doc-md-file-${params.data._id}`,
806
+ type: 'text',
807
+ label: html`<i class="fas fa-file-code"></i> ${Translate.Render('md-file-name')}
808
+ <span style="font-size: 11px; color: #888; margin-left: 8px;">(${mdFileMimetype})</span>`,
809
+ containerClass: 'in section-mp input-container-width',
810
+ placeholder: true,
811
+ value: params.data.mdFileName || '',
812
+ })
813
+ : html`
814
+ <div class="in section-mp input-container-width" style="opacity: 0.6;">
815
+ <label style="display: block; margin-bottom: 5px;">
816
+ <i class="fas fa-file-code"></i> ${Translate.Render('md-file-name')}
817
+ </label>
818
+ <div
819
+ style="padding: 10px 12px; border: 1px dashed rgba(128,128,128,0.5); border-radius: 4px; color: #888; font-style: italic;"
820
+ >
821
+ <i class="fas fa-info-circle"></i> ${Translate.Render('no-md-file-attached')}
822
+ </div>
823
+ </div>
824
+ `}
825
+ </div>
826
+
827
+ <!-- Generic File Name -->
828
+ <div class="in section-mp" style="margin-bottom: 20px;">
829
+ ${hasGenericFile
830
+ ? await Input.Render({
831
+ id: `edit-doc-file-${params.data._id}`,
832
+ type: 'text',
833
+ label: html`<i class="fas fa-file"></i> ${Translate.Render('generic-file-name')}
834
+ <span style="font-size: 11px; color: #888; margin-left: 8px;"
835
+ >(${genericFileMimetype})</span
836
+ >`,
837
+ containerClass: 'in section-mp input-container-width',
838
+ placeholder: true,
839
+ value: params.data.fileName || '',
840
+ })
841
+ : html`
842
+ <div class="in section-mp input-container-width" style="opacity: 0.6;">
843
+ <label style="display: block; margin-bottom: 5px;">
844
+ <i class="fas fa-file"></i> ${Translate.Render('generic-file-name')}
845
+ </label>
846
+ <div
847
+ style="padding: 10px 12px; border: 1px dashed rgba(128,128,128,0.5); border-radius: 4px; color: #888; font-style: italic;"
848
+ >
849
+ <i class="fas fa-info-circle"></i> ${Translate.Render('no-generic-file-attached')}
850
+ </div>
851
+ </div>
852
+ `}
853
+ </div>
854
+
855
+ <!-- Location -->
856
+ <div class="in section-mp" style="margin-bottom: 25px;">
857
+ ${await Input.Render({
858
+ id: `edit-doc-location-${params.data._id}`,
859
+ type: 'text',
860
+ label: html`<i class="fas fa-folder"></i> ${Translate.Render('location')}`,
861
+ containerClass: 'in section-mp input-container-width',
862
+ placeholder: true,
863
+ value: params.data.location || '/',
864
+ })}
865
+ </div>
866
+
867
+ <!-- Buttons -->
868
+ <div
869
+ class="fl"
870
+ style="margin-top: 30px; border-top: 1px solid rgba(128,128,128,0.3); padding-top: 20px;"
871
+ >
872
+ <div class="in fll" style="width: 50%; padding: 5px;">
873
+ ${await BtnIcon.Render({
874
+ class: `in wfa btn-edit-doc-cancel-${params.data._id}`,
875
+ label: html`<i class="fas fa-times"></i> ${Translate.Render('cancel')}`,
876
+ type: 'button',
877
+ })}
878
+ </div>
879
+ <div class="in fll" style="width: 50%; padding: 5px;">
880
+ ${await BtnIcon.Render({
881
+ class: `in wfa btn-edit-doc-submit-${params.data._id}`,
882
+ label: html`<i class="fas fa-save"></i> ${Translate.Render('save')}`,
883
+ type: 'button',
884
+ })}
885
+ </div>
886
+ </div>
887
+ </div>
888
+ `;
889
+ };
890
+
891
+ const { barConfig } = await Themes[Css.currentTheme]();
892
+
893
+ await Modal.Render({
894
+ id: editModalId,
895
+ barConfig,
896
+ title: renderViewTitle({
897
+ icon: html`<i class="fas fa-edit"></i>`,
898
+ text: Translate.Render('edit-document'),
899
+ }),
900
+ html: editFormHtml,
901
+ handleType: 'bar',
902
+ maximize: true,
903
+ mode: 'view',
904
+ slideMenu: 'modal-menu',
905
+ RouterInstance: Modal.Data[options.idModal].options.RouterInstance,
906
+ barMode: Modal.Data[options.idModal].options.barMode,
907
+ });
908
+
909
+ // Handle submit button
910
+ setTimeout(() => {
911
+ EventsUI.onClick(
912
+ `.btn-edit-doc-submit-${params.data._id}`,
913
+ async (ev) => {
914
+ ev.preventDefault();
915
+
916
+ const newTitle = s(`.edit-doc-title-${params.data._id}`).value.trim();
917
+ const newLocation = s(`.edit-doc-location-${params.data._id}`).value.trim();
918
+
919
+ // Get file names only if files exist
920
+ const newMdFileName = hasMdFile ? s(`.edit-doc-md-file-${params.data._id}`)?.value.trim() : null;
921
+ const newFileName = hasGenericFile ? s(`.edit-doc-file-${params.data._id}`)?.value.trim() : null;
922
+
923
+ if (!newTitle) {
924
+ NotificationManager.Push({
925
+ html: Translate.Render('error-title-required'),
926
+ status: 'error',
927
+ });
928
+ return;
929
+ }
930
+
931
+ const formattedLocation = FileExplorer.locationFormat({ f: { location: newLocation || '/' } });
932
+
933
+ try {
934
+ const updateBody = {
935
+ title: newTitle,
936
+ location: formattedLocation,
937
+ };
938
+
939
+ // Preserve existing fields from the original document
940
+ if (originDoc) {
941
+ if (originDoc.fileId) updateBody.fileId = originDoc.fileId._id || originDoc.fileId;
942
+ if (originDoc.mdFileId) updateBody.mdFileId = originDoc.mdFileId._id || originDoc.mdFileId;
943
+ if (originDoc.tags) updateBody.tags = originDoc.tags;
944
+ if (typeof originDoc.isPublic !== 'undefined') updateBody.isPublic = originDoc.isPublic;
945
+ }
946
+
947
+ // Include file name updates if files exist and names changed
948
+ if (hasMdFile && newMdFileName && newMdFileName !== params.data.mdFileName) {
949
+ updateBody.mdFileName = newMdFileName;
950
+ }
951
+ if (hasGenericFile && newFileName && newFileName !== params.data.fileName) {
952
+ updateBody.fileName = newFileName;
953
+ }
954
+
955
+ const { data, status } = await DocumentService.put({
956
+ id: params.data._id,
957
+ body: updateBody,
958
+ });
959
+
960
+ if (status === 'success') {
961
+ // Check if location changed
962
+ const locationChanged = formattedLocation !== params.data.location;
963
+
964
+ // Update local data
965
+ params.data.title = newTitle;
966
+ params.data.name = newTitle;
967
+ params.data.location = formattedLocation;
968
+ if (hasMdFile && newMdFileName) params.data.mdFileName = newMdFileName;
969
+ if (hasGenericFile && newFileName) params.data.fileName = newFileName;
970
+
971
+ // Update documentInstance
972
+ const docIndex = documentInstance.findIndex((d) => d._id === params.data._id);
973
+ if (docIndex !== -1) {
974
+ documentInstance[docIndex].title = newTitle;
975
+ documentInstance[docIndex].location = formattedLocation;
976
+ // Update file names in the referenced file objects
977
+ if (hasMdFile && newMdFileName && documentInstance[docIndex].mdFileId) {
978
+ documentInstance[docIndex].mdFileId.name = newMdFileName;
979
+ }
980
+ if (hasGenericFile && newFileName && documentInstance[docIndex].fileId) {
981
+ documentInstance[docIndex].fileId.name = newFileName;
982
+ }
983
+ }
984
+
985
+ // Refresh the grid with new location
986
+ const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
987
+ files = format.files;
988
+ folders = format.folders;
989
+ applySearchFilter();
990
+ currentPage = 0;
991
+ displayedFiles = getPagedFiles();
992
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
993
+ AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
994
+ updatePaginationUI();
995
+
996
+ NotificationManager.Push({
997
+ html: Translate.Render('success-update-document'),
998
+ status: 'success',
999
+ });
1000
+
1001
+ // If location changed, navigate to the new location
1002
+ if (!s(`.file-explorer-query-nav`)) s(`.main-btn-cloud`).click();
1003
+
1004
+ if (locationChanged)
1005
+ setTimeout(() => {
1006
+ location = formattedLocation;
1007
+ setPath(`${window.location.pathname}?location=${location}`);
1008
+ s(`.file-explorer-query-nav`).value = location;
1009
+ });
1010
+
1011
+ Modal.removeModal(editModalId);
1012
+ } else {
1013
+ throw new Error('Failed to update document');
1014
+ }
1015
+ } catch (error) {
1016
+ logger.error('Update document failed:', error);
1017
+ NotificationManager.Push({
1018
+ html: Translate.Render('error-update-document'),
1019
+ status: 'error',
1020
+ });
1021
+ }
1022
+ },
1023
+ { context: 'modal' },
1024
+ );
1025
+
1026
+ // Handle cancel button
1027
+ EventsUI.onClick(
1028
+ `.btn-edit-doc-cancel-${params.data._id}`,
1029
+ async (ev) => {
1030
+ ev.preventDefault();
1031
+ Modal.removeModal(editModalId);
1032
+ },
1033
+ { context: 'modal' },
1034
+ );
1035
+ });
371
1036
  },
372
1037
  { context: 'modal' },
373
1038
  );
@@ -446,8 +1111,12 @@ const FileExplorer = {
446
1111
  const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
447
1112
  files = format.files;
448
1113
  folders = format.folders;
449
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
1114
+ applySearchFilter();
1115
+ currentPage = 0;
1116
+ displayedFiles = getPagedFiles();
1117
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
450
1118
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
1119
+ updatePaginationUI();
451
1120
  },
452
1121
  { context: 'modal' },
453
1122
  );
@@ -514,6 +1183,57 @@ const FileExplorer = {
514
1183
  value: location,
515
1184
  })}
516
1185
  </div>
1186
+ ${dynamicCol({
1187
+ containerSelector: 'file-explorer-search-container',
1188
+ id: 'file-explorer-search',
1189
+ type: 'search-inputs',
1190
+ })}
1191
+ <div class="fl file-explorer-search-container">
1192
+ <div class="in fll file-explorer-search-col-a">
1193
+ ${await Input.Render({
1194
+ id: `file-explorer-search-title`,
1195
+ type: 'text',
1196
+ label: html`<i class="fas fa-search"></i> ${Translate.Render('doc-title')}`,
1197
+ containerClass: 'in section-mp input-container-width',
1198
+ placeholder: true,
1199
+ })}
1200
+ </div>
1201
+ <div class="in fll file-explorer-search-col-b">
1202
+ ${await Input.Render({
1203
+ id: `file-explorer-search-md-file`,
1204
+ type: 'text',
1205
+ label: html`<i class="fas fa-file-code"></i> ${Translate.Render('md-file-name')}`,
1206
+ containerClass: 'in section-mp input-container-width',
1207
+ placeholder: true,
1208
+ })}
1209
+ </div>
1210
+ <div class="in fll file-explorer-search-col-c">
1211
+ ${await Input.Render({
1212
+ id: `file-explorer-search-file`,
1213
+ type: 'text',
1214
+ label: html`<i class="fas fa-file"></i> ${Translate.Render('generic-file-name')}`,
1215
+ containerClass: 'in section-mp input-container-width',
1216
+ placeholder: true,
1217
+ })}
1218
+ </div>
1219
+ <div
1220
+ class="in fll file-explorer-search-col-d"
1221
+ style="display: flex; align-items: center; justify-content: center; height: 100%;"
1222
+ >
1223
+ ${await BtnIcon.Render({
1224
+ class: 'in management-table-btn-mini file-explorer-search-submit',
1225
+ label: html`<i class="fas fa-search"></i>`,
1226
+ type: 'button',
1227
+ style: 'top: 10px; margin-right: 5px;',
1228
+ })}
1229
+ ${await BtnIcon.Render({
1230
+ class: 'in management-table-btn-mini file-explorer-search-clear',
1231
+ label: html`<i class="fas fa-broom"></i>`,
1232
+ type: 'button',
1233
+ style: 'top: 10px',
1234
+ })}
1235
+ </div>
1236
+ </div>
517
1237
  </form>
518
1238
  <div class="fl file-explorer-nav">
519
1239
  <div class="in fll explorer-file-col">
@@ -553,8 +1273,12 @@ const FileExplorer = {
553
1273
  const format = this.documentDataFormat({ document: documentInstance, location });
554
1274
  files = format.files;
555
1275
  folders = format.folders;
556
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
1276
+ applySearchFilter();
1277
+ currentPage = 0;
1278
+ displayedFiles = getPagedFiles();
1279
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
557
1280
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
1281
+ updatePaginationUI();
558
1282
  },
559
1283
  },
560
1284
  {
@@ -588,8 +1312,12 @@ const FileExplorer = {
588
1312
  const format = this.documentDataFormat({ document: documentInstance, location });
589
1313
  files = format.files;
590
1314
  folders = format.folders;
591
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
1315
+ applySearchFilter();
1316
+ currentPage = 0;
1317
+ displayedFiles = getPagedFiles();
1318
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
592
1319
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
1320
+ updatePaginationUI();
593
1321
  }
594
1322
  },
595
1323
  },
@@ -616,13 +1344,37 @@ const FileExplorer = {
616
1344
  // rowData: files,
617
1345
  rowData: undefined,
618
1346
  columnDefs: [
619
- { field: 'name', flex: 2, headerName: 'Name', cellRenderer: LoadFileNameRenderer },
620
- { field: 'mimetype', flex: 1, headerName: 'Type' },
621
- { headerName: '', width: 80, cellRenderer: LoadFileActionsRenderer },
1347
+ { field: 'name', flex: 2, headerName: 'Title', cellRenderer: LoadFileNameRenderer },
1348
+ { field: 'mdFileName', flex: 1, headerName: 'MD File Name' },
1349
+ { field: 'fileName', flex: 1, headerName: 'Generic File Name' },
1350
+ { headerName: '', width: 150, cellRenderer: LoadFileActionsRenderer },
622
1351
  ],
623
1352
  },
624
1353
  })}
625
1354
  </div>
1355
+ <div class="fl file-explorer-pagination" style="padding: 5px 0;">
1356
+ <div class="in fll" style="width: 33.33%;">
1357
+ ${await BtnIcon.Render({
1358
+ class: 'in wfa file-explorer-prev-btn',
1359
+ label: html`<i class="fa-solid fa-chevron-left"></i> ${Translate.Render('previous')}`,
1360
+ type: 'button',
1361
+ })}
1362
+ </div>
1363
+ <div class="in fll" style="width: 33.33%; text-align: center;">
1364
+ <span
1365
+ class="file-explorer-pagination-info"
1366
+ style="display: inline-block; padding: 8px 0; min-width: 100px;"
1367
+ >1 / 1 (0 files)</span
1368
+ >
1369
+ </div>
1370
+ <div class="in fll" style="width: 33.33%;">
1371
+ ${await BtnIcon.Render({
1372
+ class: 'in wfa file-explorer-next-btn',
1373
+ label: html`${Translate.Render('next')} <i class="fa-solid fa-chevron-right"></i>`,
1374
+ type: 'button',
1375
+ })}
1376
+ </div>
1377
+ </div>
626
1378
  </div>
627
1379
  </div>
628
1380
  </div>
@@ -665,21 +1417,21 @@ const FileExplorer = {
665
1417
  if (f.location !== '/' && f.location[f.location.length - 1] === '/') f.location = f.location.slice(0, -1);
666
1418
  return f.location;
667
1419
  },
668
- documentDataFormat: function ({ document, location }) {
1420
+ documentDataFormat: function ({ document, location, searchFilters }) {
669
1421
  let files = document.map((f) => {
670
- if (!f.fileId)
671
- f.fileId = {
672
- name: f.title + '.md',
673
- mimetype: 'text/markdown',
674
- _id: f.mdFileId,
675
- };
676
1422
  return {
677
1423
  location: this.locationFormat({ f }),
678
- name: f.fileId.name,
679
- mimetype: f.fileId.mimetype,
680
- fileId: f.fileId._id,
1424
+ name: f.title,
1425
+ mdFileName: f.mdFileId?.name || '',
1426
+ fileName: f.fileId?.name || '',
1427
+ // Use the actual file ID for operations (prefer generic file, fallback to md file)
1428
+ fileId: f.fileId?._id || f.mdFileId?._id || null,
681
1429
  _id: f._id,
682
1430
  title: f.title,
1431
+ isPublic: f.isPublic || false,
1432
+ // Track file existence for edit form
1433
+ hasMdFile: !!f.mdFileId,
1434
+ hasGenericFile: !!f.fileId,
683
1435
  };
684
1436
  });
685
1437
  let documentId = document._id;
@@ -692,14 +1444,26 @@ const FileExplorer = {
692
1444
  locationId: `loc-${i}`,
693
1445
  };
694
1446
  });
695
- files = files.filter((f) => f.location === location);
696
- folders = folders
697
- .filter((f) => f.location.startsWith(location))
698
- .map((f) => {
699
- f.fileCount = document.filter((file) => file.location === f.location).length;
700
- return f;
701
- })
702
- .filter((f) => f.fileCount > 0);
1447
+
1448
+ const isSearching = searchFilters && (searchFilters.title || searchFilters.mdFile || searchFilters.file);
1449
+
1450
+ if (!isSearching) {
1451
+ files = files.filter((f) => f.location === location);
1452
+ folders = folders
1453
+ .filter((f) => f.location.startsWith(location))
1454
+ .map((f) => {
1455
+ f.fileCount = document.filter((file) => file.location === f.location).length;
1456
+ return f;
1457
+ })
1458
+ .filter((f) => f.fileCount > 0);
1459
+ } else {
1460
+ folders = folders
1461
+ .map((f) => {
1462
+ f.fileCount = files.filter((file) => file.location === f.location).length;
1463
+ return f;
1464
+ })
1465
+ .filter((f) => f.fileCount > 0);
1466
+ }
703
1467
  return { files, documentId, folders };
704
1468
  },
705
1469
  };