@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
@@ -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=\""
136
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)))
137
- + "\"><span class=\"icon-bin\"></span></a>\n ";
140
+ + "\" aria-label=\""
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)))
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,21 @@ 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 $fileContainer = $$1('.files-list', $fileSelector);
250
316
  let $placeholder = $$1('.empty', $fileSelector);
251
317
  let $uploader = $$1('.file-upload-container', $fileSelector);
252
318
  let parentSelector = `#${$container.attr('id')} .file-selector`;
253
319
  let $pathTitle = $fileSelector.find('h1 > .title');
254
- let $browserTitle = $$1('.file-browser > h1', $container);
320
+ let $browserTitle = $$1('.file-browser > h1.resources-title', $container);
321
+ if (!$browserTitle.length) {
322
+ $browserTitle = $$1('.file-browser > h1', $container);
323
+ }
324
+ let searchMode = false;
325
+ let initialSelectionApplied = false;
326
+ let pendingContextSelection = null;
327
+ let sort = Object.assign({}, assetSearchContract.DEFAULT_SORT);
328
+ let lastFiles = [];
255
329
 
256
330
  //set up the uploader
257
331
  if (disableUpload) {
@@ -260,15 +334,17 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
260
334
  } else {
261
335
  setUpUploader(root);
262
336
  }
337
+ $container.on(`searchmode.${ns}`, function (e, enabled) {
338
+ searchMode = !!enabled;
339
+ $fileSelector.toggleClass('search-mode', searchMode);
340
+ });
341
+
263
342
  //update current folder
