Afform - Configure SavedSearch, display, autoOpen settings for Autocomplete fields
authorColeman Watts <coleman@civicrm.org>
Sat, 5 Nov 2022 02:01:33 +0000 (22:01 -0400)
committerColeman Watts <coleman@civicrm.org>
Thu, 10 Nov 2022 17:34:45 +0000 (12:34 -0500)
Civi/Api4/Generic/AutocompleteAction.php
ang/crmUi.js
ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html
ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js
ext/afform/core/Civi/Api4/Subscriber/AutocompleteSubscriber.php
ext/afform/core/ang/af/fields/EntityRef.html
ext/search_kit/CRM/Search/DAO/SearchDisplay.php
ext/search_kit/xml/schema/CRM/Search/SearchDisplay.xml
js/Common.js

index d900f2aa80db8308f588c2c5c4e87b6bb47a6404..d2499600073b59e4430cb8610b36675400ede5c8 100644 (file)
@@ -28,6 +28,8 @@ use Civi\Api4\Utils\CoreUtil;
  * @method string getFormName()
  * @method $this setFieldName(string $fieldName) Set fieldName.
  * @method string getFieldName()
+ * @method $this setFilters(array $filters)
+ * @method array getFilters()
  */
 class AutocompleteAction extends AbstractAction {
   use Traits\SavedSearchInspectorTrait;
@@ -83,6 +85,14 @@ class AutocompleteAction extends AbstractAction {
    */
   protected $key;
 
+  /**
+   * Search conditions that will be automatically added to the WHERE or HAVING clauses
+   *
+   * Format: [fieldName => value][]
+   * @var array
+   */
+  public $filters = [];
+
   /**
    * Filters set programmatically by `civi.api.prepare` listener. Automatically trusted.
    *
@@ -138,7 +148,7 @@ class AutocompleteAction extends AbstractAction {
       $this->addFilter($labelField, $this->input);
     }
 
-    // Ensure SELECT param includes all fields & filters
+    // Ensure SELECT param includes all fields & trusted filters
     $select = [$idField];
     foreach ($this->display['settings']['columns'] as $column) {
       if ($column['type'] === 'field') {
@@ -159,7 +169,7 @@ class AutocompleteAction extends AbstractAction {
     $apiResult = \Civi\Api4\SearchDisplay::run(FALSE)
       ->setSavedSearch($this->savedSearch)
       ->setDisplay($this->display)
-      ->setFilters($this->trustedFilters)
+      ->setFilters($this->filters)
       ->setReturn($return)
       ->execute();
 
@@ -188,6 +198,7 @@ class AutocompleteAction extends AbstractAction {
    * @param mixed $value
    */
   public function addFilter(string $fieldName, $value) {
+    $this->filters[$fieldName] = $value;
     $this->trustedFilters[$fieldName] = $value;
   }
 
index 9a52b9c616a95bf23aa12261c4bafef045f3073c..0980423758f4e4f5f97057ea7e254274061ae31f 100644 (file)
         bindToController: {
           crmAutocomplete: '<',
           crmAutocompleteParams: '<',
-          multiple: '<'
+          multiple: '<',
+          autoOpen: '<'
         },
         controller: function($element, $timeout) {
           var ctrl = this;
           $timeout(function() {
             $element.crmAutocomplete(ctrl.crmAutocomplete, ctrl.crmAutocompleteParams, {
-              multiple: ctrl.multiple
+              multiple: ctrl.multiple,
+              minimumInputLength: ctrl.autoOpen ? 0 : 1
             });
             // Ensure widget is updated when model changes
             if (ctrl.ngModel) {
index b0885f97bb89dee91884aa4528fe5d799d0a74b5..668ec720b752203683936749047c1f69aa9876a9 100644 (file)
@@ -8,8 +8,12 @@
 </li>
 <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}">
+    <input placeholder="{{:: ts('Saved Search') }}" class="form-control" crm-autocomplete="'SavedSearch'" crm-autocomplete-params="{key: 'name', filters: {api_entity: $ctrl.fieldDefn.fk_entity}, formName: 'afformAdmin', fieldName: 'autocompleteSavedSearch'}" auto-open="true" ng-model="getSet('saved_search')" ng-model-options="{getterSetter: true}" ng-change="getSet('search_display')(null)">
+  </div>
+</li>
+<li ng-if="$ctrl.fieldDefn.input_type === 'EntityRef' && $ctrl.fieldDefn.saved_search" title="{{:: ts('Use a saved search to filter the autocomplete results') }}">
+  <div href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
+    <input placeholder="{{:: ts('Default Display') }}" class="form-control" crm-autocomplete="'SearchDisplay'" crm-autocomplete-params="{key: 'name', filters: {'saved_search_id.name': $ctrl.fieldDefn.saved_search}, formName: 'afformAdmin', fieldName: 'autocompleteDisplay'}" auto-open="true" ng-model="getSet('search_display')" ng-model-options="{getterSetter: true}">
   </div>
 </li>
 <li ng-if="$ctrl.fieldDefn.input_type === 'EntityRef'" title="{{:: ts('Should permissions be checked when autocompleting existing entities') }}">
     <input crm-ui-select="{data: $ctrl.editor.securityModes}" ng-model="getSet('security')" ng-model-options="{getterSetter: true}" class="form-control">
   </div>
 </li>
+<li ng-if="$ctrl.fieldDefn.input_type === 'EntityRef'">
+  <a href ng-click="toggleAttr('input_attrs.autoOpen'); $event.stopPropagation(); $event.target.blur();" title="{{:: ts('Show autocomplete results without typing') }}">
+    <i class="crm-i fa-{{ getProp('input_attrs.autoOpen') ? 'check-' : '' }}square-o"></i>
+    {{:: ts('Auto Open') }}
+  </a>
+</li>
 <li>
-  <a href ng-click="toggleRequired(); $event.stopPropagation(); $event.target.blur();" title="{{:: ts('Require this field') }}">
+  <a href ng-click="toggleAttr('required'); $event.stopPropagation(); $event.target.blur();" title="{{:: ts('Require this field') }}">
     <i class="crm-i fa-{{ getProp('required') ? 'check-' : '' }}square-o"></i>
     {{:: ts('Required') }}
   </a>
index 86bbc8b9c869c85701918fb319cd1dca07226852..1550f94faaf0aa12deb8ba0128d58d18bdc5e031 100644 (file)
         setDateOptions();
       };
 
-      $scope.toggleRequired = function() {
-        getSet('required', !getSet('required'));
+      $scope.toggleAttr = function(attr) {
+        getSet(attr, !getSet(attr));
       };
 
       $scope.toggleHelp = function(position) {
index 24bec2533748725d726027ac2c3b9fe7e8336c6a..7c8bb97f4bd54b46d06841d3c0b0da4d9a1776d7 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\Generic\AutocompleteAction;
 use Civi\Api4\Utils\CoreUtil;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
@@ -38,54 +39,96 @@ class AutocompleteSubscriber implements EventSubscriberInterface {
   public function onApiPrepare(\Civi\API\Event\PrepareEvent $event) {
     $apiRequest = $event->getApiRequest();
     if (is_object($apiRequest) && is_a($apiRequest, 'Civi\Api4\Generic\AutocompleteAction')) {
-      $formName = $apiRequest->getFormName();
-      if (!str_starts_with((string) $formName, 'afform:') || !strpos((string) $apiRequest->getFieldName(), ':')) {
-        return;
-      }
-      [$entityName, $fieldName] = explode(':', $apiRequest->getFieldName());
-      // Load afform only if user has permission
-      $afform = Afform::get()
-        ->addWhere('name', '=', str_replace('afform:', '', $formName))
-        ->addSelect('layout')
-        ->execute()->first();
-      if (!$afform) {
-        return;
-      }
-      $formDataModel = new FormDataModel($afform['layout']);
-      $entity = $formDataModel->getEntity($entityName);
-      $isId = $fieldName === CoreUtil::getIdFieldName($entity['type']);
-      $fieldSpec = civicrm_api4($entity['type'], 'getFields', [
-        'checkPermissions' => FALSE,
-        'where' => [['name', '=', $fieldName]],
-      ])->single();
-      $formField = $entity['fields'][$fieldName]['defn'] ?? [];
+      [$formType, $formName] = array_pad(explode(':', (string) $apiRequest->getFormName()), 2, '');
+      [$entityName, $fieldName] = array_pad(explode(':', (string) $apiRequest->getFieldName()), 2, '');
 
-      // Auto-add filters defined in schema
-      foreach ($fieldSpec['input_attrs']['filter'] ?? [] as $key => $value) {
-        $apiRequest->addFilter($key, $value);
+      switch ($formType) {
+        case 'afform':
+          if ($formName && $entityName && $fieldName) {
+            $this->processAfformAutocomplete($formName, $entityName, $fieldName, $apiRequest);
+          }
+          return;
+
+        case 'afformAdmin':
+          $this->processAfformAdminAutocomplete($entityName, $apiRequest);
       }
+    }
+  }
 
-      // For the "Existing Entity" selector,
-      // Look up the "type" fields (e.g. contact_type, activity_type_id, case_type_id, etc)
-      // And apply it as a filter if specified on the form.
-      if ($isId) {
-        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]);
-          }
+  /**
+   * Preprocess autocomplete fields for afforms
+   *
+   * @param string $formName
+   * @param string $entityName
+   * @param string $fieldName
+   * @param \Civi\Api4\Generic\AutocompleteAction $apiRequest
+   */
+  private function processAfformAutocomplete(string $formName, string $entityName, string $fieldName, AutocompleteAction $apiRequest):void {
+    // Load afform only if user has permission
+    $afform = Afform::get()
+      ->addWhere('name', '=', $formName)
+      ->addSelect('layout')
+      ->execute()->first();
+    if (!$afform) {
+      return;
+    }
+    $formDataModel = new FormDataModel($afform['layout']);
+    $entity = $formDataModel->getEntity($entityName);
+    $isId = $fieldName === CoreUtil::getIdFieldName($entity['type']);
+    $fieldSpec = civicrm_api4($entity['type'], 'getFields', [
+      'checkPermissions' => FALSE,
+      'where' => [['name', '=', $fieldName]],
+    ])->single();
+    $formField = $entity['fields'][$fieldName]['defn'] ?? [];
+
+    // Auto-add filters defined in schema
+    foreach ($fieldSpec['input_attrs']['filter'] ?? [] as $key => $value) {
+      $apiRequest->addFilter($key, $value);
+    }
+
+    // For the "Existing Entity" selector,
+    // Look up the "type" fields (e.g. contact_type, activity_type_id, case_type_id, etc)
+    // And apply it as a filter if specified on the form.
+    if ($isId) {
+      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]);
         }
       }
+    }
+
+    $apiRequest->setCheckPermissions(($formField['security'] ?? NULL) !== 'FBAC');
+    $apiRequest->setSavedSearch($formField['saved_search'] ?? NULL);
+    $apiRequest->setDisplay($formField['search_display'] ?? NULL);
+  }
 
-      $apiRequest->setCheckPermissions($formField['security'] !== 'FBAC');
-      $apiRequest->setSavedSearch($formField['saved_search'] ?? NULL);
+  /**
+   * Preprocess autocomplete fields on AfformAdmin screens
+   *
+   * @param string $fieldName
+   * @param \Civi\Api4\Generic\AutocompleteAction $apiRequest
+   */
+  private function processAfformAdminAutocomplete(string $fieldName, AutocompleteAction $apiRequest):void {
+    if (!\CRM_Core_Permission::check([['administer CiviCRM', 'administer afform']])) {
+      return;
+    }
+    switch ($fieldName) {
+      case 'autocompleteSavedSearch':
+        $apiRequest->addFilter('api_entity', $apiRequest->getFilters()['api_entity']);
+        return;
+
+      case 'autocompleteDisplay':
+        $apiRequest->addFilter('saved_search_id.name', $apiRequest->getFilters()['saved_search_id.name']);
+        $apiRequest->addFilter('type', 'autocomplete');
+        return;
     }
   }
 
index e10509cdc57bea7b46028c0908217faa2300850f..e2d0a604144e57ef3817103cee93fc4570d2e941 100644 (file)
@@ -7,5 +7,6 @@
        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"
+       auto-open="$ctrl.defn.input_attrs.autoOpen"
        placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}"
        ng-change="$ctrl.onSelectEntity()" >
index 033d4b48162e017162097df9d014bf5dc677d5ed..5d779bec600fbd0a30fd4e420210175a58239527 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from org.civicrm.search_kit/xml/schema/CRM/Search/SearchDisplay.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:1ba9d436c0e83839fe2b88a9d304762a)
+ * (GenCodeChecksum:5ca9cccecaa79803d415f288292376bb)
  */
 use CRM_Search_ExtensionUtil as E;
 
