@oat-sa/tao-core-ui 3.23.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.
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 +1230 -97
  8. package/dist/resourcemgr/css/resourcemgr.css.map +1 -1
  9. package/dist/resourcemgr/fileBrowser.js +815 -83
  10. package/dist/resourcemgr/filePreview.js +82 -7
  11. package/dist/resourcemgr/fileSelector.js +377 -92
  12. package/dist/resourcemgr.js +314 -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 +1230 -97
  20. package/src/resourcemgr/css/resourcemgr.css.map +1 -1
  21. package/src/resourcemgr/fileBrowser.js +872 -92
  22. package/src/resourcemgr/filePreview.js +89 -8
  23. package/src/resourcemgr/fileSelector.js +356 -46
  24. package/src/resourcemgr/scss/resourcemgr.scss +1450 -89
  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 +125 -54
  29. package/src/resourcemgr/tpl/rootFolder.tpl +3 -1
  30. package/src/resourcemgr.js +294 -47
  31. package/src/searchModal/advancedSearch.js +242 -31
@@ -1,4 +1,4 @@
1
- define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib/handlebars/helpers', 'ui/feedback', 'context', 'ui/uploader', 'core/logger'], function ($$1, _, async, __, mimeType, Handlebars, Helpers0, feedback, context, uploader, loggerFactory) { 'use strict';
1
+ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib/handlebars/helpers', 'ui/feedback', 'context', 'ui/uploader', 'core/logger', 'ui/resourcemgr/assetSearchContract'], function ($$1, _, async, __, mimeType, Handlebars, Helpers0, feedback, context, uploader, loggerFactory, assetSearchContract) { 'use strict';
2
2
 
3
3
  $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1;
4
4
  _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
@@ -18,12 +18,12 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
18
18
  var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
19
19
  this.compilerInfo = [4,'>= 1.0.0'];
20
20
  helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
21
- var buffer = "", stack1, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType="function", self=this;
21
+ var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing, self=this;
22
22
 
23
23
  function program1(depth0,data) {
24
24
 
25
25
  var buffer = "", stack1, helper;
26
- buffer += "\n <li data-type=\"";
26
+ buffer += "\n <tr data-type=\"";
27
27
  if (helper = helpers.type) { stack1 = helper.call(depth0, {hash:{},data:data}); }
28
28
  else { helper = (depth0 && depth0.type); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
29
29
  buffer += escapeExpression(stack1)
@@ -48,93 +48,98 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
48
48
  else { helper = (depth0 && depth0.viewUrl); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
49
49
  buffer += escapeExpression(stack1)
50
50
  + "\"\n ";
51
- stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.download), {hash:{},inverse:self.noop,fn:self.program(2, program2, data),data:data});
51
+ stack1 = helpers['if'].call(depth0, (depth0 && depth0.location), {hash:{},inverse:self.noop,fn:self.program(2, program2, data),data:data});
52
52
  if(stack1 || stack1 === 0) { buffer += stack1; }
53
53
  buffer += "\n ";
54
- stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.preview), {hash:{},inverse:self.noop,fn:self.program(4, program4, data),data:data});
54
+ stack1 = helpers['if'].call(depth0, (depth0 && depth0.updatedAt), {hash:{},inverse:self.noop,fn:self.program(4, program4, data),data:data});
55
55
  if(stack1 || stack1 === 0) { buffer += stack1; }
56
56
  buffer += "\n ";
57
- stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.read), {hash:{},inverse:self.noop,fn:self.program(6, program6, data),data:data});
57
+ stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.download), {hash:{},inverse:self.noop,fn:self.program(6, program6, data),data:data});
58
+ if(stack1 || stack1 === 0) { buffer += stack1; }
59
+ buffer += "\n ";
60
+ stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.preview), {hash:{},inverse:self.noop,fn:self.program(8, program8, data),data:data});
61
+ if(stack1 || stack1 === 0) { buffer += stack1; }
62
+ buffer += "\n ";
63
+ stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.read), {hash:{},inverse:self.noop,fn:self.program(10, program10, data),data:data});
58
64
  if(stack1 || stack1 === 0) { buffer += stack1; }
59
65
  buffer += "\n data-alt=\"";
60
66
  if (helper = helpers.alt) { stack1 = helper.call(depth0, {hash:{},data:data}); }
