@underpostnet/underpost 2.97.5 → 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.
@@ -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 = [
@@ -204,15 +456,25 @@ const FileExplorer = {
204
456
  const format = this.documentDataFormat({ document: documentInstance, location });
205
457
  files = format.files;
206
458
  folders = format.folders;
207
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
459
+ applySearchFilter();
460
+ currentPage = 0;
461
+ displayedFiles = getPagedFiles();
462
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
208
463
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
464
+ updatePaginationUI();
209
465
  NotificationManager.Push({
210
466
  html: Translate.Render(`${status}-upload-file`),
211
467
  status,
212
468
  });
213
469
  if (status === 'success') {
214
- s(`.btn-input-home-directory`).click();
470
+ // Clear the file input
215
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;
216
478
  }
217
479
  }
218
480
  });
@@ -229,19 +491,26 @@ const FileExplorer = {
229
491
  });
230
492
  EventsUI.onClick(`.btn-input-home-directory`, async (e) => {
231
493
  e.preventDefault();
494
+
495
+ if (getQueryParams()?.tab === 'upload') {
496
+ setQueryParams({ tab: null }, { replace: false });
497
+ return;
498
+ }
499
+
232
500
  let newLocation = '/';
233
- if (s(`.file-explorer-uploader`).style.display !== 'none') {
234
- s(`.file-explorer-uploader`).style.display = 'none';
235
- s(`.file-explorer-nav`).style.display = 'block';
236
- } else if (newLocation === location) return;
237
- else location = newLocation;
501
+ if (newLocation === location) return;
502
+ location = newLocation;
238
503
  setPath(`${window.location.pathname}?location=${location}`);
239
504
  s(`.file-explorer-query-nav`).value = location;
240
505
  const format = this.documentDataFormat({ document: documentInstance, location });
241
506
  files = format.files;
242
507
  folders = format.folders;
243
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
508
+ applySearchFilter();
509
+ currentPage = 0;
510
+ displayedFiles = getPagedFiles();
511
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
244
512
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
513
+ updatePaginationUI();
245
514
  });
246
515
  EventsUI.onClick(`.btn-input-copy-directory`, async (e) => {
247
516
  e.preventDefault();
@@ -261,8 +530,7 @@ const FileExplorer = {
261
530
  });
262
531
  EventsUI.onClick(`.btn-input-upload-file`, async (e) => {
263
532
  e.preventDefault();
264
- s(`.file-explorer-nav`).style.display = 'none';
265
- s(`.file-explorer-uploader`).style.display = 'block';
533
+ setQueryParams({ tab: 'upload' }, { replace: false });
266
534
  });
267
535
  });
268
536
 
@@ -298,6 +566,11 @@ const FileExplorer = {
298
566
  label: html`<i class="fas fa-copy"></i>`,
299
567
  type: 'button',
300
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
+ })}
301
574
  ${await BtnIcon.Render({
302
575
  class: `in fll management-table-btn-mini ${toggleId}`,
303
576
  label: isPublic
@@ -313,7 +586,10 @@ const FileExplorer = {
313
586
  const url = `${window.location.origin}${uri}`;
314
587
 
315
588
  const originObj = documentInstance.find((d) => d._id === params.data._id);
316
- 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;
317
593
 
318
594
  if (!originObj) {
319
595
  s(`.btn-file-view-${params.data._id}`).classList.add('hide');
@@ -396,10 +672,12 @@ const FileExplorer = {
396
672
  const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
397
673
  files = format.files;
398
674
  folders = format.folders;
675
+ applySearchFilter();
399
676
  // AgGrid.grids[gridFileId].setGridOption('rowData', files);
400
677
  // const selectedData = gridApi.getSelectedRows();
401
678
  AgGrid.grids[gridFileId].applyTransaction({ remove: [params.data] });
402
679
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
680
+ updatePaginationUI();
403
681
  },
404
682
  { context: 'modal' },
405
683
  );
@@ -477,6 +755,287 @@ const FileExplorer = {
477
755
  },
478
756
  { context: 'modal' },
479
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
+ });
1036
+ },
1037
+ { context: 'modal' },
1038
+ );
480
1039
  });