264
343
  $container.on(`folderselect.${ns}`, function (e, fullPath, data, activePath, content) {
265
344
  let files;
266
- data = data.map(function (dataItem) {
267
- if (Array.isArray(dataItem.permissions)) {
268
- updatePermissions(dataItem);
269
- }
270
- return dataItem;
271
- });
345
+ if (searchMode) {
346
+ return;
347
+ }
272
348
 
273
349
  //update title
274
350
  if ($container[0].querySelector('.upload')) {
@@ -282,37 +358,217 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
282
358
 
283
359
  //update content here
284
360
  if (_.isArray(data)) {
285
- files = _.filter(data, function (item) {
361
+ files = _.filter(data.map(ensurePermissions), function (item) {
286
362
  return !!item.uri;
287
363
  }).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;
364
+ return prepareFileForDisplay(file, fullPath, activePath);
297
365
  });
298
- updateFiles(fullPath, files);
299
- if (activePath) {
300
- $$1(`li[data-file="${activePath}"]`).trigger('click');
366
+ updateFiles(files);
367
+ if (pendingContextSelection) {
368
+ const pending = pendingContextSelection;
369
+ initialSelectionApplied = false;
370
+ applyInitialSelection(pending, false);
371
+ if (initialSelectionApplied) {
372
+ pendingContextSelection = null;
373
+ }
374
+ return;
301
375
  }
376
+ applyInitialSelection();
302
377
  }
303
378
  });
379
+
380
+ // Render scoped search results from the service as-is (no client MIME/auth filtering).
381
+ $container.on(`searchresults.${ns}`, function (e, result) {
382
+ const items = result && result.items || [];
383
+ const files = items.filter(function (item) {
384
+ return item && !!item.uri;
385
+ }).map(function (file) {
386
+ return prepareFileForDisplay(ensureSearchPermissions(file), result.path || '', result.path);
387
+ });
388
+ $pathTitle.text(__('Search results'));
389
+ updateFiles(files, {
390
+ emptyMessage: result && result.emptyMessage
391
+ });
392
+ if (!(result && result.error)) {
393
+ applyInitialSelection(result && result.initialSelection);
394
+ }
395
+ });
396
+
397
+ // Reopen resolve (AC3): defer preselect until folderselect loads the resolved parent folder.
398
+ $container.on(`applycontext.${ns}`, function (e, ctx) {
399
+ initialSelectionApplied = false;
400
+ options.initialSelection = ctx && ctx.selection ? ctx.selection : null;
401
+ options.currentAssetItem = ctx && ctx.currentAssetItem ? ctx.currentAssetItem : null;
402
+ pendingContextSelection = options.initialSelection || null;
403
+ $fileContainer.find('tr.active').removeClass('active');
404
+ });
304
405
  function injectTranscriptionMetadata(transcriptionUrl, metadataUri, resourceUri) {
305
406
  return `${transcriptionUrl}?metadataUri=${encodeURIComponent(metadataUri)}&resourceUri=${resourceUri.replace('taomedia://mediamanager/', '')}`;
306
407
  }
307
408
 
409
+ /**
410
+ * Normalize ACL flags unless they are already a boolean permission map.
411
+ * @param {Object} item
412
+ * @returns {Object}
413
+ */
414
+ function ensurePermissions(item) {
415
+ if (item.permissions && !Array.isArray(item.permissions) && typeof item.permissions === 'object') {
416
+ return item;
417
+ }
418
+ return updatePermissions(item);
419
+ }
420
+
421
+ /**
422
+ * Search results often omit ACL (indexed gateway). Missing/empty ACL must stay pickable
423
+ * (read/preview/download) without inventing write/delete/upload. Explicit ACL arrays and
424
+ * boolean maps are preserved / mapped via updatePermissions.
425
+ * @param {Object} item
426
+ * @returns {Object}
427
+ */
428
+ function ensureSearchPermissions(item) {
429
+ if (!item) {
430
+ return item;
431
+ }
432
+ if (item.permissions && !Array.isArray(item.permissions) && typeof item.permissions === 'object') {
433
+ return item;
434
+ }
435
+ if (!item.permissions || Array.isArray(item.permissions) && item.permissions.length === 0) {
436
+ item.permissions = {
437
+ read: true,
438
+ write: false,
439
+ preview: true,
440
+ download: true,
441
+ upload: false,
442
+ delete: false
443
+ };
444
+ return item;
445
+ }
446
+ return updatePermissions(item);
447
+ }
448
+
449
+ /**
450
+ * Map a service file record to the selector display model.
451
+ * @param {Object} file
452
+ * @param {String} fullPath
453
+ * @param {String} [folderPath]
454
+ * @returns {Object}
455
+ */
456
+ function prepareFileForDisplay(file, fullPath, folderPath) {
457
+ file.type = mimeType.getFileType(file);
458
+ if (typeof file.identifier === 'undefined') {
459
+ file.display = `${fullPath}/${file.name}`.replace('//', '/');
460
+ } else {
461
+ file.display = file.identifier + file.name;
462
+ }
463
+ file.viewUrl = `${options.downloadUrl}?${$$1.param(options.params)}&${options.pathParam || 'path'}=${encodeURIComponent(file.uri)}`;
464
+ file.downloadUrl = `${file.viewUrl}&svgzsupport=true`;
465
+ if (!file.location) {
466
+ file.location = folderPath || fullPath || '';
467
+ }
468
+ if (!file.updatedAt && file.updated_at) {
469
+ file.updatedAt = file.updated_at;
470
+ }
471
+ file.locationDisplay = formatLocationDisplay(file.location);
472
+ file.updatedAtDisplay = formatUpdatedAtUtc(file.updatedAt);
473
+ return file;
474
+ }
475
+
476
+ /**
477
+ * Sync sort classes and aria-sort on table headers.
478
+ */
479
+ function applySortHeader() {
480
+ $fileSelector.find('.files thead th[data-sort-by]').each(function () {
481
+ const $th = $$1(this);
482
+ const field = $th.attr('data-sort-by');
483
+ $th.removeClass('sorted sorted_asc sorted_desc');
484
+ if (field === sort.field) {
485
+ $th.addClass('sorted').addClass(sort.direction === 'desc' ? 'sorted_desc' : 'sorted_asc');
486
+ $th.attr('aria-sort', sort.direction === 'desc' ? 'descending' : 'ascending');
487
+ } else {
488
+ $th.attr('aria-sort', 'none');
489
+ }
490
+ });
491
+ }
492
+
493
+ /**
494
+ * Apply a column click to sort state and notify listeners.
495
+ * Skip optimistic re-sort of lastFiles (current page only) — that flashed the
496
+ * wrong order before browse/search refetch; updateFiles still sorts the response.
497
+ * @param {String} field
498
+ */
499
+ function changeSort(field) {
500
+ if (!field) {
501
+ return;
502
+ }
503
+ if (field === sort.field) {
504
+ sort = {
505
+ field,
506
+ direction: sort.direction === 'asc' ? 'desc' : 'asc'
507
+ };
508
+ } else {
509
+ sort = {
510
+ field,
511
+ direction: 'asc'
512
+ };
513
+ }
514
+ applySortHeader();
515
+ $container.trigger(`sortchange.${ns}`, [Object.assign({}, sort)]);
516
+ }
517
+
518
+ /**
519
+ * Preselect initialSelection once when the matching row is present.
520
+ * When resolve returned a currentAssetItem missing from the page, inject it.
521
+ * @param {String} [selection]
522
+ * @param {Boolean} [allowInject=true] - inject currentAssetItem when missing (skip during pending reopen)
523
+ */
524
+ function applyInitialSelection(selection, allowInject) {
525
+ const target = selection || options.initialSelection;
526
+ if (!target || initialSelectionApplied) {
527
+ return;
528
+ }
529
+ const mayInject = allowInject !== false;
530
+ let $item = $fileContainer.find('tr').filter(function () {
531
+ return $$1(this).attr('data-file') === String(target);
532
+ });
533
+ if (!$item.length && mayInject && options.currentAssetItem && String(options.currentAssetItem.uri) === String(target)) {
534
+ const injected = prepareFileForDisplay(ensurePermissions(Object.assign({}, options.currentAssetItem)), options.currentAssetItem.location || '', options.currentAssetItem.location || '');
535
+ updateFiles([injected].concat(lastFiles || []));
536
+ $item = $fileContainer.find('tr').filter(function () {
537
+ return $$1(this).attr('data-file') === String(target);
538
+ });
539
+ }
540
+ if ($item.length) {
541
+ initialSelectionApplied = true;
542
+ $item.first().trigger('click');
543
+ }
544
+ }
545
+ applySortHeader();
546
+ $$1(parentSelector).off('click.resourcemgr', '.files thead th[data-sort-by]').on('click.resourcemgr', '.files thead th[data-sort-by]', function (e) {
547
+ e.preventDefault();
548
+ e.stopImmediatePropagation();
549
+ changeSort($$1(this).attr('data-sort-by'));
550
+ }).off('keydown.resourcemgr', '.files thead th[data-sort-by]').on('keydown.resourcemgr', '.files thead th[data-sort-by]', function (e) {
551
+ if (e.key === 'Enter' || e.key === ' ') {
552
+ e.preventDefault();
553
+ e.stopImmediatePropagation();
554
+ changeSort($$1(this).attr('data-sort-by'));
555
+ }
556
+ });
557
+
308
558
  //listen for file activation
309
- $$1(parentSelector).off('click', '.files li').on('click', '.files li', function (e) {
559
+ $$1(parentSelector).off('click', ROW_SELECTOR).on('click', ROW_SELECTOR, function (e) {
560
+ if ($$1(e.target).closest('.row-actions').length) {
561
+ return;
562
+ }
563
+ if ($$1(e.target).closest('a.select, a.download, a.delete').length) {
564
+ return;
565
+ }
310
566
  const clickedItem = e.target;
311
567
  if (clickedItem.hasAttribute('data-delete') || $$1(clickedItem).hasClass('icon-bin')) {
312
568
  return;
313
569
  }
314
570
  let $selected = $$1(this);
315
- let $files = $$1('.files > li', $fileSelector);
571
+ let $files = $$1(ROW_SELECTOR, $fileSelector);
316
572
  let data = _.clone($selected.data());
317
573
  if (options.resourceMetadataUrl && options.transcriptionMetadata && data.file.includes('taomedia://mediamanager/')) {
318
574
  data.transcriptionUrl = injectTranscriptionMetadata(options.resourceMetadataUrl, options.transcriptionMetadata, data.file);
@@ -323,9 +579,9 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
323
579
  });
324
580
 
325
581
  //select a file
326
- $$1(parentSelector).off('click', '.files li a.select').on('click', '.files li a.select', function (e) {
582
+ $$1(parentSelector).off('click', `${ROW_SELECTOR} a.select`).on('click', `${ROW_SELECTOR} a.select`, function (e) {
327
583
  e.preventDefault();
328
- let data = _.pick($$1(this).parents('li').data(), ['file', 'type', 'mime', 'size', 'alt']);
584
+ let data = _.pick($$1(this).closest('tr').data(), ['file', 'type', 'mime', 'size', 'alt']);
329
585
  if (context.mediaSources && context.mediaSources.length === 0 && data.file.indexOf('local/') > -1) {
330
586
  data.file = data.file.substring(6);
331
587
  }
@@ -333,7 +589,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
333
589
  });
334
590
 
335
591
  //delete a file
336
- $$1(parentSelector).off('click', '.files li a.delete').on('click', '.files li a.delete', function (e) {
592
+ $$1(parentSelector).off('click', `${ROW_SELECTOR} a.delete`).on('click', `${ROW_SELECTOR} a.delete`, function (e) {
337
593
  // This function replaces ui/deleter and must follow the same logic.
338
594
  // The main difference is that it insert a confirmation dialog before deleting the file.
339
595
  e.preventDefault();
@@ -341,7 +597,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
341
597
  if ($elt.hasClass(options.disableClass)) {
342
598
  return;
343
599
  }
344
- const $target = $elt.closest('li');
600
+ const $target = $elt.closest('tr');
345
601
  const path = $target.data('file');
346
602
  const hooks = [];
347
603
  if (options.hooks && 'function' === typeof options.hooks.deleteFile) {
@@ -364,7 +620,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
364
620
  params = {};
365
621
  if (e.namespace === 'deleter' && $target.length) {
366
622
  path = $target.data('file');
367
- params[options.pathParam] = path;
623
+ params[options.pathParam || 'path'] = path;
368
624
  $$1.getJSON(options.deleteUrl, _.merge(params, options.params), function (response) {
369
625
  if (response.deleted) {
370
626
  $container.trigger(`filedelete.${ns}`, [path]);
@@ -380,7 +636,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
380
636
  $container.trigger(`filenew.${ns}`, [result, currentPath]);
381
637
  });
382
638
  $uploader.on('fail.uploader', function (e, file, err) {
383
- errors.push(__('Unable to upload file %s : %s', file.name, err.message));
639
+ errors.push(__('Unable to upload file %s : %s', _.escape(file && file.name), _.escape(err && err.message)));
384
640
  });
385
641
  $uploader.on('end.uploader', function () {
386
642
  if (errors.length === 0) {
@@ -396,11 +652,11 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
396
652
  $uploader.uploader({
397
653
  upload: true,
398
654
  multiple: true,
399
- uploadUrl: `${options.uploadUrl}?${$$1.param(options.params)}&${options.pathParam}=${currentPath}`,
655
+ uploadUrl: `${options.uploadUrl}?${$$1.param(options.params)}&${options.pathParam || 'path'}=${currentPath}`,
400
656
  fileSelect: function (files, done) {
401
657
  let givenLength = files.length;
402
658
  let fileNames = [];
403
- $fileContainer.find('li > .desc').each(function () {
659
+ $fileContainer.find('.desc').each(function () {
404
660
  fileNames.push($$1(this).text().toLowerCase());
405
661
  });
406
662
 
@@ -434,7 +690,7 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
434
690
  if (options.fileExistsUrl) {
435
691
  let pathParam = `${currentPath}/${file.name}`;
436
692
  pathParam.replace('//', '/');
437
- $$1.getJSON(`${options.fileExistsUrl}?${$$1.param(options.params)}&${options.pathParam}=${pathParam}`, function (response) {
693
+ $$1.getJSON(`${options.fileExistsUrl}?${$$1.param(options.params)}&${options.pathParam || 'path'}=${pathParam}`, function (response) {
438
694
  if (response && response.exists === true) {
439
695
  //eslint-disable-next-line no-alert
440
696
  result = window.confirm(__('Do you want to override "%s"?', file.name));
@@ -455,22 +711,26 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
455
711
  $container.on(`folderselect.${ns}`, function (e, fullPath, data, uri) {
456
712
  currentPath = uri;
457
713
  $uploader.uploader('options', {
458
- uploadUrl: `${options.uploadUrl}?${$$1.param(options.params)}&${options.pathParam}=${currentPath}&relPath=${currentPath}`
714
+ uploadUrl: `${options.uploadUrl}?${$$1.param(options.params)}&${options.pathParam || 'path'}=${currentPath}&relPath=${currentPath}`
459
715
  });
460
716
  });
461
717
  function setUploadMode(enableUploadMode) {
462
718
  if (!enableUploadMode) {
463
719
  isUploadMode = false;
720
+ $fileSelector.removeClass('upload-mode');
464
721
  $uploader.hide();
722
+ $filesWrapper.show();
465
723
  $fileContainer.show();
466
724
  // Note: show() would display as inline, not inline-block!
467
725
  $switcher.filter('.upload').css({
468
726
  display: 'inline-block'
469
727
  });
470
728
  $switcher.filter('.listing').hide();
471
- $browserTitle.text(__('Browse folders:'));
729
+ $browserTitle.text(__('Resources'));
472
730
  } else {
473
731
  isUploadMode = true;
732
+ $fileSelector.addClass('upload-mode');
733
+ $filesWrapper.hide();
474
734
  $fileContainer.hide();
475
735
  $placeholder.hide();
476
736
  $uploader.show();
@@ -494,15 +754,25 @@ define(['jquery', 'lodash', 'async', 'i18n', 'core/mimetype', 'handlebars', 'lib
494
754
  });
495
755
  setUploadMode(isUploadMode);
496
756
  }
497
- function updateFiles(path, files) {
757
+ function updateFiles(files, updateOptions) {
758
+ const displayOptions = updateOptions || {};
759
+ const emptyLabel = displayOptions.emptyMessage || __('No files');
760
+ lastFiles = Array.isArray(files) ? files.slice() : [];
761
+ const sorted = assetSearchContract.sortAssetItems(lastFiles, sort);
498
762
  $fileContainer.empty();
499
- if (files.length) {
763
+ if (sorted.length) {
500
764
  $placeholder.hide();
765
+ $filesWrapper.show();
501
766
  $fileContainer.append(fileSelectTpl({
502
- files: files
767
+ files: sorted
503
768
  }));
504
- } else if ($fileContainer.css('display') !== 'none') {
505
- $placeholder.show();
769
+ } else if ($fileSelector.find('.asset-search-error:not([hidden])').length === 0) {
770
+ $filesWrapper.hide();
771
+ // Stylesheet defaults `.empty` to display:none; force visible for empty states.
772
+ $placeholder.text(emptyLabel).css('display', 'block');
773
+ } else {
774
+ $filesWrapper.hide();
775
+ $placeholder.hide();
506
776
  }
507
777
  }
508
778
  }