61
67
  else { helper = (depth0 && depth0.alt); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
62
68
  buffer += escapeExpression(stack1)
63
- + "\">\n <span class=\"desc truncate\">";
69
+ + "\">\n <td class=\"files-label\">\n <span class=\"files-label-inner\">\n <span class=\"file-icon\" aria-hidden=\"true\"></span>\n <span class=\"desc truncate\">";
64
70
  if (helper = helpers.name) { stack1 = helper.call(depth0, {hash:{},data:data}); }
65
71
  else { helper = (depth0 && depth0.name); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
66
72
  buffer += escapeExpression(stack1)
67
- + "</span>\n <div class=\"actions\">\n <div class=\"tlb\">\n <div class=\"tlb-top\">\n <span class=\"tlb-box\">\n <span class=\"tlb-bar\">\n <span class=\"tlb-start\"></span>\n ";
68
- stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.read), {hash:{},inverse:self.noop,fn:self.program(8, program8, data),data:data});
73
+ + "</span>\n </span>\n </td>\n <td class=\"files-location\">\n <span class=\"meta location truncate\" title=\"";
74
+ if (helper = helpers.locationDisplay) { stack1 = helper.call(depth0, {hash:{},data:data}); }
75
+ else { helper = (depth0 && depth0.locationDisplay); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
76
+ buffer += escapeExpression(stack1)
77
+ + "\">";
78
+ if (helper = helpers.locationDisplay) { stack1 = helper.call(depth0, {hash:{},data:data}); }
79
+ else { helper = (depth0 && depth0.locationDisplay); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
80
+ buffer += escapeExpression(stack1)
81
+ + "</span>\n </td>\n <td class=\"files-updated\">\n <span class=\"files-updated-inner\">\n <span class=\"meta updated truncate\" title=\"";
82
+ if (helper = helpers.updatedAtDisplay) { stack1 = helper.call(depth0, {hash:{},data:data}); }
83
+ else { helper = (depth0 && depth0.updatedAtDisplay); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
84
+ buffer += escapeExpression(stack1)
85
+ + "\">";
86
+ if (helper = helpers.updatedAtDisplay) { stack1 = helper.call(depth0, {hash:{},data:data}); }
87
+ else { helper = (depth0 && depth0.updatedAtDisplay); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
88
+ buffer += escapeExpression(stack1)
89
+ + "</span>\n <span class=\"row-actions\">\n ";
90
+ stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1['delete']), {hash:{},inverse:self.noop,fn:self.program(12, program12, data),data:data});
69
91
  if(stack1 || stack1 === 0) { buffer += stack1; }
70
- buffer += "\n <span class=\"tlb-end\"></span>\n </span>\n </span>\n </div>\n </div>\n </div>\n </li>\n";
92
+ buffer += "\n </span>\n </span>\n </td>\n </tr>\n";
71
93
  return buffer;
72
94
  }
73
95
  function program2(depth0,data) {
74
96
 
75
-
76
- return " data-download=\"true\" ";
97
+ var buffer = "", stack1, helper;
98
+ buffer += " data-location=\"";
99
+ if (helper = helpers.location) { stack1 = helper.call(depth0, {hash:{},data:data}); }
100
+ else { helper = (depth0 && depth0.location); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
101
+ buffer += escapeExpression(stack1)
102
+ + "\" ";
103
+ return buffer;
77
104
  }
78
105
 
79
106
  function program4(depth0,data) {
80
107
 
81
-
82
- return " data-preview=\"true\" ";
108
+ var buffer = "", stack1, helper;
109
+ buffer += " data-updated=\"";
110
+ if (helper = helpers.updatedAt) { stack1 = helper.call(depth0, {hash:{},data:data}); }
111
+ else { helper = (depth0 && depth0.updatedAt); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
112
+ buffer += escapeExpression(stack1)
113
+ + "\" ";
114
+ return buffer;
83
115
  }
84
116
 
85
117
  function program6(depth0,data) {
86
118
 
87
119
 
88
- return " data-select=\"true\" ";
120
+ return " data-download=\"true\" ";
89
121
  }
90
122
 
91
123
  function program8(depth0,data) {
92
124
 
93
- var buffer = "", stack1;
94
- buffer += "\n <span class=\"tlb-group\">\n ";
95
- stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.read), {hash:{},inverse:self.noop,fn:self.program(9, program9, data),data:data});
96
- if(stack1 || stack1 === 0) { buffer += stack1; }
97
- buffer += "\n ";
98
- stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.download), {hash:{},inverse:self.noop,fn:self.program(11, program11, data),data:data});
99
- if(stack1 || stack1 === 0) { buffer += stack1; }
100
- buffer += "\n ";
101
- stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1['delete']), {hash:{},inverse:self.noop,fn:self.program(13, program13, data),data:data});
102
- if(stack1 || stack1 === 0) { buffer += stack1; }
103
- buffer += "\n </span>\n ";
104
- return buffer;
105
- }
106
- function program9(depth0,data) {
107
125
 
108
- var buffer = "", helper, options;
109
- buffer += "\n <a href=\"#\" class=\"tlb-button-off select\" title=\""
110
- + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Select this file", options) : helperMissing.call(depth0, "__", "Select this file", options)))
111
- + "\"><span class=\"icon-move-item\"></span></a>\n ";
112
- return buffer;
126
+ return " data-preview=\"true\" ";
113
127
  }
