From e9fe23ef3a8ee33ee89125b0a9fc3d93e7afa094 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Mon, 24 Oct 2022 22:11:50 -0400 Subject: [PATCH] Afform - support filters in custom contactRef & core entityRef fields Fixes dev/core#3425 Provides a common format for "filters" in both core and custom entityRef fields. For example, the `employer_id` field now automatically filters `contact_type=Organization`. Custom fields use the same format for filters (basically URL encoded format), and APIv4 now reads them both and applies them when the field is included in an Afform. --- CRM/Contact/DAO/Contact.php | 5 +- CRM/Contact/Form/Edit/Individual.php | 6 +- CRM/Core/CodeGen/Specification.php | 3 + CRM/Core/Form.php | 9 ++- .../Traits/SavedSearchInspectorTrait.php | 15 +++-- Civi/Api4/Service/Spec/SpecFormatter.php | 23 +++++++- .../Subscriber/AutocompleteSubscriber.php | 8 ++- .../api/v4/AfformAutocompleteUsageTest.php | 59 +++++++++++++++++++ tests/phpunit/api/v4/Action/GetFieldsTest.php | 7 +++ .../v4/Custom/CustomFieldGetFieldsTest.php | 17 ++++++ xml/schema/Contact/Contact.xml | 1 + xml/templates/dao.tpl | 2 +- 12 files changed, 141 insertions(+), 14 deletions(-) diff --git a/CRM/Contact/DAO/Contact.php b/CRM/Contact/DAO/Contact.php index ff31535268..19890da492 100644 --- a/CRM/Contact/DAO/Contact.php +++ b/CRM/Contact/DAO/Contact.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Contact/Contact.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:8cdd2fa476983d8770b53cc7665586cf) + * (GenCodeChecksum:44f607f6289003a3b6715c7b9103359b) */ /** @@ -1603,6 +1603,9 @@ class CRM_Contact_DAO_Contact extends CRM_Core_DAO { 'html' => [ 'type' => 'EntityRef', 'label' => ts("Current Employer"), + 'filter' => [ + 'contact_type=Organization', + ], ], 'add' => '2.1', ], diff --git a/CRM/Contact/Form/Edit/Individual.php b/CRM/Contact/Form/Edit/Individual.php index af646a350c..30bb35f67d 100644 --- a/CRM/Contact/Form/Edit/Individual.php +++ b/CRM/Contact/Form/Edit/Individual.php @@ -72,11 +72,7 @@ class CRM_Contact_Form_Edit_Individual { $form->addField('job_title', ['size' => '30']); //Current Employer Element - $props = [ - 'api' => ['params' => ['contact_type' => 'Organization']], - 'create' => TRUE, - ]; - $form->addField('employer_id', $props); + $form->addField('employer_id', ['create' => TRUE]); $form->addField('contact_source', ['class' => 'big']); } diff --git a/CRM/Core/CodeGen/Specification.php b/CRM/Core/CodeGen/Specification.php index e8b2145c52..44309594fb 100644 --- a/CRM/Core/CodeGen/Specification.php +++ b/CRM/Core/CodeGen/Specification.php @@ -413,6 +413,9 @@ class CRM_Core_CodeGen_Specification { $field['html'][$htmlOption] = $this->value($htmlOption, $fieldXML->html); } } + if (isset($fieldXML->html->filter)) { + $field['html']['filter'] = (array) $fieldXML->html->filter; + } } // in multilingual context popup, we need extra information to create appropriate widget diff --git a/CRM/Core/Form.php b/CRM/Core/Form.php index 2d5d6c745a..95834816b6 100644 --- a/CRM/Core/Form.php +++ b/CRM/Core/Form.php @@ -1788,7 +1788,9 @@ class CRM_Core_Form extends HTML_QuickForm_Page { $props['data-api-field'] = $props['name']; } } - $props += CRM_Utils_Array::value('html', $fieldSpec, []); + $htmlProps = (array) ($fieldSpec['html'] ?? []); + CRM_Utils_Array::remove($htmlProps, 'label', 'filter'); + $props += $htmlProps; if (in_array($widget, ['Select', 'Select2']) && !array_key_exists('placeholder', $props) && $placeholder = self::selectOrAnyPlaceholder($props, $required, $label)) { @@ -1889,6 +1891,11 @@ class CRM_Core_Form extends HTML_QuickForm_Page { return $this->add('wysiwyg', $name, $label, $props, $required); case 'EntityRef': + // Auto-apply filters from field metadata + foreach ($fieldSpec['html']['filter'] ?? [] as $filter) { + [$k, $v] = explode('=', $filter); + $props['api']['params'][$k] = $v; + } return $this->addEntityRef($name, $label, $props, $required); case 'Password': diff --git a/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php b/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php index c9b03ab24d..27094dd914 100644 --- a/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php +++ b/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php @@ -218,6 +218,7 @@ trait SavedSearchInspectorTrait { foreach ($fieldNames as $fieldName) { $field = $this->getField($fieldName); $dataType = $field['data_type'] ?? NULL; + $operators = ($field['operators'] ?? []) ?: CoreUtil::getOperators(); // Array is either associative `OP => VAL` or sequential `IN (...)` if (is_array($value)) { $value = array_filter($value, [$this, 'hasValue']); @@ -245,18 +246,24 @@ trait SavedSearchInspectorTrait { $filterClauses[] = ['AND', $andGroup]; } } - elseif (!empty($field['serialize'])) { + elseif (!empty($field['serialize']) && in_array('CONTAINS', $operators, TRUE)) { $filterClauses[] = [$fieldName, 'CONTAINS', $value]; } - elseif (!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) { + elseif ((!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) && in_array('=', $operators, TRUE)) { $filterClauses[] = [$fieldName, '=', $value]; } - elseif ($prefixWithWildcard) { + elseif ($prefixWithWildcard && in_array('CONTAINS', $operators, TRUE)) { $filterClauses[] = [$fieldName, 'CONTAINS', $value]; } - else { + elseif (in_array('LIKE', $operators, TRUE)) { $filterClauses[] = [$fieldName, 'LIKE', $value . '%']; } + elseif (in_array('IN', $operators, TRUE)) { + $filterClauses[] = [$fieldName, 'IN', (array) $value]; + } + else { + $filterClauses[] = [$fieldName, '=', $value]; + } } // Single field if (count($filterClauses) === 1) { diff --git a/Civi/Api4/Service/Spec/SpecFormatter.php b/Civi/Api4/Service/Spec/SpecFormatter.php index 7357960574..4f280e66c2 100644 --- a/Civi/Api4/Service/Spec/SpecFormatter.php +++ b/Civi/Api4/Service/Spec/SpecFormatter.php @@ -281,6 +281,11 @@ class SpecFormatter { $inputType = $data['html']['type'] ?? $data['html_type'] ?? NULL; $inputAttrs = $data['html'] ?? []; unset($inputAttrs['type']); + // Custom field contact ref filters + if (is_string($data['filter'] ?? NULL) && strpos($data['filter'], '=')) { + $filters = explode('&', $data['filter']); + $inputAttrs['filter'] = $filters; + } $map = [ 'Select Date' => 'Date', @@ -321,9 +326,25 @@ class SpecFormatter { foreach ($inputAttrs as $key => $val) { if ($key !== strtolower($key)) { unset($inputAttrs[$key]); - $key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key)); + $key = \CRM_Utils_String::convertStringToSnakeCase($key); $inputAttrs[$key] = $val; } + // Format EntityRef filter property (core and custom fields) + if ($key === 'filter' && is_array($val)) { + $filters = []; + foreach ($val as $filter) { + [$k, $v] = explode('=', $filter); + $filters[$k] = $v; + } + // Legacy APIv3 custom field stuff + if ($dataTypeName === 'ContactReference') { + if (!empty($filters['group'])) { + $filters['groups'] = $filters['group']; + } + unset($filters['action'], $filters['group']); + } + $inputAttrs['filter'] = $filters; + } } $fieldSpec ->setInputType($inputType) diff --git a/ext/afform/core/Civi/Api4/Subscriber/AutocompleteSubscriber.php b/ext/afform/core/Civi/Api4/Subscriber/AutocompleteSubscriber.php index d288a99c6f..5f6631fdea 100644 --- a/ext/afform/core/Civi/Api4/Subscriber/AutocompleteSubscriber.php +++ b/ext/afform/core/Civi/Api4/Subscriber/AutocompleteSubscriber.php @@ -54,12 +54,15 @@ class AutocompleteSubscriber implements EventSubscriberInterface { $formDataModel = new FormDataModel($afform['layout']); $entity = $formDataModel->getEntity($entityName); $isId = $fieldName === CoreUtil::getIdFieldName($entity['type']); + $field = civicrm_api4($entity['type'], 'getFields', [ + 'checkPermissions' => FALSE, + 'where' => [['name', '=', $fieldName]], + ])->single(); // 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) { - $typeFields = []; if ($entity['type'] === 'Contact') { $typeFields = ['contact_type', 'contact_sub_type']; } @@ -74,6 +77,9 @@ class AutocompleteSubscriber implements EventSubscriberInterface { } } } + foreach ($field['input_attrs']['filter'] ?? [] as $key => $value) { + $apiRequest->addFilter($key, $value); + } $apiRequest->setCheckPermissions($entity['security'] !== 'FBAC'); $apiRequest->setSavedSearch($entity['fields'][$fieldName]['defn']['saved_search'] ?? NULL); diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformAutocompleteUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformAutocompleteUsageTest.php index 748c8fe32e..00dab56e67 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/AfformAutocompleteUsageTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/AfformAutocompleteUsageTest.php @@ -1,6 +1,7 @@ assertEquals('B ' . $lastName, $result[1]['label']); } + public function testCustomContactRefFieldWithGroupsFilter(): void { + $lastName = uniqid(__FUNCTION__); + + $sampleData = [ + ['last_name' => $lastName, 'first_name' => 'A'], + ['last_name' => $lastName, 'first_name' => 'B'], + ['last_name' => $lastName, 'first_name' => 'C'], + ]; + + $contacts = Contact::save(FALSE) + ->setRecords($sampleData) + ->execute(); + + $group = \Civi\Api4\Group::create(FALSE) + ->addValue('name', $lastName) + ->addValue('title', $lastName) + ->addChain('A', GroupContact::create()->addValue('group_id', '$id')->addValue('contact_id', $contacts[0]['id'])) + ->addChain('B', GroupContact::create()->addValue('group_id', '$id')->addValue('contact_id', $contacts[1]['id'])) + ->execute()->single(); + + \Civi\Api4\CustomGroup::create(FALSE) + ->addValue('title', 'test_af_fields') + ->addValue('extends', 'Contact') + ->addChain('fields', \Civi\Api4\CustomField::save() + ->addDefault('custom_group_id', '$id') + ->setRecords([ + ['label' => 'contact_ref', 'data_type' => 'ContactReference', 'html_type' => 'Autocomplete', 'filter' => 'action=get&group=' . $group['id']], + ]) + ) + ->execute(); + + $layout = << + +
+
+ +
+
+ +EOHTML; + + $this->useValues([ + 'layout' => $layout, + 'permission' => CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION, + ]); + + $result = Contact::autocomplete() + ->setFormName('afform:' . $this->formName) + ->setFieldName('Individual1:test_af_fields.contact_ref') + ->setInput($lastName) + ->execute(); + + $this->assertCount(2, $result); + $this->assertEquals('A ' . $lastName, $result[0]['label']); + $this->assertEquals('B ' . $lastName, $result[1]['label']); + } + } diff --git a/tests/phpunit/api/v4/Action/GetFieldsTest.php b/tests/phpunit/api/v4/Action/GetFieldsTest.php index 840e9ceebb..006cd6cfaa 100644 --- a/tests/phpunit/api/v4/Action/GetFieldsTest.php +++ b/tests/phpunit/api/v4/Action/GetFieldsTest.php @@ -139,4 +139,11 @@ class GetFieldsTest extends Api4TestBase implements TransactionalInterface { $this->assertEquals('Contact', $tagFields['entity_id']['fk_entity']); } + public function testFiltersAreReturned(): void { + $field = Contact::getFields(FALSE) + ->addWhere('name', '=', 'employer_id') + ->execute()->single(); + $this->assertEquals(['contact_type' => 'Organization'], $field['input_attrs']['filter']); + } + } diff --git a/tests/phpunit/api/v4/Custom/CustomFieldGetFieldsTest.php b/tests/phpunit/api/v4/Custom/CustomFieldGetFieldsTest.php index 064a7cf8ef..191c956d69 100644 --- a/tests/phpunit/api/v4/Custom/CustomFieldGetFieldsTest.php +++ b/tests/phpunit/api/v4/Custom/CustomFieldGetFieldsTest.php @@ -280,4 +280,21 @@ class CustomFieldGetFieldsTest extends CustomTestBase { $this->assertArrayHasKey('always.on', $participant3Fields); } + public function testFiltersAreReturnedForContactRefFields(): void { + $grp = CustomGroup::create(FALSE) + ->addValue('extends', 'Activity') + ->addValue('title', 'act_test_grp2') + ->execute()->single(); + $field = $this->createTestRecord('CustomField', [ + 'data_type' => 'ContactReference', + 'html_type' => 'Autocomplete-Select', + 'custom_group_id' => $grp['id'], + 'filter' => 'action=get&contact_type=Household&group=2', + ]); + $getField = Activity::getFields(FALSE) + ->addWhere('custom_field_id', '=', $field['id']) + ->execute()->single(); + $this->assertEquals(['contact_type' => 'Household', 'groups' => 2], $getField['input_attrs']['filter']); + } + } diff --git a/xml/schema/Contact/Contact.xml b/xml/schema/Contact/Contact.xml index f00299697e..94fffd3b48 100644 --- a/xml/schema/Contact/Contact.xml +++ b/xml/schema/Contact/Contact.xml @@ -838,6 +838,7 @@ EntityRef + contact_type=Organization Individual diff --git a/xml/templates/dao.tpl b/xml/templates/dao.tpl index 7a14de4129..7504906dbd 100644 --- a/xml/templates/dao.tpl +++ b/xml/templates/dao.tpl @@ -229,7 +229,7 @@ class {$table.className} extends CRM_Core_DAO {ldelim} {if $field.html} 'html' => array( {foreach from=$field.html item=val key=key} - '{$key}' => {if $key eq 'label'}{$tsFunctionName}("{$val}"){else}'{$val}'{/if}, + '{$key}' => {if $key eq 'label'}{$tsFunctionName}("{$val}"){elseif is_array($val)}{$val|@print_array}{else}'{$val}'{/if}, {/foreach} ), {/if} -- 2.25.1