From: Coleman Watts <coleman@civicrm.org> Date: Fri, 18 Nov 2022 19:57:45 +0000 (-0500) Subject: Afform - Autofill contacts by relationship X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=ce4eb12bdb192d3659b2d52f9a21d027755f0b68;p=civicrm-core.git Afform - Autofill contacts by relationship Fixes dev/core#3453 --- diff --git a/ang/crmUi.js b/ang/crmUi.js index 2dc30f06a2..6b9a54c622 100644 --- a/ang/crmUi.js +++ b/ang/crmUi.js @@ -1280,6 +1280,19 @@ }; }) + // 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) { diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js index 2a783443db..3b031a4165 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js @@ -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 = {}; @@ -26,10 +26,6 @@ return afGui.meta.entities[ctrl.getEntityType()]; }; - this.getBehaviors = function() { - return CRM.afGuiEditor.behaviors[ctrl.getEntityType()]; - }; - $scope.getField = afGui.getField; $scope.valuesFields = function() { @@ -212,6 +208,12 @@ $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); + }, []); }; } }); diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEntity.html b/ext/afform/admin/ang/afGuiEditor/afGuiEntity.html index 9da4dd5505..a62492cef5 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEntity.html +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEntity.html @@ -59,11 +59,7 @@ <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> diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js b/ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js index 82448377a2..162a30526f 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js @@ -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' && @@ -47,12 +48,13 @@ 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 @@ -104,7 +106,9 @@ 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 index 0000000000..f1a4e81230 --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/behaviors/afGuiDefaultBehaviorTemplate.html @@ -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 index 0000000000..5495604fd7 --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehavior.html @@ -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 index 0000000000..0eef07219e --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.component.js @@ -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 index 0000000000..e4d6156ff2 --- /dev/null +++ b/ext/afform/admin/ang/afGuiEditor/behaviors/autofillRelationshipBehaviorForm.html @@ -0,0 +1,7 @@ +<input + class="form-control" + required + placeholder="{{ $ctrl.getPlaceholder() }}" + ng-model="$ctrl.entity['autofill-relationship']" + af-gui-field-value="$ctrl.relatedContactField" + > diff --git a/ext/afform/core/Civi/Afform/AbstractBehavior.php b/ext/afform/core/Civi/Afform/AbstractBehavior.php index 2ca00ef73c..fcfcb5b1bb 100644 --- a/ext/afform/core/Civi/Afform/AbstractBehavior.php +++ b/ext/afform/core/Civi/Afform/AbstractBehavior.php @@ -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 diff --git a/ext/afform/core/Civi/Afform/Behavior/ContactAutofill.php b/ext/afform/core/Civi/Afform/Behavior/ContactAutofill.php index 37475c499d..626f94c047 100644 --- a/ext/afform/core/Civi/Afform/Behavior/ContactAutofill.php +++ b/ext/afform/core/Civi/Afform/Behavior/ContactAutofill.php @@ -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); + } + } } } diff --git a/ext/afform/core/Civi/Afform/BehaviorInterface.php b/ext/afform/core/Civi/Afform/BehaviorInterface.php index ad11603724..0d16b6a1d6 100644 --- a/ext/afform/core/Civi/Afform/BehaviorInterface.php +++ b/ext/afform/core/Civi/Afform/BehaviorInterface.php @@ -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 index 0000000000..38f01a6d78 --- /dev/null +++ b/ext/afform/core/Civi/Afform/Event/AfformEntitySortEvent.php @@ -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(); + } + +} diff --git a/ext/afform/core/Civi/Afform/Event/AfformEventEntityTrait.php b/ext/afform/core/Civi/Afform/Event/AfformEventEntityTrait.php index 918d51e6ff..c48849a434 100644 --- a/ext/afform/core/Civi/Afform/Event/AfformEventEntityTrait.php +++ b/ext/afform/core/Civi/Afform/Event/AfformEventEntityTrait.php @@ -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 diff --git a/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php b/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php index 109e85ba86..ff7105d45f 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php @@ -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) { diff --git a/ext/afform/core/Civi/Api4/Action/AfformBehavior/Get.php b/ext/afform/core/Civi/Api4/Action/AfformBehavior/Get.php index d37cd74d1b..b3455e5015 100644 --- a/ext/afform/core/Civi/Api4/Action/AfformBehavior/Get.php +++ b/ext/afform/core/Civi/Api4/Action/AfformBehavior/Get.php @@ -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)), ]; diff --git a/ext/afform/core/Civi/Api4/AfformBehavior.php b/ext/afform/core/Civi/Api4/AfformBehavior.php index f5a6574cee..b0ee312386 100644 --- a/ext/afform/core/Civi/Api4/AfformBehavior.php +++ b/ext/afform/core/Civi/Api4/AfformBehavior.php @@ -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', diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformRelationshipUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformRelationshipUsageTest.php index 99dd5256fb..a31a7b66cf 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/AfformRelationshipUsageTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/AfformRelationshipUsageTest.php @@ -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']); + } + } diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index ad59000f3f..eba8bd5806 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -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')) && diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php index 517de4ea7d..9eadab8c5e 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php @@ -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; diff --git a/ext/search_kit/ang/crmSearchTasks.module.js b/ext/search_kit/ang/crmSearchTasks.module.js index e1d9ce4b38..74a551fc47 100644 --- a/ext/search_kit/ang/crmSearchTasks.module.js +++ b/ext/search_kit/ang/crmSearchTasks.module.js @@ -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._);