@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
@@ -33,6 +33,7 @@ import 'ui/modal';
33
33
  import 'ui/datatable';
34
34
  import 'select2';
35
35
  import request from 'core/dataProvider/request';
36
+ import __ from 'i18n';
36
37
 
37
38
  /**
38
39
  * Sort an array by a particular property.
@@ -67,6 +68,7 @@ export default function advancedSearchFactory(config) {
67
68
  let $addCriteriaInput = null;
68
69
  let $criteriaSelect = null;
69
70
  let $advancedCriteriaContainer = null;
71
+ let $appliedFiltersSummary = null;
70
72
  let criteriaState = null;
71
73
  let criteriaMapping = {};
72
74
  const criteriaTypes = {
@@ -80,7 +82,6 @@ export default function advancedSearchFactory(config) {
80
82
  };
81
83
 
82
84
  let isAdvancedSearchStatusEnabled;
83
- let isCriteriaListUpdated = false;
84
85
 
85
86
  // Creates new component
86
87
  const instance = component({
@@ -102,18 +103,22 @@ export default function advancedSearchFactory(config) {
102
103
  return Promise.resolve();
103
104
  }
104
105
 
105
- const $criteriaIcon = $('.add-criteria-container a span').eq(0);
106
- $criteriaIcon.toggleClass('icon-add').toggleClass('icon-loop');
106
+ const $criteriaIcon = $addCriteria ? $('a span', $addCriteria).eq(0) : $();
107
+ const idleIconClass = $criteriaIcon.hasClass('icon-plus') ? 'icon-plus' : 'icon-add';
108
+ $criteriaIcon.removeClass('icon-add icon-plus').addClass('icon-loop');
107
109
  return request(route)
108
110
  .then(response => {
109
111
  criteriaMapping = response.criteriaMapping || {};
110
112
  const classTree = response.classDefinition ? response.classDefinition : response;
111
113
  const criteria = formatCriteria(classTree);
112
114
  updateCriteria(criteria);
113
- isCriteriaListUpdated = true;
114
- $criteriaIcon.toggleClass('icon-add').toggleClass('icon-loop');
115
+ $criteriaIcon.removeClass('icon-loop').addClass(idleIconClass);
115
116
  })
116
- .catch(e => instance.trigger('error', e));
117
+ .catch(e => {
118
+ $criteriaIcon.removeClass('icon-loop').addClass(idleIconClass);
119
+ instance.trigger('error', e);
120
+ throw e;
121
+ });
117
122
  },
118
123
  /**
119
124
  * Access to component state
@@ -129,10 +134,19 @@ export default function advancedSearchFactory(config) {
129
134
  clear() {
130
135
  $advancedCriteriaContainer.removeClass(['scrollable', 'scroll-separator-top', 'scroll-separator-bottom']);
131
136
  $advancedCriteriaContainer.empty();
137
+ updateAppliedFiltersSummary(0);
138
+ // Rebuild selectable options: adding a criterion removes its <option>,
139
+ // so Clear must restore them (searchModal recovers via class re-select + updateCriteria).
140
+ if ($criteriaSelect && $criteriaSelect.length) {
141
+ $criteriaSelect.find('option:not(:first-child)').remove();
142
+ }
132
143
  _.forEach(criteriaState, criterion => {
133
144
  criterion.rendered = false;
134
145
  criterion.value = null;
135
146
  criterion.logic = null;
147
+ if ($criteriaSelect && $criteriaSelect.length) {
148
+ $criteriaSelect.append(createCriteriaOption(criterion));
149
+ }
136
150
  });
137
151
  },
138
152
  /**
@@ -170,7 +184,7 @@ export default function advancedSearchFactory(config) {
170
184
  return query;
171
185
  }
172
186
  })
173
- .setTemplate(advancedSearchTpl)
187
+ .setTemplate(config.layoutTemplate || advancedSearchTpl)
174
188
  .on('render', () => {
175
189
  initUiSelectors();
176
190
  initAddCriteriaSelector()
@@ -191,6 +205,14 @@ export default function advancedSearchFactory(config) {
191
205
  $addCriteriaInput = $('.add-criteria-container a', $container);
192
206
  $criteriaSelect = $('.add-criteria-container select', $container);
193
207
  $advancedCriteriaContainer = $('.advanced-criteria-container', $container);
208
+ $appliedFiltersSummary = $('.applied-filters-summary', $container);
209
+
210
+ if (config.collapsibleCriteria && $appliedFiltersSummary.length) {
211
+ $appliedFiltersSummary.off('click.advancedSearch').on('click.advancedSearch', function (e) {
212
+ e.preventDefault();
213
+ togglePreviousFilters();
214
+ });
215
+ }
194
216
 
195
217
  $advancedCriteriaContainer.on('scroll', _.throttle(animateScroll, 100));
196
218
  }
@@ -252,15 +274,24 @@ export default function advancedSearchFactory(config) {
252
274
  });
253
275
 
254
276
  // open dropdown when user clicks on add criteria input
255
- $addCriteriaInput.on('click', () => {
256
- if (isCriteriaListUpdated) {
257
- $criteriaSelect.select2('open');
258
- // if dropdown is opened above addCriteria input, top property is slightly decreased to avoid overlapping with addCriteria icon
259
- if ($('.criteria-dropdown-select2').hasClass('select2-drop-above')) {
260
- $('.criteria-dropdown-select2').css(
261
- 'top',
262
- $('.criteria-dropdown-select2').css('top').split('px')[0] - 10 + 'px'
263
- );
277
+ $addCriteriaInput.on('click', function (e) {
278
+ e.preventDefault();
279
+ e.stopPropagation();
280
+ const hasOptions =
281
+ $criteriaSelect.find('option').filter(function () {
282
+ return Boolean(this.value);
283
+ }).length > 0;
284
+ // Never open an empty select2 (mask-only / no results). Wait for ClassMetadata options.
285
+ if (!hasOptions) {
286
+ return;
287
+ }
288
+ $criteriaSelect.select2('open');
289
+ // if dropdown is opened above addCriteria input, top property is slightly decreased to avoid overlapping with addCriteria icon
290
+ const $dropdown = $('.criteria-dropdown-select2');
291
+ if ($dropdown.hasClass('select2-drop-above')) {
292
+ const top = parseFloat($dropdown.css('top'));
293
+ if (!Number.isNaN(top)) {
294
+ $dropdown.css('top', `${top - 10}px`);
264
295
  }
265
296
  }
266
297
  });
@@ -325,8 +356,12 @@ export default function advancedSearchFactory(config) {
325
356
  const criterion = criteriaState[criterionToAdd];
326
357
  const $criterionContainer = renderCriterion(criterion);
327
358
 
359
+ if (config.collapsibleCriteria) {
360
+ wrapCriterionAsSpoiler($criterionContainer, criterion);
361
+ }
362
+
328
363
  // set logic to remove criterion
329
- $('.icon-result-nok', $criterionContainer).on('click', { criterion }, removeCriterion);
364
+ $criterionContainer.find('.icon-result-nok, .filter-spoiler-delete').on('click', { criterion }, removeCriterion);
330
365
 
331
366
  // set initial value and manage value changes
332
367
  bindCriterionValue(criterion, $criterionContainer);
@@ -337,6 +372,109 @@ export default function advancedSearchFactory(config) {
337
372
  }
338
373
 
339
374
  criterion.rendered = true;
375
+
376
+ if (config.collapsibleCriteria) {
377
+ reorganizeFilterLayout();
378
+ }
379
+ }
380
+
381
+ /**
382
+ * @returns {jQuery}
383
+ */
384
+ function getAllRenderedFilters() {
385
+ return $advancedCriteriaContainer.children('.filter-container').not('.invalid-criteria-warning-container');
386
+ }
387
+
388
+ /**
389
+ * @param {jQuery} $filter
390
+ * @param {boolean} open
391
+ */
392
+ function setFilterSpoilerOpen($filter, open) {
393
+ const $spoiler = $filter.find('.filter-spoiler').first();
394
+ if (!$spoiler.length) {
395
+ return;
396
+ }
397
+ $spoiler.toggleClass('is-open', open);
398
+ $filter.find('.filter-spoiler-toggle').attr('aria-expanded', open);
399
+ }
400
+
401
+ /**
402
+ * @param {number} appliedCount
403
+ * @returns {string}
404
+ */
405
+ function getAppliedFiltersSummaryText(appliedCount) {
406
+ if (appliedCount === 1) {
407
+ return __('1 filter applied');
408
+ }
409
+ return __('%s filters applied').replace('%s', String(appliedCount));
410
+ }
411
+
412
+ /**
413
+ * Keeps each filter as its own spoiler card: newest expanded, previous collapsed (headers stay visible).
414
+ * With collapsibleCriteria, newest is prepended (index 0); otherwise newest is last.
415
+ */
416
+ function reorganizeFilterLayout() {
417
+ const $filters = getAllRenderedFilters();
418
+ const count = $filters.length;
419
+
420
+ updateAppliedFiltersSummary(count);
421
+
422
+ if (count === 0) {
423
+ return;
424
+ }
425
+
426
+ const openIndex = config.collapsibleCriteria ? 0 : count - 1;
427
+ $filters.each(function (index) {
428
+ setFilterSpoilerOpen($(this), index === openIndex);
429
+ });
430
+
431
+ if ($appliedFiltersSummary && $appliedFiltersSummary.length) {
432
+ $appliedFiltersSummary.attr('aria-expanded', 'false');
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Updates the "N filters applied" summary next to Add filter.
438
+ * @param {number} appliedCount
439
+ */
440
+ function updateAppliedFiltersSummary(appliedCount) {
441
+ if ($appliedFiltersSummary && $appliedFiltersSummary.length) {
442
+ if (appliedCount > 0) {
443
+ $appliedFiltersSummary
444
+ .text(getAppliedFiltersSummaryText(appliedCount))
445
+ .removeClass('hidden')
446
+ .prop('hidden', false);
447
+ } else {
448
+ $appliedFiltersSummary.addClass('hidden').prop('hidden', true).text('').attr('aria-expanded', 'false');
449
+ }
450
+ }
451
+
452
+ instance.trigger('criteriachange', { count: appliedCount });
453
+ }
454
+
455
+ /**
456
+ * Toggles expand/collapse for all filters except the newest one.
457
+ * With collapsibleCriteria (prepend), newest is first; otherwise newest is last.
458
+ */
459
+ function togglePreviousFilters() {
460
+ const $filters = getAllRenderedFilters();
461
+ if ($filters.length <= 1) {
462
+ return;
463
+ }
464
+
465
+ const $previous = config.collapsibleCriteria ? $filters.slice(1) : $filters.slice(0, -1);
466
+ const anyOpen = $previous.filter(function () {
467
+ return $(this).find('.filter-spoiler').first().hasClass('is-open');
468
+ }).length > 0;
469
+ const open = !anyOpen;
470
+
471
+ $previous.each(function () {
472
+ setFilterSpoilerOpen($(this), open);
473
+ });
474
+
475
+ if ($appliedFiltersSummary && $appliedFiltersSummary.length) {
476
+ $appliedFiltersSummary.attr('aria-expanded', open);
477
+ }
340
478
  }
341
479
 
342
480
  /**
@@ -355,7 +493,11 @@ export default function advancedSearchFactory(config) {
355
493
  templateToUse = listCheckboxCriterionTpl;
356
494
  }
357
495
 
358
- $advancedCriteriaContainer.append(templateToUse({ criterion }));
496
+ if (config.collapsibleCriteria) {
497
+ $advancedCriteriaContainer.prepend(templateToUse({ criterion }));
498
+ } else {
499
+ $advancedCriteriaContainer.append(templateToUse({ criterion }));
500
+ }
359
501
 
360
502
  const $criterionContainer = $(`.${criterion.id}-filter`, $container);
361
503
  const valueMapping = criteriaMapping[criterion.type];
@@ -432,15 +574,18 @@ export default function advancedSearchFactory(config) {
432
574
  * @param {object} $criterionContainer - rendered criterion
433
575
  */
434
576
  function bindCriterionValue(criterion, $criterionContainer) {
577
+ if (criterion.type === criteriaTypes.text) {
578
+ // Bind synchronously so typing / Search right after Add filter is not lost
579
+ // (getInitialCriterionLabel resolves on a microtask).
580
+ $('input', $criterionContainer).val(criterion.value);
581
+ $('input', $criterionContainer).on('input change', function () {
582
+ criterion.value = $(this).val() || null;
583
+ });
584
+ return;
585
+ }
586
+
435
587
  getInitialCriterionLabel(criterion).then(initialCriterion => {
436
- if (criterion.type === criteriaTypes.text) {
437
- // set initial value
438
- $('input', $criterionContainer).val(criterion.value);
439
- // set event to bind input value to critariaState
440
- $('input', $criterionContainer).on('change', function () {
441
- criterion.value = $(this).val() || null;
442
- });
443
- } else if (criterion.type === criteriaTypes.list && criterion.uri) {
588
+ if (criterion.type === criteriaTypes.list && criterion.uri) {
444
589
  // set initial value
445
590
  if (criterion.value) {
446
591
  $(`input[name=${criterion.id}-select]`, $criterionContainer).select2('data', initialCriterion);
@@ -477,6 +622,57 @@ export default function advancedSearchFactory(config) {
477
622
  });
478
623
  }
479
624
 
625
+ /**
626
+ * Wraps a rendered criterion in a collapsible spoiler (Resource Manager UX).
627
+ * @param {jQuery} $criterionContainer
628
+ * @param {object} criterion
629
+ */
630
+ function wrapCriterionAsSpoiler($criterionContainer, criterion) {
631
+ let $deleteBtn = $criterionContainer.find('.icon-result-nok').first();
632
+ if (!$deleteBtn.length) {
633
+ $deleteBtn = $('<button>', {
634
+ type: 'button',
635
+ class: 'filter-spoiler-delete icon-bin',
636
+ 'aria-label': __('Remove criteria')
637
+ });
638
+ } else {
639
+ $deleteBtn
640
+ .attr('type', 'button')
641
+ .removeClass('icon-result-nok')
642
+ .addClass('filter-spoiler-delete icon-bin')
643
+ .attr('aria-label', __('Remove criteria'));
644
+ }
645
+ const $title = $('<span>', { class: 'filter-spoiler-title', text: criterion.label });
646
+ const $toggle = $('<button>', {
647
+ type: 'button',
648
+ class: 'filter-spoiler-toggle',
649
+ 'aria-expanded': 'true'
650
+ }).append($title);
651
+
652
+ const $header = $('<div>', { class: 'filter-spoiler-header' }).append($toggle, $deleteBtn);
653
+
654
+ const $body = $('<div>', { class: 'filter-spoiler-body' });
655
+ $criterionContainer.children().appendTo($body);
656
+
657
+ const $spoiler = $('<div>', { class: 'filter-spoiler is-open' }).append($header, $body);
658
+ $criterionContainer.empty().append($spoiler);
659
+ $criterionContainer.addClass('filter-spoiler-wrapper');
660
+
661
+ $toggle.on('click', function (e) {
662
+ e.preventDefault();
663
+ const $spoilerEl = $criterionContainer.find('.filter-spoiler').first();
664
+ const willOpen = !$spoilerEl.hasClass('is-open');
665
+ if (willOpen && config.collapsibleCriteria) {
666
+ getAllRenderedFilters().each(function () {
667
+ if (this !== $criterionContainer.get(0)) {
668
+ setFilterSpoilerOpen($(this), false);
669
+ }
670
+ });
671
+ }
672
+ setFilterSpoilerOpen($criterionContainer, willOpen);
673
+ });
674
+ }
675
+
480
676
  /**
481
677
  * Removes a criterion from advanced criteria container when user clicks on the criterion close icon.
482
678
  * It also adds the option element to criteria select so removed criterion can be rendered again
@@ -488,7 +684,7 @@ export default function advancedSearchFactory(config) {
488
684
  const criterionKey = getCriterionStateId(criterion);
489
685
 
490
686
  // remove criterion and append new criterion to select options
491
- $(this).parent().remove();
687
+ $(this).closest('.filter-container').remove();
492
688
  $criteriaSelect.append(newOption);
493
689
 
494
690
  // reset criterion values on criteriaState
@@ -499,6 +695,10 @@ export default function advancedSearchFactory(config) {
499
695
  if ($advancedCriteriaContainer.get(0).scrollHeight <= $advancedCriteriaContainer.outerHeight()) {
500
696
  $advancedCriteriaContainer.removeClass('scrollable');
501
697
  }
698
+
699
+ if (config.collapsibleCriteria) {
700
+ reorganizeFilterLayout();
701
+ }
502
702
  }
503
703
 
504
704
  /**
@@ -512,16 +712,24 @@ export default function advancedSearchFactory(config) {
512
712
  let criteria = [];
513
713
 
514
714
  _.forEach(classTree, classInstance => {
515
- criteria.push(...classInstance.metadata);
715
+ const metadata = classInstance && classInstance.metadata;
716
+ if (Array.isArray(metadata) && metadata.length) {
717
+ criteria.push(...metadata);
718
+ }
516
719
  });
517
720
 
518
721
  // extends each criterion with an id that can be use as a valid css class
519
- _.forEach(criteria, criterion => {
722
+ criteria = criteria.filter(criterion => {
723
+ const id = String((criterion && criterion.propertyUri) || '').replace(/^[^a-zA-Z]*|[^a-zA-Z0-9]*/g, '');
724
+ if (!id) {
725
+ return false;
726
+ }
520
727
  criterion.label = getCriterionLabel(criterion);
521
- criterion.id = criterion.propertyUri.replace(/^[^a-zA-Z]*|[^a-zA-Z0-9]*/g, '');
728
+ criterion.id = id;
729
+ return true;
522
730
  });
523
731
 
524
- return criteria;
732
+ return _.uniqBy(criteria, 'propertyUri');
525
733
  }
526
734
 
527
735
  /**
@@ -537,6 +745,9 @@ export default function advancedSearchFactory(config) {
537
745
  const invalidCriteria = deleteDeprecatedCriteria(criteria);
538
746
  extendCriteria(criteria);
539
747
  renderWarningMessage(invalidCriteria);
748
+ if (config.collapsibleCriteria) {
749
+ reorganizeFilterLayout();
750
+ }
540
751
  }
541
752
 
542
753
  /**