From c26543a5c07a39260b59bf090f93cc1deeb72425 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sat, 30 Jan 2021 14:39:30 -0500 Subject: [PATCH] Search Builder - Enhance UI with Select2 and EntityRef This changes the simple select fields to use Select2 and the plain text inputs for e.g. participant_contact_id will now present a searchable dropdown for selecting a contact. --- CRM/Contact/Form/Search/Builder.php | 10 +- templates/CRM/Contact/Form/Search/Builder.js | 129 ++++++++++--------- 2 files changed, 77 insertions(+), 62 deletions(-) diff --git a/CRM/Contact/Form/Search/Builder.php b/CRM/Contact/Form/Search/Builder.php index f10f2c199f..1eef8b3633 100644 --- a/CRM/Contact/Form/Search/Builder.php +++ b/CRM/Contact/Form/Search/Builder.php @@ -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]; } /** diff --git a/templates/CRM/Contact/Form/Search/Builder.js b/templates/CRM/Contact/Form/Search/Builder.js index 4dc3523571..9c935e102c 100644 --- a/templates/CRM/Contact/Form/Search/Builder.js +++ b/templates/CRM/Contact/Form/Search/Builder.js @@ -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(); @@ -31,29 +32,32 @@ 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); - } } /** @@ -70,6 +74,35 @@ 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 @@ -77,32 +110,23 @@ * @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(''); + .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); } } @@ -111,7 +135,7 @@ * @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')}]; } @@ -121,7 +145,7 @@ 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); @@ -135,7 +159,7 @@ }); } else { - buildOptions(row, field); + buildOptions(row, field, multiSelect); } } @@ -143,32 +167,22 @@ * 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(''); + $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(); } /** @@ -176,8 +190,7 @@ * @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'); } /** @@ -313,16 +326,12 @@ // 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); } -- 2.25.1