@oat-sa/tao-core-ui 3.23.0 → 3.24.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 (31) hide show
  1. package/dist/figure/FigureStateActive.js +64 -32
  2. package/dist/image/ImgStateActive/initHelper.js +64 -32
  3. package/dist/image/ImgStateActive.js +64 -32
  4. package/dist/previewer.js +1 -1
  5. package/dist/resourcemgr/assetSearch.js +793 -0
  6. package/dist/resourcemgr/assetSearchContract.js +477 -0
  7. package/dist/resourcemgr/css/resourcemgr.css +1137 -97
  8. package/dist/resourcemgr/css/resourcemgr.css.map +1 -1
  9. package/dist/resourcemgr/fileBrowser.js +456 -79
  10. package/dist/resourcemgr/filePreview.js +40 -4
  11. package/dist/resourcemgr/fileSelector.js +362 -92
  12. package/dist/resourcemgr.js +309 -57
  13. package/dist/searchModal/advancedSearch.js +233 -31
  14. package/package.json +15 -6
  15. package/src/image/ImgStateActive/initHelper.js +69 -31
  16. package/src/previewer.js +1 -1
  17. package/src/resourcemgr/assetSearch.js +847 -0
  18. package/src/resourcemgr/assetSearchContract.js +489 -0
  19. package/src/resourcemgr/css/resourcemgr.css +1137 -97
  20. package/src/resourcemgr/css/resourcemgr.css.map +1 -1
  21. package/src/resourcemgr/fileBrowser.js +470 -91
  22. package/src/resourcemgr/filePreview.js +46 -4
  23. package/src/resourcemgr/fileSelector.js +338 -46
  24. package/src/resourcemgr/scss/resourcemgr.scss +1351 -88
  25. package/src/resourcemgr/tpl/advanced-search.tpl +10 -0
  26. package/src/resourcemgr/tpl/fileSelect.tpl +25 -28
  27. package/src/resourcemgr/tpl/folder.tpl +4 -1
  28. package/src/resourcemgr/tpl/layout.tpl +113 -54
  29. package/src/resourcemgr/tpl/rootFolder.tpl +3 -1
  30. package/src/resourcemgr.js +289 -47
  31. package/src/searchModal/advancedSearch.js +242 -31
@@ -11,27 +11,36 @@
11
11
  *
12
12
  * You should have received a copy of the GNU General Public License
13
13
  * along with this program; if not, write to the Free Software
14
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
14
+ * Foundation, Inc., 31 Milk St # 960789 Boston, MA 02196 USA.
15
15
  *
16
- * Copyright (c) 2018-2020 (original work) Open Assessment Technologies SA;
16
+ * Copyright (c) 2018-2026 (original work) Open Assessment Technologies SA;
17
17
  */
18
18
  import $ from 'jquery';
19
19
  import _ from 'lodash';
20
- import request from 'core/request';
21
20
  import paginationComponent from 'ui/pagination';
22
21
  import rootFolderTpl from 'ui/resourcemgr/tpl/rootFolder';
23
22
  import folderTpl from 'ui/resourcemgr/tpl/folder';
23
+ import loggerFactory from 'core/logger';
24
24
  import updatePermissions from './util/updatePermissions';
25
+ import { DEFAULT_SORT, sortAssetItems } from 'ui/resourcemgr/assetSearchContract';
25
26
 
26
- const ns = 'resourcemgr';
27
+ const NS = 'resourcemgr';
28
+ const LOGGER = loggerFactory(`ui/${NS}`);
29
+ const DEFAULT_AJAX_TIMEOUT_MS = 10000;
27
30
 