@@ -31,6 +31,13 @@ class CRM_Search_DAO_SearchDisplay extends CRM_Core_DAO {
    */
   public static $_icon = 'fa-clone';
 
+  /**
+   * Field to show when displaying a record.
+   *
+   * @var string
+   */
+  public static $_labelField = 'label';
+
   /**
    * Should CiviCRM log any modifications to this table in the civicrm_log table.
    *
index 1b1254db2c5de969a007b9aae24f57d785d61402..ffb8336d5f635a992c49e82c79e14ab8231c9e55 100644 (file)
@@ -7,6 +7,7 @@
   <comment>SearchKit - saved search displays</comment>
   <log>true</log>
   <icon>fa-clone</icon>
+  <labelField>label</labelField>
 
   <field>
     <name>id</name>
index 1c8d621b3f37cd3cc73a129f9d7e5ae1f843de58..2b13cd552a06c0757ef3b7a102facd34e5ccdc75 100644 (file)
@@ -527,7 +527,7 @@ if (!CRM.vars) CRM.vars = {};
   $.fn.crmAutocomplete = function(entityName, apiParams, select2Options) {
     select2Options = select2Options || {};
     return $(this).each(function() {
-      $(this).crmSelect2({
+      $(this).crmSelect2(_.extend({
         ajax: {
           quietMillis: 250,
           url: CRM.url('civicrm/ajax/api4/' + entityName + '/autocomplete'),
@@ -560,7 +560,7 @@ if (!CRM.vars) CRM.vars = {};
             callback(multiple ? result : result[0]);
           });
         }
-      });
+      }, select2Options));
     });
   };