From e6cdf7cdfe91fbc325d4c748568ea94484e56e0e Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sat, 21 Jan 2023 15:42:36 -0500 Subject: [PATCH] CustomFields - Add EntityReference field type based on APIv4 autocomplete --- CRM/Core/BAO/CustomField.php | 15 ++++++++-- CRM/Core/BAO/CustomValueTable.php | 5 ++++ CRM/Core/Form.php | 32 ++++++++++++++++++++++ CRM/Custom/Form/Field.php | 14 ++++++++++ Civi/Api4/Service/Spec/CustomFieldSpec.php | 4 +++ Civi/Api4/Service/Spec/SpecFormatter.php | 5 +++- Civi/Schema/Traits/DataTypeSpecTrait.php | 2 +- css/civicrm.css | 1 + js/Common.js | 3 ++ templates/CRM/Custom/Form/Field.tpl | 11 +++++++- 10 files changed, 87 insertions(+), 5 deletions(-) diff --git a/CRM/Core/BAO/CustomField.php b/CRM/Core/BAO/CustomField.php index 9ecbb2359a..ab97e31c26 100644 --- a/CRM/Core/BAO/CustomField.php +++ b/CRM/Core/BAO/CustomField.php @@ -16,6 +16,7 @@ */ use Civi\Api4\CustomField; +use Civi\Api4\Utils\CoreUtil; /** * Business objects for managing custom data fields. @@ -97,6 +98,11 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { 'name' => 'Contact Reference', 'label' => ts('Contact Reference'), ], + [ + 'id' => 'EntityReference', + 'name' => 'Entity Reference', + 'label' => ts('Entity Reference'), + ], ]; } @@ -120,6 +126,7 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { 'File' => CRM_Utils_Type::T_STRING, 'Link' => CRM_Utils_Type::T_STRING, 'ContactReference' => CRM_Utils_Type::T_INT, + 'EntityReference' => CRM_Utils_Type::T_INT, 'Country' => CRM_Utils_Type::T_INT, ]; } @@ -804,7 +811,6 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { if (!$field->find(TRUE)) { throw new CRM_Core_Exception('Cannot find Custom Field ' . $fieldID); } - $fieldValues = []; CRM_Core_DAO::storeValues($field, $fieldValues); @@ -1062,6 +1068,10 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { ); } + if ($field->data_type == 'EntityReference') { + $fieldAttributes['entity'] = $field->fk_entity; + $element = $qf->addAutocomplete($elementName, $label, $fieldAttributes, $useRequired && !$search); + } else { // FIXME: This won't work with customFieldOptions hook $fieldAttributes += [ @@ -2772,7 +2782,7 @@ WHERE cf.id = %1 AND cg.is_multiple = 1"; } // Do this before the "Select" string search because date fields have a "Select Date" html_type // and contactRef fields have an "Autocomplete-Select" html_type - contacts are an FK not an option list. - if (in_array($field['data_type'], ['ContactReference', 'Date'])) { + if (in_array($field['data_type'], ['EntityReference', 'ContactReference', 'Date'])) { return FALSE; } if (strpos($field['html_type'], 'Select') !== FALSE) { @@ -2882,6 +2892,7 @@ WHERE cf.id = %1 AND cg.is_multiple = 1"; 'StateProvince' => 'civicrm_state_province', 'ContactReference' => 'civicrm_contact', 'File' => 'civicrm_file', + 'EntityReference' => CoreUtil::getInfoItem((string) $field->fk_entity, 'table_name'), ]; if (isset($fkFields[$field->data_type])) { // Serialized fields store value-separated strings which are incompatible with FK constraints diff --git a/CRM/Core/BAO/CustomValueTable.php b/CRM/Core/BAO/CustomValueTable.php index 9ba11ab1a0..2312c81847 100644 --- a/CRM/Core/BAO/CustomValueTable.php +++ b/CRM/Core/BAO/CustomValueTable.php @@ -203,6 +203,10 @@ class CRM_Core_BAO_CustomValueTable { } break; + case 'EntityReference': + $type = 'Integer'; + break; + case 'RichTextEditor': $type = 'String'; break; @@ -315,6 +319,7 @@ class CRM_Core_BAO_CustomValueTable { // the below three are FK's, and have constraints added to them case 'ContactReference': + case 'EntityReference': case 'StateProvince': case 'Country': case 'File': diff --git a/CRM/Core/Form.php b/CRM/Core/Form.php index f71fb5335b..439c927f63 100644 --- a/CRM/Core/Form.php +++ b/CRM/Core/Form.php @@ -508,6 +508,11 @@ class CRM_Core_Form extends HTML_QuickForm_Page { unset($extra['option_context']); } + // Allow disabled to be a boolean + if (isset($attributes['disabled']) && $attributes['disabled'] === FALSE) { + unset($attributes['disabled']); + } + $element = $this->addElement($type, $name, CRM_Utils_String::purifyHTML($label), $attributes, $extra); if (HTML_QuickForm::isError($element)) { CRM_Core_Error::statusBounce(HTML_QuickForm::errorMessage($element)); @@ -2203,6 +2208,33 @@ class CRM_Core_Form extends HTML_QuickForm_Page { } } + /** + * @param string $name + * @param string $label + * @param array $props + * @param bool $required + * + * @return HTML_QuickForm_Element + */ + public function addAutocomplete(string $name, string $label = '', array $props = [], bool $required = FALSE) { + $props += [ + 'entity' => 'Contact', + 'api' => [], + 'select' => [], + ]; + $props['api'] += [ + 'formName' => 'qf:' . get_class($this), + 'fieldName' => $name, + ]; + $props['class'] = ltrim(($props['class'] ?? '') . ' crm-form-autocomplete'); + $props['placeholder'] = $props['placeholder'] ?? self::selectOrAnyPlaceholder($props, $required); + $props['data-select-params'] = json_encode($props['select']); + $props['data-api-params'] = json_encode($props['api']); + $props['data-api-entity'] = $props['entity']; + CRM_Utils_Array::remove($props, 'select', 'api', 'entity'); + return $this->add('text', $name, $label, $props, $required); + } + /** * Create a single or multiple entity ref field. * @param string $name diff --git a/CRM/Custom/Form/Field.php b/CRM/Custom/Form/Field.php index 0ae4d84ce0..8da250a92c 100644 --- a/CRM/Custom/Form/Field.php +++ b/CRM/Custom/Form/Field.php @@ -228,6 +228,14 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { $this->add('checkbox', 'serialize', ts('Multi-Select')); + $this->addAutocomplete('fk_entity', ts('Entity'), [ + 'class' => 'twenty', + // Don't allow entity to be changed once field is created + 'disabled' => $this->_action == CRM_Core_Action::UPDATE && !empty($this->_values['fk_entity']), + 'entity' => 'Entity', + 'select' => ['minimumInputLength' => 0], + ]); + if ($this->_action == CRM_Core_Action::UPDATE) { $this->freeze('data_type'); if (!empty($this->_values['option_group_id'])) { @@ -613,6 +621,12 @@ SELECT count(*) } } + if ($dataType === 'EntityReference') { + if (empty($fields['fk_entity'])) { + $errors['fk_entity'] = ts('Selecting an entity is required'); + } + } + if ($dataType == 'Date') { if (!$fields['date_format']) { $errors['date_format'] = ts('Please select a date format.'); diff --git a/Civi/Api4/Service/Spec/CustomFieldSpec.php b/Civi/Api4/Service/Spec/CustomFieldSpec.php index 1d5b8fe5fb..803036a336 100644 --- a/Civi/Api4/Service/Spec/CustomFieldSpec.php +++ b/Civi/Api4/Service/Spec/CustomFieldSpec.php @@ -38,6 +38,10 @@ class CustomFieldSpec extends FieldSpec { $dataType = 'Integer'; break; + case 'EntityReference': + $dataType = 'Integer'; + break; + case 'File': case 'StateProvince': case 'Country': diff --git a/Civi/Api4/Service/Spec/SpecFormatter.php b/Civi/Api4/Service/Spec/SpecFormatter.php index cfdfec5586..3b9ebb4e22 100644 --- a/Civi/Api4/Service/Spec/SpecFormatter.php +++ b/Civi/Api4/Service/Spec/SpecFormatter.php @@ -37,6 +37,9 @@ class SpecFormatter { $field->setType('Field'); $field->setTableName($data['custom_group_id.table_name']); } + if ($dataTypeName === 'EntityReference') { + $field->setFkEntity($data['fk_entity']); + } $field->setColumnName($data['column_name']); $field->setNullable(empty($data['is_required'])); $field->setCustomFieldId($data['id'] ?? NULL); @@ -293,7 +296,7 @@ class SpecFormatter { 'Link' => 'Url', ]; $inputType = $map[$inputType] ?? $inputType; - if ($dataTypeName === 'ContactReference') { + if ($dataTypeName === 'ContactReference' || $dataTypeName === 'EntityReference') { $inputType = 'EntityRef'; } if (in_array($inputType, ['Select', 'EntityRef'], TRUE) && !empty($data['serialize'])) { diff --git a/Civi/Schema/Traits/DataTypeSpecTrait.php b/Civi/Schema/Traits/DataTypeSpecTrait.php index 0afbef7e26..06b38dd82b 100644 --- a/Civi/Schema/Traits/DataTypeSpecTrait.php +++ b/Civi/Schema/Traits/DataTypeSpecTrait.php @@ -71,7 +71,7 @@ trait DataTypeSpecTrait { } if (!in_array($dataType, $this->getValidDataTypes())) { - throw new \CRM_Core_Exception(sprintf('Invalid data type "%s', $dataType)); + throw new \CRM_Core_Exception(sprintf('Invalid data type "%s"', $dataType)); } $this->dataType = $dataType; diff --git a/css/civicrm.css b/css/civicrm.css index e958d97ba0..fa52f86e9c 100644 --- a/css/civicrm.css +++ b/css/civicrm.css @@ -200,6 +200,7 @@ input.crm-form-checkbox + label { width: 15em; } .crm-container .huge, +input.crm-form-autocomplete, input.crm-form-entityref { width: 25em; } diff --git a/js/Common.js b/js/Common.js index 7d26948058..021254d07c 100644 --- a/js/Common.js +++ b/js/Common.js @@ -1133,6 +1133,9 @@ if (!CRM.vars) CRM.vars = {}; } $('.crm-select2:not(.select2-offscreen, .select2-container)', e.target).crmSelect2(); $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e.target).crmEntityRef(); + $('.crm-form-autocomplete:not(.select2-offscreen, .select2-container)[data-api-entity]', e.target).each(function() { + $(this).crmAutocomplete($(this).data('apiEntity'), $(this).data('apiParams'), $(this).data('selectParams')); + }); $('select.crm-chain-select-control', e.target).off('.chainSelect').on('change.chainSelect', chainSelect); $('.crm-form-text[data-crm-datepicker]', e.target).each(function() { $(this).crmDatepicker($(this).data('crmDatepicker')); diff --git a/templates/CRM/Custom/Form/Field.tpl b/templates/CRM/Custom/Form/Field.tpl index c2e13ab33c..05df6d9dd1 100644 --- a/templates/CRM/Custom/Form/Field.tpl +++ b/templates/CRM/Custom/Form/Field.tpl @@ -26,6 +26,10 @@ {$form.html_type.label} {$form.html_type.html} + + {$form.fk_entity.label} * + {$form.fk_entity.html} + {$form.serialize.label} {$form.serialize.html} @@ -185,8 +189,13 @@ if (!$('#html_type', $form).val()) { $('#html_type', $form).val(dataToHTML[dataType][0]).change(); } + // Hide html_type if there is only one option + $('.crm-custom-field-form-block-html_type').toggle(allowedHtmlTypes.length > 1); customOptionHtmlType(dataType); makeDefaultValueField(dataType); + + // Show/hide entityReference selector + $('.crm-custom-field-form-block-fk_entity').toggle(dataType === 'EntityReference'); } function onChangeHtmlType() { @@ -282,7 +291,7 @@ $("#noteColumns, #noteRows, #noteLength", $form).toggle(dataType === 'Memo'); - $(".crm-custom-field-form-block-serialize", $form).toggle(htmlType === 'Select' || htmlType === 'Autocomplete-Select'); + $(".crm-custom-field-form-block-serialize", $form).toggle(htmlType === 'Select' || htmlType === 'Autocomplete-Select' && dataType !== 'EntityReference'); } function makeDefaultValueField(dataType) { -- 2.25.1