Afform - Autofill contacts by relationship
authorColeman Watts <coleman@civicrm.org>
Fri, 18 Nov 2022 19:57:45 +0000 (14:57 -0500)
committerColeman Watts <coleman@civicrm.org>
Fri, 25 Nov 2022 18:42:52 +0000 (13:42 -0500)
Fixes dev/core#3453

20 files changed:
ang/crmUi.js
ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js
ext/afform/admin/ang/afGuiEditor/afGuiEntity.html
ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js
ext/afform/admin/ang/afGuiEditor/behaviors/afGuiDefaultBehaviorTemplate.html [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehavior.html [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.component.js [new file with mode: 0644]
ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.html [new file with mode: 0644]
ext/afform/core/Civi/Afform/AbstractBehavior.php
ext/afform/core/Civi/Afform/Behavior/ContactAutofill.php
ext/afform/core/Civi/Afform/BehaviorInterface.php
ext/afform/core/Civi/Afform/Event/AfformEntitySortEvent.php [new file with mode: 0644]
ext/afform/core/Civi/Afform/Event/AfformEventEntityTrait.php
ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php
ext/afform/core/Civi/Api4/Action/AfformBehavior/Get.php
ext/afform/core/Civi/Api4/AfformBehavior.php
ext/afform/mock/tests/phpunit/api/v4/AfformRelationshipUsageTest.php
ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php
ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php
ext/search_kit/ang/crmSearchTasks.module.js

index 2dc30f06a244df5222abd8463bf22555873ed91e..6b9a54c622b41a4d61275d84917cd5bcb76587b3 100644 (file)
       };
     })
 
+    // Reformat an array of objects for compatibility with select2
+    .factory('formatForSelect2', function() {
+      return function(input, key, label, extra) {
+        return _.transform(input, function(result, item) {
+          var formatted = {id: item[key], text: item[label]};
+          if (extra) {
+            _.merge(formatted, _.pick(item, extra));
+          }
+          result.push(formatted);
+        }, []);
+      };
+    })
+
     .run(function($rootScope, $location) {
       /// Example: <button ng-click="goto('home')">Go home!</button>
       $rootScope.goto = function(path) {
index 2a783443dbeb612baadb826e296f357d397b2aef..3b031a4165592a9826a71e75fbfd28ecdb03e4b1 100644 (file)
@@ -8,7 +8,7 @@
       entity: '<'
     },
     require: {editor: '^^afGuiEditor'},
-    controller: function ($scope, $timeout, afGui) {
+    controller: function ($scope, $timeout, afGui, formatForSelect2) {
       var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin');
       var ctrl = this;
       $scope.controls = {};
         return afGui.meta.entities[ctrl.getEntityType()];
       };
 
-      this.getBehaviors = function() {
-        return CRM.afGuiEditor.behaviors[ctrl.getEntityType()];
-      };
-
       $scope.getField = afGui.getField;
 
       $scope.valuesFields = function() {
           $scope.controls.fieldSearch = '';
           ctrl.buildPaletteLists();
         });
+
+        ctrl.behaviors = _.transform(CRM.afGuiEditor.behaviors[ctrl.getEntityType()], function(behaviors, behavior) {
+          var item = _.cloneDeep(behavior);
+          item.options = formatForSelect2(item.modes, 'name', 'label', ['description', 'icon']);
+          behaviors.push(item);
+        }, []);
       };
     }
   });
index 9da4dd5505a27a6d04ec8d1c40ac0ddd5706c1f8..a62492cef525d3178e4def216de691349ac1a8a1 100644 (file)
   <div ng-include="'~/afGuiEditor/entityConfig/Options.html'"></div>
 </fieldset>
 
-<fieldset ng-repeat="behavior in $ctrl.getBehaviors()">
-  <label for="{{:: behavior.key + '_mode' }}">{{:: behavior.title }}</label>
-  <select id="{{:: behavior.key + '_mode' }}" class="form-control" ng-model="$ctrl.entity[behavior.key]">
-    <option value="">{{:: ts('None') }}</option>
-    <option value="{{:: mode.name }}" ng-repeat="mode in behavior.modes">{{:: mode.label }}</option>
-  </select>
-  <p class="help-block" ng-show=":: behavior.description">{{:: behavior.description }}</p>
+<fieldset ng-repeat="behavior in $ctrl.behaviors">
+  <legend>{{:: behavior.title }}</legend>
+  <div ng-include="behavior.template || '~/afGuiEditor/behaviors/afGuiDefaultBehaviorTemplate.html'"></div>
 </fieldset>