114
128
 
115
- function program11(depth0,data) {
129
+ function program10(depth0,data) {
116
130
 
117
- var buffer = "", stack1, helper, options;
118
- buffer += "\n <a href=\"";
119
- if (helper = helpers.downloadUrl) { stack1 = helper.call(depth0, {hash:{},data:data}); }
120
- else { helper = (depth0 && depth0.downloadUrl); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
121
- buffer += escapeExpression(stack1)
122
- + "\" download=\"";
123
- if (helper = helpers.name) { stack1 = helper.call(depth0, {hash:{},data:data}); }
124
- else { helper = (depth0 && depth0.name); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
125
- buffer += escapeExpression(stack1)
126
- + "\" target=\"_blank\" class=\"tlb-button-off download\" title=\""
127
- + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Download this file", options) : helperMissing.call(depth0, "__", "Download this file", options)))
128
- + "\"><span class=\"icon-download\"></span></a>\n ";
129
- return buffer;
131
+
132
+ return " data-select=\"true\" ";
130
133
  }
131
134
 
132
- function program13(depth0,data) {
135
+ function program12(depth0,data) {
133
136
 
134
137
  var buffer = "", helper, options;
135
- buffer += "\n <a href=\"#\" class=\"tlb-button-off delete\" title=\""
138
+ buffer += "\n <a href=\"#\" class=\"delete\" title=\""
139
+ + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Remove this file", options) : helperMissing.call(depth0, "__", "Remove this file", options)))
140
+ + "\" aria-label=\""
136
141
  + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Remove this file", options) : helperMissing.call(depth0, "__", "Remove this file", options)))
137
- + "\"><span class=\"icon-bin\"></span></a>\n ";
142
+ + "\">\n <span class=\"icon-bin\" aria-hidden=\"true\"></span>\n </a>\n ";
138
143
  return buffer;
139
144
  }
140
145
 
@@ -213,13 +218,14 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
213
218
  *
214
219
  * You should have received a copy of the GNU General Public License
215
220
  * along with this program; if not, write to the Free Software
216
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
221
+ * Foundation, Inc., 31 Milk St # 960789 Boston, MA 02196 USA.
217
222
  *
218
- * Copyright (c) 2015-2024 (original work) Open Assessment Technologies SA;
223
+ * Copyright (c) 2015-2026 (original work) Open Assessment Technologies SA;
219
224
  *
220
225
  */
221
226
  const ns = 'resourcemgr';
222
227
  const logger = loggerFactory(`ui/${ns}`);
228
+ const ROW_SELECTOR = '.files-list tr';
223
229
  function shortenPath(path) {
224
230
  let tokens = path.replace(/\/$/, '').split('/');
225
231
  let start = tokens.length - 3;
@@ -230,6 +236,65 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
230
236
  title = title.filter(Boolean);
231
237
  return title.join('/');
232
238
  }
239
+
240
+ /**
241
+ * Normalize a catalog folder path for the Location column.
242
+ * Strips URI schemes and media-root labels (Assets/Media); keeps directory path only.
243
+ * @param {String} location
244
+ * @returns {String}
245
+ */
246
+ function formatLocationDisplay(location) {
247
+ if (location === null || typeof location === 'undefined' || location === '') {
248
+ return '';
249
+ }
250
+ let path = String(location).trim().replace(/\\/g, '/');
251
+ if (!path) {
252
+ return '';
253
+ }
254
+
255
+ // Drop URI schemes (taomedia://…, https://…, asset://…)
256
+ path = path.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
257
+ // Residual host/path from http(s) after scheme strip
258
+ if (path.indexOf('/') > 0 && /^[^/]+\.[^/]+\//.test(path)) {
259
+ path = path.slice(path.indexOf('/'));
260
+ }
261
+ const segments = path.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
262
+ if (!segments.length) {
263
+ return '/';
264
+ }
265
+
266
+ // Directory path only: drop trailing filename-like segment
267
+ const last = segments[segments.length - 1];
268
+ if (segments.length > 1 && /\.[a-z0-9]{1,8}$/i.test(last)) {
269
+ segments.pop();
270
+ }
271
+
272
+ // Strip media-tree root label prefix (Assets, Media, …)
273
+ const rootLabels = ['assets', 'media', 'mediamanager'];
274
+ if (segments.length && rootLabels.indexOf(segments[0].toLowerCase()) !== -1) {
275
+ segments.shift();
276
+ }
277
+ return segments.length ? `/${segments.join('/')}` : '/';
278
+ }
279
+
280
+ /**
281
+ * Format updatedAt as a fixed UTC date-time (YYYY-MM-DD HH:mm).
282
+ * @param {String} value
283
+ * @returns {String}
284
+ */
285
+ function formatUpdatedAtUtc(value) {
286
+ if (!value) {
287
+ return '';
288
+ }
289
+ const date = new Date(value);
290
+ if (Number.isNaN(date.getTime()) || date.getTime() <= 0) {
291
+ return '—';
292
+ }
293
+ const pad = function (n) {
294
+ return String(n).padStart(2, '0');
295
+ };
296
+ return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ` + `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
297
+ }
233
298
  function isTextLarger($element, text) {
234
299
  let $dummy = $element.clone().detach().css({
235
300
  position: 'absolute',
@@ -246,12 +311,23 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
246
311
  let disableUpload = options.disableUpload || false;
247
312
  let $container = options.$target;
248
313
  let $fileSelector = $$1('.file-selector', $container);
249
- let $fileContainer = $$1('.files', $fileSelector);
314
+ let $filesWrapper = $$1('.files-wrapper', $fileSelector);
315
+ let $filesListLoading = $$1('.files-list-loading', $filesWrapper);
316
+ let $filesListLoadingStatus = $$1('.files-list-loading-status', $filesListLoading);
317
+ let $fileContainer = $$1('.files-list', $fileSelector);
250
318
  let $placeholder = $$1('.empty', $fileSelector);
251
319
  let $uploader = $$1('.file-upload-container', $fileSelector);
252
320
  let parentSelector = `#${$container.attr('id')} .file-selector`;
253
321
  let $pathTitle = $fileSelector.find('h1 > .title');
254
- let $browserTitle = $$1('.file-browser > h1', $container);
322
+ let $browserTitle = $$1('.file-browser > h1.resources-title', $container);
323
+ if (!$browserTitle.length) {
324
+ $browserTitle = $$1('.file-browser > h1', $container);
325
+ }
326
+ let searchMode = false;
327
+ let initialSelectionApplied = false;
328
+ let pendingContextSelection = null;
329
+ let sort = Object.assign({}, assetSearchContract.DEFAULT_SORT);
330
+ let lastFiles = [];
255
331
 
256
332
  //set up the uploader
257
333
  if (disableUpload) {
@@ -260,15 +336,30 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
260
336
  } else {
261
337
  setUpUploader(root);
262
338
  }
339
+ $container.on(`searchmode.${ns}`, function (e, enabled) {
340
+ searchMode = !!enabled;
341
+ $fileSelector.toggleClass('search-mode', searchMode);
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
+ });
355
+
263
356
  //update current folder
264
357
  $container.on(`folderselect.${ns}`, function (e, fullPath, data, activePath, content) {
265
358
  let files;
266
- data = data.map(function (dataItem) {
267
- if (Array.isArray(dataItem.permissions)) {
268
- updatePermissions(dataItem);
269
- }
270
- return dataItem;
271
- });
359
+ if (searchMode) {
360
+ return;
361
+ }
362
+ applyFilesListLoading(false);
272
363
 
273
364
  //update title
274
365
  if ($container[0].querySelector('.upload')) {
@@ -282,37 +373,217 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
282
373
 
283
374
  //update content here
284
375
  if (_.isArray(data)) {
285
- files = _.filter(data, function (item) {
376
+ files = _.filter(data.map(ensurePermissions), function (item) {
286
377
  return !!item.uri;
287
378
  }).map(function (file) {
288
- file.type = mimeType.getFileType(file);
289
- if (typeof file.identifier === 'undefined') {
290
- file.display = `${fullPath}/${file.name}`.replace('//', '/');
291
- } else {
292
- file.display = file.identifier + file.name;
293
- }
294
- file.viewUrl = `${options.downloadUrl}?${$$1.param(options.params)}&${options.pathParam}=${encodeURIComponent(file.uri)}`;
295
- file.downloadUrl = `${file.viewUrl}&svgzsupport=true`;
296
- return file;
379
+ return prepareFileForDisplay(file, fullPath, activePath);
297
380
  });
298
- updateFiles(fullPath, files);
299
- if (activePath) {
300
- $$1(`li[data-file="${activePath}"]`).trigger('click');
381
+ updateFiles(files);
382
+ if (pendingContextSelection) {
383
+ const pending = pendingContextSelection;
384
+ initialSelectionApplied = false;
385
+ applyInitialSelection(pending, false);
386
+ if (initialSelectionApplied) {
387
+ pendingContextSelection = null;
388
+ }
389
+ return;
301
390
  }
391
+ applyInitialSelection();
302
392
  }
303
393
  });
394
+
395
+ // Render scoped search results from the service as-is (no client MIME/auth filtering).
396
+ $container.on(`searchresults.${ns}`, function (e, result) {
397
+ const items = result && result.items || [];
398
+ const files = items.filter(function (item) {
399
+ return item && !!item.uri;
400
+ }).map(function (file) {
401
+ return prepareFileForDisplay(ensureSearchPermissions(file), result.path || '', result.path);
402
+ });
403
+ $pathTitle.text(__('Search results'));
404
+ updateFiles(files, {
405
+ emptyMessage: result && result.emptyMessage
406
+ });
407
+ if (!(result && result.error)) {
408
+ applyInitialSelection(result && result.initialSelection);
409
+ }
410
+ });
411
+
412
+ // Reopen resolve (AC3): defer preselect until folderselect loads the resolved parent folder.
413
+ $container.on(`applycontext.${ns}`, function (e, ctx) {
414
+ initialSelectionApplied = false;
415
+ options.initialSelection = ctx && ctx.selection ? ctx.selection : null;
416
+ options.currentAssetItem = ctx && ctx.currentAssetItem ? ctx.currentAssetItem : null;
417
+ pendingContextSelection = options.initialSelection || null;
418
+ $fileContainer.find('tr.active').removeClass('active');
419
+ });
304
420
  function injectTranscriptionMetadata(transcriptionUrl, metadataUri, resourceUri) {
305
421
  return `${transcriptionUrl}?metadataUri=${encodeURIComponent(metadataUri)}&resourceUri=${resourceUri.replace('taomedia://mediamanager/', '')}`;
306
422
  }
307
423
 
424
+ /**
425
+ * Normalize ACL flags unless they are already a boolean permission map.
426
+ * @param {Object} item
427
+ * @returns {Object}
428
+ */
429
+ function ensurePermissions(item) {
430
+ if (item.permissions && !Array.isArray(item.permissions) && typeof item.permissions === 'object') {
431
+ return item;
432
+ }
433
+ return updatePermissions(item);
434
+ }
435
+
436
+ /**
437
+ * Search results often omit ACL (indexed gateway). Missing/empty ACL must stay pickable
438
+ * (read/preview/download) without inventing write/delete/upload. Explicit ACL arrays and
439
+ * boolean maps are preserved / mapped via updatePermissions.
440
+ * @param {Object} item
441
+ * @returns {Object}
442
+ */
443
+ function ensureSearchPermissions(item) {
444
+ if (!item) {
445
+ return item;
446
+ }
447
+ if (item.permissions && !Array.isArray(item.permissions) && typeof item.permissions === 'object') {
448
+ return item;
449
+ }
450
+ if (!item.permissions || Array.isArray(item.permissions) && item.permissions.length === 0) {
451
+ item.permissions = {
452
+ read: true,
453
+ write: false,
454
+ preview: true,
455
+ download: true,
456
+ upload: false,
457
+ delete: false
458
+ };
459
+ return item;
460
+ }
461
+ return updatePermissions(item);
462
+ }
463
+
464
+ /**
465
+ * Map a service file record to the selector display model.
466
+ * @param {Object} file
467
+ * @param {String} fullPath
468
+ * @param {String} [folderPath]
469
+ * @returns {Object}
470
+ */
471
+ function prepareFileForDisplay(file, fullPath, folderPath) {
472
+ file.type = mimeType.getFileType(file);
473
+ if (typeof file.identifier === 'undefined') {
474
+ file.display = `${fullPath}/${file.name}`.replace('//', '/');
475
+ } else {
476
+ file.display = file.identifier + file.name;
477
+ }
478
+ file.viewUrl = `${options.downloadUrl}?${$$1.param(options.params)}&${options.pathParam || 'path'}=${encodeURIComponent(file.uri)}`;
479
+ file.downloadUrl = `${file.viewUrl}&svgzsupport=true`;
480
+ if (!file.location) {
481
+ file.location = folderPath || fullPath || '';
482
+ }
483
+ if (!file.updatedAt && file.updated_at) {
484
+ file.updatedAt = file.updated_at;
485
+ }
486
+ file.locationDisplay = formatLocationDisplay(file.location);
487
+ file.updatedAtDisplay = formatUpdatedAtUtc(file.updatedAt);
488
+ return file;
489
+ }
490
+
491
+ /**
492
+ * Sync sort classes and aria-sort on table headers.
493
+ */
494
+ function applySortHeader() {
495
+ $fileSelector.find('.files thead th[data-sort-by]').each(function () {
496
+ const $th = $$1(this);
497
+ const field = $th.attr('data-sort-by');
498
+ $th.removeClass('sorted sorted_asc sorted_desc');
499
+ if (field === sort.field) {
500
+ $th.addClass('sorted').addClass(sort.direction === 'desc' ? 'sorted_desc' : 'sorted_asc');
501
+ $th.attr('aria-sort', sort.direction === 'desc' ? 'descending' : 'ascending');
502
+ } else {
503
+ $th.attr('aria-sort', 'none');
504
+ }
505
+ });
506
+ }
507
+
508
+ /**
509
+ * Apply a column click to sort state and notify listeners.
510
+ * Skip optimistic re-sort of lastFiles (current page only) — that flashed the
511
+ * wrong order before browse/search refetch; updateFiles still sorts the response.
512
+ * @param {String} field
513
+ */
514
+ function changeSort(field) {
515
+ if (!field) {
516
+ return;
517
+ }
518
+ if (field === sort.field) {
519
+ sort = {
520
+ field,
521
+ direction: sort.direction === 'asc' ? 'desc' : 'asc'
522
+ };
523
+ } else {
524
+ sort = {
525
+ field,
526
+ direction: 'asc'
527
+ };
528
+ }
529
+ applySortHeader();
530
+ $container.trigger(`sortchange.${ns}`, [Object.assign({}, sort)]);
531
+ }
532
+
533
+ /**
534
+ * Preselect initialSelection once when the matching row is present.
535
+ * When resolve returned a currentAssetItem missing from the page, inject it.
536
+ * @param {String} [selection]
537
+ * @param {Boolean} [allowInject=true] - inject currentAssetItem when missing (skip during pending reopen)
538
+ */
539
+ function applyInitialSelection(selection, allowInject) {
540
+ const target = selection || options.initialSelection;
541
+ if (!target || initialSelectionApplied) {
542
+ return;
543
+ }
544
+ const mayInject = allowInject !== false;
545
+ let $item = $fileContainer.find('tr').filter(function () {
546
+ return $$1(this).attr('data-file') === String(target);
547
+ });
548
+ if (!$item.length && mayInject && options.currentAssetItem && String(options.currentAssetItem.uri) === String(target)) {
549
+ const injected = prepareFileForDisplay(ensurePermissions(Object.assign({}, options.currentAssetItem)), options.currentAssetItem.location || '', options.currentAssetItem.location || '');
550
+ updateFiles([injected].concat(lastFiles || []));
551
+ $item = $fileContainer.find('tr').filter(function () {
552
+ return $$1(this).attr('data-file') === String(target);
553
+ });
554
+ }
555
+ if ($item.length) {
556
+ initialSelectionApplied = true;
557
+ $item.first().trigger('click');
558
+ }
559
+ }
560
+ applySortHeader();
561
+ $$1(parentSelector).off('click.resourcemgr', '.files thead th[data-sort-by]').on('click.resourcemgr', '.files thead th[data-sort-by]', function (e) {
562
+ e.preventDefault();
563
+ e.stopImmediatePropagation();
564
+ changeSort($$1(this).attr('data-sort-by'));
565
+ }).off('keydown.resourcemgr', '.files thead th[data-sort-by]').on('keydown.resourcemgr', '.files thead th[data-sort-by]', function (e) {
566
+ if (e.key === 'Enter' || e.key === ' ') {
567
+ e.preventDefault();
568
+ e.stopImmediatePropagation();
569
+ changeSort($$1(this).attr('data-sort-by'));
570
+ }
571
+ });
572
+
308
573
  //listen for file activation
309
- $$1(parentSelector).off('click', '.files li').on('click', '.files li', function (e) {
574
+ $$1(parentSelector).off('click', ROW_SELECTOR).on('click', ROW_SELECTOR, function (e) {
575
+ if ($$1(e.target).closest('.row-actions').length) {
576
+ return;
577
+ }
578
+ if ($$1(e.target).closest('a.select, a.download, a.delete').length) {
579
+ return;
580
+ }
310
581
  const clickedItem = e.target;
311
582
  if (clickedItem.hasAttribute('data-delete') || $$1(clickedItem).hasClass('icon-bin')) {
312
583
  return;
313
584
  }
314
585
  let $selected = $$1(this);
315
- let $files = $$1('.files > li', $fileSelector);
586
+ let $files = $$1(ROW_SELECTOR, $fileSelector);
316
587
  let data = _.clone($selected.data());
317
588
  if (options.resourceMetadataUrl && options.transcriptionMetadata && data.file.includes('taomedia://mediamanager/')) {
318
589
  data.transcriptionUrl = injectTranscriptionMetadata(options.resourceMetadataUrl, options.transcriptionMetadata, data.file);
@@ -323,9 +594,9 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
323
594
  });
324
595
 
325
596
  //select a file
326
- $$1(parentSelector).off('click', '.files li a.select').on('click', '.files li a.select', function (e) {
597
+ $$1(parentSelector).off('click', `${ROW_SELECTOR} a.select`).on('click', `${ROW_SELECTOR} a.select`, function (e) {
327
598
  e.preventDefault();
328
- let data = _.pick($$1(this).parents('li').data(), ['file', 'type', 'mime', 'size', 'alt']);
599
+ let data = _.pick($$1(this).closest('tr').data(), ['file', 'type', 'mime', 'size', 'alt']);
329
600
  if (context.mediaSources && context.mediaSources.length === 0 && data.file.indexOf('local/') > -1) {
330
601
  data.file = data.file.substring(6);
331
602
  }
@@ -333,7 +604,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
333
604
  });
334
605
 
335
606
  //delete a file
336
- $$1(parentSelector).off('click', '.files li a.delete').on('click', '.files li a.delete', function (e) {
607
+ $$1(parentSelector).off('click', `${ROW_SELECTOR} a.delete`).on('click', `${ROW_SELECTOR} a.delete`, function (e) {
337
608
  // This function replaces ui/deleter and must follow the same logic.
338
609
  // The main difference is that it insert a confirmation dialog before deleting the file.
339
610
  e.preventDefault();
@@ -341,7 +612,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
341
612
  if ($elt.hasClass(options.disableClass)) {
342
613
  return;
343
614
  }
344
- const $target = $elt.closest('li');
615
+ const $target = $elt.closest('tr');
345
616
  const path = $target.data('file');
346
617
  const hooks = [];
347
618
  if (options.hooks && 'function' === typeof options.hooks.deleteFile) {
@@ -364,7 +635,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
364
635
  params = {};
365
636
  if (e.namespace === 'deleter' && $target.length) {
366
637
  path = $target.data('file');
367
- params[options.pathParam] = path;
638
+ params[options.pathParam || 'path'] = path;
368
639
  $$1.getJSON(options.deleteUrl, _.merge(params, options.params), function (response) {
369
640
  if (response.deleted) {
370
641
  $container.trigger(`filedelete.${ns}`, [path]);
@@ -380,7 +651,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
380
651
  $container.trigger(`filenew.${ns}`, [result, currentPath]);
381
652
  });
382
653
  $uploader.on('fail.uploader', function (e, file, err) {
383
- errors.push(__('Unable to upload file %s : %s', file.name, err.message));
654
+ errors.push(__('Unable to upload file %s : %s', _.escape(file && file.name), _.escape(err && err.message)));
384
655
  });
385
656
  $uploader.on('end.uploader', function () {
386
657
  if (errors.length === 0) {
@@ -396,11 +667,11 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
396
667
  $uploader.uploader({
397
668
  upload: true,
398
669
  multiple: true,
399
- uploadUrl: `${options.uploadUrl}?${$$1.param(options.params)}&${options.pathParam}=${currentPath}`,
670
+ uploadUrl: `${options.uploadUrl}?${$$1.param(options.params)}&${options.pathParam || 'path'}=${currentPath}`,
400
671
  fileSelect: function (files, done) {
401
672
  let givenLength = files.length;
402
673
  let fileNames = [];
403
- $fileContainer.find('li > .desc').each(function () {
674
+ $fileContainer.find('.desc').each(function () {
404
675
  fileNames.push($$1(this).text().toLowerCase());
405
676
  });
406
677
 
@@ -434,7 +705,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
434
705
  if (options.fileExistsUrl) {
435
706
  let pathParam = `${currentPath}/${file.name}`;
436
707
  pathParam.replace('//', '/');
437
- $$1.getJSON(`${options.fileExistsUrl}?${$$1.param(options.params)}&${options.pathParam}=${pathParam}`, function (response) {
708
+ $$1.getJSON(`${options.fileExistsUrl}?${$$1.param(options.params)}&${options.pathParam || 'path'}=${pathParam}`, function (response) {
438
709
  if (response && response.exists === true) {
439
710
  //eslint-disable-next-line no-alert
440
711
  result = window.confirm(__('Do you want to override "%s"?', file.name));
@@ -455,22 +726,26 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
455
726
  $container.on(`folderselect.${ns}`, function (e, fullPath, data, uri) {
456
727
  currentPath = uri;
457
728
  $uploader.uploader('options', {
458
- uploadUrl: `${options.uploadUrl}?${$$1.param(options.params)}&${options.pathParam}=${currentPath}&relPath=${currentPath}`
729
+ uploadUrl: `${options.uploadUrl}?${$$1.param(options.params)}&${options.pathParam || 'path'}=${currentPath}&relPath=${currentPath}`
459
730
  });
460
731
  });
461
732
  function setUploadMode(enableUploadMode) {
462
733
  if (!enableUploadMode) {
463
734
  isUploadMode = false;
735
+ $fileSelector.removeClass('upload-mode');
464
736
  $uploader.hide();
737
+ $filesWrapper.show();
465
738
  $fileContainer.show();
466
739
  // Note: show() would display as inline, not inline-block!
467
740
  $switcher.filter('.upload').css({
468
741
  display: 'inline-block'
469
742
  });
470
743
  $switcher.filter('.listing').hide();
471
- $browserTitle.text(__('Browse folders:'));
744
+ $browserTitle.text(__('Resources'));
472
745
  } else {
473
746
  isUploadMode = true;
747
+ $fileSelector.addClass('upload-mode');
748
+ $filesWrapper.hide();
474
749
  $fileContainer.hide();
475
750
  $placeholder.hide();
476
751
  $uploader.show();
@@ -494,15 +769,25 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
494
769
  });
495
770
  setUploadMode(isUploadMode);
496
771
  }
497
- function updateFiles(path, files) {
772
+ function updateFiles(files, updateOptions) {
773
+ const displayOptions = updateOptions || {};
774
+ const emptyLabel = displayOptions.emptyMessage || __('No files');
775
+ lastFiles = Array.isArray(files) ? files.slice() : [];
776
+ const sorted = assetSearchContract.sortAssetItems(lastFiles, sort);
498
777
  $fileContainer.empty();
499
- if (files.length) {
778
+ if (sorted.length) {
500
779
  $placeholder.hide();
780
+ $filesWrapper.show();
501
781
  $fileContainer.append(fileSelectTpl({
502
- files: files
782
+ files: sorted
503
783
  }));
504
- } else if ($fileContainer.css('display') !== 'none') {
505
- $placeholder.show();
784
+ } else if ($fileSelector.find('.asset-search-error:not([hidden])').length === 0) {
785
+ $filesWrapper.hide();
786
+ // Stylesheet defaults `.empty` to display:none; force visible for empty states.
787
+ $placeholder.text(emptyLabel).css('display', 'block');
788
+ } else {
789
+ $filesWrapper.hide();
790
+ $placeholder.hide();
506
791
  }
507
792
  }
508
793
  }