@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.
- package/dist/figure/FigureStateActive.js +64 -32
- package/dist/image/ImgStateActive/initHelper.js +64 -32
- package/dist/image/ImgStateActive.js +64 -32
- package/dist/previewer.js +1 -1
- package/dist/resourcemgr/assetSearch.js +793 -0
- package/dist/resourcemgr/assetSearchContract.js +477 -0
- package/dist/resourcemgr/css/resourcemgr.css +1230 -97
- package/dist/resourcemgr/css/resourcemgr.css.map +1 -1
- package/dist/resourcemgr/fileBrowser.js +815 -83
- package/dist/resourcemgr/filePreview.js +82 -7
- package/dist/resourcemgr/fileSelector.js +377 -92
- package/dist/resourcemgr.js +314 -57
- package/dist/searchModal/advancedSearch.js +233 -31
- package/package.json +15 -6
- package/src/image/ImgStateActive/initHelper.js +69 -31
- package/src/previewer.js +1 -1
- package/src/resourcemgr/assetSearch.js +847 -0
- package/src/resourcemgr/assetSearchContract.js +489 -0
- package/src/resourcemgr/css/resourcemgr.css +1230 -97
- package/src/resourcemgr/css/resourcemgr.css.map +1 -1
- package/src/resourcemgr/fileBrowser.js +872 -92
- package/src/resourcemgr/filePreview.js +89 -8
- package/src/resourcemgr/fileSelector.js +356 -46
- package/src/resourcemgr/scss/resourcemgr.scss +1450 -89
- package/src/resourcemgr/tpl/advanced-search.tpl +10 -0
- package/src/resourcemgr/tpl/fileSelect.tpl +25 -28
- package/src/resourcemgr/tpl/folder.tpl +4 -1
- package/src/resourcemgr/tpl/layout.tpl +125 -54
- package/src/resourcemgr/tpl/rootFolder.tpl +3 -1
- package/src/resourcemgr.js +294 -47
- package/src/searchModal/advancedSearch.js +242 -31
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
define(['jquery', 'i18n', 'ui/pagination', 'ui/searchModal/advancedSearch', 'handlebars', 'lib/handlebars/helpers', 'util/url', 'util/shortcut/registry', 'ui/resourcemgr/assetSearchContract', 'css!ui/searchModal/css/advancedSearch.css'], function ($$1, __, paginationComponent, advancedSearchFactory, Handlebars, Helpers0, urlUtil, shortcutRegistry, assetSearchContract, advancedSearch_css) { 'use strict';
|
|
2
|
+
|
|
3
|
+
$$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1;
|
|
4
|
+
__ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __;
|
|
5
|
+
paginationComponent = paginationComponent && Object.prototype.hasOwnProperty.call(paginationComponent, 'default') ? paginationComponent['default'] : paginationComponent;
|
|
6
|
+
advancedSearchFactory = advancedSearchFactory && Object.prototype.hasOwnProperty.call(advancedSearchFactory, 'default') ? advancedSearchFactory['default'] : advancedSearchFactory;
|
|
7
|
+
Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars;
|
|
8
|
+
Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;
|
|
9
|
+
urlUtil = urlUtil && Object.prototype.hasOwnProperty.call(urlUtil, 'default') ? urlUtil['default'] : urlUtil;
|
|
10
|
+
shortcutRegistry = shortcutRegistry && Object.prototype.hasOwnProperty.call(shortcutRegistry, 'default') ? shortcutRegistry['default'] : shortcutRegistry;
|
|
11
|
+
|
|
12
|
+
if (!Helpers0.__initialized) {
|
|
13
|
+
Helpers0(Handlebars);
|
|
14
|
+
Helpers0.__initialized = true;
|
|
15
|
+
}
|
|
16
|
+
var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
|
|
17
|
+
this.compilerInfo = [4,'>= 1.0.0'];
|
|
18
|
+
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
|
|
19
|
+
var buffer = "", helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
buffer += "<div class=\"advanced-search-container\">\n <div class=\"add-criteria-container\">\n <a href=\"#\" class=\"toggle-modal-button\" role=\"button\"><span class=\"icon-plus\" aria-hidden=\"true\"></span>"
|
|
23
|
+
+ escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "add criteria", options) : helperMissing.call(depth0, "__", "add criteria", options)))
|
|
24
|
+
+ "</a>\n <button type=\"button\" class=\"applied-filters-summary hidden\" hidden aria-expanded=\"false\"></button>\n <select name=\"criteria-select\">\n <option></option>\n </select>\n </div>\n <div class=\"advanced-criteria-container\"></div>\n</div>\n";
|
|
25
|
+
return buffer;
|
|
26
|
+
});
|
|
27
|
+
function rmAdvancedSearchTpl(data, options, asString) {
|
|
28
|
+
var html = Template(data, options);
|
|
29
|
+
return (asString || true) ? html : $(html);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This program is free software; you can redistribute it and/or
|
|
34
|
+
* modify it under the terms of the GNU General Public License
|
|
35
|
+
* as published by the Free Software Foundation; under version 2
|
|
36
|
+
* of the License (non-upgradable).
|
|
37
|
+
*
|
|
38
|
+
* This program is distributed in the hope that it will be useful,
|
|
39
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
40
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
41
|
+
* GNU General Public License for more details.
|
|
42
|
+
*
|
|
43
|
+
* You should have received a copy of the GNU General Public License
|
|
44
|
+
* along with this program; if not, write to the Free Software
|
|
45
|
+
* Foundation, Inc., 31 Milk St # 960789 Boston, MA 02196 USA.
|
|
46
|
+
*
|
|
47
|
+
* Copyright (c) 2026 (original work) Open Assessment Technologies SA;
|
|
48
|
+
*/
|
|
49
|
+
const ns = 'resourcemgr';
|
|
50
|
+
const EVENT_NS = 'resourcemgrAssetSearch';
|
|
51
|
+
const DEFAULT_AJAX_TIMEOUT_MS = 30000;
|
|
52
|
+
const DEFAULT_ROOT_CLASS_URI = 'http://www.tao.lu/Ontologies/TAOMedia.rdf#Media';
|
|
53
|
+
const SEARCH_MODE_MODAL_TITLE = __('Please select a file from the resource manager.');
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Encode a URI the TAO way (same as views/js/uri.encode / tao_helpers_Uri::encode).
|
|
57
|
+
* ClassMetadata expects this form; searchModal gets it from the resource tree.
|
|
58
|
+
* @param {string} uri
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function encodeTaoUri(uri) {
|
|
62
|
+
if (!uri || typeof uri !== 'string') {
|
|
63
|
+
return uri;
|
|
64
|
+
}
|
|
65
|
+
if (/^[a-z]+_2_/i.test(uri)) {
|
|
66
|
+
return uri;
|
|
67
|
+
}
|
|
68
|
+
if (!/^http/.test(uri)) {
|
|
69
|
+
return uri;
|
|
70
|
+
}
|
|
71
|
+
return uri.replace(/:\/\//g, '_2_').replace(/#/g, '_3_').replace(/:/g, '_4_').replace(/\//g, '_1_').replace(/\./g, '_0_');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {Object} options - Resource Manager options
|
|
76
|
+
*/
|
|
77
|
+
function assetSearch(options) {
|
|
78
|
+
if (!options || !options.searchUrl) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const $container = options.$target;
|
|
82
|
+
const $fileSelector = $$1('.file-selector', $container);
|
|
83
|
+
const $searchRoot = $$1('.asset-search', $container);
|
|
84
|
+
const $searchToggle = $$1('.asset-search-toggle', $searchRoot);
|
|
85
|
+
const $scopeName = $$1('.asset-search-scope-name', $searchRoot);
|
|
86
|
+
const $scopeSubfolders = $$1('.asset-search-scope-subfolders', $searchRoot);
|
|
87
|
+
const $scope = $$1('.asset-search-scope', $searchRoot);
|
|
88
|
+
const $appliedCount = $$1('.asset-search-applied-count', $searchRoot);
|
|
89
|
+
const $input = $$1('.asset-search-input', $searchRoot);
|
|
90
|
+
const $filtersMount = $$1('.asset-search-filters', $searchRoot);
|
|
91
|
+
const $clearButton = $$1('.asset-search-clear', $searchRoot);
|
|
92
|
+
const $searchButton = $$1('.asset-search-submit', $searchRoot);
|
|
93
|
+
const $searchButtonSpinner = $$1('.asset-search-submit-spinner', $searchButton);
|
|
94
|
+
const $searchButtonLabel = $$1('.asset-search-submit-label', $searchButton);
|
|
95
|
+
const $error = $$1('.asset-search-error', $fileSelector);
|
|
96
|
+
const $errorMessage = $$1('.asset-search-error-message', $error);
|
|
97
|
+
const $retry = $$1('.asset-search-retry', $error);
|
|
98
|
+
const $resultsRegion = $$1('.files-wrapper', $fileSelector);
|
|
99
|
+
const $paginationContainer = $$1('.pagination-bottom', $container);
|
|
100
|
+
const $uploadSwitcher = $$1('.upload-switcher', $fileSelector);
|
|
101
|
+
const $modalTitle = $container.closest('.resourcemgr').find('> h2').first();
|
|
102
|
+
const browseModalTitle = options.title || ($modalTitle.length ? $modalTitle.text() : '');
|
|
103
|
+
const rootClassUri = options.rootClassUri || DEFAULT_ROOT_CLASS_URI;
|
|
104
|
+
const classMappingUrl = options.classMappingUrl;
|
|
105
|
+
const statusUrl = options.statusUrl;
|
|
106
|
+
const maxListSize = options.maxListSize || 5;
|
|
107
|
+
let scopePath = options.initialPath || options.path || '/';
|
|
108
|
+
let scopeLabel = '';
|
|
109
|
+
let query = '';
|
|
110
|
+
let metadata = {};
|
|
111
|
+
let sort = Object.assign({}, assetSearchContract.DEFAULT_SORT);
|
|
112
|
+
let page = 1;
|
|
113
|
+
let pageSize = assetSearchContract.DEFAULT_PAGE_SIZE;
|
|
114
|
+
let total = 0;
|
|
115
|
+
let searchMode = false;
|
|
116
|
+
let searchCollapsed = false;
|
|
117
|
+
let appliedFilterCount = 0;
|
|
118
|
+
let requestSeq = 0;
|
|
119
|
+
let searchInFlight = false;
|
|
120
|
+
let advancedSearch = null;
|
|
121
|
+
let shortcuts = null;
|
|
122
|
+
const previousTeardown = $container.data('assetSearchTeardown');
|
|
123
|
+
if (typeof previousTeardown === 'function') {
|
|
124
|
+
previousTeardown();
|
|
125
|
+
}
|
|
126
|
+
$searchRoot.removeClass('hidden').removeAttr('hidden');
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether browse tree content already lists directory children.
|
|
130
|
+
* Conservative: only true when FE knows dirs exist; omit phrase when unknown/leaf.
|
|
131
|
+
* @param {Object} [content]
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
function folderHasSubfolders(content) {
|
|
135
|
+
if (!content || content.empty === true) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(content.children)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return content.children.some(function (child) {
|
|
142
|
+
return Boolean(child && child.path);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether a value is a human folder name (not a browse path / media URI).
|
|
148
|
+
* Mediamanager initialPath is often `taomedia://…`; never show that in U8.
|
|
149
|
+
* @param {*} value
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
function isFolderDisplayLabel(value) {
|
|
153
|
+
if (value === null || typeof value === 'undefined') {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const text = String(value).trim();
|
|
157
|
+
if (!text) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
// URI / scheme browse keys (taomedia://…, http(s)://…, asset://…)
|
|
161
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(text)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Render U8/U9 scope line from folder label + optional browse payload.
|
|
169
|
+
* Keeps the previous display name until a real label arrives (avoids URI flash).
|
|
170
|
+
* @param {string} [folderLabel]
|
|
171
|
+
* @param {Object} [content]
|
|
172
|
+
*/
|
|
173
|
+
function updateScopeDisplay(folderLabel, content) {
|
|
174
|
+
if (isFolderDisplayLabel(folderLabel)) {
|
|
175
|
+
scopeLabel = String(folderLabel).trim();
|
|
176
|
+
}
|
|
177
|
+
const label = isFolderDisplayLabel(scopeLabel) ? scopeLabel : '';
|
|
178
|
+
$scopeName.text(label);
|
|
179
|
+
if (label) {
|
|
180
|
+
$scopeName.attr('title', label);
|
|
181
|
+
} else {
|
|
182
|
+
$scopeName.removeAttr('title');
|
|
183
|
+
}
|
|
184
|
+
if (folderHasSubfolders(content)) {
|
|
185
|
+
$scopeSubfolders.removeClass('hidden').removeAttr('hidden');
|
|
186
|
+
} else {
|
|
187
|
+
$scopeSubfolders.addClass('hidden').attr('hidden', 'hidden');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Count rendered metadata filter cards (U11 — text query is never included).
|
|
193
|
+
* @returns {number}
|
|
194
|
+
*/
|
|
195
|
+
function countRenderedFilters() {
|
|
196
|
+
return $$1('.advanced-criteria-container > .filter-container', $filtersMount).not('.invalid-criteria-warning-container').length;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether the author has typed a query or added filters (Clear all / Search enable).
|
|
201
|
+
* @returns {boolean}
|
|
202
|
+
*/
|
|
203
|
+
function hasPendingSearchParams() {
|
|
204
|
+
return Boolean(String($input.val() || '').trim()) || countRenderedFilters() > 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @returns {Object|null}
|
|
209
|
+
*/
|
|
210
|
+
function getCriteriaState() {
|
|
211
|
+
if (!advancedSearch || typeof advancedSearch.getState !== 'function') {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return advancedSearch.getState();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @returns {boolean}
|
|
219
|
+
*/
|
|
220
|
+
function canSubmitSearch() {
|
|
221
|
+
syncCriteriaFromDom();
|
|
222
|
+
return assetSearchContract.canSubmitScopedAssetSearch(buildTextQuery(), getCriteriaState());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Clear all is hidden until params change; Search is disabled (grey) until then.
|
|
227
|
+
*/
|
|
228
|
+
function updateSearchActionState() {
|
|
229
|
+
const pending = hasPendingSearchParams();
|
|
230
|
+
if (pending || searchMode) {
|
|
231
|
+
$clearButton.removeClass('hidden').removeAttr('hidden');
|
|
232
|
+
} else {
|
|
233
|
+
$clearButton.addClass('hidden').attr('hidden', 'hidden');
|
|
234
|
+
}
|
|
235
|
+
if (searchInFlight) {
|
|
236
|
+
$searchButton.prop('disabled', true);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (canSubmitSearch()) {
|
|
240
|
+
$searchButton.prop('disabled', false);
|
|
241
|
+
} else {
|
|
242
|
+
$searchButton.prop('disabled', true);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Collapsed-header applied-filter count (U11). Text query is never counted.
|
|
248
|
+
* When collapsed with filters, replace "Searching in" with applied-filters-summary text.
|
|
249
|
+
*/
|
|
250
|
+
function updateCollapsedAppliedCount() {
|
|
251
|
+
appliedFilterCount = countRenderedFilters();
|
|
252
|
+
if (searchCollapsed && appliedFilterCount > 0) {
|
|
253
|
+
const $summary = $$1('.applied-filters-summary', $filtersMount);
|
|
254
|
+
const summaryText = String($summary.length && $summary.text() || '').trim();
|
|
255
|
+
const countText = summaryText || (appliedFilterCount === 1 ? __('1 filter applied') : __('%s filters applied').replace('%s', String(appliedFilterCount)));
|
|
256
|
+
$scope.addClass('hidden').prop('hidden', true);
|
|
257
|
+
$appliedCount.text(countText).removeClass('hidden').prop('hidden', false);
|
|
258
|
+
} else {
|
|
259
|
+
$scope.removeClass('hidden').prop('hidden', false);
|
|
260
|
+
$appliedCount.addClass('hidden').prop('hidden', true).text('');
|
|
261
|
+
}
|
|
262
|
+
updateSearchActionState();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Expand or collapse the Search section (U10). Session-only; default expanded.
|
|
267
|
+
* @param {boolean} collapsed
|
|
268
|
+
*/
|
|
269
|
+
function setSearchCollapsed(collapsed) {
|
|
270
|
+
searchCollapsed = Boolean(collapsed);
|
|
271
|
+
$searchRoot.toggleClass('is-collapsed', searchCollapsed);
|
|
272
|
+
$searchToggle.attr('aria-expanded', searchCollapsed ? 'false' : 'true');
|
|
273
|
+
$searchToggle.find('.asset-search-chevron').toggleClass('icon-up', !searchCollapsed).toggleClass('icon-down', searchCollapsed);
|
|
274
|
+
updateCollapsedAppliedCount();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Push pending text-criterion DOM values into advancedSearch state
|
|
279
|
+
* (covers Search click before blur when only change was bound historically).
|
|
280
|
+
*/
|
|
281
|
+
function syncCriteriaFromDom() {
|
|
282
|
+
$$1('.advanced-criteria-container .filter-container[data-type="text"] input', $filtersMount).trigger('change');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Universal text from the search input (metadata is sent separately).
|
|
287
|
+
* @returns {string}
|
|
288
|
+
*/
|
|
289
|
+
function buildTextQuery() {
|
|
290
|
+
return String($input.val() || '').trim();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Structured metadata map from Advanced Search state.
|
|
295
|
+
* @returns {Object.<string, string>}
|
|
296
|
+
*/
|
|
297
|
+
function buildMetadata() {
|
|
298
|
+
syncCriteriaFromDom();
|
|
299
|
+
if (!advancedSearch || typeof advancedSearch.getState !== 'function') {
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
return assetSearchContract.buildMetadataFromCriteriaState(advancedSearch.getState());
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @returns {boolean}
|
|
307
|
+
*/
|
|
308
|
+
function hasActiveSearch() {
|
|
309
|
+
return Boolean(query) || Object.keys(metadata).length > 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Keep the search field focused whenever the picker opens.
|
|
314
|
+
*/
|
|
315
|
+
function focusSearchInput() {
|
|
316
|
+
window.setTimeout(function () {
|
|
317
|
+
if ($input.is(':visible') && !$input.prop('disabled')) {
|
|
318
|
+
$input.trigger('focus');
|
|
319
|
+
}
|
|
320
|
+
}, 0);
|
|
321
|
+
}
|
|
322
|
+
$container.on(`opened.modal.${EVENT_NS}`, focusSearchInput);
|
|
323
|
+
focusSearchInput();
|
|
324
|
+
setSearchCollapsed(false);
|
|
325
|
+
// Do not seed the scope name from scopePath: mediamanager paths are URIs
|
|
326
|
+
// (taomedia://…) and flash into the header before folderselect brings a label.
|
|
327
|
+
updateScopeDisplay(scopeLabel);
|
|
328
|
+
updateSearchActionState();
|
|
329
|
+
$input.on(`input.${EVENT_NS}`, updateSearchActionState);
|
|
330
|
+
|
|
331
|
+
// advancedSearch fires criteriachange on add/remove only — re-evaluate Search when values change.
|
|
332
|
+
$filtersMount.on(`input.${EVENT_NS} change.${EVENT_NS}`, '.advanced-criteria-container input', updateSearchActionState);
|
|
333
|
+
$searchToggle.on(`click.${EVENT_NS}`, function (e) {
|
|
334
|
+
e.preventDefault();
|
|
335
|
+
setSearchCollapsed(!searchCollapsed);
|
|
336
|
+
});
|
|
337
|
+
$container.on(`folderselect.${ns}.${EVENT_NS}`, function (e, label, files, folderPath, content) {
|
|
338
|
+
const previousPath = scopePath;
|
|
339
|
+
if (folderPath) {
|
|
340
|
+
scopePath = folderPath;
|
|
341
|
+
} else if (!searchMode && label) {
|
|
342
|
+
scopePath = label;
|
|
343
|
+
}
|
|
344
|
+
page = 1;
|
|
345
|
+
updateScopeDisplay(label || content && content.label, content);
|
|
346
|
+
if (searchMode && hasActiveSearch() && scopePath !== previousPath) {
|
|
347
|
+
runSearch();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
$container.on(`folderpath.${ns}.${EVENT_NS}`, function (e, folderPath, folderLabel) {
|
|
351
|
+
const previousPath = scopePath;
|
|
352
|
+
scopePath = folderPath || scopePath;
|
|
353
|
+
page = 1;
|
|
354
|
+
if (folderLabel) {
|
|
355
|
+
updateScopeDisplay(folderLabel);
|
|
356
|
+
}
|
|
357
|
+
if (searchMode && hasActiveSearch() && scopePath !== previousPath) {
|
|
358
|
+
runSearch();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
const ajaxTimeoutMs = Number.isFinite(Number(options.ajaxTimeoutMs)) && Number(options.ajaxTimeoutMs) > 0 ? Number(options.ajaxTimeoutMs) : DEFAULT_AJAX_TIMEOUT_MS;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Relabel shared advancedSearch "add criteria" control to match RM UX.
|
|
365
|
+
*/
|
|
366
|
+
function applyAddFilterLabel() {
|
|
367
|
+
const $link = $$1('.add-criteria-container a', $filtersMount);
|
|
368
|
+
if (!$link.length) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
$link.find('.icon-add').removeClass('icon-add').addClass('icon-plus');
|
|
372
|
+
$link.contents().filter(function () {
|
|
373
|
+
return this.nodeType === 3;
|
|
374
|
+
}).remove();
|
|
375
|
+
$link.append(document.createTextNode(` ${__('Add filter')}`));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Whether the Add filter select currently has selectable criteria.
|
|
380
|
+
* @returns {boolean}
|
|
381
|
+
*/
|
|
382
|
+
function hasCriteriaOptions() {
|
|
383
|
+
return $$1('.add-criteria-container select option', $filtersMount).filter(function () {
|
|
384
|
+
return Boolean(this.value);
|
|
385
|
+
}).length > 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Keep Add filter hidden when Advanced Search is off or criteria are unavailable.
|
|
390
|
+
* @param {boolean} ready
|
|
391
|
+
*/
|
|
392
|
+
function setAddFilterReady(ready) {
|
|
393
|
+
const $addCriteria = $$1('.add-criteria-container', $filtersMount);
|
|
394
|
+
if (!$addCriteria.length) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!advancedSearch || !advancedSearch.isEnabled()) {
|
|
398
|
+
$addCriteria.addClass('disabled');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
$addCriteria.toggleClass('disabled', !ready);
|
|
402
|
+
if (ready) {
|
|
403
|
+
applyAddFilterLabel();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Mount Advanced Search criteria UI and load Asset-class metadata.
|
|
409
|
+
*/
|
|
410
|
+
function initAdvancedSearch() {
|
|
411
|
+
if (!statusUrl) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
advancedSearch = advancedSearchFactory({
|
|
415
|
+
renderTo: $filtersMount,
|
|
416
|
+
statusUrl: statusUrl,
|
|
417
|
+
rootClassUri: rootClassUri,
|
|
418
|
+
hideCriteria: options.hideCriteria,
|
|
419
|
+
collapsibleCriteria: true,
|
|
420
|
+
layoutTemplate: rmAdvancedSearchTpl
|
|
421
|
+
});
|
|
422
|
+
advancedSearch.on('ready', function () {
|
|
423
|
+
applyAddFilterLabel();
|
|
424
|
+
if (!rootClassUri || !classMappingUrl || !advancedSearch.isEnabled()) {
|
|
425
|
+
setAddFilterReady(false);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// Visible while ClassMetadata loads (icon-loop); click is a no-op until options exist.
|
|
429
|
+
setAddFilterReady(true);
|
|
430
|
+
const route = urlUtil.build(classMappingUrl, {
|
|
431
|
+
classUri: encodeTaoUri(rootClassUri),
|
|
432
|
+
maxListSize: maxListSize
|
|
433
|
+
});
|
|
434
|
+
advancedSearch.updateCriteria(route).then(function () {
|
|
435
|
+
applyAddFilterLabel();
|
|
436
|
+
// Only keep Add filter if ClassMetadata returned selectable criteria.
|
|
437
|
+
// Opening select2 with zero options only shows an empty drop mask.
|
|
438
|
+
setAddFilterReady(hasCriteriaOptions());
|
|
439
|
+
}).catch(function () {
|
|
440
|
+
applyAddFilterLabel();
|
|
441
|
+
setAddFilterReady(false);
|
|
442
|
+
});
|
|
443
|
+
}).on('criteriachange', function () {
|
|
444
|
+
updateCollapsedAppliedCount();
|
|
445
|
+
updateSearchActionState();
|
|
446
|
+
}).on('error', function () {
|
|
447
|
+
setAddFilterReady(false);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Label override even if status request fails before ready
|
|
451
|
+
window.setTimeout(applyAddFilterLabel, 0);
|
|
452
|
+
window.setTimeout(applyAddFilterLabel, 300);
|
|
453
|
+
}
|
|
454
|
+
initAdvancedSearch();
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Detach asset-search handlers and destroy Advanced Search.
|
|
458
|
+
*/
|
|
459
|
+
function teardown() {
|
|
460
|
+
requestSeq += 1;
|
|
461
|
+
searchMode = false;
|
|
462
|
+
$fileSelector.removeClass('search-mode');
|
|
463
|
+
$uploadSwitcher.removeClass('hidden');
|
|
464
|
+
hideLoading();
|
|
465
|
+
$container.off(`.${EVENT_NS}`);
|
|
466
|
+
$filtersMount.off(`.${EVENT_NS}`);
|
|
467
|
+
$input.off(`.${EVENT_NS}`);
|
|
468
|
+
$retry.off(`.${EVENT_NS}`);
|
|
469
|
+
$clearButton.off(`.${EVENT_NS}`);
|
|
470
|
+
$searchButton.off(`.${EVENT_NS}`);
|
|
471
|
+
$searchToggle.off(`.${EVENT_NS}`);
|
|
472
|
+
if (shortcuts && typeof shortcuts.clear === 'function') {
|
|
473
|
+
shortcuts.clear();
|
|
474
|
+
}
|
|
475
|
+
shortcuts = null;
|
|
476
|
+
if (advancedSearch && typeof advancedSearch.destroy === 'function') {
|
|
477
|
+
advancedSearch.destroy();
|
|
478
|
+
}
|
|
479
|
+
advancedSearch = null;
|
|
480
|
+
appliedFilterCount = 0;
|
|
481
|
+
searchCollapsed = false;
|
|
482
|
+
}
|
|
483
|
+
$container.data('assetSearchTeardown', teardown);
|
|
484
|
+
$container.on(`destroy.${ns}.${EVENT_NS}`, teardown);
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Apply the current input/criteria and run search, or exit when empty.
|
|
488
|
+
* Primary triggers: Search button and Enter (same as ui/searchModal).
|
|
489
|
+
*/
|
|
490
|
+
function submitSearch() {
|
|
491
|
+
if (!canSubmitSearch()) {
|
|
492
|
+
updateSearchActionState();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
query = buildTextQuery();
|
|
496
|
+
metadata = buildMetadata();
|
|
497
|
+
page = 1;
|
|
498
|
+
if (!hasActiveSearch()) {
|
|
499
|
+
exitSearchMode();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
enterSearchMode();
|
|
503
|
+
runSearch();
|
|
504
|
+
}
|
|
505
|
+
$searchButton.on(`click.${EVENT_NS}`, function (e) {
|
|
506
|
+
e.preventDefault();
|
|
507
|
+
submitSearch();
|
|
508
|
+
});
|
|
509
|
+
$clearButton.on(`click.${EVENT_NS}`, function (e) {
|
|
510
|
+
e.preventDefault();
|
|
511
|
+
clearSearch();
|
|
512
|
+
});
|
|
513
|
+
shortcuts = shortcutRegistry($input);
|
|
514
|
+
shortcuts.clear().add('enter', function () {
|
|
515
|
+
submitSearch();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Clear text, criteria, and return to browse mode.
|
|
520
|
+
*/
|
|
521
|
+
function clearSearch() {
|
|
522
|
+
$input.val('');
|
|
523
|
+
if (advancedSearch && typeof advancedSearch.clear === 'function') {
|
|
524
|
+
advancedSearch.clear();
|
|
525
|
+
}
|
|
526
|
+
// clear() restores select options; keep Add filter when criteria exist
|
|
527
|
+
if (advancedSearch && advancedSearch.isEnabled()) {
|
|
528
|
+
setAddFilterReady(hasCriteriaOptions());
|
|
529
|
+
}
|
|
530
|
+
query = '';
|
|
531
|
+
metadata = {};
|
|
532
|
+
appliedFilterCount = 0;
|
|
533
|
+
updateCollapsedAppliedCount();
|
|
534
|
+
exitSearchMode();
|
|
535
|
+
focusSearchInput();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Retry the last failed search request.
|
|
540
|
+
* @param {jQuery.Event} e
|
|
541
|
+
*/
|
|
542
|
+
$retry.on(`click.${EVENT_NS}`, function (e) {
|
|
543
|
+
e.preventDefault();
|
|
544
|
+
runSearch();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Re-run search when the table sort changes in search mode.
|
|
549
|
+
* @param {jQuery.Event} e
|
|
550
|
+
* @param {{field: string, direction: string}} nextSort
|
|
551
|
+
*/
|
|
552
|
+
$container.on(`sortchange.${ns}.${EVENT_NS}`, function (e, nextSort) {
|
|
553
|
+
sort = Object.assign({}, assetSearchContract.DEFAULT_SORT, nextSort || {});
|
|
554
|
+
if (!searchMode || !hasActiveSearch()) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
page = 1;
|
|
558
|
+
runSearch({
|
|
559
|
+
quiet: true
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
$container.on(`requestexitsearch.${ns}.${EVENT_NS}`, function () {
|
|
563
|
+
if (searchMode) {
|
|
564
|
+
exitSearchMode();
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Enter search mode UI.
|
|
570
|
+
*/
|
|
571
|
+
function enterSearchMode() {
|
|
572
|
+
searchMode = true;
|
|
573
|
+
$fileSelector.addClass('search-mode');
|
|
574
|
+
$uploadSwitcher.addClass('hidden');
|
|
575
|
+
if ($modalTitle.length && browseModalTitle) {
|
|
576
|
+
$modalTitle.text(SEARCH_MODE_MODAL_TITLE);
|
|
577
|
+
}
|
|
578
|
+
$container.trigger(`searchmode.${ns}`, [true]);
|
|
579
|
+
updateSearchActionState();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Leave search mode and reload the browse listing with the current sort.
|
|
584
|
+
*/
|
|
585
|
+
function exitSearchMode() {
|
|
586
|
+
const wasSearch = searchMode;
|
|
587
|
+
searchMode = false;
|
|
588
|
+
requestSeq += 1;
|
|
589
|
+
$fileSelector.removeClass('search-mode');
|
|
590
|
+
$uploadSwitcher.removeClass('hidden');
|
|
591
|
+
if ($modalTitle.length && browseModalTitle) {
|
|
592
|
+
$modalTitle.text(browseModalTitle);
|
|
593
|
+
}
|
|
594
|
+
hideLoading();
|
|
595
|
+
hideError();
|
|
596
|
+
$paginationContainer.empty();
|
|
597
|
+
$container.trigger(`searchmode.${ns}`, [false]);
|
|
598
|
+
if (wasSearch) {
|
|
599
|
+
$container.trigger(`searchclear.${ns}`, [scopePath]);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Execute the scoped search request.
|
|
605
|
+
* @param {{quiet?: boolean}} [runOptions]
|
|
606
|
+
*/
|
|
607
|
+
function runSearch(runOptions) {
|
|
608
|
+
if (!hasActiveSearch()) {
|
|
609
|
+
exitSearchMode();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const quiet = Boolean(runOptions && runOptions.quiet);
|
|
613
|
+
const seq = ++requestSeq;
|
|
614
|
+
if (quiet) {
|
|
615
|
+
$resultsRegion.attr('aria-busy', 'true');
|
|
616
|
+
} else {
|
|
617
|
+
showLoading();
|
|
618
|
+
}
|
|
619
|
+
hideError();
|
|
620
|
+
const data = assetSearchContract.buildSearchRequestParams({
|
|
621
|
+
path: scopePath,
|
|
622
|
+
query,
|
|
623
|
+
metadata,
|
|
624
|
+
sort,
|
|
625
|
+
page,
|
|
626
|
+
pageSize,
|
|
627
|
+
pathParam: options.pathParam || 'path',
|
|
628
|
+
params: options.params
|
|
629
|
+
});
|
|
630
|
+
$$1.ajax({
|
|
631
|
+
url: options.searchUrl,
|
|
632
|
+
method: 'GET',
|
|
633
|
+
dataType: 'json',
|
|
634
|
+
timeout: ajaxTimeoutMs,
|
|
635
|
+
data
|
|
636
|
+
}).done(function (response) {
|
|
637
|
+
if (seq !== requestSeq) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (quiet) {
|
|
641
|
+
$resultsRegion.attr('aria-busy', 'false');
|
|
642
|
+
} else {
|
|
643
|
+
hideLoading();
|
|
644
|
+
}
|
|
645
|
+
let normalized = assetSearchContract.normalizeSearchResponse(response);
|
|
646
|
+
// Browse-shaped payloads (`children`, no `items`) are filtered locally when
|
|
647
|
+
// browseSearchFallback is enabled (PoC / dev). Real search APIs return `items`.
|
|
648
|
+
// Metadata filters require indexed search and cannot use this fallback.
|
|
649
|
+
if (assetSearchContract.isBrowseShapedSearchPayload(response)) {
|
|
650
|
+
if (options.browseSearchFallback === true) {
|
|
651
|
+
normalized = assetSearchContract.applyLocalSearchFallback(normalized, {
|
|
652
|
+
query,
|
|
653
|
+
metadata,
|
|
654
|
+
sort,
|
|
655
|
+
page,
|
|
656
|
+
pageSize,
|
|
657
|
+
filters: options.params && options.params.filters
|
|
658
|
+
});
|
|
659
|
+
} else {
|
|
660
|
+
normalized = {
|
|
661
|
+
items: [],
|
|
662
|
+
total: 0,
|
|
663
|
+
page: 1,
|
|
664
|
+
pageSize
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
total = normalized.total;
|
|
669
|
+
page = normalized.page;
|
|
670
|
+
pageSize = normalized.pageSize;
|
|
671
|
+
let emptyMessage = '';
|
|
672
|
+
if (normalized.total === 0) {
|
|
673
|
+
if (normalized.metadataUnsupported) {
|
|
674
|
+
emptyMessage = __('Metadata filters require indexed search.');
|
|
675
|
+
} else if (assetSearchContract.isBrowseShapedSearchPayload(response) && options.browseSearchFallback !== true) {
|
|
676
|
+
emptyMessage = __('Search is unavailable for this endpoint.');
|
|
677
|
+
} else {
|
|
678
|
+
emptyMessage = __('No assets match your search.');
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Render search rows as-is (no client MIME/auth filtering).
|
|
683
|
+
$container.trigger(`searchresults.${ns}`, [{
|
|
684
|
+
query,
|
|
685
|
+
metadata,
|
|
686
|
+
path: scopePath,
|
|
687
|
+
items: normalized.items,
|
|
688
|
+
total: normalized.total,
|
|
689
|
+
page: normalized.page,
|
|
690
|
+
pageSize: normalized.pageSize,
|
|
691
|
+
sort: Object.assign({}, sort),
|
|
692
|
+
initialSelection: options.initialSelection,
|
|
693
|
+
emptyMessage
|
|
694
|
+
}]);
|
|
695
|
+
renderPagination();
|
|
696
|
+
}).fail(function () {
|
|
697
|
+
if (seq !== requestSeq) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (quiet) {
|
|
701
|
+
$resultsRegion.attr('aria-busy', 'false');
|
|
702
|
+
} else {
|
|
703
|
+
hideLoading();
|
|
704
|
+
}
|
|
705
|
+
page = 1;
|
|
706
|
+
total = 0;
|
|
707
|
+
$paginationContainer.empty();
|
|
708
|
+
const message = __('Unable to search assets. Please try again.');
|
|
709
|
+
showError(message);
|
|
710
|
+
$container.trigger(`searchresults.${ns}`, [{
|
|
711
|
+
query,
|
|
712
|
+
metadata,
|
|
713
|
+
path: scopePath,
|
|
714
|
+
items: [],
|
|
715
|
+
total: 0,
|
|
716
|
+
page: 1,
|
|
717
|
+
pageSize,
|
|
718
|
+
sort: Object.assign({}, sort),
|
|
719
|
+
error: true
|
|
720
|
+
}]);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Render search pagination when more than one page is available.
|
|
726
|
+
*/
|
|
727
|
+
function renderPagination() {
|
|
728
|
+
$paginationContainer.empty();
|
|
729
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
730
|
+
if (!(total > 0 && totalPages > 1)) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
paginationComponent({
|
|
734
|
+
mode: 'simple',
|
|
735
|
+
activePage: page,
|
|
736
|
+
totalPages
|
|
737
|
+
}).on('prev', function () {
|
|
738
|
+
page -= 1;
|
|
739
|
+
runSearch({
|
|
740
|
+
quiet: true
|
|
741
|
+
});
|
|
742
|
+
}).on('next', function () {
|
|
743
|
+
page += 1;
|
|
744
|
+
runSearch({
|
|
745
|
+
quiet: true
|
|
746
|
+
});
|
|
747
|
+
}).render($paginationContainer);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Show in-button search progress (spinner + Searching label).
|
|
752
|
+
*/
|
|
753
|
+
function showLoading() {
|
|
754
|
+
searchInFlight = true;
|
|
755
|
+
$searchButton.prop('disabled', true).addClass('is-searching').attr('aria-busy', 'true');
|
|
756
|
+
$searchButtonSpinner.removeClass('hidden');
|
|
757
|
+
$searchButtonLabel.text(__('Searching'));
|
|
758
|
+
$resultsRegion.attr('aria-busy', 'true');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Restore Search button after request completes.
|
|
763
|
+
*/
|
|
764
|
+
function hideLoading() {
|
|
765
|
+
searchInFlight = false;
|
|
766
|
+
$searchButton.removeClass('is-searching').attr('aria-busy', 'false');
|
|
767
|
+
$searchButtonSpinner.addClass('hidden');
|
|
768
|
+
$searchButtonLabel.text(__('Search'));
|
|
769
|
+
$resultsRegion.attr('aria-busy', 'false');
|
|
770
|
+
updateSearchActionState();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Show a recoverable search error.
|
|
775
|
+
* @param {string} message
|
|
776
|
+
*/
|
|
777
|
+
function showError(message) {
|
|
778
|
+
$error.removeClass('hidden').removeAttr('hidden');
|
|
779
|
+
$errorMessage.text(message);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Hide the search error UI.
|
|
784
|
+
*/
|
|
785
|
+
function hideError() {
|
|
786
|
+
$error.addClass('hidden').attr('hidden', 'hidden');
|
|
787
|
+
$errorMessage.text('');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return assetSearch;
|
|
792
|
+
|
|
793
|
+
});
|