index 82448377a2567f2c91e9ac2901e02af52493187c..162a30526ffb4b4cd002a4c41d367c85500086d2 100644 (file)
@@ -24,6 +24,7 @@
             inputType = field.input_type,
             dataType = field.data_type;
           multi = field.serialize || dataType === 'Array';
+          $el.crmSelect2('destroy').crmDatepicker('destroy');
           // Allow input_type to override dataType
           if (inputType) {
             multi = (dataType !== 'Boolean' &&
                   if (entity.data && entity.data[key] && entity.data[key] != value) {
                     filtersMatch = false;
                   }
-                })
+                });
                 if (filtersMatch) {
                   options.push({id: entity.name, label: entity.label, icon: afGui.meta.entities[entity.type].icon});
                 }
               });
-              $el.crmAutocomplete(field.fk_entity, {fieldName: field.entity + '.' + field.name}, {
+              var params = field.entity && field.name ? {fieldName: field.entity + '.' + field.name} : {filters: filters};
+              $el.crmAutocomplete(field.fk_entity, params, {
                 multiple: multi,
                 "static": options,
                 minimumInputLength: options.length ? 1 : 0
           ctrl.ngModel.$isEmpty = function(value) {
             return !value || !value.length;
           };
+        };
 
+        this.$onChanges = function() {
           $timeout(function() {
             makeWidget(ctrl.field);
           });
diff --git a/ext/afform/admin/ang/afGuiEditor/behaviors/afGuiDefaultBehaviorTemplate.html b/ext/afform/admin/ang/afGuiEditor/behaviors/afGuiDefaultBehaviorTemplate.html
new file mode 100644 (file)
index 0000000..f1a4e81
--- /dev/null
@@ -0,0 +1,3 @@
+<!-- Default template for behaviors that do not provide their own template -->
+<input title="{{:: behavior.title }}" crm-ui-select="{data: behavior.options, placeholder: ts('None')}" class="form-control" ng-model="$ctrl.entity[behavior.key]">
+<p class="help-block" ng-show=":: behavior.description">{{:: behavior.description }}</p>
diff --git a/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehavior.html b/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehavior.html
new file mode 100644 (file)
index 0000000..5495604
--- /dev/null
@@ -0,0 +1,8 @@
+<input title="{{:: behavior.title }}" crm-ui-select="{data: behavior.options, placeholder: ts('None')}" class="form-control" ng-model="$ctrl.entity[behavior.key]">
+
+<p class="help-block" ng-show="!$ctrl.entity.autofill">{{:: behavior.description }}</p>
+
+<div ng-if="$ctrl.entity.autofill && $ctrl.entity.autofill.indexOf('relationship:') === 0">
+  <autofill-relationship-behavior-form entity="$ctrl.entity" rel-types="behavior.modes" selected-type="$ctrl.entity.autofill">
+  </autofill-relationship-behavior-form>
+</div>
diff --git a/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.component.js b/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.component.js
new file mode 100644 (file)
index 0000000..0eef072
--- /dev/null
@@ -0,0 +1,43 @@
+// https://civicrm.org/licensing
+(function(angular, $, _) {
+  "use strict";
+
+  // For configuring autofill by related contact
+  angular.module('afGuiEditor').component('autofillRelationshipBehaviorForm', {
+    templateUrl: '~/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.html',
+    bindings: {
+      entity: '<',
+      selectedType: '<',
+      relTypes: '<'
+    },
+    controller: function($scope, afGui) {
+      var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin'),
+        ctrl = this;
+
+      this.getPlaceholder = function() {
+        var selectedType = _.find(ctrl.relTypes, {name: ctrl.selectedType}).contact_type || 'Contact';
+        return ts('Select %1', {1: afGui.getEntity(selectedType).label});
+      };
+
+      // Initialize or rebuild form field
+      this.$onChanges = function(changes) {
+        if (changes.selectedType) {
+          var selectedType = _.find(ctrl.relTypes, {name: ctrl.selectedType});
+          if (!ctrl.relatedContactField || ctrl.relatedContactField.input_attrs.filter.contact_type !== selectedType.contact_type) {
+            // Replacing the variable with a new object will trigger the afGuiFieldValue to refresh
+            ctrl.relatedContactField = {
+              input_type: 'EntityRef',
+              data_type: 'Integer',
+              fk_entity: 'Contact',
+              input_attrs: {filter: {}}
+            };
+            if (selectedType.contact_type) {
+              ctrl.relatedContactField.input_attrs.filter.contact_type = selectedType.contact_type;
+            }
+          }
+        }
+      };
+    }
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.html b/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.html
new file mode 100644 (file)
index 0000000..e4d6156
--- /dev/null
@@ -0,0 +1,7 @@
+<input
+  class="form-control"
+  required
+  placeholder="{{ $ctrl.getPlaceholder() }}"
+  ng-model="$ctrl.entity['autofill-relationship']"
+  af-gui-field-value="$ctrl.relatedContactField"
+  >
index 2ca00ef73c8cdea0c35077ab08f6c307d4d6c238..fcfcb5b1bbf8745f58f719b543a0e89a2b47df39 100644 (file)
@@ -39,6 +39,15 @@ abstract class AbstractBehavior extends AutoService implements BehaviorInterface
     return NULL;
   }
 
+  /**
+   * Optional template for configuring the behavior in the AfformGuiEditor
+   *
+   * @return string|null
+   */
+  public static function getTemplate(): ?string {
+    return NULL;
+  }
+
   /**
    * Dashed name, name of entity attribute for selected mode
    * @return string
index 37475c499dee8fd8b85e0a019760392862901f77..626f94c047ac20fd27d2d00171ad917c3c6cb729 100644 (file)
@@ -2,6 +2,7 @@
 namespace Civi\Afform\Behavior;
 
 use Civi\Afform\AbstractBehavior;
+use Civi\Afform\Event\AfformEntitySortEvent;
 use Civi\Afform\Event\AfformPrefillEvent;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use CRM_Afform_ExtensionUtil as E;
@@ -17,12 +18,13 @@ class ContactAutofill extends AbstractBehavior implements EventSubscriberInterfa
    */
   public static function getSubscribedEvents() {
     return [
+      'civi.afform.sort.prefill' => 'onAfformSortPrefill',
       'civi.afform.prefill' => ['onAfformPrefill', 99],
     ];
   }
 
   public static function getEntities():array {
-    return ['Individual'];
+    return \CRM_Contact_BAO_ContactType::basicTypes();
   }
 
   public static function getTitle():string {
@@ -35,29 +37,95 @@ class ContactAutofill extends AbstractBehavior implements EventSubscriberInterfa
   }
 
   public static function getDescription():string {
-    return E::ts('Automatically identify this contact');
+    return E::ts('Automatically identify this contact based on logged-in status or relationship to another contact on the form.');
   }
 
-  public static function getModes(string $entityName):array {
+  public static function getTemplate(): ?string {
+    return '~/afGuiEditor/behaviors/autofillRelationshipBehavior.html';
+  }
+
+  public static function getModes(string $contactType):array {
     $modes = [];
-    $modes[] = [
-      'name' => 'user',
-      'label' => E::ts('Current User'),
-    ];
+    if ($contactType === 'Individual') {
+      $modes[] = [
+        'name' => 'user',
+        'label' => E::ts('Current User'),
+        'description' => E::ts('Auto-select logged-in user'),
+        'icon' => 'fa-user-circle',
+      ];
+    }
+    $relationshipTypes = \Civi\Api4\RelationshipType::get(FALSE)
+      ->addSelect('name_a_b', 'name_b_a', 'label_a_b', 'label_b_a', 'description', 'contact_type_a', 'contact_type_b')
+      ->addWhere('is_active', '=', TRUE)
+      ->addClause('OR', ['contact_type_a', '=', $contactType], ['contact_type_a', 'IS NULL'], ['contact_type_b', '=', $contactType], ['contact_type_b', 'IS NULL'])
+      ->execute();
+    foreach ($relationshipTypes as $relationshipType) {
+      if (!$relationshipType['contact_type_a'] || $relationshipType['contact_type_a'] === $contactType) {
+        $modes[] = [
+          'name' => 'relationship:' . $relationshipType['name_a_b'],
+          'label' => $relationshipType['label_a_b'],
+          'description' => $relationshipType['description'],
+          'icon' => 'fa-handshake-o',
+          'contact_type' => $relationshipType['contact_type_b'],
+        ];
+      }
+      if (
+        $relationshipType['name_b_a'] && $relationshipType['name_a_b'] != $relationshipType['name_b_a'] &&
+        (!$relationshipType['contact_type_b'] || $relationshipType['contact_type_b'] === $contactType)
+      ) {
+        $modes[] = [
+          'name' => 'relationship:' . $relationshipType['name_b_a'],
+          'label' => $relationshipType['label_b_a'],
+          'description' => $relationshipType['description'],
+          'icon' => 'fa-handshake-o',
+          'contact_type' => $relationshipType['contact_type_a'],
+        ];
+      }
+    }
     return $modes;
   }
 
-  public static function onAfformPrefill(AfformPrefillEvent $event) {
+  public static function onAfformSortPrefill(AfformEntitySortEvent $event): void {
+    foreach ($event->getFormDataModel()->getEntities() as $entityName => $entity) {
+      $autoFillMode = $entity['autofill'] ?? '';
+      $relatedContact = $entity['autofill-relationship'] ?? NULL;
+      if ($relatedContact && strpos($autoFillMode, 'relationship:') === 0) {
+        $event->addDependency($entityName, $relatedContact);
+      }
+    }
+  }
+
+  public static function onAfformPrefill(AfformPrefillEvent $event): void {
     if ($event->getEntityType() === 'Contact') {
       $entity = $event->getEntity();
       $id = $event->getEntityId();
+      $autoFillMode = $entity['autofill'] ?? '';
+      $relatedContact = $entity['autofill-relationship'] ?? NULL;
       // Autofill with current user
-      if (!$id && ($entity['autofill'] ?? NULL) === 'user') {
+      if (!$id && $autoFillMode === 'user') {
         $id = \CRM_Core_Session::getLoggedInContactID();
         if ($id) {
           $event->getApiRequest()->loadEntity($entity, [$id]);
         }
       }
+      // Autofill by relationship
+      if (!$id && $relatedContact && strpos($autoFillMode, 'relationship:') === 0) {
+        $relationshipType = substr($autoFillMode, strlen('relationship:'));
+        $relatedEntity = $event->getFormDataModel()->getEntity($relatedContact);
+        if ($relatedEntity) {
+          $relatedContact = $event->getEntityIds($relatedContact)[0] ?? NULL;
+        }
+        if ($relatedContact) {
+          $relations = \Civi\Api4\RelationshipCache::get(FALSE)
+            ->addSelect('near_contact_id')
+            ->addWhere('near_relation', '=', $relationshipType)
+            ->addWhere('far_contact_id', '=', $relatedContact)
+            ->addWhere('near_contact_id.is_deleted', '=', FALSE)
+            ->addWhere('is_current', '=', TRUE)
+            ->execute()->column('near_contact_id');
+          $event->getApiRequest()->loadEntity($entity, $relations);
+        }
+      }
     }
   }
 
index ad11603724e6e93d01b085e9bfe5e936ed2fc6ee..0d16b6a1d6a60fd044004635bcc8770b136c8d50 100644 (file)
@@ -30,6 +30,13 @@ interface BehaviorInterface {
    */
   public static function getDescription():? string;
 
+  /**
+   * Optional template for configuring the behavior in the AfformGuiEditor
+   *
+   * @return string|null
+   */
+  public static function getTemplate(): ?string;
+
   /**
    * Dashed name, name of entity attribute for selected mode
    * @return string
diff --git a/ext/afform/core/Civi/Afform/Event/AfformEntitySortEvent.php b/ext/afform/core/Civi/Afform/Event/AfformEntitySortEvent.php
new file mode 100644 (file)
index 0000000..38f01a6
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+namespace Civi\Afform\Event;
+
+use MJS\TopSort\Implementations\FixedArraySort;
+
+/**
+ * This event allows listeners to declare that entities depend on others.
+ * These dependencies change the order in which entities are resolved.
+ */
+class AfformEntitySortEvent extends AfformBaseEvent {
+
+  private $dependencies = [];
+
+  /**
+   * @param string $dependentEntity
+   * @param string $dependsOnEntity
+   */
+  public function addDependency(string $dependentEntity, string $dependsOnEntity): void {
+    $this->dependencies[$dependentEntity][$dependsOnEntity] = $dependsOnEntity;
+  }
+
+  /**
+   * Returns entity names sorted by their dependencies
+   *
+   * @return array
+   */
+  public function getSortedEnties(): array {
+    $sorter = new FixedArraySort();
+    $formEntities = array_keys($this->getFormDataModel()->getEntities());
+    foreach ($formEntities as $entityName) {
+      // Add all dependencies that are the valid name of another entitiy
+      $dependencies = array_intersect($this->dependencies[$entityName] ?? [], $formEntities);
+      $sorter->add($entityName, $dependencies);
+    }
+    return $sorter->sort();
+  }
+
+}
index 918d51e6ff40a864a73598ce89617a4084ade65e..c48849a4345d9babfa32e822aec8f3148cb6fa17 100644 (file)
@@ -74,15 +74,29 @@ trait AfformEventEntityTrait {
   }
 
   /**
-   * Get the id of a saved record
+   * Get the id of an instance of the current entity
    * @param int $index
    * @return mixed
    */
   public function getEntityId(int $index = 0) {
-    $idField = CoreUtil::getIdFieldName($this->entityName);
+    $apiEntity = $this->getFormDataModel()->getEntity($this->entityName)['type'];
+    $idField = CoreUtil::getIdFieldName($apiEntity);
     return $this->entityIds[$this->entityName][$index][$idField] ?? NULL;
   }
 
+  /**
+   * Get the id(s) of an entity
+   *
+   * @param string|null $entityName
+   * @return array
+   */
+  public function getEntityIds(string $entityName = NULL): array {
+    $entityName = $entityName ?: $this->entityName;
+    $apiEntity = $this->getFormDataModel()->getEntity($this->entityName)['type'];
+    $idField = CoreUtil::getIdFieldName($apiEntity);
+    return array_column($this->entityIds[$entityName] ?? [], $idField);
+  }
+
   /**
    * @param int $index
    * @param string $joinEntity
index 109e85ba864a820e730ab7323c59a1d92e26dfe6..ff7105d45f74a21d766edfaef61ec80e94f8c4d5 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Civi\Api4\Action\Afform;
 
+use Civi\Afform\Event\AfformEntitySortEvent;
 use Civi\Afform\Event\AfformPrefillEvent;
 use Civi\Afform\FormDataModel;
 use Civi\Api4\Generic\Result;
@@ -77,7 +78,11 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction {
    * Load all entities
    */
   protected function loadEntities() {
-    foreach ($this->_formDataModel->getEntities() as $entityName => $entity) {
+    $sorter = new AfformEntitySortEvent($this->_afform, $this->_formDataModel, $this);
+    \Civi::dispatcher()->dispatch('civi.afform.sort.prefill', $sorter);
+    $sortedEntities = $sorter->getSortedEnties();
+    foreach ($sortedEntities as $entityName) {
+      $entity = $this->_formDataModel->getEntity($entityName);
       $this->_entityIds[$entityName] = [];
       $idField = CoreUtil::getIdFieldName($entity['type']);
       if (!empty($entity['actions']['update'])) {
@@ -86,8 +91,6 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction {
           (!empty($entity['url-autofill']) || isset($entity['fields'][$idField]))
         ) {
           $ids = (array) $this->args[$entityName];
-          // Limit number of records to 1 unless using af-repeat
-          $ids = array_slice($ids, 0, !empty($entity['af-repeat']) ? $entity['max'] ?? NULL : 1);
           $this->loadEntity($entity, $ids);
         }
       }
@@ -103,9 +106,13 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction {
    * @param array $ids
    */
   public function loadEntity(array $entity, array $ids) {
+    // Limit number of records based on af-repeat settings
+    // If 'min' is set then it is repeatable, and max will either be a number or NULL for unlimited.
+    $ids = array_slice($ids, 0, isset($entity['min']) ? $entity['max'] : 1);
+
     $api4 = $this->_formDataModel->getSecureApi4($entity['name']);
     $idField = CoreUtil::getIdFieldName($entity['type']);
-    if (!empty($entity['fields'][$idField]['saved_search'])) {
+    if ($ids && !empty($entity['fields'][$idField]['saved_search'])) {
       $ids = $this->validateBySavedSearch($entity, $ids);
     }
     if (!$ids) {
index d37cd74d1b864b97a15b5ee942f6894de5f5cf62..b3455e50159de02b5e89579c5dd57806a922d4d9 100644 (file)
@@ -31,6 +31,7 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
         'title' => $behaviorClass::getTitle(),
         'description' => $behaviorClass::getDescription(),
         'entities' => $entities,
+        'template' => $behaviorClass::getTemplate(),
         // Get modes for every supported entity
         'modes' => array_map([$behaviorClass, 'getModes'], array_combine($entities, $entities)),
       ];
index f5a6574cee5e37e5697fac54ddb5daea102a21fe..b0ee312386eeb3735b496d1d2c4a5082becdb07f 100644 (file)
@@ -52,6 +52,11 @@ class AfformBehavior extends Generic\AbstractEntity {
           'data_type' => 'String',
           'description' => 'Optional localized description displayed on admin screen',
         ],
+        [
+          'name' => 'template',
+          'data_type' => 'String',
+          'description' => 'Optional template for configuring the behavior in the AfformGuiEditor',
+        ],
         [
           'name' => 'entities',
           'data_type' => 'Array',
index 99dd5256fb5cfcbf50007fb4feba16b94e0611e8..a31a7b66cf31bc298d34857242ecfbac16745cb0 100644 (file)
@@ -62,4 +62,60 @@ EOHTML;
     $this->assertEquals('Firsty3', $saved[1]['contact_id_a.first_name']);
   }
 
+  public function testPrefillContactsByRelationship(): void {
+    $layout = <<<EOHTML
+<af-form ctrl="afform">
+  <af-entity data="{contact_type: 'Individual'}" type="Contact" name="Individual1" label="Individual 1" actions="{create: true, update: true}" security="RBAC" autofill="relationship:Child of" autofill-relationship="Individual2"/>
+  <af-entity data="{contact_type: 'Organization'}" type="Contact" name="Organization1" label="Organization 1" actions="{create: true, update: true}" security="RBAC" url-autofill="1" />
+  <af-entity data="{contact_type: 'Individual'}" type="Contact" name="Individual2" label="Individual 2" actions="{create: true, update: true}" security="RBAC" autofill="relationship:Employee of" autofill-relationship="Organization1"/>
+  <fieldset af-fieldset="Individual1" class="af-container" af-title="Individual 1" af-repeat="Add" min="1" max="2">
+    <afblock-name-individual></afblock-name-individual>
+  </fieldset>
+  <fieldset af-fieldset="Individual2" class="af-container" af-title="Individual 2">
+    <afblock-name-individual></afblock-name-individual>
+  </fieldset>
+  <fieldset af-fieldset="Organization1" class="af-container" af-title="Organization1">
+    <afblock-name-organization></afblock-name-organization>
+  </fieldset>
+</af-form>
+EOHTML;
+
+    $this->useValues([
+      'layout' => $layout,
+      'permission' => CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION,
+    ]);
+
+    $contact = \Civi\Api4\Contact::save(FALSE)
+      ->addRecord(['first_name' => 'Child1'])
+      ->addRecord(['first_name' => 'Child2', 'is_deleted' => TRUE])
+      ->addRecord(['first_name' => 'Parent'])
+      ->addRecord(['organization_name' => 'Employer'])
+      ->addRecord(['first_name' => 'Child3'])
+      ->addRecord(['first_name' => 'Child4'])
+      ->addRecord(['first_name' => 'Child5'])
+      ->execute()->column('id');
+
+    Relationship::save(FALSE)
+      ->addRecord(['contact_id_a' => $contact[2], 'contact_id_b' => $contact[3], 'relationship_type_id:name' => 'Employee of'])
+      ->addRecord(['contact_id_a' => $contact[0], 'contact_id_b' => $contact[2], 'relationship_type_id:name' => 'Child of'])
+      ->addRecord(['contact_id_a' => $contact[1], 'contact_id_b' => $contact[2], 'relationship_type_id:name' => 'Child of'])
+      ->addRecord(['contact_id_a' => $contact[4], 'contact_id_b' => $contact[2], 'relationship_type_id:name' => 'Child of', 'is_active' => FALSE])
+      ->addRecord(['contact_id_a' => $contact[5], 'contact_id_b' => $contact[2], 'relationship_type_id:name' => 'Child of'])
+      ->addRecord(['contact_id_a' => $contact[6], 'contact_id_b' => $contact[2], 'relationship_type_id:name' => 'Child of'])
+      ->execute();
+
+    $prefill = Civi\Api4\Afform::prefill(FALSE)
+      ->setName($this->formName)
+      ->setArgs(['Organization1' => $contact[3]])
+      ->execute()
+      ->indexBy('name');
+
+    $this->assertEquals('Employer', $prefill['Organization1']['values'][0]['fields']['organization_name']);
+    $this->assertEquals('Parent', $prefill['Individual2']['values'][0]['fields']['first_name']);
+    $this->assertEquals('Child1', $prefill['Individual1']['values'][0]['fields']['first_name']);
+    $this->assertEquals('Child4', $prefill['Individual1']['values'][1]['fields']['first_name']);
+    // No room on form for a 3rd child because af-repeat max=2
+    $this->assertCount(2, $prefill['Individual1']['values']);
+  }
+
 }
index ad59000f3fd6ff1853c2d75a0a2b475ed14560ed..eba8bd580612e567eb9e3905448dea9d998d2d9d 100644 (file)
@@ -831,7 +831,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
     $result = [];
     $selectAliases = array_map(function($select) {
       return array_slice(explode(' AS ', $select), -1)[0];
-    }, $this->savedSearch['api_params']['select']);
+    }, $this->_apiParams['select']);
     foreach ($selectAliases as $alias) {
       [$alias] = explode(':', $alias);
       $result[] = $alias;
@@ -886,9 +886,6 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
    * @param array $apiParams
    */
   protected function augmentSelectClause(&$apiParams): void {
-    $existing = array_map(function($item) {
-      return explode(' AS ', $item)[1] ?? $item;
-    }, $apiParams['select']);
     // Add primary key field if actions are enabled
     // (only needed for non-dao entities, as Api4SelectQuery will auto-add the id)
     if (!in_array('DAOEntity', CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'type')) &&
index 517de4ea7d5fb2017f30f79e258d65994ace5195..9eadab8c5e3d29e81dde443b129d291b82f49f15 100644 (file)
@@ -43,6 +43,9 @@ class Run extends AbstractRunAction {
     // Pager can operate in "page" mode for traditional pager, or "scroll" mode for infinite scrolling
     $pagerMode = NULL;
 
+    $this->augmentSelectClause($apiParams);
+    $this->applyFilters();
+
     switch ($this->return) {
       case 'id':
         $key = CoreUtil::getIdFieldName($entityName);
@@ -58,7 +61,6 @@ class Run extends AbstractRunAction {
         break;
 
       case 'tally':
-        $this->applyFilters();
         unset($apiParams['orderBy'], $apiParams['limit']);
         $api = Request::create($entityName, 'get', $apiParams);
         $query = new Api4SelectQuery($api);
@@ -100,11 +102,8 @@ class Run extends AbstractRunAction {
           $apiParams['limit']++;
         }
         $apiParams['orderBy'] = $this->getOrderByFromSort();
-        $this->augmentSelectClause($apiParams);
     }
 
-    $this->applyFilters();
-
     $apiResult = civicrm_api4($entityName, 'get', $apiParams, $index);
     // Copy over meta properties to this result
     $result->rowCount = $apiResult->rowCount;
index e1d9ce4b38da08a32d903e230accae70f03d9417..74a551fc477147ea853d0804b1a77f6a673a3f25 100644 (file)
@@ -2,20 +2,6 @@
   "use strict";
 
   // Declare module
-  angular.module('crmSearchTasks', CRM.angRequires('crmSearchTasks'))
-
-    // Reformat an array of objects for compatibility with select2
-    // Todo this probably belongs in core
-    .factory('formatForSelect2', function() {
-      return function(input, key, label, extra) {
-        return _.transform(input, function(result, item) {
-          var formatted = {id: item[key], text: item[label]};
-          if (extra) {
-            _.merge(formatted, _.pick(item, extra));
-          }
-          result.push(formatted);
-        }, []);
-      };
-    });
+  angular.module('crmSearchTasks', CRM.angRequires('crmSearchTasks'));
 
 })(angular, CRM.$, CRM._);