Merge pull request #19471 from colemanw/searchBuilderSelect2
authorSeamus Lee <seamuslee001@gmail.com>
Sun, 31 Jan 2021 20:38:58 +0000 (07:38 +1100)
committerGitHub <noreply@github.com>
Sun, 31 Jan 2021 20:38:58 +0000 (07:38 +1100)
Search Builder - Enhance UI with Select2 and EntityRef

CRM/Contact/Form/Search/Builder.php
templates/CRM/Contact/Form/Search/Builder.js

index f10f2c199f844769b915c240a135bee30839f8eb..1eef8b3633f251ef97549f9f926f3cac27ed78ae 100644 (file)
@@ -92,6 +92,7 @@ class CRM_Contact_Form_Search_Builder extends CRM_Contact_Form_Search {
         $searchByLabelFields[] = $name;
       }
     }
+    [$fieldOptions, $fkEntities] = self::fieldOptions();
     // Add javascript
     CRM_Core_Resources::singleton()
       ->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Search/Builder.js', 1, 'html-header')
@@ -99,7 +100,8 @@ class CRM_Contact_Form_Search_Builder extends CRM_Contact_Form_Search {
         'searchBuilder' => [
           // Index of newly added/expanded block (1-based index)
           'newBlock' => $this->get('newBlock'),
-          'fieldOptions' => self::fieldOptions(),
+          'fieldOptions' => $fieldOptions,
+          'fkEntities' => $fkEntities,
           'searchByLabelFields' => $searchByLabelFields,
           'fieldTypes' => $fieldNameTypes,
           'generalOperators' => ['' => ts('-operator-')] + CRM_Core_SelectValues::getSearchBuilderOperators(),
@@ -450,6 +452,7 @@ class CRM_Contact_Form_Search_Builder extends CRM_Contact_Form_Search {
       'grant',
     ];
     CRM_Contact_BAO_Query_Hook::singleton()->alterSearchBuilderOptions($entities, $options);
+    $fkEntities = [];
     foreach ($entities as $entity) {
       $fields = civicrm_api3($entity, 'getfields');
       foreach ($fields['values'] as $field => $info) {
@@ -465,6 +468,9 @@ class CRM_Contact_Form_Search_Builder extends CRM_Contact_Form_Search {
             $options[$field] = $entity;
           }
         }
+        elseif (!empty($info['FKApiName'])) {
+          $fkEntities[$field] = $info['FKApiName'];
+        }
         elseif (in_array(substr($field, 0, 3), [
           'is_',
           'do_',
@@ -480,7 +486,7 @@ class CRM_Contact_Form_Search_Builder extends CRM_Contact_Form_Search {
         }
       }
     }
-    return $options;
+    return [$options, $fkEntities];
   }
 
   /**
index 4dc3523571e949d94e59e099d407dcf72bb3e06f..9c935e102c16fab2e0a364e63147a0697326329e 100644 (file)
@@ -10,8 +10,9 @@
    * field for the given field and row.
    */
   function handleUserInputField() {
-    var row = $(this).closest('tr');
-    var field = $('select[id^=mapper][id$="_1"]', row).val();
+    var row = $(this).closest('tr'),
+      entity = $('select[id^=mapper][id$="_0"]', row).val(),
+      field = $('select[id^=mapper][id$="_1"]', row).val();
     field = (field === 'world_region') ? 'worldregion_id': field;
     var operator = $('select[id^=operator]', row);
     var op = operator.val();
       buildOperator(operator, operators);
     }
 
+    removeDate(row);
+
     // These Ops don't get any input field.
     var noFieldOps = ['', 'IS EMPTY', 'IS NOT EMPTY', 'IS NULL', 'IS NOT NULL'];
 
     if ($.inArray(op, noFieldOps) > -1) {
       // Hide the fields and return.
-      $('.crm-search-value', row).hide().find('input, select').val('');
+      $('.crm-search-value', row).hide().find('input[id^=value]').val('');
       return;
     }
     $('.crm-search-value', row).show();
 
-    if (!CRM.searchBuilder.fieldOptions[field]) {
-      removeSelect(row);
+    if (CRM.searchBuilder.fieldOptions[field]) {
+      buildSelect(row, field, op, false);
+    }
+    // Add entityRef widget for all fields except an entity's own id
+    else if (CRM.searchBuilder.fkEntities[field] && field !== (entity.toLowerCase() + '_id')) {
+      buildEntityRef(row, field, op);
     }
     else {
-      buildSelect(row, field, op, false);
+      removeSelect(row);
     }
 
     if (CRM.searchBuilder.fieldTypes[field] === 'Date' || CRM.searchBuilder.fieldTypes[field] === 'Timestamp') {
       buildDate(row, op, CRM.searchBuilder.fieldTypes[field] === 'Timestamp');
     }
-    else {
-      removeDate(row);
-    }
   }
 
   /**
     operator.val(selected);
   }
 
+  function getSelectType(op) {
+    // Operators that will get a single drop down list of choices.
+    var dropDownSingleOps = ['=', '!='];
+    // Multiple select drop down list.
+    var dropDownMultipleOps = ['IN', 'NOT IN'];
+
+    if ($.inArray(op, dropDownMultipleOps) > -1) {
+      return true;
+    }
+    if ($.inArray(op, dropDownSingleOps) > -1) {
+      return false;
+    }
+  }
+
+  function buildEntityRef(row, field, op) {
+    var selectType = getSelectType(op);
+
+    if (typeof selectType === 'undefined') {
+      removeSelect(row);
+      return;
+    }
+
+    $('input[id^=value]', row)
+      .crmEntityRef({
+        entity: CRM.searchBuilder.fkEntities[field],
+        select: {multiple: selectType, placeholder: ts('Select'), allowClear: true}
+      });
+  }
+
   /**
    * Add select list if appropriate for this operation
    * @param row: jQuery object
    * @param skip_fetch: boolean
    */
   function buildSelect(row, field, op, skip_fetch) {
-    var multiSelect = '';
-    // Operators that will get a single drop down list of choices.
-    var dropDownSingleOps = ['=', '!='];
-    // Multiple select drop down list.
-    var dropDownMultipleOps = ['IN', 'NOT IN'];
+    var selectType = getSelectType(op);
 
-    if ($.inArray(op, dropDownMultipleOps) > -1) {
-      multiSelect = 'multiple="multiple"';
-    }
-    else if ($.inArray(op, dropDownSingleOps) < 0) {
-      // If this op is neither supported by single or multiple selects, then we should not render a select list.
+    if (typeof selectType === 'undefined') {
       removeSelect(row);
       return;
     }
 
-    $('.crm-search-value select', row).remove();
     $('input[id^=value]', row)
-      .hide()
-      .after('<select class="crm-form-' + multiSelect.substr(0, 5) + 'select required" ' + multiSelect + '><option value="">' + ts('Loading') + '...</option></select>');
+      .addClass('loading')
+      .crmSelect2({data: [], disabled: true, multiple: selectType, placeholder: ts('Select'), allowClear: true});
 
     // Avoid reloading state/county options IF already built, identified by skip_fetch
     if (skip_fetch) {
-      buildOptions(row, field);
+      buildOptions(row, field, selectType);
     }
     else {
-      fetchOptions(row, field);
+      fetchOptions(row, field, selectType);
     }
   }
 
    * @param row: jQuery object
    * @param field: string
    */
-  function fetchOptions(row, field) {
+  function fetchOptions(row, field, multiSelect) {
     if (CRM.searchBuilder.fieldOptions[field] === 'yesno') {
       CRM.searchBuilder.fieldOptions[field] = [{key: 1, value: ts('Yes')}, {key: 0, value: ts('No')}];
     }
           var field = settings.field;
           if (result.count) {
             CRM.searchBuilder.fieldOptions[field] = result.values;
-            buildOptions(settings.row, field);
+            buildOptions(settings.row, field, multiSelect);
           }
           else {
             removeSelect(settings.row);
       });
     }
     else {
-      buildOptions(row, field);
+      buildOptions(row, field, multiSelect);
     }
   }
 
    * Populate option list for given row
    * @param row: jQuery object
    * @param field: string
+   * @param multiSelect: bool
    */
-  function buildOptions(row, field) {
-    var select = $('.crm-search-value select', row);
-    var value = $('input[id^=value]', row).val();
-    if (value.length && value.charAt(0) == '(' && value.charAt(value.length - 1) == ')') {
-      value = value.slice(1, -1);
-    }
-    var options = value.split(',');
-    if (select.attr('multiple') == 'multiple') {
-      select.find('option').remove();
+  function buildOptions(row, field, multiSelect) {
+    var $el = $('input[id^=value]', row).removeClass('loading'),
+      value = $el.val();
+    if (value.length && value.charAt(0) === '(' && value.charAt(value.length - 1) === ')') {
+      $el.val(value.slice(1, -1));
     }
-    else {
-      select.find('option').text(ts('- select -'));
-      if (options.length > 1) {
-        options = [options[0]];
-      }
-    }
-    $.each(CRM.searchBuilder.fieldOptions[field], function(key, option) {
-      var optionKey = option.key;
-      if ($.inArray(field, CRM.searchBuilder.searchByLabelFields) >= 0) {
-        optionKey = option.value;
-      }
-      var selected = ($.inArray(''+optionKey, options) > -1) ? 'selected="selected"' : '';
-      select.append('<option value="' + optionKey + '"' + selected + '>' + option.value + '</option>');
+    $el.crmSelect2({
+      multiple: multiSelect,
+      placeholder: ts('Select'),
+      allowClear: true,
+      data: _.transform(CRM.searchBuilder.fieldOptions[field], function(options, opt) {
+        options.push({id: opt.key, text: opt.value});
+      }, [])
     });
-    select.change();
   }
 
   /**
    * @param row: jQuery object
    */
   function removeSelect(row) {
-    $('.crm-search-value input', row).not('.crm-hidden-date').show();
-    $('.crm-search-value select', row).remove();
+    $('input[id^=value]', row).crmEntityRef('destroy');
   }
 
   /**
       // Handle field and operator selection
       .on('change', 'select[id^=mapper][id$="_1"], select[id^=operator]', handleUserInputField)
       // Handle option selection - update hidden value field
-      .on('change', '.crm-search-value select', function() {
+      .on('change', '.crm-search-value input[id^=value]', function() {
         var value = $(this).val() || '';
-        if ($(this).attr('multiple') == 'multiple' && value.length) {
-          value = value.join(',');
-        }
-        $(this).siblings('input').val(value);
         if (value !== '') {
-          var mapper = $('#' + $(this).siblings('input').attr('id').replace('value_', 'mapper_') + '_1').val();
-          var location_type = $('#' + $(this).siblings('input').attr('id').replace('value_', 'mapper_') + '_2').val();
-          var section = $(this).siblings('input').attr('id').replace('value_', '').split('_')[0];
+          var mapper = $('#' + $(this).attr('id').replace('value_', 'mapper_') + '_1').val();
+          var location_type = $('#' + $(this).attr('id').replace('value_', 'mapper_') + '_2').val();
+          var section = $(this).attr('id').replace('value_', '').split('_')[0];
           if ($.inArray(mapper, ['state_province', 'country']) > -1) {
             chainSelect(mapper + '_id', value, location_type, section);
           }