@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.
@@ -26,7 +26,18 @@ import { DEFAULT_SORT, sortAssetItems } from 'ui/resourcemgr/assetSearchContract
26
26
 
27
27
  const NS = 'resourcemgr';
28
28
  const LOGGER = loggerFactory(`ui/${NS}`);
29
- const DEFAULT_AJAX_TIMEOUT_MS = 10000;
29
+ const DEFAULT_AJAX_TIMEOUT_MS = 30000;
30
+ const TREE_LOADING_REF_KEY = `${NS}TreeLoadingRefCount`;
31
+
32
+ /**
33
+ * @param {Object} data
34
+ * @returns {Array}
35
+ */
36
+ function folderChildrenOnly(data) {
37
+ return _.filter(data && data.children, function (child) {
38
+ return child && child.path && !child.uri;
39
+ });
40
+ }
30
41
 
31
42
  export default function (options) {
32
43
  if (!options.browseUrl && options.url) {
@@ -45,18 +56,53 @@ export default function (options) {
45
56
  // for pagination
46
57
  let selectedClass = {
47
58
  path: rootPath,
48
- childrenLimit: 10,
59
+ childrenLimit: 11,
49
60
  total: 0,
50
61
  page: 1
51
62
  };
52
63
  let searchMode = false;
53
64
  let sort = Object.assign({}, DEFAULT_SORT);
65
+ const splitBrowse = options.splitBrowse !== false;
54
66
  const ajaxTimeoutMs = Number.isFinite(Number(options.ajaxTimeoutMs)) && Number(options.ajaxTimeoutMs) > 0
55
67
  ? Number(options.ajaxTimeoutMs)
56
68
  : DEFAULT_AJAX_TIMEOUT_MS;
69
+ const $treeLoadingOverlay = $('.file-browser-tree-loading', $fileBrowser);
70
+ const $treeLoadingStatus = $('.file-browser-tree-loading-status', $treeLoadingOverlay);
71
+
72
+ /**
73
+ * Ref-count in-flight browse requests and toggle the tree loading overlay.
74
+ * @param {Boolean} increment - true when a load starts, false when it finishes
75
+ */
76
+ function setTreeLoading(increment) {
77
+ let treeLoadingRefCount = Number($fileBrowser.data(TREE_LOADING_REF_KEY)) || 0;
78
+ treeLoadingRefCount += increment ? 1 : -1;
79
+ if (treeLoadingRefCount < 0) {
80
+ treeLoadingRefCount = 0;
81
+ }
82
+ $fileBrowser.data(TREE_LOADING_REF_KEY, treeLoadingRefCount);
83
+ const loading = treeLoadingRefCount > 0;
84
+ $fileBrowser.toggleClass('is-tree-loading', loading);
85
+ $fileBrowser.attr('aria-busy', loading ? 'true' : 'false');
86
+ $treeLoadingOverlay.toggleClass('hidden', !loading).attr('aria-hidden', loading ? 'false' : 'true');
87
+ $treeLoadingStatus.attr('aria-busy', loading ? 'true' : 'false');
88
+ }
89
+
90
+ /**
91
+ * Show or hide the asset table loading overlay (browse mode only).
92
+ * @param {Boolean} loading
93
+ */
94
+ function setFilesLoading(loading) {
95
+ if (searchMode) {
96
+ return;
97
+ }
98
+ $container.trigger(`filesloading.${NS}`, [!!loading]);
99
+ }
57
100
 
58
101
  $container.on(`searchmode.${NS}`, function (e, enabled) {
59
102
  searchMode = !!enabled;
103
+ if (searchMode) {
104
+ $container.trigger(`filesloading.${NS}`, [false]);
105
+ }
60
106
  });
61
107
 
62
108
  $container.on(`sortchange.${NS}`, function (e, nextSort) {
@@ -77,9 +123,14 @@ export default function (options) {
77
123
  return;
78
124
  }
79
125
  const subTree = getByExactPath(fileTree, targetPath) || getByPath(fileTree, targetPath) || fileTree;
126
+ if (!searchMode) {
127
+ setFilesLoading(true);
128
+ }
80
129
  getFolderContent(subTree, targetPath, function (content) {
81
130
  if (content) {
82
131
  selectFolder(content, targetPath);
132
+ } else {
133
+ setFilesLoading(false);
83
134
  }
84
135
  });
85
136
  });
@@ -103,8 +154,15 @@ export default function (options) {
103
154
  });
104
155
 
105
156
  //load the content of the ROOT
157
+ const showInitialListLoading = !searchMode && root === 'local';
158
+ if (showInitialListLoading) {
159
+ setFilesLoading(true);
160
+ }
106
161
  getFolderContent(fileTree, rootPath, function (content) {
107
162
  if (!content) {
163
+ if (showInitialListLoading) {
164
+ setFilesLoading(false);
165
+ }
108
166
  return;
109
167
  }
110
168
  indexTree(content);
@@ -139,6 +197,8 @@ export default function (options) {
139
197
  if (root !== 'local') {
140
198
  options.hasAlreadySelected = true;
141
199
  }
200
+ } else if (showInitialListLoading) {
201
+ setFilesLoading(false);
142
202
  }
143
203
  });
144
204
 
@@ -148,6 +208,15 @@ export default function (options) {
148
208
  const $selected = $(this);
149
209
  const fullPath = $selected.data('path');
150
210
  const subTree = getByExactPath(fileTree, fullPath);
211
+ const openingFolder = !searchMode && fullPath !== selectedClass.path;
212
+
213
+ if (openingFolder) {
214
+ selectedClass.page = 1;
215
+ }
216
+ if (!searchMode) {
217
+ setFilesLoading(true);
218
+ }
219
+ invalidateFolderFiles(fullPath);
151
220
 
152
221
  //get the folder content
153
222
  getFolderContent(subTree, fullPath, function (content) {
@@ -179,6 +248,8 @@ export default function (options) {
179
248
 
180
249
  //internal event to set the file-selector content
181
250
  selectFolder(content, fullPath);
251
+ } else if (!searchMode) {
252
+ setFilesLoading(false);
182
253
  }
183
254
  });
184
255
  });
@@ -201,11 +272,13 @@ export default function (options) {
201
272
 
202
273
  if (targetPath !== selectedClass.path) {
203
274
  selectedClass.path = targetPath;
204
- selectedClass.page = 1;
205
275
  }
276
+ selectedClass.page = 1;
206
277
 
207
278
  invalidateFolderFiles(targetPath);
208
- reloadSortedFolder();
279
+ const pendingUpload =
280
+ file && !file.error && (file.uri || file.name) ? file : null;
281
+ reloadSortedFolder(pendingUpload ? [pendingUpload] : []);
209
282
  });