28
31
  export default function (options) {
32
+ if (!options.browseUrl && options.url) {
33
+ options.browseUrl = options.url;
34
+ }
35
+
29
36
  const root = options.root || 'local';
30
37
  const rootPath = options.path || '/';
38
+ const initialPath = options.initialPath || rootPath;
31
39
  const $container = options.$target;
32
40
  const $fileBrowser = $('.file-browser .file-browser-wrapper', $container);
33
41
  const $divContainer = $(`.${root}`, $fileBrowser);
34
42
  const $folderContainer = $('.folders', $divContainer);
43
+ const $paginationContainer = $('.pagination-bottom', $container);
35
44
  const fileTree = {};
36
45
  // for pagination
37
46
  let selectedClass = {
@@ -40,32 +49,92 @@ export default function (options) {
40
49
  total: 0,
41
50
  page: 1
42
51
  };
52
+ let searchMode = false;
53
+ let sort = Object.assign({}, DEFAULT_SORT);
54
+ const ajaxTimeoutMs = Number.isFinite(Number(options.ajaxTimeoutMs)) && Number(options.ajaxTimeoutMs) > 0
55
+ ? Number(options.ajaxTimeoutMs)
56
+ : DEFAULT_AJAX_TIMEOUT_MS;
57
+
58
+ $container.on(`searchmode.${NS}`, function (e, enabled) {
59
+ searchMode = !!enabled;
60
+ });
61
+
62
+ $container.on(`sortchange.${NS}`, function (e, nextSort) {
63
+ sort = Object.assign({}, DEFAULT_SORT, nextSort || {});
64
+ selectedClass.page = 1;
65
+ invalidateFolderFiles(selectedClass.path);
66
+ if (searchMode || !isActiveBrowser()) {
67
+ return;
68
+ }
69
+ reloadSortedFolder();
70
+ });
71
+
72
+ $container.on(`searchclear.${NS}`, function (e, path) {
73
+ const targetPath = path || selectedClass.path;
74
+ selectedClass.page = 1;
75
+ invalidateFolderFiles(targetPath);
76
+ if (!isActiveBrowser()) {
77
+ return;
78
+ }
79
+ const subTree = getByExactPath(fileTree, targetPath) || getByPath(fileTree, targetPath) || fileTree;
80
+ getFolderContent(subTree, targetPath, function (content) {
81
+ if (content) {
82
+ selectFolder(content, targetPath);
83
+ }
84
+ });
85
+ });
86
+
87
+ // Reopen with resolved parent (AC3 edit/change): leave search, open folder again.
88
+ $container.on(`applycontext.${NS}`, function (e, ctx) {
89
+ const path = (ctx && ctx.path) || rootPath;
90
+ const pathIsTaomedia = path && String(path).indexOf('taomedia://') === 0;
91
+ if (pathIsTaomedia && root === 'local') {
92
+ return;
93
+ }
94
+ if (!pathIsTaomedia && root !== 'local') {
95
+ return;
96
+ }
97
+ $container.data('activeFileBrowserRoot', root);
98
+ if (searchMode) {
99
+ $container.trigger(`requestexitsearch.${NS}`);
100
+ }
101
+ selectedClass.page = 1;
102
+ openInitialPath(path);
103
+ });
43
104
 
44
105
  //load the content of the ROOT
45
106
  getFolderContent(fileTree, rootPath, function (content) {
107
+ if (!content) {
108
+ return;
109
+ }
46
110
  indexTree(content);
47
111
 
48
112
  //create the tree node for the ROOT folder by default once the initial content loaded
49
- $folderContainer.append(rootFolderTpl(content));
113
+ $folderContainer.append(
114
+ rootFolderTpl(
115
+ Object.assign({}, content, {
116
+ showToggle: hasNestedFolderChildren(content) !== false
117
+ })
118
+ )
119
+ );
50
120
 
51
121
  const $rootNode = $('.root-folder', $folderContainer);
52
122
  //create an inner list and append found elements
53
123
  const $innerList = $('.root ul', $folderContainer);
54
- if (content.children) {
124
+ if (hasNestedFolderChildren(content)) {
55
125
  $rootNode.addClass('opened');
126
+ } else if (hasNestedFolderChildren(content) === false) {
127
+ setFolderToggleState($rootNode, false);
56
128
  }
57
129
  updateFolders(content, $innerList);
58
130
 
59
- if (content.permissions.read && !options.hasAlreadySelected) {
60
- $('.file-browser').find('li.active').removeClass('active');
61
- updateSelectedClass(content.path, content.total, content.childrenLimit);
62
- $container.trigger('folderselect.'.concat(ns), [
63
- content.label,
64
- getPage(content.children),
65
- content.path,
66
- content
67
- ]);
68
- renderPagination();
131
+ if (content.permissions && content.permissions.read && !options.hasAlreadySelected) {
132
+ if (initialPath && initialPath !== rootPath) {
133
+ openInitialPath(initialPath);
134
+ } else {
135
+ selectFolder(content, content.path);
136
+ syncTreeActiveFolder(content.path || rootPath);
137
+ }
69
138
 
70
139
  if (root !== 'local') {
71
140
  options.hasAlreadySelected = true;
@@ -77,9 +146,8 @@ export default function (options) {
77
146
  $divContainer.off('click', '.folders a').on('click', '.folders a', function (e) {
78
147
  e.preventDefault();
79
148
  const $selected = $(this);
80
- const $folders = $('.folders li', $fileBrowser);
81
149
  const fullPath = $selected.data('path');
82
- const subTree = getByPath(fileTree, fullPath);
150
+ const subTree = getByExactPath(fileTree, fullPath);
83
151
 
84
152
  //get the folder content
85
153
  getFolderContent(subTree, fullPath, function (content) {
@@ -88,10 +156,12 @@ export default function (options) {
88
156
  if (content) {
89
157
  //either create the inner list of the content is new or just show it
90
158
  let $innerList = $selected.siblings('ul');
91
- if (!$innerList.length && content.children && _.find(content.children, 'path') && !content.empty) {
159
+ const nested = hasNestedFolderChildren(content);
160
+ if (!$innerList.length && nested) {
92
161
  $innerList = $('<ul></ul>').insertAfter($selected);
93
162
  updateFolders(content, $innerList);
94
163
  $selected.addClass('opened');
164
+ setFolderToggleState($selected, true);
95
165
  } else if ($innerList.length) {
96
166
  if ($innerList.css('display') === 'none') {
97
167
  $innerList.show();
@@ -100,68 +170,215 @@ export default function (options) {
100
170
  $innerList.hide();
101
171
  $selected.removeClass('opened');
102
172
  }
173
+ } else if (nested === false) {
174
+ // Leaf folder: keep alignment spacer, hide expand chevron.
175
+ setFolderToggleState($selected, false);
103
176
  }
104
177
 
105
- //toggle active element
106
- $folders.removeClass('active');
107
- $selected.parent('li').addClass('active');
178
+ syncTreeActiveFolder(fullPath, $selected);
108
179
 
109
180
  //internal event to set the file-selector content
110
- updateSelectedClass(fullPath, content.total, content.childrenLimit);
111
- $container.trigger(`folderselect.${ns}`, [
112
- content.label,
113
- getPage(content.children),
114
- content.path,
115
- content
116
- ]);
117
- renderPagination();
181
+ selectFolder(content, fullPath);
118
182
  }
119
183
  });
120
184
  });
121
185
 
122
- $container.on(`filenew.${ns}`, function (e, file, path) {
123
- const subTree = getByPath(fileTree, path);
124
- if (subTree) {
125
- if (!subTree.children) {
126
- subTree.children = [];
127
- }
128
- if (root !== 'local' || !_.find(subTree.children, { name: file.name })) {
129
- updatePermissions(file);
130
- const childrenFilesOnly = _.filter(subTree.children, function (child) {
131
- // Only file object has link property
132
- return Object.prototype.hasOwnProperty.call(child, 'link');
133
- });
186
+ $container.on(`filenew.${NS}`, function (e, file, path) {
187
+ const targetPath = path || selectedClass.path;
188
+ if (!targetPath || searchMode) {
189
+ return;
190
+ }
134
191
 
135
- if (childrenFilesOnly.length === subTree.total) {
136
- // all children loaded new file can be pushed to the end of tree
137
- // if not all, new file will be loaded with next page
138
- subTree.children.push(file);
139
- }
140
- subTree.total = Number.isFinite(Number(subTree.total)) ? Number(subTree.total) + 1 : 1;
141
- if (selectedClass.path === path) {
142
- selectedClass.total = subTree.total;
143
- }
144
- $container.trigger(`folderselect.${ns}`, [subTree.label, getPage(subTree.children), path, subTree]);
145
- renderPagination();
146
- }
192
+ const activeRoot = $container.data('activeFileBrowserRoot');
193
+ if (activeRoot && activeRoot !== root) {
194
+ return;
195
+ }
196
+
197
+ const subTree = getByExactPath(fileTree, targetPath) || getByPath(fileTree, targetPath);
198
+ if (subTree && root === 'local' && file && file.name && _.find(subTree.children, { name: file.name })) {
199
+ return;
147
200
  }
201
+
202
+ if (targetPath !== selectedClass.path) {
203
+ selectedClass.path = targetPath;
204
+ selectedClass.page = 1;
205
+ }
206
+
207
+ invalidateFolderFiles(targetPath);
208
+ reloadSortedFolder();
148
209
  });
149
210
 
150
- $container.on(`filedelete.${ns}`, function (e, path) {
151
- if (removeFromPath(fileTree, path)) {
152
- selectedClass.total--;
153
- loadPage();
211
+ $container.on(`filedelete.${NS}`, function (e, path) {
212
+ if (searchMode) {
213
+ return;
214
+ }
215
+
216
+ const activeRoot = $container.data('activeFileBrowserRoot');
217
+ if (activeRoot && activeRoot !== root) {
218
+ return;
219
+ }
220
+
221
+ if (path) {
222
+ removeFromPath(fileTree, path);
154
223
  }
224
+
225
+ invalidateFolderFiles(selectedClass.path);
226
+ reloadSortedFolder();
155
227
  });
228
+
229
+ /**
230
+ * Open and select an initial folder path after the root tree is available.
231
+ * @param {String} path
232
+ */
233
+ function findFolderLink(path) {
234
+ return $folderContainer.find('a').filter(function () {
235
+ return $(this).data('path') === path;
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Render directory children under an expanded folder anchor (replace / reopen).
241
+ * @param {String} parentPath
242
+ * @param {Object} parentContent
243
+ */
244
+ function mountFolderBranch(parentPath, parentContent) {
245
+ const $parentLink = findFolderLink(parentPath);
246
+ if (!$parentLink.length || !parentContent) {
247
+ return;
248
+ }
249
+ let $innerList = $parentLink.siblings('ul');
250
+ if (!$innerList.length) {
251
+ $innerList = $('<ul></ul>').insertAfter($parentLink);
252
+ }
253
+ updateFolders(parentContent, $innerList);
254
+ $parentLink.addClass('opened');
255
+ if ($innerList.css('display') === 'none') {
256
+ $innerList.show();
257
+ }
258
+ setFolderToggleState($parentLink, hasNestedFolderChildren(parentContent) !== false);
259
+ }
260
+
261
+ function markFolderActive(path) {
262
+ const $targetLink = findFolderLink(path);
263
+ if ($targetLink.length) {
264
+ $targetLink.parents('li').each(function () {
265
+ const $li = $(this);
266
+ const $anchor = $li.children('a');
267
+ const $list = $li.children('ul');
268
+ $anchor.addClass('opened');
269
+ if ($list.length) {
270
+ $list.show();
271
+ }
272
+ });
273
+ syncTreeActiveFolder(path, $targetLink);
274
+ } else if (path === rootPath) {
275
+ syncTreeActiveFolder(rootPath);
276
+ }
277
+ }
278
+
279
+ function openInitialPath(path) {
280
+ getFolderContent(fileTree, path, function (content) {
281
+ indexTree(fileTree);
282
+ if (!content) {
283
+ const rootContent = getByPath(fileTree, rootPath) || fileTree;
284
+ const fallbackPath = rootContent.path || rootPath;
285
+ selectFolder(rootContent, fallbackPath);
286
+ syncTreeActiveFolder(fallbackPath);
287
+ return;
288
+ }
289
+
290
+ function finishOpen() {
291
+ markFolderActive(path);
292
+ selectFolder(content, path);
293
+ }
294
+
295
+ if (findFolderLink(path).length) {
296
+ finishOpen();
297
+ return;
298
+ }
299
+
300
+ const parentFolderPath = content.parentFolderPath;
301
+ if (parentFolderPath && parentFolderPath !== path) {
302
+ getFolderContent(fileTree, parentFolderPath, function (parentContent) {
303
+ mountFolderBranch(parentFolderPath, parentContent);
304
+ finishOpen();
305
+ });
306
+ return;
307
+ }
308
+
309
+ finishOpen();
310
+ });
311
+ }
312
+
313
+ /**
314
+ * Mark the folder row that matches path as active (open folder icon for leaves).
315
+ * @param {String} path
316
+ * @param {jQuery} [$link]
317
+ */
318
+ function syncTreeActiveFolder(path, $link) {
319
+ $('.folders li', $fileBrowser).removeClass('active');
320
+ const $target =
321
+ $link && $link.length
322
+ ? $link
323
+ : $folderContainer.find('a').filter(function () {
324
+ return $(this).data('path') === path;
325
+ });
326
+ if ($target.length) {
327
+ $target.parent('li').addClass('active');
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Whether this media source currently owns the file table.
333
+ * @returns {Boolean}
334
+ */
335
+ function isActiveBrowser() {
336
+ return $container.data('activeFileBrowserRoot') === root;
337
+ }
338
+
339
+ /**
340
+ * Select a folder and publish its page of files to the selector.
341
+ * @param {Object} content
342
+ * @param {String} path
343
+ */
344
+ function selectFolder(content, path) {
345
+ if (!content) {
346
+ return;
347
+ }
348
+ $container.data('activeFileBrowserRoot', root);
349
+ if (searchMode) {
350
+ $container.trigger(`folderpath.${NS}`, [path, content.label]);
351
+ $container.trigger(`folderselect.${NS}`, [
352
+ content.label,
353
+ getPage(content.children || []),
354
+ path,
355
+ content
356
+ ]);
357
+ return;
358
+ }
359
+ updateSelectedClass(path, content.total, content.childrenLimit);
360
+ $container.trigger(`folderpath.${NS}`, [path, content.label]);
361
+ $container.trigger(`folderselect.${NS}`, [
362
+ content.label,
363
+ getPage(content.children || []),
364
+ path,
365
+ content
366
+ ]);
367
+ renderPagination();
368
+ }
369
+
156
370
  /**
157
371
  * Get files for page
158
372
  * @param {Array} children
159
373
  * @returns {Array} files for this page
160
374
  */
161
375
  function getPage(children) {
162
- const files = _.filter(children, function (item) {
163
- return !!item.uri;
164
- });
376
+ const files = sortAssetItems(
377
+ _.filter(children, function (item) {
378
+ return !!item.uri;
379
+ }),
380
+ sort
381
+ );
165
382
  if (selectedClass.childrenLimit) {
166
383
  return files.slice(
167
384
  (selectedClass.page - 1) * selectedClass.childrenLimit,
@@ -176,6 +393,13 @@ export default function (options) {
176
393
  * @param {String} path - the folder path (relative to the root)
177
394
  * @param {Function} cb - called back with the content in 1st parameter
178
395
  */
396
+ function markFolderEmptyAtPath(tree, path) {
397
+ const node = getByExactPath(tree, path);
398
+ if (node) {
399
+ node.empty = true;
400
+ }
401
+ }
402
+
179
403
  function getFolderContent(tree, path, cb) {
180
404
  let content = getByPath(tree, path);
181
405
  if (!content || (!content.children && !content.empty)) {
@@ -183,32 +407,50 @@ export default function (options) {
183
407
  if (!tree.path) {
184
408
  tree = _.merge(tree, data);
185
409
  } else if (data.children) {
410
+ setToPath(tree, path, data);
186
411
  if (!_.find(data.children, 'path')) {
187
412
  // no subfolders inside folder
188
- tree.empty = true;
413
+ markFolderEmptyAtPath(tree, path);
189
414
  }
190
- setToPath(tree, path, data);
191
415
  } else {
192
- tree.empty = true;
416
+ setToPath(tree, path, data);
417
+ markFolderEmptyAtPath(tree, path);
193
418
  }
194
419
  cb(data);
420
+ }).catch(function () {
421
+ cb(null);
195
422
  });
196
423
  } else if (content.children) {
197
424
  const files = _.filter(content.children, function (item) {
198
425
  return !!item.uri;
199
426
  });
200
- // if files less then total and need toload this page
201
- if (files.length < selectedClass.total && files.length < selectedClass.page * selectedClass.childrenLimit) {
427
+ // Use folder total (not selectedClass): openInitialPath runs before selectFolder.
428
+ const expectedTotal = Number(content.total);
429
+ const pageSize = Number(content.childrenLimit) || selectedClass.childrenLimit || 10;
430
+ const page = selectedClass.page || 1;
431
+ // Missing/NaN total → unknown size; refetch so invalidated cache is not treated as complete.
432
+ if (
433
+ !Number.isFinite(expectedTotal) ||
434
+ (files.length < expectedTotal && files.length < page * pageSize)
435
+ ) {
202
436
  loadContent(path).then(function (data) {
203
437
  const loadedFiles = _.filter(data.children, function (item) {
204
438
  return !!item.uri;
205
439
  });
440
+ const node = getByExactPath(tree, path);
441
+ if (node && !Number.isFinite(expectedTotal)) {
442
+ node.children = _.filter(node.children, function (item) {
443
+ return !item.uri;
444
+ });
445
+ }
206
446
  setToPath(tree, path, {
207
447
  children: loadedFiles,
208
448
  total: data.total,
209
449
  childrenLimit: data.childrenLimit
210
450
  });
211
- content = getByPath(tree, path);
451
+ content = getByExactPath(tree, path) || getByPath(tree, path);
452
+ cb(content);
453
+ }).catch(function () {
212
454
  cb(content);
213
455
  });
214
456
  } else {
@@ -257,6 +499,29 @@ export default function (options) {
257
499
  return match;
258
500
  }
259
501
 
502
+ /**
503
+ * Get a subtree by exact path equality.
504
+ * @param {Object} tree
505
+ * @param {String} path
506
+ * @returns {Object|undefined}
507
+ */
508
+ function getByExactPath(tree, path) {
509
+ let match;
510
+ if (tree) {
511
+ if (tree.path === path) {
512
+ match = tree;
513
+ } else if (tree.children) {
514
+ _.forEach(tree.children, function (child) {
515
+ match = getByExactPath(child, path);
516
+ if (match) {
517
+ return false;
518
+ }
519
+ });
520
+ }
521
+ }
522
+ return match;
523
+ }
524
+
260
525
  /**
261
526
  * Merge data into at into the subtree
262
527
  * @param {Object} tree - the tree model
@@ -300,8 +565,11 @@ export default function (options) {
300
565
  return child.path === path || (child.name && tree.path + child.name === path) || child.uri === path;
301
566
  });
302
567
  done = removed.length > 0;
303
- tree.total--;
304
- if (!done) {
568
+ if (done) {
569
+ if (Number.isFinite(Number(tree.total))) {
570
+ tree.total = Math.max(0, Number(tree.total) - removed.length);
571
+ }
572
+ } else {
305
573
  _.forEach(tree.children, function (child) {
306
574
  done = removeFromPath(child, path);
307
575
  if (done) {
@@ -313,31 +581,116 @@ export default function (options) {
313
581
  return done;
314
582
  }
315
583
 
584
+ /**
585
+ * Drop cached file rows for a folder so the next load hits the service
586
+ * with the current sort. Nested folder nodes are kept.
587
+ * @param {String} path
588
+ */
589
+ function invalidateFolderFiles(path) {
590
+ const content = getByExactPath(fileTree, path);
591
+ if (content && Array.isArray(content.children)) {
592
+ content.children = content.children.filter(function (child) {
593
+ return child.path && !child.uri;
594
+ });
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Replace a folder node with a freshly loaded payload (files + folders).
600
+ * @param {String} path
601
+ * @param {Object} data
602
+ */
603
+ function replaceFolderContent(path, data) {
604
+ const content = getByExactPath(fileTree, path);
605
+ if (!content || !data) {
606
+ return;
607
+ }
608
+ if (Object.prototype.hasOwnProperty.call(data, 'children')) {
609
+ content.children = data.children;
610
+ } else {
611
+ content.children = [];
612
+ content.empty = true;
613
+ content.total = 0;
614
+ }
615
+ if (Object.prototype.hasOwnProperty.call(data, 'total')) {
616
+ content.total = data.total;
617
+ }
618
+ if (Object.prototype.hasOwnProperty.call(data, 'childrenLimit')) {
619
+ content.childrenLimit = data.childrenLimit;
620
+ }
621
+ if (data.label) {
622
+ content.label = data.label;
623
+ }
624
+ if (data.path) {
625
+ content.path = data.path;
626
+ }
627
+ }
628
+
316
629
  /**
317
630
  * Get the content of a folder
318
631
  * @param {String} path - the folder path
319
- * @returns {jQuery.Deferred} the defferred object to run done/complete/fail
632
+ * @returns {Promise} resolves with folder content
320
633
  */
321
634
  function loadContent(path) {
322
635
  const parameters = {};
323
- parameters[options.pathParam] = path;
324
- return request({
325
- url: options.browseUrl,
326
- method: 'GET',
327
- dataType: 'json',
328
- data: _.merge(parameters, options.params, {
329
- childrenOffset: (selectedClass.page - 1) * selectedClass.childrenLimit
330
- }),
331
- noToken: true
332
- })
333
- .then(response => response.data)
334
- .then(response => {
335
- response = updatePermissions(response);
336
- if (response.children && response.children.length > 0) {
337
- response.children.map(responseChildren => updatePermissions(responseChildren));
338
- }
339
- return response;
340
- });
636
+ parameters[options.pathParam || 'path'] = path;
637
+ return Promise.resolve(
638
+ $.ajax({
639
+ url: options.browseUrl,
640
+ method: 'GET',
641
+ dataType: 'json',
642
+ timeout: ajaxTimeoutMs,
643
+ data: _.merge(parameters, options.params, {
644
+ // depth=2 so each rendered child folder includes its own dir children,
645
+ // allowing leaf folders to hide the expand chevron without an extra click.
646
+ depth: 2,
647
+ childrenOffset: (selectedClass.page - 1) * selectedClass.childrenLimit,
648
+ sortBy: sort.field,
649
+ sortDir: sort.direction
650
+ })
651
+ })
652
+ ).then(function (response) {
653
+ if (response && response.success === false) {
654
+ return Promise.reject(response);
655
+ }
656
+ let data = response && response.data ? response.data : response;
657
+ data = updatePermissions(data);
658
+ if (data.children && data.children.length > 0) {
659
+ data.children.map(responseChildren => updatePermissions(responseChildren));
660
+ }
661
+ return data;
662
+ });
663
+ }
664
+
665
+ /**
666
+ * Whether a folder node has nested directory children.
667
+ * @param {Object} node
668
+ * @returns {boolean|null} true/false when known; null when children were not loaded yet
669
+ */
670
+ function hasNestedFolderChildren(node) {
671
+ if (!node || node.empty === true) {
672
+ return false;
673
+ }
674
+ if (!Array.isArray(node.children)) {
675
+ return null;
676
+ }
677
+ return Boolean(_.find(node.children, 'path'));
678
+ }
679
+
680
+ /**
681
+ * Show or hide the expand chevron on a folder anchor (keep spacer width).
682
+ * @param {jQuery} $anchor
683
+ * @param {boolean} hasNested
684
+ */
685
+ function setFolderToggleState($anchor, hasNested) {
686
+ const $toggle = $anchor.children('.tree-toggle');
687
+ if (hasNested) {
688
+ $toggle.addClass('icon-right').removeClass('is-leaf');
689
+ $anchor.removeClass('is-leaf');
690
+ } else {
691
+ $toggle.removeClass('icon-right').addClass('is-leaf');
692
+ $anchor.removeClass('opened').addClass('is-leaf');
693
+ }
341
694
  }
342
695
 
343
696
  /**
@@ -351,7 +704,10 @@ export default function (options) {
351
704
  if (typeof data.relPath === 'undefined') {
352
705
  data.relPath = data.path;
353
706
  }
707
+ // Unknown nested state (lazy depth) keeps the chevron until the folder is opened.
708
+ data.showToggle = hasNestedFolderChildren(data) !== false;
354
709
  $parent.append(folderTpl(data));
710
+ return;
355
711
  }
356
712
  if (data && data.children && _.isArray(data.children) && !data.empty) {
357
713
  _.forEach(data.children, function (child) {
@@ -384,7 +740,11 @@ export default function (options) {
384
740
  * Render pagination
385
741
  */
386
742
  function renderPagination() {
387
- const $paginationContainer = $('.pagination-bottom', $container);
743
+ if (searchMode) {
744
+ // Drop browse pagination controls/handlers when search owns the table.
745
+ $paginationContainer.empty();
746
+ return;
747
+ }
388
748
  const total = Number(selectedClass.total);
389
749
  const childrenLimit = Number(selectedClass.childrenLimit);
390
750
 
@@ -413,11 +773,30 @@ export default function (options) {
413
773
  .render($paginationContainer);
414
774
  }
415
775
  }
776
+ /**
777
+ * Re-fetch the current folder with the active sort and publish files.
778
+ */
779
+ function reloadSortedFolder() {
780
+ const path = selectedClass.path;
781
+ loadContent(path)
782
+ .then(function (data) {
783
+ if (!data) {
784
+ return;
785
+ }
786
+ replaceFolderContent(path, data);
787
+ const content = getByExactPath(fileTree, path) || data;
788
+ selectFolder(content, content.path || path);
789
+ })
790
+ .catch(function (error) {
791
+ LOGGER.error(error);
792
+ });
793
+ }
794
+
416
795
  /**
417
796
  * Load page
418
797
  */
419
798
  function loadPage() {
420
- const subTree = getByPath(fileTree, selectedClass.path);
799
+ const subTree = getByPath(fileTree, selectedClass.path) || fileTree;
421
800
 
422
801
  //get the folder content
423
802
  getFolderContent(subTree, selectedClass.path, function (content) {
@@ -425,7 +804,7 @@ export default function (options) {
425
804
 
426
805
  if (content) {
427
806
  //internal event to set the file-selector content
428
- $container.trigger(`folderselect.${ns}`, [content.label, getPage(content.children), content.path]);
807
+ $container.trigger(`folderselect.${NS}`, [content.label, getPage(content.children), content.path, content]);
429
808
  }
430
809
  });
431
810
  }