"use strict";
define([
"models/searchSelect/SearchSelect",
"collections/queryFields/QueryFields",
], (SearchSelect, QueryFields) => {
/**
* @class QueryFieldSearchSelect
* @classdesc An extension of SearchSelect that sets the options to the query
* fields (e.g. Solr fields) available for searching.
* @classcategory Models/SearchSelect
* @since 2.31.0
* @augments Backbone.Model
*/
const QueryFieldSearchSelect = SearchSelect.extend({
/** @lends QueryFieldSearchSelect.prototype */
/**
* An additional field object contains the properties an additional query
* field to add that are required to render it correctly. An additional
* query field is one that does not actually exist in the query service
* index.
* @typedef {object} AdditionalField
* @property {string} name - A unique ID to represent this field. It must
* not match the name of any other query fields.
* @property {string[]} fields - The list of real query fields that this
* abstracted field will represent. It must exactly match the names of the
* query fields that actually exist.
* @property {string} label - A user-facing label to display.
* @property {string} description - A description for this field.
* @property {string} category - The name of the category under which to
* place this field. It must match one of the category names for an existing
* query field.
* @since 2.15.0
*/
/**
* @returns {object} The default attributes for this model
* @property {AdditionalField[]} addFields - A list of additional fields
* which are not retrieved from the query service index, but which should be
* added to the list of options. This can be used to add abstracted fields
* which are a combination of multiple query fields, or to add a duplicate
* field that has a different label.
* @property {string[]} commonFields - A list of query fields names to
* display at the top of the menu, above all other category headers
* @property {string[]} categoriesToAlphabetize - The names of categories
* that should have items sorted alphabetically. Names must exactly match
* those in the {@link QueryField#categoriesMap Query Field model}
* @property {boolean} excludeNonSearchable - Whether or not to exclude
* fields which are not searchable. Set to false to keep query fields that
* are not searchable in the returned list
* @property {string} submenuStyle - The submenu style is set to "accordion"
* by default for this submodel
* @property {string[]} excludeFields - A list of query field names to
* exclude from the list of options.
*/
defaults() {
return {
...SearchSelect.prototype.defaults(),
placeholderText: "Search for or select a field",
inputLabel: "Select one or more metadata fields to query",
addFields: [],
commonFields: ["text", "documents-special-field"],
categoriesToAlphabetize: ["General"],
excludeNonSearchable: true,
submenuStyle: "accordion",
excludeFields: [],
};
},
/**
* Initializes the QueryFieldSearchSelect model
* @param {object} attributes - A literal object with model attributes
* @param {object} options - A literal object with options
* @param {boolean} options.collectionQuery - Set this to true to
* automatically set the excludeFields and addFields to the collection query
* defaults set in the appModel. See
* {@link AppModel#collectionQueryExcludeFields} and
* {@link AppModel#collectionQuerySpecialFields}.
*/
initialize(attributes, options = {}) {
if (options.collectionQuery) {
this.set(
"excludeFields",
MetacatUI.appModel.get("collectionQueryExcludeFields"),
);
this.set(
"addFields",
MetacatUI.appModel.get("collectionQuerySpecialFields"),
);
}
this.getQueryFieldOptions();
SearchSelect.prototype.initialize.call(this, attributes, options);
},
/**
* Fetches the query fields from the query service, converts them to the
* format required by the SearchSelectView, and sets them as the options
* for this model
*/
async getQueryFieldOptions() {
const queryFields = await this.fetchQueryFields();
const fields = queryFields.toJSON();
const excludedFields = this.excludeFields(fields);
const addedFields = this.addFields(excludedFields);
const options = addedFields.map(this.fieldToOption);
const sortedOptions = this.sortFields(options);
this.updateOptions(sortedOptions);
},
/**
* Fetches the query fields from the query service
* @returns {Promise} A promise that resolves with the query fields
* collection
*/
async fetchQueryFields() {
return new Promise((resolve) => {
if (MetacatUI.queryFields?.length) {
resolve(MetacatUI.queryFields);
return;
}
if (!MetacatUI.queryFields) MetacatUI.queryFields = new QueryFields();
MetacatUI.queryFields.fetch({
success: () => resolve(MetacatUI.queryFields),
error: () => resolve([]),
});
});
},
/**
* Filters out any objects in the fieldsJSON array that have a ".name"
* property that matches one of the strings in the fieldsToExclude array
* @param {object[]} fieldsJSON - JSON returned from QueryFields.toJSON()
* @returns {object[]} The filtered fieldsJSON array
*/
excludeFields(fieldsJSON) {
const fieldsToExclude = this.get("excludeFields");
const excludeNonSearchable = this.get("excludeNonSearchable");
let filteredJSON = fieldsJSON;
if (fieldsToExclude?.length) {
filteredJSON = fieldsJSON.filter(
(field) => !fieldsToExclude.includes(field.name),
);
}
if (excludeNonSearchable) {
filteredJSON = filteredJSON.filter(
(field) => field.searchable !== false && field.searchable !== "false",
);
}
return filteredJSON;
},
/**
* Adds fields to the fieldsJSON array that are specified in the addFields
* property of this model
* @param {object[]} fieldsJSON - JSON returned from QueryFields.toJSON()
* @returns {object[]} The fieldsJSON array with additional fields added
*/
addFields(fieldsJSON) {
const fieldsToAdd = this.get("addFields");
if (!fieldsToAdd?.length) return fieldsJSON;
const fieldsWithCategoryInfo = fieldsToAdd.map((fieldToAdd) => {
const field = { ...fieldToAdd };
if (field.category) {
const categoryInfo = fieldsJSON.find(
(f) => f.category === field.category,
);
if (categoryInfo) {
field.icon = field.icon || categoryInfo.icon;
field.categoryOrder =
field.categoryOrder || categoryInfo.categoryOrder;
}
}
return field;
});
return fieldsJSON.concat(fieldsWithCategoryInfo);
},
/**
* Converts an object that represents a QueryField model to the format
* specified by the SearchSelectView.options
* @param {object} field An object with properties corresponding to a
* QueryField model
* @returns {object} An object with properties that match the format
* specified by the SearchSelectView.options
*/
fieldToOption(field) {
if (!field) return {};
return {
label: field.label || field.name,
value: field.name,
description: field.friendlyDescription || field.description,
icon: field.icon,
category: field.category,
categoryOrder: field.categoryOrder,
type: field.type,
};
},
/**
* Sorts the fieldsJSON array by categoryOrder and then alphabetically
* within each category if the category is specified in the
* categoriesToAlphabetize property of this model.
* @param {object[]} unsortedOptions - An array of objects that represent
* attributes for SearchSelectOptions.
* @returns {object[]} The sorted options
*/
sortFields(unsortedOptions) {
const options = unsortedOptions;
const commonFields = this.get("commonFields");
if (commonFields?.length) {
commonFields.forEach((commonFieldName) => {
const i = options.findIndex(
(field) => field.value === commonFieldName,
);
if (i > 0) {
options[i] = {
...options[i],
category: "",
categoryOrder: 0,
icon: "star",
};
}
});
}
options.sort((a, b) => a.categoryOrder - b.categoryOrder);
const sortCategories = this.get("categoriesToAlphabetize");
if (sortCategories?.length) {
sortCategories.forEach((categoryName) => {
const category = options.filter(
(field) => field.category === categoryName,
);
category.sort((a, b) =>
a.label.toLowerCase().localeCompare(b.label.toLowerCase()),
);
const categoryIndex = options.findIndex(
(field) => field.category === categoryName,
);
options.splice(categoryIndex, category.length, ...category);
});
}
return options;
},
/**
* For options that are added fields, not real query fields from the query
* service, this method sets fields and types attributes on the option model
* that are the real query fields that the added field represents.
* @param {SearchSelectOption} option - The option model to update
*/
setAddedFieldDetails(option) {
const addFields = this.get("addFields");
const addedField = addFields?.find(
(field) => field.name === option?.get("value"),
);
if (!addedField) return;
const specialField = { ...addedField };
const { fields } = specialField;
const types = fields.map((fieldName) => {
const realField = MetacatUI.queryFields.findWhere({ name: fieldName });
return realField ? realField.get("type") : "special field";
});
option.set({
fields,
types,
});
},
/**
* Extends the isValidValue method of the SearchSelect model to allow for
* the addition of fields that are excluded by default, if they are selected
* @param {string} value - The value to check
* @returns {boolean} - Returns true if the value is valid, false otherwise
*/
isValidValue(value) {
const excludedFields = this.get("excludeFields");
if (!excludedFields || !excludedFields.includes(value)) {
return SearchSelect.prototype.isValidValue.call(this, value);
}
let newField = MetacatUI.queryFields.findWhere({ name: value });
if (newField) {
newField = this.fieldToOption(newField.toJSON());
}
this.get("options").add(newField);
return true;
},
});
return QueryFieldSearchSelect;
});