@oat-sa/tao-core-ui 3.24.0 → 3.24.1

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.
@@ -204,7 +204,18 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
204
204
  */
205
205
  const NS = 'resourcemgr';
206
206
  const LOGGER = loggerFactory(`ui/${NS}`);
207
- const DEFAULT_AJAX_TIMEOUT_MS = 10000;
207
+ const DEFAULT_AJAX_TIMEOUT_MS = 30000;
208
+ const TREE_LOADING_REF_KEY = `${NS}TreeLoadingRefCount`;
209
+
210
+ /**
211
+ * @param {Object} data
212
+ * @returns {Array}
213
+ */
214
+ function folderChildrenOnly(data) {
215
+ return _.filter(data && data.children, function (child) {
216
+ return child && child.path && !child.uri;
217
+ });
218
+ }
208
219
  function fileBrowser (options) {
209
220
  if (!options.browseUrl && options.url) {
210
221
  options.browseUrl = options.url;
@@ -221,15 +232,50 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
221
232
  // for pagination
222
233
  let selectedClass = {
223
234
  path: rootPath,
224
- childrenLimit: 10,
235
+ childrenLimit: 11,
225
236
  total: 0,
226
237
  page: 1
227
238
  };
228
239
  let searchMode = false;
229
240
  let sort = Object.assign({}, assetSearchContract.DEFAULT_SORT);
241
+ const splitBrowse = options.splitBrowse !== false;
230
242
  const ajaxTimeoutMs = Number.isFinite(Number(options.ajaxTimeoutMs)) && Number(options.ajaxTimeoutMs) > 0 ? Number(options.ajaxTimeoutMs) : DEFAULT_AJAX_TIMEOUT_MS;
243
+ const $treeLoadingOverlay = $$1('.file-browser-tree-loading', $fileBrowser);
244
+ const $treeLoadingStatus = $$1('.file-browser-tree-loading-status', $treeLoadingOverlay);
245
+
246
+ /**
247
+ * Ref-count in-flight browse requests and toggle the tree loading overlay.
248
+ * @param {Boolean} increment - true when a load starts, false when it finishes
249
+ */
250
+ function setTreeLoading(increment) {
251
+ let treeLoadingRefCount = Number($fileBrowser.data(TREE_LOADING_REF_KEY)) || 0;
252
+ treeLoadingRefCount += increment ? 1 : -1;
253
+ if (treeLoadingRefCount < 0) {
254
+ treeLoadingRefCount = 0;
255
+ }
256
+ $fileBrowser.data(TREE_LOADING_REF_KEY, treeLoadingRefCount);
257
+ const loading = treeLoadingRefCount > 0;
258
+ $fileBrowser.toggleClass('is-tree-loading', loading);
259
+ $fileBrowser.attr('aria-busy', loading ? 'true' : 'false');
260
+ $treeLoadingOverlay.toggleClass('hidden', !loading).attr('aria-hidden', loading ? 'false' : 'true');
261
+ $treeLoadingStatus.attr('aria-busy', loading ? 'true' : 'false');
262
+ }
263
+
264
+ /**
265
+ * Show or hide the asset table loading overlay (browse mode only).
266
+ * @param {Boolean} loading
267
+ */
268
+ function setFilesLoading(loading) {
269
+ if (searchMode) {
270
+ return;
271
+ }
272
+ $container.trigger(`filesloading.${NS}`, [!!loading]);
273
+ }
231
274
  $container.on(`searchmode.${NS}`, function (e, enabled) {
232
275
  searchMode = !!enabled;
276
+ if (searchMode) {
277
+ $container.trigger(`filesloading.${NS}`, [false]);
278
+ }
233
279
  });
234
280
  $container.on(`sortchange.${NS}`, function (e, nextSort) {
235
281
  sort = Object.assign({}, assetSearchContract.DEFAULT_SORT, nextSort || {});
@@ -248,9 +294,14 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
248
294
  return;
249
295
  }
250
296
  const subTree = getByExactPath(fileTree, targetPath) || getByPath(fileTree, targetPath) || fileTree;
297
+ if (!searchMode) {
298
+ setFilesLoading(true);
299
+ }
251
300
  getFolderContent(subTree, targetPath, function (content) {
252
301
  if (content) {
253
302
  selectFolder(content, targetPath);
303
+ } else {
304
+ setFilesLoading(false);
254
305
  }
255
306
  });
256
307
  });
@@ -274,8 +325,15 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
274
325
  });
