Afform - Use APIv4 autocomplete for all EntityRef fields
authorColeman Watts <coleman@civicrm.org>
Mon, 24 Oct 2022 21:51:39 +0000 (17:51 -0400)
committerColeman Watts <coleman@civicrm.org>
Thu, 10 Nov 2022 17:34:45 +0000 (12:34 -0500)
12 files changed:
ang/crmUi.js
ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html
ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js
ext/afform/admin/ang/afGuiEditor/inputType/Existing.html [deleted file]
ext/afform/core/Civi/Afform/AfformMetadataInjector.php
ext/afform/core/Civi/Afform/FormDataModel.php
ext/afform/core/Civi/Api4/Subscriber/AutocompleteSubscriber.php
ext/afform/core/ang/af/afField.component.js
ext/afform/core/ang/af/fields/EntityRef.html
ext/afform/core/ang/af/fields/Existing.html [deleted file]
ext/afform/core/tests/phpunit/Civi/Afform/AfformMetadataTest.php

index b9ee8a086ec3e48e052a14f742df9e40199f3bcd..9a52b9c616a95bf23aa12261c4bafef045f3073c 100644 (file)
         },
         bindToController: {
           crmAutocomplete: '<',
-          crmAutocompleteParams: '<'
+          crmAutocompleteParams: '<',
+          multiple: '<'
         },
         controller: function($element, $timeout) {
           var ctrl = this;
           $timeout(function() {
-            $element.crmAutocomplete(ctrl.crmAutocomplete, ctrl.crmAutocompleteParams);
+            $element.crmAutocomplete(ctrl.crmAutocomplete, ctrl.crmAutocompleteParams, {
+              multiple: ctrl.multiple
+            });
             // Ensure widget is updated when model changes
             if (ctrl.ngModel) {
               ctrl.ngModel.$render = function() {
index 4b16828c3ebcfd52efa1ba8790788b16adb7732e..7ed1ca938df5f725af8d20bc8c473c100b07a897 100644 (file)
@@ -127,8 +127,9 @@ class AfformAdminMeta {
       // Add existing entity field
       $idField = CoreUtil::getIdFieldName($entityName);
       $fields[$idField]['readonly'] = FALSE;
-      $fields[$idField]['input_type'] = 'Existing';
+      $fields[$idField]['input_type'] = 'EntityRef';
       $fields[$idField]['is_id'] = TRUE;
+      $fields[$idField]['fk_entity'] = $entityName;
       $fields[$idField]['label'] = E::ts('Existing %1', [1 => CoreUtil::getInfoItem($entityName, 'title')]);
       // Mix in alterations declared by afform entities
       $afEntity = self::getMetadata()['entities'][$entityName] ?? [];
index b72d473f8b5ca681a2632fd2888448e1f5b974bd..ecd36dd3d5941e5dd523431fd3ff4a7a8db9429d 100644 (file)
@@ -1,4 +1,4 @@
-<li ng-if="$ctrl.fieldDefn.input_type !== 'Existing'">
+<li ng-if="!$ctrl.fieldDefn.is_id">
   <div href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
     <label>{{:: ts('Type:') }}</label>
     <select class="form-control" ng-model="getSet('input_type')" ng-model-options="{getterSetter: true}" title="{{:: ts('Field type') }}">
@@ -6,7 +6,7 @@
     </select>
   </div>
 </li>
-<li ng-if="$ctrl.fieldDefn.input_type === 'Existing'" title="{{:: ts('Use a saved search to filter the autocomplete results') }}">
+<li ng-if="$ctrl.fieldDefn.input_type === 'EntityRef'" title="{{:: ts('Use a saved search to filter the autocomplete results') }}">
   <div href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
     <label>{{:: ts('Saved Search:') }}</label>
     <input class="form-control" crm-entityref="{entity: 'SavedSearch', api: {id_field: 'name', params: {api_entity: $ctrl.getEntity().name}}}" ng-model="getSet('saved_search')" ng-model-options="{getterSetter: true}">
     {{:: ts('Required') }}
   </a>
 </li>
-<li ng-if="$ctrl.fieldDefn.input_type !== 'Existing'">
+<li ng-if="!$ctrl.fieldDefn.is_id">
   <a href ng-click="toggleDefaultValue(); $event.stopPropagation(); $event.target.blur();" title="{{:: ts('Pre-fill this field with a value') }}">
     <i class="crm-i fa-{{ $ctrl.hasDefaultValue ? 'check-' : '' }}square-o"></i>
     {{:: ts('Default value') }}
   </a>
 </li>
-<li ng-if="$ctrl.fieldDefn.input_type !== 'Existing' && $ctrl.hasDefaultValue">
+<li ng-if="$ctrl.hasDefaultValue">
   <form ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown form-inline">
     <input class="form-control" af-gui-field-value="$ctrl.fieldDefn" ng-model="getSet('afform_default')" ng-model-options="{getterSetter: true}" >
   </form>
index 8a3638ae68d3a9dc528cdf03533e05639d1917f4..86bbc8b9c869c85701918fb319cd1dca07226852 100644 (file)
@@ -51,8 +51,7 @@
       };
 
       this.getFkEntity = function() {
-        var defn = ctrl.getDefn(),
-          fkEntity = defn.is_id ? ctrl.container.getMainEntityType() : defn.fk_entity;
+        var fkEntity = ctrl.getDefn().fk_entity;
         return ctrl.editor.meta.entities[fkEntity];
       };
 
diff --git a/ext/afform/admin/ang/afGuiEditor/inputType/Existing.html b/ext/afform/admin/ang/afGuiEditor/inputType/Existing.html
deleted file mode 100644 (file)
index d253836..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<div class="form-inline">
-  <div class="input-group">
-    <input autocomplete="off" type="text" class="form-control" placeholder="{{:: ts('Select %1', {1: $ctrl.getFkEntity().label}) }}" title="{{:: ts('Click to add placeholder text') }}" ng-model="getSet('input_attrs.placeholder')" ng-model-options="$ctrl.editor.debounceWithGetterSetter">
-    <div class="input-group-btn">
-      <button type="button" class="btn btn-default" disabled><i class="crm-i fa-search"></i></button>
-    </div>
-  </div>
-</div>
index 5810e62a17c69b477bf05de3f4fafd5949598d1a..a994be24a36a75acff10aae8244bb6be20283ebd 100644 (file)
@@ -124,7 +124,7 @@ class AfformMetadataInjector {
     if ($inputType === 'Select' || $inputType === 'ChainSelect') {
       $fieldInfo['input_attrs']['placeholder'] = E::ts('Select');
     }
-    elseif ($inputType === 'EntityRef') {
+    elseif ($inputType === 'EntityRef' && empty($field['is_id'])) {
       $info = civicrm_api4('Entity', 'get', [
         'where' => [['name', '=', $fieldInfo['fk_entity']]],
         'checkPermissions' => FALSE,
index 18cc358689e3878c82861ddb43649fbb890282ad..646f2641f5bba3ad50d86344defd752735cb05e8 100644 (file)
@@ -231,8 +231,9 @@ class FormDataModel {
     // Id field for selecting existing entity
     if ($action === 'create' && $field['name'] === CoreUtil::getIdFieldName($entityName)) {
       $entityTitle = CoreUtil::getInfoItem($entityName, 'title');
-      $field['input_type'] = 'Existing';
-      $field['entity'] = $entityName;
+      $field['input_type'] = 'EntityRef';
+      $field['fk_entity'] = $entityName;
+      $field['is_id'] = TRUE;
       $field['label'] = E::ts('Existing %1', [1 => $entityTitle]);
       $field['input_attrs']['placeholder'] = E::ts('Select %1', [1 => $entityTitle]);
     }
index 685a99c95d94a60cb700bf15beba830998a83749..d288a99c6f7de980b398ad183d67a7763172d371 100644 (file)
@@ -14,6 +14,7 @@ namespace Civi\Api4\Subscriber;
 use Civi\Afform\FormDataModel;
 use Civi\API\Events;
 use Civi\Api4\Afform;
+use Civi\Api4\Utils\CoreUtil;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -52,20 +53,25 @@ class AutocompleteSubscriber implements EventSubscriberInterface {
       }
       $formDataModel = new FormDataModel($afform['layout']);
       $entity = $formDataModel->getEntity($entityName);
+      $isId = $fieldName === CoreUtil::getIdFieldName($entity['type']);
 
+      // For the "Existing Entity" selector,
       // Look up the "type" fields (e.g. contact_type, activity_type_id, case_type_id, etc)
-      $typeFields = [];
-      if ($entity['type'] === 'Contact') {
-        $typeFields = ['contact_type', 'contact_sub_type'];
-      }
-      else {
-        $extends = array_column(\CRM_Core_BAO_CustomGroup::getCustomGroupExtendsOptions(), 'grouping', 'id');
-        $typeFields = (array) ($extends[$entity['type']] ?? NULL);
-      }
-      // If entity has a type set in the values, auto-apply that to filters
-      foreach ($typeFields as $typeField) {
-        if (!empty($entity['data'][$typeField])) {
-          $apiRequest->addFilter($typeField, $entity['data'][$typeField]);
+      // And apply it as a filter if specified on the form.
+      if ($isId) {
+        $typeFields = [];
+        if ($entity['type'] === 'Contact') {
+          $typeFields = ['contact_type', 'contact_sub_type'];
+        }
+        else {
+          $extends = array_column(\CRM_Core_BAO_CustomGroup::getCustomGroupExtendsOptions(), 'grouping', 'id');
+          $typeFields = (array) ($extends[$entity['type']] ?? NULL);
+        }
+        // If entity has a type set in the values, auto-apply that to filters
+        foreach ($typeFields as $typeField) {
+          if (!empty($entity['data'][$typeField])) {
+            $apiRequest->addFilter($typeField, $entity['data'][$typeField]);
+          }
         }
       }
 
index 78a5ba04032adb075545c28e0bb63755bc79682d..6bdece38886fb2645d48fcbaac4623d1fddad7f1 100644 (file)
         }
       };
 
+      ctrl.isReadonly = function() {
+        if (ctrl.defn.is_id) {
+          return ctrl.afFieldset.getEntity().actions.update === false;
+        }
+        // TODO: Not actually used, but could be used if we wanted to render displayOnly
+        // fields as more than just raw data. I think we probably ought to do so for entityRef fields
+        // Since the ids are kind of meaningless. Making that change would require adding a function
+        // to get the widget template rather than just concatenating the input_type into an ngInclude.
+        return ctrl.defn.input_type === 'DisplayOnly';
+      };
+
       // ngChange callback from Existing entity field
-      ctrl.onSelectExisting = function() {
-        var val = $scope.getSetSelect();
-        var entity = ctrl.afFieldset.modelName;
-        var index = ctrl.getEntityIndex();
-        ctrl.afFieldset.afFormCtrl.loadData(entity, index, val);
+      ctrl.onSelectEntity = function() {
+        if (ctrl.defn.is_id) {
+          var val = $scope.getSetSelect();
+          var entity = ctrl.afFieldset.modelName;
+          var index = ctrl.getEntityIndex();
+          ctrl.afFieldset.afFormCtrl.loadData(entity, index, val);
+        }
       };
 
       // Params for the Afform.submitFile API when uploading a file field
index 70cb8d2284cbd1b65d147b26340db92d5929dddc..e10509cdc57bea7b46028c0908217faa2300850f 100644 (file)
@@ -1 +1,11 @@
-<input class="form-control" id="{{:: fieldId }}" ng-required="$ctrl.defn.required" ng-model="getSetSelect" ng-model-options="{getterSetter: true}" crm-entityref="{entity: $ctrl.defn.fk_entity, select: {multiple: !!$ctrl.defn.input_attrs.multiple, placeholder: $ctrl.defn.input_attrs.placeholder}}" >
+<input id="{{:: fieldId }}"
+       class="form-control"
+       ng-disabled="$ctrl.isReadonly()"
+       ng-required="$ctrl.defn.required"
+       ng-model="getSetSelect"
+       ng-model-options="{getterSetter: true}"
+       crm-autocomplete="$ctrl.defn.fk_entity"
+       crm-autocomplete-params="{formName: 'afform:' + $ctrl.afFieldset.getFormName(), fieldName: $ctrl.afFieldset.modelName + ':' + $ctrl.fieldName}"
+       multiple="$ctrl.defn.input_attrs.multiple"
+       placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}"
+       ng-change="$ctrl.onSelectEntity()" >
diff --git a/ext/afform/core/ang/af/fields/Existing.html b/ext/afform/core/ang/af/fields/Existing.html
deleted file mode 100644 (file)
index 7824c6d..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<input id="{{:: fieldId }}"
-       class="form-control"
-       ng-disabled="$ctrl.afFieldset.getEntity().actions.update === false"
-       ng-model="getSetSelect"
-       ng-model-options="{getterSetter: true}"
-       crm-autocomplete="$ctrl.defn.entity"
-       crm-autocomplete-params="{formName: 'afform:' + $ctrl.afFieldset.getFormName(), fieldName: $ctrl.afFieldset.modelName + ':' + $ctrl.fieldName}"
-       placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}"
-       ng-change="$ctrl.onSelectExisting()" >
index 5dfc62e1bc19e0807673d43acc0f06842fb782b5..607ee3851a0e6b931b8fa5dc71718c7a5fa37ed0 100644 (file)
@@ -29,7 +29,7 @@ class AfformMetadataTest extends \PHPUnit\Framework\TestCase implements Headless
 
     // Ensure the "Existing" contact field exists
     $this->assertEquals('Existing Contact', $individualFields['id']['label']);
-    $this->assertEquals('Existing', $individualFields['id']['input_type']);
+    $this->assertEquals('EntityRef', $individualFields['id']['input_type']);
   }
 
 }