CustomFields - Add EntityReference field type based on APIv4 autocomplete
authorColeman Watts <coleman@civicrm.org>
Sat, 21 Jan 2023 20:42:36 +0000 (15:42 -0500)
committerColeman Watts <coleman@civicrm.org>
Mon, 20 Feb 2023 16:44:59 +0000 (11:44 -0500)
CRM/Core/BAO/CustomField.php
CRM/Core/BAO/CustomValueTable.php
CRM/Core/Form.php
CRM/Custom/Form/Field.php
Civi/Api4/Service/Spec/CustomFieldSpec.php
Civi/Api4/Service/Spec/SpecFormatter.php
Civi/Schema/Traits/DataTypeSpecTrait.php
css/civicrm.css
js/Common.js
templates/CRM/Custom/Form/Field.tpl

index 9ecbb2359a5e110c61fee3c3293afa01adf7bfcd..ab97e31c2650afb00b4f50afec0a68a538a8b433 100644 (file)
@@ -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
index 9ba11ab1a0527db8860e8684fa06063eed60c4b7..2312c81847053fd2353411e76d993a3213f3a785 100644 (file)
@@ -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':
index f71fb5335bb5b712050b08c6bb78e85e3e6a1feb..439c927f63d46025cfe5967e22d94bbeac08ab8f 100644 (file)
@@ -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
index 0ae4d84ce06fe29459ae77fc50a90e340d1cd104..8da250a92cafd290e53a8100faac8c25533f6588 100644 (file)
@@ -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.');
index 1d5b8fe5fb53c226ceb63a8f8f18fc353a9ec710..803036a3369302424366a96edb7a2f9cf8b2fbc5 100644 (file)
@@ -38,6 +38,10 @@ class CustomFieldSpec extends FieldSpec {
         $dataType = 'Integer';
         break;
 
+      case 'EntityReference':
+        $dataType = 'Integer';
+        break;
+
       case 'File':
       case 'StateProvince':
       case 'Country':
index cfdfec55868778a55bc2781bd4243ea8afda75c6..3b9ebb4e2225f48a1e8ba9465f5416e7562d2e5b 100644 (file)
@@ -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'])) {
index 0afbef7e269515e23f942a82ae7825dcea21c402..06b38dd82b216e09fc036f599b20b932e71e235b 100644 (file)
@@ -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;
index e958d97ba0ac8616c9787909440c99f31d79cae0..fa52f86e9c6712c68725e98c4ad210f0047f49df 100644 (file)
@@ -200,6 +200,7 @@ input.crm-form-checkbox + label {
   width: 15em;
 }
 .crm-container .huge,
+input.crm-form-autocomplete,
 input.crm-form-entityref {
   width: 25em;
 }
index 7d269480584a1e1f0c34d54079e713389b7eea51..021254d07ccc26769ae4c8d3c9e3771bf0a52594 100644 (file)
@@ -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'));
index c2e13ab33cc58917d593bf878a614cc0d07f0262..05df6d9dd1dc934fb67ba914d659f44272e199f1 100644 (file)
       <td class="label">{$form.html_type.label}</td>
       <td class="html-adjust">{$form.html_type.html}</td>
     </tr>
+    <tr class="crm-custom-field-form-block-fk_entity">
+      <td class="label">{$form.fk_entity.label} <span class="crm-marker">*</span></td>
+      <td class="html-adjust">{$form.fk_entity.html}</td>
+    </tr>
     <tr class="crm-custom-field-form-block-serialize">
       <td class="label">{$form.serialize.label}</td>
       <td class="html-adjust">{$form.serialize.html}</td>
       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() {
 
       $("#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) {