210
283
 
211
284
  $container.on(`filedelete.${NS}`, function (e, path) {
@@ -348,23 +421,26 @@ export default function (options) {
348
421
  $container.data('activeFileBrowserRoot', root);
349
422
  if (searchMode) {
350
423
  $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
- }
424
+ $container.trigger(`folderselect.${NS}`, [
425
+ content.label,
426
+ getFilesForDisplay(content),
427
+ path,
428
+ content
429
+ ]);
430
+ return;
431
+ }
359
432
  updateSelectedClass(path, content.total, content.childrenLimit);
360
433
  $container.trigger(`folderpath.${NS}`, [path, content.label]);
361
434
  $container.trigger(`folderselect.${NS}`, [
362
435
  content.label,
363
- getPage(content.children || []),
436
+ getFilesForDisplay(content),
364
437
  path,
365
438
  content
366
439
  ]);
367
440
  renderPagination();
441
+ if (!searchMode) {
442
+ setFilesLoading(false);
443
+ }
368
444
  }
369
445
 
370
446
  /**
@@ -387,6 +463,180 @@ export default function (options) {
387
463
  }
388
464
  return files;
389
465
  }
466
+
467
+ /**
468
+ * Files for the table (split list payload or legacy children slice).
469
+ * @param {Object} content
470
+ * @returns {Array}
471
+ */
472
+ function getFilesForDisplay(content) {
473
+ if (!content) {
474
+ return [];
475
+ }
476
+ if (splitBrowse) {
477
+ return content.listItems || [];
478
+ }
479
+ return getPage(content.children || []);
480
+ }
481
+
482
+ function listCacheKey() {
483
+ return `${sort.field}:${sort.direction}:${selectedClass.page}`;
484
+ }
485
+
486
+ /**
487
+ * Snapshot list request identity (folder + page/sort) when a browse starts.
488
+ * @returns {{path: String, cacheKey: String}}
489
+ */
490
+ function captureListRequestContext() {
491
+ return {
492
+ path: selectedClass.path,
493
+ cacheKey: listCacheKey()
494
+ };
495
+ }
496
+
497
+ /**
498
+ * Whether navigation changed before an in-flight list response should apply.
499
+ * @param {{path: String, cacheKey: String}} captured
500
+ * @returns {Boolean}
501
+ */
502
+ function isListRequestStale(captured) {
503
+ return (
504
+ !captured ||
505
+ captured.path !== selectedClass.path ||
506
+ captured.cacheKey !== listCacheKey()
507
+ );
508
+ }
509
+
510
+ /**
511
+ * @param {Object} response
512
+ * @returns {Object}
513
+ */
514
+ function unwrapBrowseResponse(response) {
515
+ if (response && response.success === false) {
516
+ return Promise.reject(response);
517
+ }
518
+ let data = response && response.data ? response.data : response;
519
+ data = updatePermissions(data);
520
+ if (data.children && data.children.length > 0) {
521
+ data.children = data.children.map(function (child) {
522
+ return updatePermissions(child);
523
+ });
524
+ }
525
+ if (data.items && data.items.length > 0) {
526
+ data.items = data.items.map(function (item) {
527
+ return updatePermissions(item);
528
+ });
529
+ }
530
+ return data;
531
+ }
532
+
533
+ /**
534
+ * @param {String} path
535
+ * @param {Object} extraData
536
+ * @returns {Promise}
537
+ */
538
+ function browseRequest(path, extraData) {
539
+ const parameters = {};
540
+ parameters[options.pathParam || 'path'] = path;
541
+ return Promise.resolve(
542
+ $.ajax({
543
+ url: options.browseUrl,
544
+ method: 'GET',
545
+ dataType: 'json',
546
+ timeout: ajaxTimeoutMs,
547
+ data: _.merge(parameters, options.params, extraData)
548
+ })
549
+ ).then(unwrapBrowseResponse);
550
+ }
551
+
552
+ /**
553
+ * @param {Object} tree
554
+ * @param {String} path
555
+ * @param {Object} data
556
+ */
557
+ function applyTreePayload(tree, path, data) {
558
+ const folderData = Object.assign({}, data, { children: folderChildrenOnly(data) });
559
+ if (!tree.path) {
560
+ _.merge(tree, folderData);
561
+ } else if (folderData.children) {
562
+ setToPath(tree, path, folderData);
563
+ if (!_.find(folderData.children, 'path')) {
564
+ markFolderEmptyAtPath(tree, path);
565
+ }
566
+ } else {
567
+ setToPath(tree, path, folderData);
568
+ markFolderEmptyAtPath(tree, path);
569
+ }
570
+ }
571
+
572
+ /**
573
+ * @param {Object} tree
574
+ * @param {String} path
575
+ * @param {Object} listData
576
+ * @param {String} cacheKey
577
+ */
578
+ function applyListPayload(tree, path, listData, cacheKey) {
579
+ const node = getByExactPath(tree, path) || getByPath(tree, path);
580
+ if (!node || !listData) {
581
+ return;
582
+ }
583
+ node.listItems = listData.items || [];
584
+ node.total = listData.total;
585
+ node.childrenLimit = listData.pageSize || listData.childrenLimit || selectedClass.childrenLimit;
586
+ node.listPage = listData.page;
587
+ node.totalIsApproximate = listData.totalIsApproximate;
588
+ node.listCacheKey = cacheKey;
589
+ }
590
+
591
+ /**
592
+ * Ensure upload responses appear in the table before Elasticsearch catches up.
593
+ * @param {String} path
594
+ * @param {Array<Object>} uploads
595
+ */
596
+ function mergePendingUploads(path, uploads) {
597
+ if (!uploads || !uploads.length) {
598
+ return;
599
+ }
600
+ const node = getByExactPath(fileTree, path) || getByPath(fileTree, path);
601
+ if (!node) {
602
+ return;
603
+ }
604
+ if (!Array.isArray(node.listItems)) {
605
+ node.listItems = [];
606
+ }
607
+ let added = 0;
608
+ uploads.forEach(function (uploaded) {
609
+ if (!uploaded || uploaded.error) {
610
+ return;
611
+ }
612
+ const normalized = updatePermissions(uploaded);
613
+ if (!normalized.uri && !normalized.name) {
614
+ return;
615
+ }
616
+ const exists = _.some(node.listItems, function (item) {
617
+ return (
618
+ (normalized.uri && item.uri === normalized.uri) ||
619
+ (normalized.name && item.name === normalized.name)
620
+ );
621
+ });
622
+ if (exists) {
623
+ return;
624
+ }
625
+ node.listItems.push(normalized);
626
+ added++;
627
+ });
628
+ if (!added) {
629
+ return;
630
+ }
631
+ if (splitBrowse) {
632
+ node.listItems = sortAssetItems(node.listItems, sort);
633
+ }
634
+ const total = Number(node.total);
635
+ if (Number.isFinite(total) && total >= 0) {
636
+ node.total = total + added;
637
+ }
638
+ }
639
+
390
640
  /**
391
641
  * Get the content of a folder, either in the model or load it
392
642
  * @param {Object} tree - the tree model
@@ -401,6 +651,57 @@ export default function (options) {
401
651
  }
402
652
 
403
653
  function getFolderContent(tree, path, cb) {
654
+ if (splitBrowse) {
655
+ const existing = getByExactPath(tree, path) || getByPath(tree, path);
656
+ const needTree = !existing || (!existing.children && !existing.empty);
657
+ const cacheKey = listCacheKey();
658
+ const needList = !existing || existing.listCacheKey !== cacheKey;
659
+
660
+ const treePromise = needTree
661
+ ? (setTreeLoading(true),
662
+ browseRequest(path, {
663
+ part: 'tree',
664
+ depth: 1,
665
+ sortBy: sort.field,
666
+ sortDir: sort.direction
667
+ }).then(
668
+ function (data) {
669
+ setTreeLoading(false);
670
+ return data;
671
+ },
672
+ function (error) {
673
+ setTreeLoading(false);
674
+ return Promise.reject(error);
675
+ }
676
+ ))
677
+ : Promise.resolve(null);
678
+
679
+ const listPromise = needList
680
+ ? browseRequest(path, {
681
+ part: 'list',
682
+ page: selectedClass.page,
683
+ pageSize: selectedClass.childrenLimit || 11,
684
+ sortBy: sort.field,
685
+ sortDir: sort.direction
686
+ })
687
+ : Promise.resolve(null);
688
+
689
+ Promise.all([treePromise, listPromise])
690
+ .then(function ([treeData, listData]) {
691
+ if (treeData) {
692
+ applyTreePayload(tree, path, treeData);
693
+ }
694
+ if (listData) {
695
+ applyListPayload(tree, path, listData, cacheKey);
696
+ }
697
+ cb(getByExactPath(tree, path) || getByPath(tree, path));
698
+ })
699
+ .catch(function () {
700
+ cb(null);
701
+ });
702
+ return;
703
+ }
704
+
404
705
  let content = getByPath(tree, path);
405
706
  if (!content || (!content.children && !content.empty)) {
406
707
  loadContent(path).then(function (data) {
@@ -426,7 +727,7 @@ export default function (options) {
426
727
  });
427
728
  // Use folder total (not selectedClass): openInitialPath runs before selectFolder.
428
729
  const expectedTotal = Number(content.total);
429
- const pageSize = Number(content.childrenLimit) || selectedClass.childrenLimit || 10;
730
+ const pageSize = Number(content.childrenLimit) || selectedClass.childrenLimit || 11;
430
731
  const page = selectedClass.page || 1;
431
732
  // Missing/NaN total → unknown size; refetch so invalidated cache is not treated as complete.
432
733
  if (
@@ -588,7 +889,16 @@ export default function (options) {
588
889
  */
589
890
  function invalidateFolderFiles(path) {
590
891
  const content = getByExactPath(fileTree, path);
591
- if (content && Array.isArray(content.children)) {
892
+ if (!content) {
893
+ return;
894
+ }
895
+ if (splitBrowse) {
896
+ delete content.listItems;
897
+ delete content.listCacheKey;
898
+ delete content.listPage;
899
+ return;
900
+ }
901
+ if (Array.isArray(content.children)) {
592
902
  content.children = content.children.filter(function (child) {
593
903
  return child.path && !child.uri;
594
904
  });
@@ -600,11 +910,15 @@ export default function (options) {
600
910
  * @param {String} path
601
911
  * @param {Object} data
602
912
  */
603
- function replaceFolderContent(path, data) {
913
+ function replaceFolderContent(path, data, cacheKey) {
604
914
  const content = getByExactPath(fileTree, path);
605
915
  if (!content || !data) {
606
916
  return;
607
917
  }
918
+ if (splitBrowse && Object.prototype.hasOwnProperty.call(data, 'items')) {
919
+ applyListPayload(fileTree, path, data, cacheKey || listCacheKey());
920
+ return;
921
+ }
608
922
  if (Object.prototype.hasOwnProperty.call(data, 'children')) {
609
923
  content.children = data.children;
610
924
  } else {
@@ -634,6 +948,7 @@ export default function (options) {
634
948
  function loadContent(path) {
635
949
  const parameters = {};
636
950
  parameters[options.pathParam || 'path'] = path;
951
+ setTreeLoading(true);
637
952
  return Promise.resolve(
638
953
  $.ajax({
639
954
  url: options.browseUrl,
@@ -649,17 +964,28 @@ export default function (options) {
649
964
  sortDir: sort.direction
650
965
  })
651
966
  })
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
- });
967
+ )
968
+ .then(function (response) {
969
+ if (response && response.success === false) {
970
+ return Promise.reject(response);
971
+ }
972
+ let data = response && response.data ? response.data : response;
973
+ data = updatePermissions(data);
974
+ if (data.children && data.children.length > 0) {
975
+ data.children.map(responseChildren => updatePermissions(responseChildren));
976
+ }
977
+ return data;
978
+ })
979
+ .then(
980
+ function (data) {
981
+ setTreeLoading(false);
982
+ return data;
983
+ },
984
+ function (error) {
985
+ setTreeLoading(false);
986
+ return Promise.reject(error);
987
+ }
988
+ );
663
989
  }
664
990
 
665
991
  /**
@@ -732,7 +1058,7 @@ export default function (options) {
732
1058
  childrenLimit:
733
1059
  Number.isFinite(normalizedChildrenLimit) && normalizedChildrenLimit > 0
734
1060
  ? normalizedChildrenLimit
735
- : selectedClass.childrenLimit || 10,
1061
+ : selectedClass.childrenLimit || 11,
736
1062
  page: 1
737
1063
  };
738
1064
  }
@@ -776,19 +1102,40 @@ export default function (options) {
776
1102
  /**
777
1103
  * Re-fetch the current folder with the active sort and publish files.
778
1104
  */
779
- function reloadSortedFolder() {
780
- const path = selectedClass.path;
781
- loadContent(path)
1105
+ function reloadSortedFolder(pendingUploads) {
1106
+ const listRequest = captureListRequestContext();
1107
+ const path = listRequest.path;
1108
+ if (!searchMode && isActiveBrowser()) {
1109
+ setFilesLoading(true);
1110
+ }
1111
+ const loadPromise = splitBrowse
1112
+ ? browseRequest(path, {
1113
+ part: 'list',
1114
+ page: selectedClass.page,
1115
+ pageSize: selectedClass.childrenLimit || 11,
1116
+ sortBy: sort.field,
1117
+ sortDir: sort.direction
1118
+ })
1119
+ : loadContent(path);
1120
+
1121
+ loadPromise
782
1122
  .then(function (data) {
783
1123
  if (!data) {
1124
+ setFilesLoading(false);
1125
+ return;
1126
+ }
1127
+ if (isListRequestStale(listRequest)) {
1128
+ setFilesLoading(false);
784
1129
  return;
785
1130
  }
786
- replaceFolderContent(path, data);
1131
+ replaceFolderContent(path, data, listRequest.cacheKey);
1132
+ mergePendingUploads(path, pendingUploads);
787
1133
  const content = getByExactPath(fileTree, path) || data;
788
1134
  selectFolder(content, content.path || path);
789
1135
  })
790
1136
  .catch(function (error) {
791
1137
  LOGGER.error(error);
1138
+ setFilesLoading(false);
792
1139
  });
793
1140
  }
794
1141
 
@@ -796,15 +1143,69 @@ export default function (options) {
796
1143
  * Load page
797
1144
  */
798
1145
  function loadPage() {
1146
+ const listRequest = captureListRequestContext();
1147
+
1148
+ if (!searchMode && isActiveBrowser()) {
1149
+ setFilesLoading(true);
1150
+ }
1151
+
1152
+ if (splitBrowse) {
1153
+ const path = listRequest.path;
1154
+ browseRequest(path, {
1155
+ part: 'list',
1156
+ page: selectedClass.page,
1157
+ pageSize: selectedClass.childrenLimit || 11,
1158
+ sortBy: sort.field,
1159
+ sortDir: sort.direction
1160
+ })
1161
+ .then(function (data) {
1162
+ if (isListRequestStale(listRequest)) {
1163
+ setFilesLoading(false);
1164
+ return;
1165
+ }
1166
+ applyListPayload(fileTree, path, data, listRequest.cacheKey);
1167
+ const content = getByExactPath(fileTree, path);
1168
+ if (content) {
1169
+ $container.trigger(`folderselect.${NS}`, [
1170
+ content.label,
1171
+ getFilesForDisplay(content),
1172
+ content.path,
1173
+ content
1174
+ ]);
1175
+ renderPagination();
1176
+ }
1177
+ setFilesLoading(false);
1178
+ })
1179
+ .catch(function (error) {
1180
+ LOGGER.error(error);
1181
+ setFilesLoading(false);
1182
+ });
1183
+ return;
1184
+ }
1185
+
799
1186
  const subTree = getByPath(fileTree, selectedClass.path) || fileTree;
800
1187
 
801
1188
  //get the folder content
802
1189
  getFolderContent(subTree, selectedClass.path, function (content) {
1190
+ if (isListRequestStale(listRequest)) {
1191
+ if (!searchMode) {
1192
+ setFilesLoading(false);
1193
+ }
1194
+ return;
1195
+ }
803
1196
  indexTree(fileTree);
804
1197
 
805
1198
  if (content) {
806
1199
  //internal event to set the file-selector content
807
- $container.trigger(`folderselect.${NS}`, [content.label, getPage(content.children), content.path, content]);
1200
+ $container.trigger(`folderselect.${NS}`, [
1201
+ content.label,
1202
+ getFilesForDisplay(content),
1203
+ content.path,
1204
+ content
1205
+ ]);
1206
+ }
1207
+ if (!searchMode) {
1208
+ setFilesLoading(false);
808
1209
  }
809
1210
  });
810
1211
  }
@@ -72,6 +72,39 @@ export default function(options) {
72
72
  return String(value);
73
73
  }
74
74
 
75
+ /**
76
+ * Row markup uses data-download for permission flag (boolean), not the URL.
77
+ * @param {*} file
78
+ * @returns {string}
79
+ */
80
+ function downloadHref(file) {
81
+ if (typeof file.downloadUrl === 'string' && file.downloadUrl) {
82
+ return file.downloadUrl;
83
+ }
84
+ if (typeof file.url === 'string' && file.url) {
85
+ const query = file.url.split('#')[0].split('?')[1] || '';
86
+ const hasSvgzSupport = query.split('&').some(param => param.split('=')[0] === 'svgzsupport');
87
+ return hasSvgzSupport ? file.url : `${file.url}&svgzsupport=true`;
88
+ }
89
+ if (typeof file.download === 'string' && file.download) {
90
+ return file.download;
91
+ }
92
+ return '';
93
+ }
94
+
95
+ /**
96
+ * @param {*} file
97
+ * @returns {string}
98
+ */
99
+ function downloadFilename(file) {
100
+ if (typeof file.display === 'string' && file.display) {
101
+ return file.display;
102
+ }
103
+ const path = file.file || '';
104
+ const idx = path.lastIndexOf('/');
105
+ return idx >= 0 ? path.slice(idx + 1) : path;
106
+ }
107
+
75
108
  function startPreview(file, preview, download, select) {
76
109
  if (preview) {
77
110
  $previewer.previewer(file);
@@ -91,10 +124,16 @@ export default function(options) {
91
124
  $propType.text(EMPTY_META);
92
125
  $propSize.text(EMPTY_META);
93
126
  }
94
- if(download) {
95
- $link.attr('href', file.download).attr('download', file.file);
96
- if ($link.hasClass('hidden')) {
97
- $link.removeClass('hidden');
127
+ if (download) {
128
+ const href = downloadHref(file);
129
+ if (href) {
130
+ $link.attr('href', href).attr('download', downloadFilename(file));
131
+ if ($link.hasClass('hidden')) {
132
+ $link.removeClass('hidden');
133
+ }
134
+ } else {
135
+ $link.attr('href', '#').removeAttr('download');
136
+ $link.addClass('hidden');
98
137
  }
99
138
  } else {
100
139
  $link.attr('href', '#').attr('download', '#');