275
326
 
276
327
  //load the content of the ROOT
328
+ const showInitialListLoading = !searchMode && root === 'local';
329
+ if (showInitialListLoading) {
330
+ setFilesLoading(true);
331
+ }
277
332
  getFolderContent(fileTree, rootPath, function (content) {
278
333
  if (!content) {
334
+ if (showInitialListLoading) {
335
+ setFilesLoading(false);
336
+ }
279
337
  return;
280
338
  }
281
339
  indexTree(content);
@@ -303,6 +361,8 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
303
361
  if (root !== 'local') {
304
362
  options.hasAlreadySelected = true;
305
363
  }
364
+ } else if (showInitialListLoading) {
365
+ setFilesLoading(false);
306
366
  }
307
367
  });
308
368
 
@@ -312,6 +372,14 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
312
372
  const $selected = $$1(this);
313
373
  const fullPath = $selected.data('path');
314
374
  const subTree = getByExactPath(fileTree, fullPath);
375
+ const openingFolder = !searchMode && fullPath !== selectedClass.path;
376
+ if (openingFolder) {
377
+ selectedClass.page = 1;
378
+ }
379
+ if (!searchMode) {
380
+ setFilesLoading(true);
381
+ }
382
+ invalidateFolderFiles(fullPath);
315
383
 
316
384
  //get the folder content
317
385
  getFolderContent(subTree, fullPath, function (content) {
@@ -341,6 +409,8 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
341
409
 
342
410
  //internal event to set the file-selector content
343
411
  selectFolder(content, fullPath);
412
+ } else if (!searchMode) {
413
+ setFilesLoading(false);
344
414
  }
345
415
  });
346
416
  });
@@ -361,10 +431,11 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
361
431
  }
362
432
  if (targetPath !== selectedClass.path) {
363
433
  selectedClass.path = targetPath;
364
- selectedClass.page = 1;
365
434
  }
435
+ selectedClass.page = 1;
366
436
  invalidateFolderFiles(targetPath);
367
- reloadSortedFolder();
437
+ const pendingUpload = file && !file.error && (file.uri || file.name) ? file : null;
438
+ reloadSortedFolder(pendingUpload ? [pendingUpload] : []);
368
439
  });