481
1040
  }
482
1041
 
@@ -552,8 +1111,12 @@ const FileExplorer = {
552
1111
  const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
553
1112
  files = format.files;
554
1113
  folders = format.folders;
555
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
1114
+ applySearchFilter();
1115
+ currentPage = 0;
1116
+ displayedFiles = getPagedFiles();
1117
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
556
1118
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
1119
+ updatePaginationUI();
557
1120
  },
558
1121
  { context: 'modal' },
559
1122
  );
@@ -620,6 +1183,57 @@ const FileExplorer = {
620
1183
  value: location,
621
1184
  })}
622
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>
623
1237
  </form>
624
1238
  <div class="fl file-explorer-nav">
625
1239
  <div class="in fll explorer-file-col">
@@ -659,8 +1273,12 @@ const FileExplorer = {
659
1273
  const format = this.documentDataFormat({ document: documentInstance, location });
660
1274
  files = format.files;
661
1275
  folders = format.folders;
662
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
1276
+ applySearchFilter();
1277
+ currentPage = 0;
1278
+ displayedFiles = getPagedFiles();
1279
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
663
1280
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
1281
+ updatePaginationUI();
664
1282
  },
665
1283
  },
666
1284
  {
@@ -694,8 +1312,12 @@ const FileExplorer = {
694
1312
  const format = this.documentDataFormat({ document: documentInstance, location });
695
1313
  files = format.files;
696
1314
  folders = format.folders;
697
- AgGrid.grids[gridFileId].setGridOption('rowData', files);
1315
+ applySearchFilter();
1316
+ currentPage = 0;
1317
+ displayedFiles = getPagedFiles();
1318
+ AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
698
1319
  AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
1320
+ updatePaginationUI();
699
1321
  }
700
1322
  },
701
1323
  },
@@ -722,13 +1344,37 @@ const FileExplorer = {
722
1344
  // rowData: files,
723
1345
  rowData: undefined,
724
1346
  columnDefs: [
725
- { field: 'name', flex: 2, headerName: 'Name', cellRenderer: LoadFileNameRenderer },
726
- { field: 'mimetype', flex: 1, headerName: 'Type' },
727
- { headerName: '', width: 120, 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 },
728
1351
  ],
729
1352
  },
730
1353
  })}
731
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>
732
1378
  </div>
733
1379
  </div>
734
1380
  </div>
@@ -771,22 +1417,21 @@ const FileExplorer = {
771
1417
  if (f.location !== '/' && f.location[f.location.length - 1] === '/') f.location = f.location.slice(0, -1);
772
1418
  return f.location;
773
1419
  },
774
- documentDataFormat: function ({ document, location }) {
1420
+ documentDataFormat: function ({ document, location, searchFilters }) {
775
1421
  let files = document.map((f) => {
776
- if (!f.fileId)
777
- f.fileId = {
778
- name: f.title + '.md',
779
- mimetype: 'text/markdown',
780
- _id: f.mdFileId,
781
- };
782
1422
  return {
783
1423
  location: this.locationFormat({ f }),
784
- name: f.fileId.name,
785
- mimetype: f.fileId.mimetype,
786
- 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,
787
1429
  _id: f._id,
788
1430
  title: f.title,
789
1431
  isPublic: f.isPublic || false,
1432
+ // Track file existence for edit form
1433
+ hasMdFile: !!f.mdFileId,
1434
+ hasGenericFile: !!f.fileId,
790
1435
  };
791
1436
  });
792
1437
  let documentId = document._id;
@@ -799,14 +1444,26 @@ const FileExplorer = {
799
1444
  locationId: `loc-${i}`,
800
1445
  };
801
1446
  });
802
- files = files.filter((f) => f.location === location);
803
- folders = folders
804
- .filter((f) => f.location.startsWith(location))
805
- .map((f) => {
806
- f.fileCount = document.filter((file) => file.location === f.location).length;
807
- return f;
808
- })
809
- .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
+ }
810
1467
  return { files, documentId, folders };
811
1468
  },
812
1469
  };