From 190d81fd4f22890193e03443d21629806c52c91d Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Mon, 28 Nov 2022 14:50:40 -0500 Subject: [PATCH] Afform - Support ContactType-specific tabs and blocks --- CRM/Contact/DAO/ContactType.php | 9 +- CRM/Contact/Page/View/Summary.php | 10 +- .../ContactTypeAutocompleteProvider.php | 54 +++++++ Civi/Api4/Utils/FormattingUtil.php | 2 +- .../admin/ang/afGuiEditor/config-form.html | 4 + ext/afform/core/Civi/Api4/Afform.php | 5 + ext/afform/core/afform.php | 91 +++++++---- .../Civi/Afform/AfformContactSummaryTest.php | 145 ++++++++++++++++++ xml/schema/Contact/ContactType.xml | 7 + 9 files changed, 292 insertions(+), 35 deletions(-) create mode 100644 Civi/Api4/Service/Autocomplete/ContactTypeAutocompleteProvider.php create mode 100644 ext/afform/core/tests/phpunit/Civi/Afform/AfformContactSummaryTest.php diff --git a/CRM/Contact/DAO/ContactType.php b/CRM/Contact/DAO/ContactType.php index 835c6a31c3..a85c0bde73 100644 --- a/CRM/Contact/DAO/ContactType.php +++ b/CRM/Contact/DAO/ContactType.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Contact/ContactType.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:6ac1343e8af16b17a161fda746457d93) + * (GenCodeChecksum:b66036d325985536154ec623ab362e39) */ /** @@ -23,6 +23,13 @@ class CRM_Contact_DAO_ContactType extends CRM_Core_DAO { */ public static $_tableName = 'civicrm_contact_type'; + /** + * 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. * diff --git a/CRM/Contact/Page/View/Summary.php b/CRM/Contact/Page/View/Summary.php index 07bded7595..3112a4ad70 100644 --- a/CRM/Contact/Page/View/Summary.php +++ b/CRM/Contact/Page/View/Summary.php @@ -250,7 +250,7 @@ class CRM_Contact_Page_View_Summary extends CRM_Contact_Page_View { $changeLog = $this->_viewOptions['log']; $this->assign_by_ref('changeLog', $changeLog); - $this->assign('allTabs', $this->getTabs()); + $this->assign('allTabs', $this->getTabs($defaults)); // hook for contact summary // ignored but needed to prevent warnings @@ -342,7 +342,7 @@ class CRM_Contact_Page_View_Summary extends CRM_Contact_Page_View { * @return array * @throws \CRM_Core_Exception */ - public function getTabs() { + public function getTabs(array $contact) { $allTabs = []; $getCountParams = []; $weight = 10; @@ -418,7 +418,11 @@ class CRM_Contact_Page_View_Summary extends CRM_Contact_Page_View { } // Allow other modules to add or remove tabs - $context = ['contact_id' => $this->_contactId]; + $context = [ + 'contact_id' => $contact['id'], + 'contact_type' => $contact['contact_type'], + 'contact_sub_type' => CRM_Utils_Array::explodePadded($contact['contact_sub_type'] ?? NULL), + ]; CRM_Utils_Hook::tabset('civicrm/contact/view', $allTabs, $context); $expectedKeys = ['count', 'class', 'template', 'hideCount', 'icon']; diff --git a/Civi/Api4/Service/Autocomplete/ContactTypeAutocompleteProvider.php b/Civi/Api4/Service/Autocomplete/ContactTypeAutocompleteProvider.php new file mode 100644 index 0000000000..d2da560c47 --- /dev/null +++ b/Civi/Api4/Service/Autocomplete/ContactTypeAutocompleteProvider.php @@ -0,0 +1,54 @@ +display['settings'] || $e->display['type'] !== 'autocomplete' || $e->savedSearch['api_entity'] !== 'ContactType') { + return; + } + $e->display['settings'] = [ + 'sort' => [ + ['label', 'ASC'], + ], + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'label', + 'icons' => [ + ['field' => 'icon'], + ], + ], + [ + 'type' => 'field', + 'key' => 'description', + 'rewrite' => "{if '[description]'}[description]{elseif '[parent_id]'}" . ts('Subtype of %1', [1 => '[parent_id:label]']) . "{/if}", + ], + ], + ]; + } + +} diff --git a/Civi/Api4/Utils/FormattingUtil.php b/Civi/Api4/Utils/FormattingUtil.php index 59c2f1e8df..d91138107c 100644 --- a/Civi/Api4/Utils/FormattingUtil.php +++ b/Civi/Api4/Utils/FormattingUtil.php @@ -126,7 +126,7 @@ class FormattingUtil { } // Special handling for 'current_user' and user lookups - if ($fk === 'Contact' && !is_numeric($value)) { + if ($fk === 'Contact' && isset($value) && !is_numeric($value)) { $value = \_civicrm_api3_resolve_contactID($value); if ('unknown-user' === $value) { throw new \CRM_Core_Exception("\"{$fieldSpec['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $fieldSpec['name'], "type" => "integer"]); diff --git a/ext/afform/admin/ang/afGuiEditor/config-form.html b/ext/afform/admin/ang/afGuiEditor/config-form.html index 9648f8adb6..bc858eef3e 100644 --- a/ext/afform/admin/ang/afGuiEditor/config-form.html +++ b/ext/afform/admin/ang/afGuiEditor/config-form.html @@ -93,6 +93,10 @@ +
+ + +

{{:: ts('Placement can be configured using the Contact Layout Editor.') }}

diff --git a/ext/afform/core/Civi/Api4/Afform.php b/ext/afform/core/Civi/Api4/Afform.php index 8da02bd89d..71ce29fa65 100644 --- a/ext/afform/core/Civi/Api4/Afform.php +++ b/ext/afform/core/Civi/Api4/Afform.php @@ -176,6 +176,11 @@ class Afform extends Generic\AbstractEntity { 'tab' => ts('Contact Summary Tab'), ], ], + [ + 'name' => 'summary_contact_type', + 'data_type' => 'Array', + 'options' => \CRM_Contact_BAO_ContactType::contactTypePairs(), + ], [ 'name' => 'icon', 'description' => 'Icon shown in the contact summary tab', diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php index 2dba9ea74c..2b085cbe16 100644 --- a/ext/afform/core/afform.php +++ b/ext/afform/core/afform.php @@ -214,25 +214,29 @@ function afform_civicrm_tabset($tabsetName, &$tabs, $context) { if ($tabsetName !== 'civicrm/contact/view') { return; } + $contactTypes = array_merge([$context['contact_type']], $context['contact_sub_type'] ?? []); $afforms = Civi\Api4\Afform::get(FALSE) + ->addSelect('name', 'title', 'icon', 'module_name', 'directive_name', 'summary_contact_type') ->addWhere('contact_summary', '=', 'tab') - ->addSelect('name', 'title', 'icon', 'module_name', 'directive_name') + ->addOrderBy('title') ->execute(); $weight = 111; foreach ($afforms as $afform) { - $tabs[] = [ - 'id' => $afform['name'], - 'title' => $afform['title'], - 'weight' => $weight++, - 'icon' => 'crm-i ' . ($afform['icon'] ?: 'fa-list-alt'), - 'is_active' => TRUE, - 'template' => 'afform/contactSummary/AfformTab.tpl', - 'module' => $afform['module_name'], - 'directive' => $afform['directive_name'], - ]; - // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module. - if (empty($context['caller'])) { - Civi::service('angularjs.loader')->addModules($afform['module_name']); + if (empty($afform['summary_contact_type']) || array_intersect($afform['summary_contact_type'], $contactTypes)) { + $tabs[] = [ + 'id' => $afform['name'], + 'title' => $afform['title'], + 'weight' => $weight++, + 'icon' => 'crm-i ' . ($afform['icon'] ?: 'fa-list-alt'), + 'is_active' => TRUE, + 'template' => 'afform/contactSummary/AfformTab.tpl', + 'module' => $afform['module_name'], + 'directive' => $afform['directive_name'], + ]; + // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module. + if (empty($context['caller'])) { + Civi::service('angularjs.loader')->addModules($afform['module_name']); + } } } } @@ -246,24 +250,40 @@ function afform_civicrm_pageRun(&$page) { if (!in_array(get_class($page), ['CRM_Contact_Page_View_Summary', 'CRM_Contact_Page_View_Print'])) { return; } - $scanner = \Civi::service('afform_scanner'); + $afforms = Civi\Api4\Afform::get(FALSE) + ->addSelect('name', 'title', 'icon', 'module_name', 'directive_name', 'summary_contact_type') + ->addWhere('contact_summary', '=', 'block') + ->addOrderBy('title') + ->execute(); $cid = $page->get('cid'); + $contact = NULL; $side = 'left'; - foreach ($scanner->getMetas() as $afform) { - if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'block') { - $module = _afform_angular_module_name($afform['name']); - $block = [ - 'module' => $module, - 'directive' => _afform_angular_module_name($afform['name'], 'dash'), - ]; - $content = CRM_Core_Smarty::singleton()->fetchWith('afform/contactSummary/AfformBlock.tpl', ['contactId' => $cid, 'block' => $block]); - CRM_Core_Region::instance("contact-basic-info-$side")->add([ - 'markup' => '
' . $content . '
', - 'weight' => 1, - ]); - Civi::service('angularjs.loader')->addModules($module); - $side = $side === 'left' ? 'right' : 'left'; + $weight = ['left' => 1, 'right' => 1]; + foreach ($afforms as $afform) { + // If Afform specifies a contact type, lookup the contact and compare + if (!empty($afform['summary_contact_type'])) { + // Contact.get only needs to happen once + $contact = $contact ?? civicrm_api4('Contact', 'get', [ + 'select' => ['contact_type', 'contact_sub_type'], + 'where' => [['id', '=', $cid]], + ])->first(); + $contactTypes = array_merge([$contact['contact_type']], $contact['contact_sub_type'] ?? []); + if (!array_intersect($afform['summary_contact_type'], $contactTypes)) { + continue; + } } + $block = [ + 'module' => $afform['module_name'], + 'directive' => _afform_angular_module_name($afform['name'], 'dash'), + ]; + $content = CRM_Core_Smarty::singleton()->fetchWith('afform/contactSummary/AfformBlock.tpl', ['contactId' => $cid, 'block' => $block]); + CRM_Core_Region::instance("contact-basic-info-$side")->add([ + 'markup' => '
' . $content . '
', + 'name' => 'afform:' . $afform['name'], + 'weight' => $weight[$side]++, + ]); + Civi::service('angularjs.loader')->addModules($afform['module_name']); + $side = $side === 'left' ? 'right' : 'left'; } } @@ -274,9 +294,11 @@ function afform_civicrm_pageRun(&$page) { */ function afform_civicrm_contactSummaryBlocks(&$blocks) { $afforms = \Civi\Api4\Afform::get(FALSE) - ->setSelect(['name', 'title', 'directive_name', 'module_name', 'type', 'type:icon', 'type:label']) + ->setSelect(['name', 'title', 'directive_name', 'module_name', 'type', 'type:icon', 'type:label', 'summary_contact_type']) ->addWhere('contact_summary', '=', 'block') + ->addOrderBy('title') ->execute(); + $allContactTypes = \CRM_Contact_BAO_ContactType::getAllContactTypes(); foreach ($afforms as $index => $afform) { // Create a group per afform type $blocks += [ @@ -286,8 +308,17 @@ function afform_civicrm_contactSummaryBlocks(&$blocks) { 'blocks' => [], ], ]; + $contactType = []; + // If the form specifies contact types, resolve them to just the parent types (Individual, Organization, Household) + // because ContactLayout doesn't care about sub-types + foreach ($afform['summary_contact_type'] ?? [] as $name) { + $parent = $allContactTypes[$name]['parent'] ?? $name; + $contactType[$parent] = $parent; + } $blocks["afform_{$afform['type']}"]['blocks'][$afform['name']] = [ 'title' => $afform['title'], + // ContactLayout only supports a single contact type + 'contact_type' => count($contactType) === 1 ? CRM_Utils_Array::first($contactType) : NULL, 'tpl_file' => 'afform/contactSummary/AfformBlock.tpl', 'module' => $afform['module_name'], 'directive' => $afform['directive_name'], diff --git a/ext/afform/core/tests/phpunit/Civi/Afform/AfformContactSummaryTest.php b/ext/afform/core/tests/phpunit/Civi/Afform/AfformContactSummaryTest.php new file mode 100644 index 0000000000..3b892aed15 --- /dev/null +++ b/ext/afform/core/tests/phpunit/Civi/Afform/AfformContactSummaryTest.php @@ -0,0 +1,145 @@ +installMe(__DIR__)->install('org.civicrm.search_kit')->apply(); + } + + public function tearDown(): void { + Afform::revert(FALSE)->addWhere('name', 'IN', $this->formNames)->execute(); + parent::tearDown(); + } + + public function testAfformContactSummaryTab(): void { + $this->saveTestRecords('ContactType', [ + 'records' => [ + ['name' => 'FooBar', 'label' => 'FooBar', 'parent_id:name' => 'Individual'], + ], + 'match' => ['name'], + ]); + + Afform::create() + ->addValue('name', $this->formNames[0]) + ->addValue('title', 'Test B') + ->addValue('contact_summary', 'tab') + ->addValue('summary_contact_type', ['Organization']) + ->execute(); + Afform::create() + ->addValue('name', $this->formNames[1]) + ->addValue('title', 'Test C') + ->addValue('contact_summary', 'tab') + ->addValue('summary_contact_type', ['FooBar']) + ->addValue('icon', 'smiley-face') + ->execute(); + Afform::create() + ->addValue('name', $this->formNames[2]) + ->addValue('title', 'Test A') + ->addValue('contact_summary', 'tab') + ->execute(); + + $tabs = []; + $context = [ + 'contact_id' => 0, + 'contact_type' => 'Individual', + 'contact_sub_type' => ['FooBar'], + 'caller' => 'UnitTests', + ]; + \CRM_Utils_Hook::tabset('civicrm/contact/view', $tabs, $context); + + $tabs = array_column($tabs, NULL, 'id'); + + $this->assertArrayHasKey($this->formNames[1], $tabs); + $this->assertArrayHasKey($this->formNames[2], $tabs); + $this->assertArrayNotHasKey($this->formNames[0], $tabs); + $this->assertEquals('Test C', $tabs[$this->formNames[1]]['title']); + $this->assertEquals('Test A', $tabs[$this->formNames[2]]['title']); + $this->assertEquals('crm-i smiley-face', $tabs[$this->formNames[1]]['icon']); + // Fallback icon + $this->assertEquals('crm-i fa-list-alt', $tabs[$this->formNames[2]]['icon']); + // Forms should be sorted by title alphabetically + $this->assertGreaterThan($tabs[$this->formNames[2]]['weight'], $tabs[$this->formNames[1]]['weight']); + } + + public function testAfformContactSummaryBlock(): void { + $this->saveTestRecords('ContactType', [ + 'records' => [ + ['name' => 'Farm', 'label' => 'Farm', 'parent_id:name' => 'Organization'], + ], + 'match' => ['name'], + ]); + + $cid = $this->createTestRecord('Contact', [ + 'contact_type' => 'Organization', + 'contact_sub_type' => ['Farm'], + ])['id']; + + Afform::create() + ->addValue('name', $this->formNames[0]) + ->addValue('title', 'Test B') + ->addValue('type', 'search') + ->addValue('contact_summary', 'block') + ->addValue('summary_contact_type', ['Individual', 'Household']) + ->execute(); + Afform::create() + ->addValue('name', $this->formNames[1]) + ->addValue('title', 'Test C') + ->addValue('type', 'form') + ->addValue('contact_summary', 'block') + ->addValue('summary_contact_type', ['Farm']) + ->addValue('icon', 'smiley-face') + ->execute(); + Afform::create() + ->addValue('name', $this->formNames[2]) + ->addValue('type', 'form') + ->addValue('title', 'Test A') + ->addValue('contact_summary', 'block') + ->execute(); + + // Call pageRun hook and then assert afforms have been added to the appropriate region + $dummy = new \CRM_Contact_Page_View_Summary(); + $dummy->set('cid', $cid); + \CRM_Utils_Hook::pageRun($dummy); + + // TODO: Be more flexible + // The presence of any other afform blocks in the system might alter the left-right assumptions here + $blockA = \CRM_Core_Region::instance('contact-basic-info-left')->get('afform:' . $this->formNames[2]); + $this->assertStringContainsString("", $blockA['markup']); + + $blockB = \CRM_Core_Region::instance('contact-basic-info-right')->get('afform:' . $this->formNames[1]); + $this->assertStringContainsString("", $blockB['markup']); + + // Block for wrong contact type should not appear + $this->assertNull(\CRM_Core_Region::instance('contact-basic-info-left')->get('afform:' . $this->formNames[0])); + $this->assertNull(\CRM_Core_Region::instance('contact-basic-info-right')->get('afform:' . $this->formNames[0])); + + // Ensure blocks show up in ContactLayoutEditor + $blocks = []; + afform_civicrm_contactSummaryBlocks($blocks); + + // ContactLayout doesn't support > 1 contact type, so this ought to be null + $this->assertNull($blocks['afform_search']['blocks'][$this->formNames[0]]['contact_type']); + // Sub-type should have been converted to parent type + $this->assertEquals('Organization', $blocks['afform_form']['blocks'][$this->formNames[1]]['contact_type']); + $this->assertNull($blocks['afform_form']['blocks'][$this->formNames[2]]['contact_type']); + // Forms should be sorted by title + $order = array_flip(array_keys($blocks['afform_form']['blocks'])); + $this->assertGreaterThan($order[$this->formNames[2]], $order[$this->formNames[1]]); + } + +} diff --git a/xml/schema/Contact/ContactType.xml b/xml/schema/Contact/ContactType.xml index 7b0af31307..7acb29f926 100644 --- a/xml/schema/Contact/ContactType.xml +++ b/xml/schema/Contact/ContactType.xml @@ -6,6 +6,7 @@ civicrm_contact_type Provide type information for contacts 3.1 + label civicrm/admin/options/subtype/edit?action=add&reset=1 civicrm/admin/options/subtype/edit?action=update&id=[id]&reset=1 @@ -33,6 +34,7 @@ Internal name of Contact Type (or Subtype). + Text 3.1 true @@ -49,6 +51,10 @@ varchar 64 localized Name of Contact Type. + + + Text + true 3.1 @@ -95,6 +101,7 @@ + Select 3.1 -- 2.25.1