369
440
  $container.on(`filedelete.${NS}`, function (e, path) {
370
441
  if (searchMode) {
@@ -494,13 +565,16 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
494
565
  $container.data('activeFileBrowserRoot', root);
495
566
  if (searchMode) {
496
567
  $container.trigger(`folderpath.${NS}`, [path, content.label]);
497
- $container.trigger(`folderselect.${NS}`, [content.label, getPage(content.children || []), path, content]);
568
+ $container.trigger(`folderselect.${NS}`, [content.label, getFilesForDisplay(content), path, content]);
498
569
  return;
499
570
  }
500
571
  updateSelectedClass(path, content.total, content.childrenLimit);
501
572
  $container.trigger(`folderpath.${NS}`, [path, content.label]);
502
- $container.trigger(`folderselect.${NS}`, [content.label, getPage(content.children || []), path, content]);
573
+ $container.trigger(`folderselect.${NS}`, [content.label, getFilesForDisplay(content), path, content]);
503
574
  renderPagination();
575
+ if (!searchMode) {
576
+ setFilesLoading(false);
577
+ }
504
578
  }
505
579
 
506
580
  /**
@@ -517,6 +591,172 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
517
591
  }
518
592
  return files;
519
593
  }
594
+
595
+ /**
596
+ * Files for the table (split list payload or legacy children slice).
597
+ * @param {Object} content
598
+ * @returns {Array}
599
+ */
600
+ function getFilesForDisplay(content) {
601
+ if (!content) {
602
+ return [];
603
+ }
604
+ if (splitBrowse) {
605
+ return content.listItems || [];
606
+ }
607
+ return getPage(content.children || []);
608
+ }
609
+ function listCacheKey() {
610
+ return `${sort.field}:${sort.direction}:${selectedClass.page}`;
611
+ }
612
+
613
+ /**
614
+ * Snapshot list request identity (folder + page/sort) when a browse starts.
615
+ * @returns {{path: String, cacheKey: String}}
616
+ */
617
+ function captureListRequestContext() {
618
+ return {
619
+ path: selectedClass.path,
620
+ cacheKey: listCacheKey()
621
+ };
622
+ }
623
+
624
+ /**
625
+ * Whether navigation changed before an in-flight list response should apply.
626
+ * @param {{path: String, cacheKey: String}} captured
627
+ * @returns {Boolean}
628
+ */
629
+ function isListRequestStale(captured) {
630
+ return !captured || captured.path !== selectedClass.path || captured.cacheKey !== listCacheKey();
631
+ }
632
+
633
+ /**
634
+ * @param {Object} response
635
+ * @returns {Object}
636
+ */
637
+ function unwrapBrowseResponse(response) {
638
+ if (response && response.success === false) {
639
+ return Promise.reject(response);
640
+ }
641
+ let data = response && response.data ? response.data : response;
642
+ data = updatePermissions(data);
643
+ if (data.children && data.children.length > 0) {
644
+ data.children = data.children.map(function (child) {
645
+ return updatePermissions(child);
646
+ });
647
+ }
648
+ if (data.items && data.items.length > 0) {
649
+ data.items = data.items.map(function (item) {
650
+ return updatePermissions(item);
651
+ });
652
+ }
653
+ return data;
654
+ }
655
+
656
+ /**
657
+ * @param {String} path
658
+ * @param {Object} extraData
659
+ * @returns {Promise}
660
+ */
661
+ function browseRequest(path, extraData) {
662
+ const parameters = {};
663
+ parameters[options.pathParam || 'path'] = path;
664
+ return Promise.resolve($$1.ajax({
665
+ url: options.browseUrl,
666
+ method: 'GET',
667
+ dataType: 'json',
668
+ timeout: ajaxTimeoutMs,
669
+ data: _.merge(parameters, options.params, extraData)
670
+ })).then(unwrapBrowseResponse);
671
+ }
672
+
673
+ /**
674
+ * @param {Object} tree
675
+ * @param {String} path
676
+ * @param {Object} data
677
+ */
678
+ function applyTreePayload(tree, path, data) {
679
+ const folderData = Object.assign({}, data, {
680
+ children: folderChildrenOnly(data)
681
+ });
682
+ if (!tree.path) {
683
+ _.merge(tree, folderData);
684
+ } else if (folderData.children) {
685
+ setToPath(tree, path, folderData);
686
+ if (!_.find(folderData.children, 'path')) {
687
+ markFolderEmptyAtPath(tree, path);
688
+ }
689
+ } else {
690
+ setToPath(tree, path, folderData);
691
+ markFolderEmptyAtPath(tree, path);
692
+ }
693
+ }
694
+
695
+ /**
696
+ * @param {Object} tree
697
+ * @param {String} path
698
+ * @param {Object} listData
699
+ * @param {String} cacheKey
700
+ */
701
+ function applyListPayload(tree, path, listData, cacheKey) {
702
+ const node = getByExactPath(tree, path) || getByPath(tree, path);
703
+ if (!node || !listData) {
704
+ return;
705
+ }
706
+ node.listItems = listData.items || [];
707
+ node.total = listData.total;
708
+ node.childrenLimit = listData.pageSize || listData.childrenLimit || selectedClass.childrenLimit;
709
+ node.listPage = listData.page;
710
+ node.totalIsApproximate = listData.totalIsApproximate;
711
+ node.listCacheKey = cacheKey;
712
+ }
713
+
714
+ /**
715
+ * Ensure upload responses appear in the table before Elasticsearch catches up.
716
+ * @param {String} path
717
+ * @param {Array<Object>} uploads
718
+ */
719
+ function mergePendingUploads(path, uploads) {
720
+ if (!uploads || !uploads.length) {
721
+ return;
722
+ }
723
+ const node = getByExactPath(fileTree, path) || getByPath(fileTree, path);
724
+ if (!node) {
725
+ return;
726
+ }
727
+ if (!Array.isArray(node.listItems)) {
728
+ node.listItems = [];
729
+ }
730
+ let added = 0;
731
+ uploads.forEach(function (uploaded) {
732
+ if (!uploaded || uploaded.error) {
733
+ return;
734
+ }
735
+ const normalized = updatePermissions(uploaded);
736
+ if (!normalized.uri && !normalized.name) {
737
+ return;
738
+ }
739
+ const exists = _.some(node.listItems, function (item) {
740
+ return normalized.uri && item.uri === normalized.uri || normalized.name && item.name === normalized.name;
741
+ });
742
+ if (exists) {
743
+ return;
744
+ }
745
+ node.listItems.push(normalized);
746
+ added++;
747
+ });
748
+ if (!added) {
749
+ return;
750
+ }
751
+ if (splitBrowse) {
752
+ node.listItems = assetSearchContract.sortAssetItems(node.listItems, sort);
753
+ }
754
+ const total = Number(node.total);
755
+ if (Number.isFinite(total) && total >= 0) {
756
+ node.total = total + added;
757
+ }
758
+ }
759
+
520
760
  /**
521
761
  * Get the content of a folder, either in the model or load it
522
762
  * @param {Object} tree - the tree model
@@ -530,6 +770,44 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
530
770
  }
531
771
  }
532
772
  function getFolderContent(tree, path, cb) {
773
+ if (splitBrowse) {
774
+ const existing = getByExactPath(tree, path) || getByPath(tree, path);
775
+ const needTree = !existing || !existing.children && !existing.empty;
776
+ const cacheKey = listCacheKey();
777
+ const needList = !existing || existing.listCacheKey !== cacheKey;
778
+ const treePromise = needTree ? (setTreeLoading(true), browseRequest(path, {
779
+ part: 'tree',
780
+ depth: 1,
781
+ sortBy: sort.field,
782
+ sortDir: sort.direction
783
+ }).then(function (data) {
784
+ setTreeLoading(false);
785
+ return data;
786
+ }, function (error) {
787
+ setTreeLoading(false);
788
+ return Promise.reject(error);
789
+ })) : Promise.resolve(null);
790
+ const listPromise = needList ? browseRequest(path, {
791
+ part: 'list',
792
+ page: selectedClass.page,
793
+ pageSize: selectedClass.childrenLimit || 11,
794
+ sortBy: sort.field,
795
+ sortDir: sort.direction
796
+ }) : Promise.resolve(null);
797
+ Promise.all([treePromise, listPromise]).then(function (_ref) {
798
+ let [treeData, listData] = _ref;
799
+ if (treeData) {
800
+ applyTreePayload(tree, path, treeData);
801
+ }
802
+ if (listData) {
803
+ applyListPayload(tree, path, listData, cacheKey);
804
+ }
805
+ cb(getByExactPath(tree, path) || getByPath(tree, path));
806
+ }).catch(function () {
807
+ cb(null);
808
+ });
809
+ return;
810
+ }
533
811
  let content = getByPath(tree, path);
534
812
  if (!content || !content.children && !content.empty) {
535
813
  loadContent(path).then(function (data) {
@@ -555,7 +833,7 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
555
833
  });
556
834
  // Use folder total (not selectedClass): openInitialPath runs before selectFolder.
557
835
  const expectedTotal = Number(content.total);
558
- const pageSize = Number(content.childrenLimit) || selectedClass.childrenLimit || 10;
836
+ const pageSize = Number(content.childrenLimit) || selectedClass.childrenLimit || 11;
559
837
  const page = selectedClass.page || 1;
560
838
  // Missing/NaN total → unknown size; refetch so invalidated cache is not treated as complete.
561
839
  if (!Number.isFinite(expectedTotal) || files.length < expectedTotal && files.length < page * pageSize) {
@@ -715,7 +993,16 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
715
993
  */
716
994
  function invalidateFolderFiles(path) {
717
995
  const content = getByExactPath(fileTree, path);
718
- if (content && Array.isArray(content.children)) {
996
+ if (!content) {
997
+ return;
998
+ }
999
+ if (splitBrowse) {
1000
+ delete content.listItems;
1001
+ delete content.listCacheKey;
1002
+ delete content.listPage;
1003
+ return;
1004
+ }
1005
+ if (Array.isArray(content.children)) {
719
1006
  content.children = content.children.filter(function (child) {
720
1007
  return child.path && !child.uri;
721
1008
  });
@@ -727,11 +1014,15 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
727
1014
  * @param {String} path
728
1015
  * @param {Object} data
729
1016
  */
730
- function replaceFolderContent(path, data) {
1017
+ function replaceFolderContent(path, data, cacheKey) {
731
1018
  const content = getByExactPath(fileTree, path);
732
1019
  if (!content || !data) {
733
1020
  return;
734
1021
  }
1022
+ if (splitBrowse && Object.prototype.hasOwnProperty.call(data, 'items')) {
1023
+ applyListPayload(fileTree, path, data, cacheKey || listCacheKey());
1024
+ return;
1025
+ }
735
1026
  if (Object.prototype.hasOwnProperty.call(data, 'children')) {
736
1027
  content.children = data.children;
737
1028
  } else {
@@ -761,6 +1052,7 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
761
1052
  function loadContent(path) {
762
1053
  const parameters = {};
763
1054
  parameters[options.pathParam || 'path'] = path;
1055
+ setTreeLoading(true);
764
1056
  return Promise.resolve($$1.ajax({
765
1057
  url: options.browseUrl,
766
1058
  method: 'GET',
@@ -784,6 +1076,12 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
784
1076
  data.children.map(responseChildren => updatePermissions(responseChildren));
785
1077
  }
786
1078
  return data;
1079
+ }).then(function (data) {
1080
+ setTreeLoading(false);
1081
+ return data;
1082
+ }, function (error) {
1083
+ setTreeLoading(false);
1084
+ return Promise.reject(error);
787
1085
  });
788
1086
  }
789
1087
 
@@ -853,7 +1151,7 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
853
1151
  selectedClass = {
854
1152
  path,
855
1153
  total: Number.isFinite(normalizedTotal) && normalizedTotal >= 0 ? normalizedTotal : 0,
856
- childrenLimit: Number.isFinite(normalizedChildrenLimit) && normalizedChildrenLimit > 0 ? normalizedChildrenLimit : selectedClass.childrenLimit || 10,
1154
+ childrenLimit: Number.isFinite(normalizedChildrenLimit) && normalizedChildrenLimit > 0 ? normalizedChildrenLimit : selectedClass.childrenLimit || 11,
857
1155
  page: 1
858
1156
  };
859
1157
  }
@@ -890,17 +1188,35 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
890
1188
  /**
891
1189
  * Re-fetch the current folder with the active sort and publish files.
892
1190
  */
893
- function reloadSortedFolder() {
894
- const path = selectedClass.path;
895
- loadContent(path).then(function (data) {
1191
+ function reloadSortedFolder(pendingUploads) {
1192
+ const listRequest = captureListRequestContext();
1193
+ const path = listRequest.path;
1194
+ if (!searchMode && isActiveBrowser()) {
1195
+ setFilesLoading(true);
1196
+ }
1197
+ const loadPromise = splitBrowse ? browseRequest(path, {
1198
+ part: 'list',
1199
+ page: selectedClass.page,
1200
+ pageSize: selectedClass.childrenLimit || 11,
1201
+ sortBy: sort.field,
1202
+ sortDir: sort.direction
1203
+ }) : loadContent(path);
1204
+ loadPromise.then(function (data) {
896
1205
  if (!data) {
1206
+ setFilesLoading(false);
897
1207
  return;
898
1208
  }
899
- replaceFolderContent(path, data);
1209
+ if (isListRequestStale(listRequest)) {
1210
+ setFilesLoading(false);
1211
+ return;
1212
+ }
1213
+ replaceFolderContent(path, data, listRequest.cacheKey);
1214
+ mergePendingUploads(path, pendingUploads);
900
1215
  const content = getByExactPath(fileTree, path) || data;
901
1216
  selectFolder(content, content.path || path);
902
1217
  }).catch(function (error) {
903
1218
  LOGGER.error(error);
1219
+ setFilesLoading(false);
904
1220
  });
905
1221
  }
906
1222
 
@@ -908,14 +1224,53 @@ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpe
908
1224
  * Load page
909
1225
  */
910
1226
  function loadPage() {
1227
+ const listRequest = captureListRequestContext();
1228
+ if (!searchMode && isActiveBrowser()) {
1229
+ setFilesLoading(true);
1230
+ }
1231
+ if (splitBrowse) {
1232
+ const path = listRequest.path;
1233
+ browseRequest(path, {
1234
+ part: 'list',
1235
+ page: selectedClass.page,
1236
+ pageSize: selectedClass.childrenLimit || 11,
1237
+ sortBy: sort.field,
1238
+ sortDir: sort.direction
1239
+ }).then(function (data) {
1240
+ if (isListRequestStale(listRequest)) {
1241
+ setFilesLoading(false);
1242
+ return;
1243
+ }
1244
+ applyListPayload(fileTree, path, data, listRequest.cacheKey);
1245
+ const content = getByExactPath(fileTree, path);
1246
+ if (content) {
1247
+ $container.trigger(`folderselect.${NS}`, [content.label, getFilesForDisplay(content), content.path, content]);
1248
+ renderPagination();
1249
+ }
1250
+ setFilesLoading(false);
1251
+ }).catch(function (error) {
1252
+ LOGGER.error(error);
1253
+ setFilesLoading(false);
1254
+ });
1255
+ return;
1256
+ }
911
1257
  const subTree = getByPath(fileTree, selectedClass.path) || fileTree;
912
1258
 
913
1259
  //get the folder content
914
1260
  getFolderContent(subTree, selectedClass.path, function (content) {
1261
+ if (isListRequestStale(listRequest)) {
1262
+ if (!searchMode) {
1263
+ setFilesLoading(false);
1264
+ }
1265
+ return;
1266
+ }
915
1267
  indexTree(fileTree);
916
1268
  if (content) {
917
1269
  //internal event to set the file-selector content
918
- $container.trigger(`folderselect.${NS}`, [content.label, getPage(content.children), content.path, content]);
1270
+ $container.trigger(`folderselect.${NS}`, [content.label, getFilesForDisplay(content), content.path, content]);
1271
+ }
1272
+ if (!searchMode) {
1273
+ setFilesLoading(false);
919
1274
  }
920
1275
  });
921
1276
  }
@@ -67,6 +67,39 @@ define(['jquery', 'lodash', 'util/bytes', 'context', 'ui/previewer'], function (
67
67
  }
68
68
  return String(value);
69
69
  }
70
+
71
+ /**
72
+ * Row markup uses data-download for permission flag (boolean), not the URL.
73
+ * @param {*} file
74
+ * @returns {string}
75
+ */
76
+ function downloadHref(file) {
77
+ if (typeof file.downloadUrl === 'string' && file.downloadUrl) {
78
+ return file.downloadUrl;
79
+ }
80
+ if (typeof file.url === 'string' && file.url) {
81
+ const query = file.url.split('#')[0].split('?')[1] || '';
82
+ const hasSvgzSupport = query.split('&').some(param => param.split('=')[0] === 'svgzsupport');
83
+ return hasSvgzSupport ? file.url : `${file.url}&svgzsupport=true`;
84
+ }
85
+ if (typeof file.download === 'string' && file.download) {
86
+ return file.download;
87
+ }
88
+ return '';
89
+ }
90
+
91
+ /**
92
+ * @param {*} file
93
+ * @returns {string}
94
+ */
95
+ function downloadFilename(file) {
96
+ if (typeof file.display === 'string' && file.display) {
97
+ return file.display;
98
+ }
99
+ const path = file.file || '';
100
+ const idx = path.lastIndexOf('/');
101
+ return idx >= 0 ? path.slice(idx + 1) : path;
102
+ }
70
103
  function startPreview(file, preview, download, select) {
71
104
  if (preview) {
72
105
  $previewer.previewer(file);
@@ -81,9 +114,15 @@ define(['jquery', 'lodash', 'util/bytes', 'context', 'ui/previewer'], function (
81
114
  $propSize.text(EMPTY_META);
82
115
  }
83
116
  if (download) {
84
- $link.attr('href', file.download).attr('download', file.file);
85
- if ($link.hasClass('hidden')) {
86
- $link.removeClass('hidden');
117
+ const href = downloadHref(file);
118
+ if (href) {
119
+ $link.attr('href', href).attr('download', downloadFilename(file));
120
+ if ($link.hasClass('hidden')) {
121
+ $link.removeClass('hidden');
122
+ }
123
+ } else {
124
+ $link.attr('href', '#').removeAttr('download');
125
+ $link.addClass('hidden');
87
126
  }
88
127
  } else {
89
128
  $link.attr('href', '#').attr('download', '#');
@@ -312,6 +312,8 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
312
312
  let $container = options.$target;
313
313
  let $fileSelector = $$1('.file-selector', $container);
314
314
  let $filesWrapper = $$1('.files-wrapper', $fileSelector);
315
+ let $filesListLoading = $$1('.files-list-loading', $filesWrapper);
316
+ let $filesListLoadingStatus = $$1('.files-list-loading-status', $filesListLoading);
315
317
  let $fileContainer = $$1('.files-list', $fileSelector);
316
318
  let $placeholder = $$1('.empty', $fileSelector);
317
319
  let $uploader = $$1('.file-upload-container', $fileSelector);
@@ -338,6 +340,18 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
338
340
  searchMode = !!enabled;
339
341
  $fileSelector.toggleClass('search-mode', searchMode);
340
342
  });
343
+ function applyFilesListLoading(loading) {
344
+ $filesWrapper.toggleClass('is-files-loading', !!loading);
345
+ $filesWrapper.attr('aria-busy', loading ? 'true' : 'false');
346
+ $filesListLoading.toggleClass('hidden', !loading).attr('aria-hidden', loading ? 'false' : 'true');
347
+ $filesListLoadingStatus.attr('aria-busy', loading ? 'true' : 'false');
348
+ }
349
+ $container.on(`filesloading.${ns}`, function (e, loading) {
350
+ if (searchMode) {
351
+ return;
352
+ }
353
+ applyFilesListLoading(loading);
354
+ });
341
355
 
342
356
  //update current folder
343
357
  $container.on(`folderselect.${ns}`, function (e, fullPath, data, activePath, content) {
@@ -345,6 +359,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
345
359
  if (searchMode) {
346
360
  return;
347
361
  }
362
+ applyFilesListLoading(false);
348
363
 
349
364
  //update title
350
365
  if ($container[0].querySelector('.upload')) {