# Source: src/js/views/queryBuilder/QueryRuleView.js

    define([
      "jquery",
      "underscore",
      "backbone",
      "views/searchSelect/SearchableSelectView",
      "views/searchSelect/QueryFieldSelectView",
      "views/searchSelect/NodeSelectView",
      "views/searchSelect/AccountSelectView",
      "views/filters/NumericFilterView",
      "views/filters/DateFilterView",
      "views/searchSelect/ObjectFormatSelectView",
      "views/searchSelect/AnnotationFilterView",
      "models/filters/Filter",
      "models/filters/BooleanFilter",
      "models/filters/NumericFilter",
      "models/filters/DateFilter"
    ],
      function (
        $, _, Backbone, SearchableSelect, QueryFieldSelect, NodeSelect, AccountSelect,
        NumericFilterView, DateFilterView, ObjectFormatSelect, AnnotationFilter,
        Filter, BooleanFilter,  NumericFilter, DateFilter
      ) {

        /**
         * @class QueryRuleView
         * @classdesc A view that provides an UI for a user to construct a single filter that
         * is part of a complex query
         * @classcategory Views/QueryBuilder
         * @screenshot views/QueryRuleView.png
         * @extends Backbone.View
         * @constructor
         * @since 2.14.0
         */
        return Backbone.View.extend(
          /** @lends QueryRuleView.prototype */
          {
            /**
             * The type of View this is
             * @type {string}
             */
            type: "QueryRule",

            /**
             * The HTML class names for this view element
             * @type {string}
             */
            className: "query-rule",

            /**
             * The class to add to the rule number and other information on the left
             * @type {string}
             */
            ruleInfoClass: "rule-info",

            /**
             * The class to add to the field select element
             * @type {string}
             */
            fieldsClass: "field",

            /**
             * The class to add to the operator select element
             * @type {string}
             */
            operatorClass: "operator",

            /**
             * The class to add to the value select element
             * @type {string}
             */
            valuesClass: "value",

            /**
             * The class to add to the element that a user should click to remove a rule.
             * @type {string}
             */
            removeClass: "remove-rule",

            /**
             * An ID for the element that a user should click to remove a rule. A unique ID
             * will be appended to this ID, and the ID will be added to the template.
             * @type {string}
             */
            removeRuleID: "remove-rule-",

            /**
             * The maximum number of levels of nested Rule Groups (i.e. nested FilterGroup
             * models) that a user is permitted to build in the Query Builder that contains
             * this rule. This value should be passed to the rule by the parent Query Builder.
             * This value minus one will be passed on to any child Query Builders (those that
             * render nested FilterGroup models).
             * @type {number}
             * @since 2.17.0
             */
            nestedLevelsAllowed: 1,

            /**
             * An array of hex color codes used to help distinguish between different rules.
             * If this is a nested Query Rule, and the rule should inherit its colour from
             * the parent Query Rule, then set ruleColorPalette to "inherit".
             * @type {string[]|string}
             */
            ruleColorPalette: ["#44AA99", "#137733", "#c9a538", "#CC6677", "#882355",
              "#AA4499", "#332288"],

            /**
             * Search index fields to exclude in the metadata field selector
             * @type {string[]}
             */
            excludeFields: [],

            /**
             * Query fields to exclude in the metadata field selector for any Query Rules that
             * are in nested Query Builders (i.e. in nested Filter Groups). This is a list of
             * field names that exist in the query service index (i.e. Solr), but which should
             * be hidden in nested Query Builders
             * @type {string[]}
             */
            nestedExcludeFields: [],

            /**
             * A single Filter model that is part of a Filters collection, such as the
             * definition filters for a Collection or Portal or the filters for a Search
             * model. The Filter model must be part of a Filters collection (i.e. there must
             * be a model.collection property)
             * @type {Filter|BooleanFilter|NumericFilter|DateFilter|FilterGroup}
             */
            model: undefined,

            /**
             * A function that creates and returns the Backbone events object.
             * @return {Object} Returns a Backbone events object
             */
            events: function () {
              var events = {};
              var removeID = "#" + this.removeRuleID + this.cid;
              events["click " + removeID] = "removeSelf";
              events["mouseover " + removeID] = "previewRemove";
              events["mouseout " + removeID] = "previewRemove";
              return events
            },

            /**
             * A list of additional fields which are not retrieved from the query API, 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. These special fields are passed on
             * to {@link QueryFieldSelectView#addFields}.
             *
             * @type {SpecialField[]}
             *
             * @since 2.15.0
             */
            specialFields: [
              {
                name: "documents-special-field",
                fields: ["documents"],
                label: "Contains Data Files",
                description: "Limit results to packages that include data files. Without" +
                  " this rule, results may include packages with metadata but no data.",
                category: "General",
                values: ["*"]
              },
              {
                name: "year-data-collection",
                fields: ["beginDate", "endDate"],
                label: "Year of Data Collection",
                description: "The temporal range of content described by the metadata",
                category: "Dates"
              }
            ],

            /**
             * An operator option is an object that lists the properties of one of the
             * operators that will be displayed to the user in the Query Rule "operator"
             * dropdown list. The operator properties are used to pre-select the correct
             * operator based on attributes in the associated
             * {@link Filter#defaults Filter model}, as well as to update the Filter model
             * when a user selects a new operator. Operators can set the exclude and
             * matchSubstring properties of the model, and sometimes the values as well.
             * Either the types property OR the fields property must be set, not both.
             *
             * @typedef {Object} OperatorOption
             * @property {string} label - The label to display to the user
             * @property {string} icon - An icon that represents the operator
             * @property {boolean} matchSubstring - Whether the matchSubstring attribute is
             * true or false in the filter model that matches this operator
             * @property {boolean} exclude - Whether the exclude attribute is true or false in
             * the filter model that matches this operator
             * @property {boolean} hasMax - Whether the filter model that matches this
             * operator must have a max attribute
             * @property {boolean} hasMin - Whether the filter model that matches this
             * operator must have a min attribute
             * @property {string[]} values - For this operator to work as desired, the values
             * that should be set in the filter (e.g. ["true"] for the operator "is true")
             * @property {string[]} [types] - The node names of the filters that this operator
             * is used for (e.g. "filter", "booleanFilter")
             * @property {string[]} [fields] - The query field names of the filters that this
             * operator is used for. If this is used for a
             * {@link QueryRuleView#specialFields special field}, then list the special field
             * name (id), and not the real query field names. If this fields property is set,
             * then the types property will be ignored. (i.e. fields is more specific than
             * types.)
             */

            /**
             * The list of operators that will be available in the dropdown list that connects
             * the query fields to the values. Each operator must be unique.
             *
             * @type {OperatorOption[]}
             */
            operatorOptions: [
              {
                label: "is true",
                description: "The data package includes data files (and not only metadata)",
                icon: "ok-circle",
                matchSubstring: false,
                exclude: false,
                values: ["*"],
                fields: ["documents-special-field"]
              },
              {
                label: "is false",
                description: "The data package only contains metadata; it contains no data files.",
                icon: "ban-circle",
                matchSubstring: false,
                exclude: true,
                values: ["*"],
                fields: ["documents-special-field"]
              },
              {
                label: "equals",
                description: "The text in the metadata field is an exact match to the" +
                  " selected value",
                icon: "equal",
                matchSubstring: false,
                exclude: false,
                types: ["filter"]
              },
              {
                label: "does not equal",
                description: "The text in the metadata field is anything except an exact" +
                  " match to the selected value",
                icon: "not-equal",
                matchSubstring: false,
                exclude: true,
                types: ["filter"]
              },
              {
                label: "contains",
                description: "The text in the metadata field matches or contains the words" +
                  " or phrase selected",
                icon: "ok-circle",
                matchSubstring: true,
                exclude: false,
                types: ["filter"]
              },
              {
                label: "does not contain",
                description: "The words or phrase selected are not contained within the" +
                  " metadata field",
                icon: "ban-circle",
                matchSubstring: true,
                exclude: true,
                types: ["filter"]
              },
              {
                label: "is empty",
                description: "The metadata field contains no text or value",
                icon: "circle-blank",
                matchSubstring: false,
                exclude: true,
                values: ["*"],
                types: ["filter"]
              },
              {
                label: "is not empty",
                description: "The metadata field is filled in with any text at all",
                icon: "circle",
                matchSubstring: false,
                exclude: false,
                values: ["*"],
                types: ["filter"]
              },
              {
                label: "is true",
                description: "The metadata field is set to true",
                icon: "ok-circle",
                matchSubstring: false,
                exclude: false,
                values: [true],
                types: ["booleanFilter"]
              },
              {
                label: "is false",
                description: "The metadata field is set to false",
                icon: "ban-circle",
                matchSubstring: false,
                exclude: false,
                values: [false],
                types: ["booleanFilter"]
              },
              {
                label: "is between",
                description: "The metadata field is a value between the range selected" +
                  " (inclusive of both values)",
                icon: "resize-horizontal",
                matchSubstring: false,
                exclude: false,
                hasMin: true,
                hasMax: true,
                types: ["numericFilter", "dateFilter"]
              },
              {
                label: "is less than or equal to",
                description: "The metadata field is a number less than the value selected",
                icon: "less-than-or-eq",
                matchSubstring: false,
                exclude: false,
                hasMin: false,
                hasMax: true,
                types: ["numericFilter"]
              },
              {
                label: "is greater than or equal to",
                description: "The metadata field is a number greater than the value selected",
                icon: "greater-than-or-eq",
                matchSubstring: false,
                exclude: false,
                hasMin: true,
                hasMax: false,
                types: ["numericFilter"]
              },
              {
                label: "is exactly",
                description: "The metadata field exactly equals the value selected",
                icon: "equal",
                matchSubstring: false,
                exclude: false,
                hasMin: false,
                hasMax: false,
                types: ["numericFilter"]
              },
              // TODO: The dateFilter model & view need to be updated for these to work:
              // {
              //   label: "is during or before", icon: "less-than-or-eq", matchSubstring:
              //   false, exclude: false, hasMin: false, hasMax: true, types: ["dateFilter"]
              // },
              // {
              //   label: "is during or after", icon: "greater-than", matchSubstring: false,
              //   exclude: false, hasMin: true, hasMax: false, types: ["dateFilter"]
              // },
              // {
              //   label: "is in the year", icon: "equal", matchSubstring: false, exclude:
              //   false, hasMin: false, hasMax: false, types: ["dateFilter"]
              // }
            ],

            /**
             * The third input in each Query Rule is where the user enters a value, minimum,
             * or maximum for the filter model. Different types of values are appropriate for
             * different solr query fields, and so we display different interfaces depending
             * on the type and category of the selected query fields. A Value Input Option
             * object defines a of interface to show for a given type and category.
             *
             * @typedef {Object} ValueInputOption
             * @property {string[]} filterTypes - An array of one or more filter types that
             * are allowed for this interface.  If none are provided then any filter type is
             * allowed. Filter types are one of the four keys defined in
             * {@link QueryField#filterTypesMap}.
             * @property {string[]} categories - An array of one or more categories that are
             * allowed for this interface. These strings must exactly match the categories
             * provided in QueryField.categoriesMap(). If none are provided then any category
             * is allowed.
             * @property {string[]} queryFields - Specific names of fields that are allowed in
             * this interface. If none are provided, then any query fields are allowed that
             * match the other properties. If this value select should be used for a
             * {@link QueryRuleView#specialFields special field}, then use the name (id) of
             * the special field, not the actual query fields that it represents.
             * @property {string} label - If the interface does not include a label (e.g.
             * number filter), include a string to display here.
             * @property {function} uiFunction - A function that returns the UI view to use
             * with all appropriate options set. The function will be called with this view as
             * the context.
             */

            /**
             * This list defines which type of value input to show depending on filter type,
             * category, and query fields. The value input options are ordered from *most*
             * specific to *least*, since the first match will be selected. The filter model
             * must match either the queryFields, or both the filterTypes AND the categories
             * for a UI to be selected.
             * @type {ValueInputOption[]}
             */
            valueSelectUImap: [
              // serviceCoupling field
              {
                queryFields: ["serviceCoupling"],
                uiFunction: function () {
                  return new SearchableSelect({
                    options: [
                      {
                        label: "tight",
                        description: "Tight coupled service work only on the data described" +
                          " by this metadata document."
                      },
                      {
                        label: "mixed",
                        description: "Mixed coupling means service works on data described" +
                        " by this metadata document but may work on other data."
                      },
                      {
                        label: "loose",
                        description: "Loose coupling means service works on any data."
                      }
                    ],
                    allowMulti: true,
                    allowAdditions: false,
                    inputLabel: "Select a coupling",
                    selected: this.model.get("values"),
                    separatorText: this.model.get("operator")
                  })
                }
              },
              // Metadata format IDs
              {
                queryFields: ["formatId"],
                uiFunction: function () {
                  return new ObjectFormatSelect({
                    selected: this.model.get("values"),
                    separatorText: this.model.get("operator")
                  })
                }
              },
              // Semantic annotation picker
              {
                queryFields: ["sem_annotation"],
                uiFunction: function () {
                  // A bioportalAPIKey is required for the Annotation Filter UI
                  if (MetacatUI.appModel.get("bioportalAPIKey")) {
                    return new AnnotationFilter({
                      selected: this.model.get("values").slice(),
                      separatorText: this.model.get("operator"),
                      multiselect: true,
                      inputLabel: "Type a value",
                    });
                    // If there's no API key, render the default UI (the last in this list)
                  } else {
                    return this.valueSelectUImap.slice(-1)[0].uiFunction.call(this);
                  }
                }
              },
              // User/Organization account ID lookup
              {
                queryFields: ["writePermission", "readPermission", "changePermission", "rightsHolder", "submitter"],
                uiFunction: function () {
                  return new AccountSelect({
                    selected: this.model.get("values"),
                    separatorText: this.model.get("operator")
                  });
                },
              },
              // Repository picker for fields that need a member node ID
              {
                filterTypes: ["filter"],
                queryFields: ["blockedReplicationMN", "preferredReplicationMN", "replicaMN",
                  "authoritativeMN", "datasource"],
                uiFunction: function () {
                  return new NodeSelect({
                    selected: this.model.get("values"),
                    separatorText: this.model.get("operator")
                  })
                }
              },
              // Any numeric fields don't fit one of the above options
              {
                filterTypes: ["numericFilter"],
                label: "Choose a value",
                uiFunction: function () {
                  return new NumericFilterView({
                    model: this.model,
                    showButton: false,
                    separatorText: this.model.get("operator")
                  })
                }
              },
              // Any date fields that don't fit one of the above options
              {
                filterTypes: ["dateFilter"],
                label: "Choose a year",
                uiFunction: function () {
                  return new DateFilterView({
                    model: this.model,
                    separatorText: this.model.get("operator")
                  })
                }
              },
              // The last is the default value selection UI
              {
                uiFunction: function () {
                  return new SearchableSelect({
                    options: [],
                    allowMulti: true,
                    allowAdditions: true,
                    inputLabel: "Type a value",
                    selected: this.model.get("values"),
                    separatorText: this.model.get("operator")
                  })
                }
              }
            ],

            /**
             * Creates a new QueryRuleView
             * @param {Object} options - A literal object with options to pass to the view
             */
            initialize: function (options) {
              try {

                // Get all the options and apply them to this view
                if (typeof options == "object") {
                  var optionKeys = Object.keys(options);
                  _.each(optionKeys, function (key, i) {
                    this[key] = options[key];
                  }, this);
                }

                // If no model is provided in the options, we cannot render this view. A
                // filter model cannot be created, because it must be part of a collection.
                if (!this.model || !this.model.collection) {
                  console.error("error: A Filter model that's part of a Filters collection"
                    + " is required to initialize a Query Rule view.")
                  return
                }

                // The model may be removed during the save process if it's empty. Remove this
                // Rule Group view when that happens.
                this.stopListening(this.model, "remove");
                this.listenTo(this.model, "remove", function () {
                  this.removeSelf();
                });

              } catch (e) {
                console.log("Failed to initialize a Query Builder View, error message:", e);
              }
            },

            /**
             * render - Render the view
             *
             * @return {QueryRule}  Returns the view
             */
            render: function () {

              try {

                // Add the Rule number.
                // TODO: Also add the number of datasets related to rule
                this.addRuleInfo();
                this.stopListening(this.model.collection, "remove");
                this.listenTo(this.model.collection, "remove", this.updateRuleInfo);
                // Nested rules should also listen for changes in Filters of their parent Rule
                if(this.parentRule){
                  this.stopListening(this.parentRule.model.collection, "remove");
                  this.listenTo(this.parentRule.model.collection, "remove", this.updateRuleInfo);
                }

                // The remove button is needed for both FilterGroups and other Filter models
                this.addRemoveButton();

                // Render nested filter group views as another Query Builder.
                if(this.model.type == "FilterGroup"){

                  this.$el.addClass("rule-group");

                  // We must initialize a QueryBuilderView using the inline require syntax to
                  // avoid the problem of circular dependencies. QueryRuleView requires
                  // QueryBuilderView, and QueryBuilderView requires QueryRuleView. For more
                  // info, see https://requirejs.org/docs/api.html#circular
                  var QueryBuilderView = require('views/queryBuilder/QueryBuilderView');

                  // The default
                  nestedLevelsAllowed = 1
                  // If we are adding a query builer, then it is a nested level. Subtract one
                  // from the total levels allowed.
                  if(typeof this.nestedLevelsAllowed == "number"){
                    nestedLevelsAllowed = this.nestedLevelsAllowed - 1
                  }

                  // If there is a special list of fields to exclude in nested Query Builders
                  // (i.e. in nested FilterGroup models), then pass this list on as the
                  // excludeFields list in the child QueryBuilder
                  var excludeFields = this.excludeFields;
                  if(this.nestedExcludeFields && Array.isArray(this.nestedExcludeFields)){
                    excludeFields = this.nestedExcludeFields
                  }

                  // Insert QueryRuleView
                  var ruleGroup = new QueryBuilderView({
                    filterGroup: this.model,
                    // Nested Query Rules have the same color as their parent rule
                    ruleColorPalette: "inherit",
                    excludeFields: excludeFields,
                    specialFields: this.specialFields,
                    parentRule: this,
                    nestedLevelsAllowed: nestedLevelsAllowed
                  });
                  this.el.append(ruleGroup.el);
                  ruleGroup.render();
                } else {
                  // For any other filter type... Add a metadata selector field whether the
                  // rule is new or has already been created
                  this.addFieldSelect();

                  // Operator field and value field Add an operator input only for already
                  // existing filters (For new filters, a metadata field needs to be selected
                  // first)
                  if (
                    this.model.get("fields") &&
                    this.model.get("fields").length
                  ) {
                    this.addOperatorSelect();
                    this.addValueSelect();
                  }
                }

                return this;

              } catch (e) {
                console.error("Error rendering the query Rule View, error message: ", e);
              }
            },

            /**
             * Insert container for the color-coded rule numbering.
             */
            addRuleInfo: function () {
              try {
                this.$indexEl = $(document.createElement("span"));
                this.$ruleInfoEl = $(document.createElement("div"))
                  .addClass(this.ruleInfoClass);
                this.$ruleInfoEl.append(this.$indexEl);

                this.$el.append(this.$ruleInfoEl);
                this.updateRuleInfo();
              } catch (error) {
                console.log(
                  "Error adding rule info container for a Query Rule, details: " + error
                );
              }
            },

            /**
             * Selects a color from the
             * {@link QueryRuleView#ruleColorPalette rule colour palette array}, given an
             * index. If the index is greater than the length of the palette, then the palette
             * is effectively repeated until long enough (i.e. colours will be recycled). If
             * no index in provided, the first colour in the palette will be selected.
             *
             * @param  {number} [index=0] - The position of the rule within the Filters
             * collection.
             * @param  {string} [defaultColor="#57b39c"] - A default colour to use in case
             * there is problem with this function (hex color code beginning with '#').
             * @return {string} - Returns a hex color code string
             */
            getPaletteColor: function (index = 0, defaultColor = "#57b39c") {
              try {
                // Allow the rule to inherit it's color from the parent rule within which it's
                // nested
                if(this.ruleColorPalette == "inherit"){
                  return null
                }
                if (!this.ruleColorPalette || !this.ruleColorPalette.length) {
                  return defaultColor;
                }
                var numCols = this.ruleColorPalette.length;
                if ((index + 1) > numCols) {
                  var n = Math.floor(index / numCols);
                  index = index - (numCols * n);
                }
                return this.ruleColorPalette[index];
              } catch (error) {
                console.log(
                  "Error getting a color for a Query Rule, using the default colour"
                  + " instead. Error details: " + error
                );
                return defaultColor;
              }
            },

            /**
             * Adds or updates the color-coded Query Rule information displayed to the user.
             * This needs to be run when rules are added or removed. Rule information includes
             * the rule number, but may one day also display information such as the number of
             * results that there are for this individual rule.
             */
            updateRuleInfo: function () {
              try {

                // Rules are numbered in the order in which they appear in the Filters
                // collection, excluding any invisible filter models. Rules nested in Rule
                // Groups (within Filter Models) get numbered 3A, 3B, etc.
                var letter = ""
                var index = ""
                // If this is a filter model nested in a filter group
                if(this.parentRule){
                  index = this.parentRule.ruleNumber;
                  var letterIndex = this.model.collection.visibleIndexOf(this.model);
                  if(typeof letterIndex === "number"){
                    letter = String.fromCharCode(94 + letterIndex + 3).toUpperCase();
                  }
                // For top-level filter models
                } else {
                  index = this.model.collection.visibleIndexOf(this.model);
                }

                if(typeof index == "number"){
                  index = index + 1;
                }

                var ruleNumber = index + letter;

                // Set the rule number of the parent view to be accessed by any nested child
                // rules
                this.ruleNumber = ruleNumber;

                // if(this.model.type == "FilterGroup")
                if (ruleNumber && ruleNumber.length) {
                  this.$indexEl.text("Rule " + ruleNumber);
                } else {
                  this.$indexEl.text("");
                  return
                }
                var color = this.getPaletteColor(index);
                if (color) {
                  this.el.style.setProperty('--rule-color', color);
                }
              } catch (error) {
                console.log(
                  "Error updating the rule numbering for a Query Rule. Details: " + error
                );
              }
            },

            /**
             * addRemoveButton - Create and insert the button to remove the Query Rule
             */
            addRemoveButton: function () {
              try {
                var removeButton = $(
                  "<i id='" + this.removeRuleID + this.cid +
                  "' class='" + this.removeClass +
                  " icon icon-remove' title='Remove this Query Rule'></i>"
                );
                this.el.append(removeButton[0]);
              } catch (e) {
                console.error("Failed to create a remove button for a Query Rule, error details: " + e);
              }
            },

            /**
             * Determines whether the filter model that this rule renders matches one of the
             * {@link QueryRuleView#specialFields special fields} set on this view. If it
             * does, returns the first special field object that matches. For a filter model
             * to match to one of the special fields, it must contain all of the fields listed
             * in the special field's "fields" property. If the special field has an array set
             * for "values", then the model's values must also exactly match the special
             * field's values.
             *
             * @param  {string[]} [fields] - Optionally set a list of query fields to search
             * with. If not set, then the fields that are set on the view's filter model are
             * used.
             * @returns {SpecialField|null} - The matching special field, or null if no match
             * was found.
             *
             * @since 2.15.0
             */
            getSpecialField: function(fields){

                // Get information about the filter model (or used the fields passed to this
                // function)
                var selectedFields = fields || this.model.get("fields");
                var selectedFields = _.clone(selectedFields);
                var selectedValues = this.model.get("values");

                if(!this.specialFields || !Array.isArray(this.specialFields)){
                  return null
                }

                var matchingSpecialField = _.find(this.specialFields, function(specialField){

                  var fieldsMatch = false,
                      mustMatchValues = false,
                      valuesMatch = false;

                  // If *all* the fields in the fields array are present in the list
                  // of fields that the special field represents, then count this as a match.
                  var commonFields = _.intersection(specialField.fields, selectedFields);
                  if(commonFields.length === specialField.fields.length){
                    fieldsMatch = true
                  }

                  // The selected value must *exactly match* if one is set in the special
                  // field
                  if(specialField.values){
                    mustMatchValues = true;
                    valuesMatch = _.isEqual(specialField.values, selectedValues)
                  }

                  return fieldsMatch && (
                    !mustMatchValues || (mustMatchValues && valuesMatch)
                  )

                }, this);

                // If this model matches one of the special fields, render it differently
                return matchingSpecialField || null
            },

            /**
             * Takes a list of query field names, checks if the model matches any of the
             * special fields, and if it does, returns the list of fields with the actual
             * field names replaced with the
             * {@link QueryRuleView#specialFields special field name}. This function is the
             * opposite of {@link QueryRuleView#convertFromSpecialFields}
             * @param  {string[]} fields - The list of field names to convert
             * @returns {string[]} - The converted list of field names. If there were no
             * special fields detected, or if there's an error, then then the field names are
             * returned unchanged.
             *
             * @param {string[]} fields - The list of fields to convert to special fields, if
             * the model matches any of the special field objects
             * @returns {string[]} - Returns the list of fields with actual query field names
             * replaced with special field names, if any match
             *
             * @since 2.15.0
             */
            convertToSpecialFields: function(fields){

              try {

                var fields = _.clone(fields);

                // Insert the special field name at the same position as the associated
                // query fields that we will remove
                var replaceWithSpecialField = function(fields, specialField){
                  if(specialField){
                    position = _.findIndex(fields, function(selectedField){
                      return specialField.fields.includes(selectedField);
                    }, this);
                    fields.splice(position, 0, specialField.name);
                    fields = _.difference(fields, specialField.fields);
                  }
                  return fields
                }

                // If the user selected a special field, make sure we convert those first
                if( this.selectedSpecialFields && this.selectedSpecialFields.length ){
                  this.selectedSpecialFields.forEach(function(specialFiend){
                    fields = replaceWithSpecialField(fields, specialFiend)
                  }, this);
                }

                // Search for remaining special fields given the fields and model values
                var matchingSpecialField = this.getSpecialField(fields);

                // There may be more than one special field in the list of fields...
                while(matchingSpecialField !== null){
                  fields = replaceWithSpecialField(fields, matchingSpecialField)
                  // Check if there are more special fields remaining
                  matchingSpecialField = this.getSpecialField(fields);

                }

                return fields;

              } catch (error) {
                console.log(
                  "Error converting query field names to special field names in" +
                  " a Query Rule View. Returning the list of fields unchanged." +
                  " Error details : " + error
                );
                return fields
              }

            },

            /**
             * Takes a list of query field names and checks if it contains any of the
             * {@link QueryRuleView#specialFields special field names}. Returns the list with
             * the special field names replaced with the actual field names that those special
             * fields represent. Stores the name of each special field name removed in an
             * array set on the view's selectedSpecialFields property. selectedSpecialFields
             * is cleared each time this function runs. This function is the opposite of
             * {@link QueryRuleView#convertToSpecialFields}
             * @param  {string[]} fields] - The list of field names to convert
             * @returns {string[]} - The converted list of field names. If there were no
             * special fields detected, or if there's an error, then then the field names are
             * returned unchanged.
             *
             * @param {string[]} fields - The list of fields to convert to actual query
             * service index fields
             * @returns {string[]} - Returns the list of fields with any special field
             * replaced with real fields from the query service index
             *
             * @since 2.15.0
             */
            convertFromSpecialFields: function(fields){
              try {
                this.selectedSpecialFields = [];
                if(this.specialFields){
                  this.specialFields.forEach(function(specialField){
                    var index = fields.indexOf(specialField.name);
                    if(index >= 0){
                      // Keep a record that the user selected a special field (useful in the
                      // case that the special field is just a duplicate of another field)
                      this.selectedSpecialFields.push(specialField);
                      fields.splice.apply(fields, [index, 1].concat(specialField.fields));
                    }
                  }, this);
                }
                return fields
              } catch (error) {
                console.log(
                  "Error converting special query fields to query fields that" +
                  " exist in the index in a Query Rule View. Returning the fields" +
                  " unchanged. Error details: " + error
                );
                return fields
              }
            },

            /**
             * Create and insert an input that allows the user to select a metadata field to
             * query
             */
            addFieldSelect: function () {

              try {

                // Check whether the filter model set on this view contains query fields
                // and values that match one of the special rules. If it does,
                // convert the list of field names to special field to pass on to the
                // Query Field Select View.
                var selectedFields = _.clone(this.model.get("fields"));
                var selectedFields = this.convertToSpecialFields(selectedFields);

                this.fieldSelect = new QueryFieldSelect({
                  selected: selectedFields,
                  excludeFields: this.excludeFields,
                  addFields: this.specialFields,
                  separatorText: this.model.get("fieldsOperator"),
                });
                this.fieldSelect.$el.addClass(this.fieldsClass);
                this.el.append(this.fieldSelect.el);
                this.fieldSelect.render();

                // Update the model when the fieldsOperator changes
                this.stopListening(
                  this.fieldSelect,
                  'separatorChanged'
                );
                this.listenTo(
                  this.fieldSelect,
                  'separatorChanged',
                  function(newOperator){
                    this.model.set("fieldsOperator", newOperator)
                  }
                );
                // Update model when the selected fields change
                this.stopListening(
                  this.fieldSelect,
                  'changeSelection'
                );
                this.listenTo(
                  this.fieldSelect,
                  'changeSelection',
                  this.handleFieldChange
                );

              } catch (e) {
                console.error("Error adding a metadata selector input in the Query Rule"
                  + " View, error message:", e);
              }
            },

            /**
             * handleFieldChange - Called when the Query Field Select View triggers a change
             * event. Updates the model with the new fields, and if required,
             * 1) converts the filter model to a different type based on the types of fields
             *    selected, 2) updates the operator select and the value select
             *
             * @param  {string[]} newFields The list of new query fields that were selected
             */
            handleFieldChange: function (newFields) {

              try {

                // Uncomment the following chunk to clear operator & values when the field
                // input is cleared.
                // if(!newFields || newFields.length === 0 || newFields[0] === ""){
                //   if(this.operatorSelect){
                //     this.operatorSelect.changeSelection([""]);
                //   }
                //   this.model.set("fields", this.model.defaults().fields);
                //   return
                // }

                // Get the selected operator before the field changed
                var opBefore = this.getSelectedOperator();

                // If any of the new fields are special fields, replace them with the
                // actual query fields before setting them in the model...
                newFields = this.convertFromSpecialFields(newFields);

                // Get the current type of filter and required type given the newly selected
                // fields
                var typeBefore = this.model.get("nodeName"),
                    typeAfter = MetacatUI.queryFields.getRequiredFilterType(newFields);

                // If the type has changed, then replace the model with one of the correct
                // type, update the value and operator inputs, and do nothing else
                if (typeBefore != typeAfter) {
                  this.model = this.model.collection.replaceModel(
                    this.model,
                    { filterType: typeAfter, fields: newFields }
                  );
                  this.removeInput("value")
                  this.removeInput("operator")
                  this.addOperatorSelect("");
                  return
                }

                // If the filter model type is the same, and the operator options are the same
                // for the selected fields, then update the model
                this.model.set("fields", newFields);

                // Get the selected operator now that we've updated the model with new fields
                var opAfter = this.getSelectedOperator();

                // Add an empty operator input field, if there isn't one
                if (!this.operatorSelect) {
                  this.addOperatorSelect("");
                // If the operator options have changed, refresh the operator input
                } else if (opAfter !== opBefore){
                  this.removeInput("operator");
                  // Make sure that we overwrite any values that don't apply to the new options.
                  this.handleOperatorChange([""]);
                  this.addOperatorSelect("");
                  return
                }

                // Refresh the value select in case a different value input is required for
                // the new fields
                if (this.valueSelect) {
                  this.removeInput("value");
                  this.addValueSelect();
                }

              } catch (e) {
                console.error("Failed to handle query field change in the Query Rule View," +
                  " error message: " + e);
              }

            },

            /**
             * Create and insert an input field where the user can select an operator for the
             * given rule. Operators will vary depending on filter model type.
             *
             * @param {string} selectedOperator - optional. The label of an operator to
             * pre-select. Set to an empty string to render an empty operator selector.
             */
            addOperatorSelect: function (selectedOperator) {
              try {

                var view = this;
                var operatorError = false;

                var options = this.getOperatorOptions();

                // Identify the selected operator for existing models
                if (typeof selectedOperator !== "string") {
                  selectedOperator = this.getSelectedOperator();
                  // If there was no operator found, then this is probably an unsupported
                  // combination of exclude + matchSubstring + filterType
                  if (selectedOperator === "") {
                    operatorError = true;
                  }
                }

                if (selectedOperator === "") {
                  selectedOperator = []
                } else {
                  selectedOperator = [selectedOperator]
                }

                this.operatorSelect = new SearchableSelect({
                  options: options,
                  allowMulti: false,
                  inputLabel: "Select an operator",
                  clearable: false,
                  placeholderText: "Select an operator",
                  selected: selectedOperator
                });
                this.operatorSelect.$el.addClass(this.operatorClass);
                this.el.append(this.operatorSelect.el);

                if (operatorError) {
                  view.listenToOnce(view.operatorSelect, "postRender", function () {
                    view.operatorSelect.showMessage(
                      "Please select a valid operator",
                      "error",
                      true
                    )
                  })
                }

                this.operatorSelect.render();

                // Update model when the values change
                this.stopListening(
                  this.operatorSelect,
                  'changeSelection'
                );
                this.listenTo(
                  this.operatorSelect,
                  'changeSelection',
                  this.handleOperatorChange
                );

              } catch (e) {
                console.error("Error adding an operator selector input in the Query Rule " +
                  "View, error message:", e);
              }
            },

            /**
             * handleOperatorChange - When the operator selection is changed, update the model
             * and re-set the value UI when required
             *
             * @param  {string[]} newOperatorLabel The new operator label within an array,
             * e.g. ["is greater than"]
             */
            handleOperatorChange: function (newOperatorLabel) {

              try {

                var view = this;

                if (!newOperatorLabel || newOperatorLabel[0] == "") {
                  var modelDefaults = this.model.defaults();
                  this.model.set({
                    min: modelDefaults.min,
                    max: modelDefaults.max,
                    values: modelDefaults.values
                  })
                  this.removeInput("value");
                  return;
                }

                // Get the properties of the newly selected operator. The newOperatorLabel
                // will be an array with one value. Select only from the available options,
                // since there may be multiple options with the same label in
                // this.operatorOptions.
                var options = this.getOperatorOptions();
                var operator = _.findWhere( options, { label: newOperatorLabel[0] });

                // Gather  information about which values are currently set on the model, and
                // which are required
                var // Type
                  type = view.model.get("nodeName"),
                  isNumeric = ["dateFilter", "numericFilter"].includes(type),
                  isRange = operator.hasMin && operator.hasMax,

                  // Values
                  modelValues = this.model.get("values"),
                  modelHasValues = modelValues ? modelValues && modelValues.length : false,
                  modelFirstValue = modelHasValues ? modelValues[0] : null,
                  modelValueInt = parseInt(modelFirstValue) ? parseInt(modelFirstValue) : null,
                  needsValue = isNumeric && !modelValueInt && !operator.hasMin && !operator.hasMax,

                  // Min
                  modelMin = this.model.get("min"),
                  modelHasMin = modelMin === 0 || modelMin,
                  needsMin = operator.hasMin && !modelHasMin,

                  // Max
                  modelMax = this.model.get("max"),
                  modelHasMax = modelMax === 0 || modelMax,
                  needsMax = operator.hasMax && !modelHasMax;

                // Some operator options include a specific value to be set on the model. For
                // example, "is not empty", should set the model value to the "*" wildcard.
                // For operators with these specific value requirements, update the filter
                // model value and remove the value select input.
                if (operator.values && operator.values.length) {
                  this.removeInput("value");
                  this.model.set("values", operator.values);
                  // If the operator does not have a default value, then ensure that there is
                  // a value select available.
                } else {
                  if (!this.valueSelect) {
                    this.model.set("values", view.model.defaults().values);
                    this.addValueSelect();
                  }
                }

                // Update the model with true or false for matchSubstring and exclude
                ["matchSubstring", "exclude"].forEach((prop, i) => {
                  if (typeof operator[prop] !== "undefined") {
                    view.model.set(prop, operator[prop]);
                  } else {
                    view.model.set(prop, view.model.defaults()[prop]);
                  }
                });

                // Set min & max values as required by the operator
                // TODO - test this strategy with dates...

                // Add a minimum value if one is needed
                if (needsMin) {
                  // Search for the min in the values, then in the max
                  if (modelValueInt || modelValueInt === 0) {
                    this.model.set("min", modelValueInt)
                  } else if (modelHasMax) {
                    this.model.set("min", modelMax)
                  } else {
                    this.model.set("min", 0)
                  }
                }

                // Add a maximum value if one is needed
                if (needsMax) {
                  // Search for the min in the values, then in the max
                  if (modelValueInt || modelValueInt === 0) {
                    this.model.set("max", modelValueInt)
                  } else if (modelHasMin) {
                    this.model.set("max", modelMin)
                  } else {
                    this.model.set("max", 0)
                  }
                }

                // Add a value if one is needed
                if (needsValue) {
                  if (modelHasMin) {
                    this.model.set("values", [modelMin])
                  } else if (modelHasMax) {
                    this.model.set("values", [modelMax])
                  } else {
                    this.model.set("values", [0])
                  }
                }

                // Remove the minimum and max if they should not be included in the filter
                if (modelHasMax && !operator.hasMax) {
                  this.model.set("max", this.model.defaults().max)
                }
                if (modelHasMin && !operator.hasMin) {
                  this.model.set("min", this.model.defaults().min)
                }

                if (isRange) {
                  this.model.set("range", true)
                } else {
                  if (isNumeric) {
                    this.model.set("range", false)
                  } else {
                    this.model.unset("range")
                  }
                }

                // If the operator changed for a numeric or date field, reset the value
                // select. This way it can change from a range to a single value input if
                // needed.
                if (isNumeric) {
                  this.removeInput("value");
                  this.addValueSelect();
                }
              } catch (e) {
                console.error("Failed to handle the operator selection in a Query Rule " +
                  "view, error message: " + e);
              }
            },

            /**
             * Get a list of {@link QueryRuleView#operatorOptions operatorOptions} that are
             * allowed for this view's filter model
             *
             * @param  {string[]} [fields] - Optional list of fields to use instead of the
             * fields set on this view's Filter model
             *
             * @since 2.15.0
             */
            getOperatorOptions: function(fields){

              try {
                // Check which type of rule this is (boolean, numeric, text, date)
                var type = this.model.get("nodeName");

                // If this rule contains a special field, replace the real query field names
                // with the special field names for the purpose of selecting operator options
                var fields = fields || this.model.get("fields");
                var fields = _.clone(fields);
                var fields = this.convertToSpecialFields(fields);

                // Get the list of options for a user to select from based on field name.
                // All of the rule's fields must be contained within the operator option's
                // list of allowed fields for it to be a match.
                var options = _.filter(this.operatorOptions, function (option) {
                  if(option.fields){
                    return _.every(fields, function(fieldName){
                      return option.fields.includes(fieldName)
                    })
                  }
                });

                // Get the list of options for a user to select from based on type, if there
                // were none that matched based on field names
                if(!options || !options.length){
                  options = _.filter(this.operatorOptions, function (option) {
                    if(option.types){
                      return option.types.includes(type)
                    }
                  }, this);
                }

                return options
              } catch (error) {
                console.log("Error getting operator options in a Query Rule View, " +
                "Error details: " + error);
              }
            },

            /**
             * getSelectedOperator - Based on values set on the model, get the label to show
             * in the "operator" filed of the Query Rule
             *
             * @return {string} The operator label
             */
            getSelectedOperator: function () {

              try {

                // This view
                var view = this,
                  // The options that we will filter down
                  options = this.operatorOptions,
                  // The user-facing operator label that we will return
                  selectedOperator = "";

                // --- Filter 1 - Filter options by type --- //

                // Reduce list of options to only  those that apply to the current filter type
                var type = view.model.get("nodeName");
                var options = this.getOperatorOptions();

                // --- Filter 2 - filter by 'matchSubstring', 'exclude', 'min', 'max' --- //

                // Create the conditions based on the model
                var conditions = _.pick(
                  this.model.attributes,
                  'matchSubstring', 'exclude', 'min', 'max'
                );

                var isNumeric = ["dateFilter", "numericFilter"].includes(type);

                if (!conditions.min && conditions.min !== 0) {
                  if (isNumeric) {
                    conditions.hasMin = false
                  }
                } else if (isNumeric) {
                  conditions.hasMin = true
                }
                if (!conditions.max && conditions.max !== 0) {
                  if (isNumeric) {
                    conditions.hasMax = false
                  }
                } else if (isNumeric) {
                  conditions.hasMax = true
                }

                delete conditions.min
                delete conditions.max

                var options = _.where(options, conditions);

                // --- Filter 3 - filter based on the value, if there's > 1 option --- //

                if (options.length > 1) {
                  // Model values that determine the user-facing operator eg ["*"], [true],
                  // [false]
                  var specialValues = _.compact(
                    _.pluck(this.operatorOptions, "values")
                  ),
                    specialValues = specialValues.map(val => JSON.stringify(val)),
                    specialValues = _.uniq(specialValues);

                  options = options.filter(function (option) {
                    var modelValsStringified = JSON.stringify(view.model.get("values"));
                    if (specialValues.includes(modelValsStringified)) {
                      if (JSON.stringify(option.values) === modelValsStringified) {
                        return true
                      }
                    } else {
                      if (!option.values) {
                        return true
                      }
                    }
                  })
                }
                // --- Return value --- //

                if (options.length === 1) {
                  selectedOperator = options[0].label
                }

                return selectedOperator
              } catch (e) {
                console.error("Failed to select an operator in the Query Rule View, error" +
                  " message: " + e);
              }
            },

            /**
             * getCategory - Given an array of query fields, get the user-facing category that
             * these fields belong to. If there are fields from multiple categories, then a
             * default "Text" category is returned.
             *
             * @param  {string[]} fields An array of query (Solr) fields
             * @return {string} The label for the category that the given fields belong to
             */
            getCategory: function (fields) {

              try {
                var categories = [],
                  // When fields is empty or are different types
                  defaultCategory = "Text";

                if (!fields || fields.length === 0 || fields[0] === "") {
                  return defaultCategory
                }

                fields.forEach((field, i) => {
                  // Get the category of the field from the matching filter model in the Query
                  // Fields Collection
                  var fieldModel = MetacatUI.queryFields.findWhere({ name: field });
                  categories.push(fieldModel.get("category"))
                });

                // Test of all the fields are of the same type
                var allEqual = categories.every((val, i, arr) => val === arr[0]);

                if (allEqual) {
                  return categories[0]
                } else {
                  return defaultCategory
                }

              } catch (e) {
                console.log("Failed to detect the category for a group of filters in the" +
                  " Query Rule View, error message: " + e);
              }

            },

            /**
             * Create and insert an input field where the user can provide a search value
             */
            addValueSelect: function () {
              try {

                var view = this
                  fields = this.model.get("fields"),
                  filterType = MetacatUI.queryFields.getRequiredFilterType(fields),
                  category = this.getCategory(fields),
                  interfaces = this.valueSelectUImap,
                  label = "";

                // To help guide users to create valid queries, the type of value field will
                // vary based on the type of field (i.e. filter nodeName), and the operator
                // selected.

                // Some user-facing operators (e.g. "is true") don't require a value to be set
                var selectedOperator = _.findWhere(
                  this.operatorOptions,
                  { label: this.getSelectedOperator() }
                );
                if (selectedOperator) {
                  if (selectedOperator.values && selectedOperator.values.length) {
                    return
                  }
                }

                // Find the appropriate UI to use the the value select field. Find the first
                // match in the valueSelectUImap according to the filter type and the
                // categories associated with the metadata field.
                var interfaceProperties = _.find(interfaces, function (thisInterface) {
                  var typesMatch = true,
                    categoriesMatch = true,
                    namesMatch = true;
                  if (thisInterface.queryFields && thisInterface.queryFields.length) {
                    fields.forEach((field, i) => {
                      if (thisInterface.queryFields.includes(field) === false) {
                        namesMatch = false;
                      }
                    });
                  }
                  if (thisInterface.filterTypes && thisInterface.filterTypes.length) {
                    typesMatch = thisInterface.filterTypes.includes(filterType)
                  }
                  if (thisInterface.categories && thisInterface.categories.length) {
                    categoriesMatch = thisInterface.categories.includes(category)
                  }
                  return typesMatch && categoriesMatch && namesMatch
                });

                this.valueSelect = interfaceProperties.uiFunction.call(this);
                if (interfaceProperties.label && interfaceProperties.label.length) {
                  label = $(
                    "<p class='subtle searchable-select-label'>" +
                    interfaceProperties.label + "</p>"
                  );
                }

                // Append and render the chosen value selector
                this.el.append(view.valueSelect.el);
                this.valueSelect.$el.addClass(this.valuesClass);
                view.valueSelect.render();
                if (label) {
                  view.valueSelect.$el.prepend(label)
                }

                // Make sure the listeners set below are not set multiple times
                this.stopListening(view.valueSelect, 'changeSelection inputFocus separatorChanged');

                // Update model when the values change - note that the date & numeric filter
                // views do not trigger a 'changeSelection' event, (because they are not based
                // on a SearchSelect View) but update the models directly
                this.listenTo(
                  view.valueSelect,
                  'changeSelection',
                  this.handleValueChange
                );

                // Update the model when the operator changes
                this.listenTo(
                  view.valueSelect,
                  'separatorChanged',
                  function (newOperator) {
                    this.model.set("operator", newOperator)
                  }
                );

                // Show a message that reminds the user that capitalization matters when they
                // are typing a value for a field that is case-sensitive.
                this.listenTo(
                  view.valueSelect,
                  'inputFocus',
                  function(event){
                    var fields = this.model.get("fields");
                    var isCaseSensitive = _.some(fields, function(field){
                      return MetacatUI.queryFields.findWhere({
                        name: field,
                        caseSensitive: true
                      });
                    })
                    if(isCaseSensitive){
                      var fieldsText = "The field"
                      if(fields.length > 1){
                        fieldsText = "At least one of the fields"
                      }
                      var message = "<i class='icon-lightbulb icon-on-left'></i> <b>Hint:</b> " +
                            fieldsText +
                            " you selected is case-sensitive. Capitalization matters here."
                      view.valueSelect.showMessage(message, type = "info", removeOnChange = false)
                    } else {
                      view.valueSelect.removeMessages()
                    }
                  }
                )

                // Set the value to the value provided if there was one. Then validateValue()
              } catch (e) {
                console.error("Error adding a search value input in the Query Rule View," +
                  " error message:", e);
              }
            },

            /**
             * handleValueChange - Called when the select values for rule are changed. Updates
             * the model.
             *
             * @param  {string[]} newValues The new values that were selected
             */
            handleValueChange: function (newValues) {

              try {
                // TODO: validate values

                // Don't add empty values to the model
                newValues = _.reject(newValues, function (val) { return val === "" });
                this.model.set("values", newValues);
              } catch (e) {
                console.error("Failed to handle a change in select values in the Query Ryle" +
                  " View, error message: " + e);
              }
            },

            // /**
            //  * Ensure the value entered is valid, given the metadata field selected.
            //  * If it's not, show an error. If it is, remove the error if there was one.
            //  *
            //  * @return {type}  description
            //    */
            // validateValue: function() {// TODO
            // },

            /**
             * Remove one of the three input fields from the rule
             *
             * @param  {string} inputType Which of the inputs to remove? "field", "operator",
             * or "value"
             */
            removeInput: function (inputType) {
              try {
                // TODO - what, if any, model updates should happen here?
                switch (inputType) {
                  case "value":
                    if (this.valueSelect) {
                      this.stopListening( this.valueSelect, 'changeSelection inputFocus' );
                      this.valueSelect.remove();
                      this.valueSelect = null;
                    }
                    break;
                  case "operator":
                    if (this.operatorSelect) {
                      this.stopListening(this.operatorSelect, 'changeSelection');
                      this.operatorSelect.remove();
                      this.operatorSelect = null;
                    }
                    break;
                  case "field":
                    if (this.fieldSelect) {
                      this.stopListening(this.fieldSelect, 'changeSelection');
                      this.fieldSelect.remove();
                      this.fieldSelect = null;
                    }
                    break;
                  default:
                    console.error("Must specify either value, operator, or field in the" +
                      " removeInput function in the Query Rule View")
                }
              } catch (e) {
                console.error("Error removing an input from the Query Rule View, error" +
                  " message:", e);
              }
            },

            /**
             * Indicate to the user that the rule will be removed when they hover over the
             * remove button.
             */
            previewRemove: function (e) {
              try {

                var normalOpacity = 1.0,
                    previewOpacity = 0.2,
                    speed = 175;

                var removeEl = e.target;
                var subElements = this.$el.children().not(removeEl);

                if(e.type === "mouseover"){
                  subElements.fadeTo(speed, previewOpacity)
                  $(removeEl).fadeTo(speed, normalOpacity)
                }
                if(e.type === "mouseout"){
                  subElements.fadeTo(speed, normalOpacity)
                  $(removeEl).fadeTo(speed, previewOpacity)
                }

              } catch (error) {
                console.log("Error showing a preview of the removal of a Query Rule View," +
                  " details: " + error);
              }
            },

            /**
             * removeSelf - When the delete button is clicked, remove this entire View and
             * associated model
             */
            removeSelf: function () {
              try {
                $("body .popover").remove();
                $("body .tooltip").remove();
                if (this.model && this.model.collection) {
                  this.model.collection.remove(this.model);
                }
                this.remove();
              } catch (error) {
                console.log("Error removing a Query Rule View, details: " + error);
              }
            },

          });
      });