From 9316120d32747860b9d9239467b8f2a7c9b22f68 Mon Sep 17 00:00:00 2001 From: colemanw Date: Fri, 22 Dec 2023 11:36:01 -0500 Subject: [PATCH] SearchKit - Show multiple 'create' links in toolbar --- CRM/Activity/Form/ActivityLinks.php | 1 + Civi/Api4/Action/GetLinks.php | 10 +++ .../Service/Links/ContactLinksProvider.php | 69 +++++++++++++------ .../SearchDisplay/AbstractRunAction.php | 40 +++++++++-- .../Civi/Api4/Action/SearchDisplay/Run.php | 2 +- .../ang/crmSearchDisplay/toolbar.html | 23 +++++-- .../crmSearchDisplayGrid.html | 2 +- .../crmSearchDisplayList.html | 2 +- .../crmSearchDisplayTable.html | 2 +- .../api/v4/SearchDisplay/SearchRunTest.php | 18 +++-- tests/phpunit/api/v4/Action/GetLinksTest.php | 2 + 11 files changed, 129 insertions(+), 42 deletions(-) diff --git a/CRM/Activity/Form/ActivityLinks.php b/CRM/Activity/Form/ActivityLinks.php index c8b351556a..754a0f9503 100644 --- a/CRM/Activity/Form/ActivityLinks.php +++ b/CRM/Activity/Form/ActivityLinks.php @@ -35,6 +35,7 @@ class CRM_Activity_Form_ActivityLinks extends CRM_Core_Form { $activityLinks = \Civi\Api4\Activity::getLinks() ->addValue('target_contact_id', $contactId) ->addWhere('ui_action', '=', 'add') + ->setExpandMultiple(TRUE) ->execute(); foreach ($activityLinks as $activityLink) { $activityTypes[] = [ diff --git a/Civi/Api4/Action/GetLinks.php b/Civi/Api4/Action/GetLinks.php index 9978ab4bdc..baaf6058f5 100644 --- a/Civi/Api4/Action/GetLinks.php +++ b/Civi/Api4/Action/GetLinks.php @@ -22,6 +22,10 @@ use Civi\Core\Event\GenericHookEvent; * Get action links for the $ENTITY entity. * * Action links are paths to forms for e.g. view, edit, or delete actions. + * @method string getEntityTitle() + * @method $this setEntityTitle(string $entityTitle) + * @method bool getExpandMultiple() + * @method $this setExpandMultiple(bool $expandMultiple) */ class GetLinks extends BasicGetAction { use \Civi\Api4\Generic\Traits\GetSetValueTrait; @@ -38,6 +42,12 @@ class GetLinks extends BasicGetAction { */ protected $entityTitle = TRUE; + /** + * Should multiple links e.g. to create different subtypes all be returned? + * @var bool + */ + protected bool $expandMultiple = FALSE; + public function _run(Result $result) { parent::_run($result); // Do expensive processing *after* parent has filtered down the result per the WHERE clause diff --git a/Civi/Api4/Service/Links/ContactLinksProvider.php b/Civi/Api4/Service/Links/ContactLinksProvider.php index 273eb3b170..099430693a 100644 --- a/Civi/Api4/Service/Links/ContactLinksProvider.php +++ b/Civi/Api4/Service/Links/ContactLinksProvider.php @@ -12,6 +12,7 @@ namespace Civi\Api4\Service\Links; +use Civi\API\Event\RespondEvent; use Civi\Api4\Utils\CoreUtil; use Civi\Core\Event\GenericHookEvent; @@ -25,6 +26,7 @@ class ContactLinksProvider extends \Civi\Core\Service\AutoSubscriber { public static function getSubscribedEvents(): array { return [ 'civi.api4.getLinks' => 'alterContactLinks', + 'civi.api.respond' => 'alterContactLinksResult', ]; } @@ -32,49 +34,74 @@ class ContactLinksProvider extends \Civi\Core\Service\AutoSubscriber { if (CoreUtil::isContact($e->entity)) { foreach ($e->links as $index => $link) { // Contacts are too cumbersome to view in a popup - if (in_array($link['ui_action'], ['view', 'update'], TRUE)) { + if (in_array($link['ui_action'], ['add', 'view', 'update'], TRUE)) { $e->links[$index]['target'] = ''; } } + } + } + + public static function alterContactLinksResult(RespondEvent $e): void { + $request = $e->getApiRequest(); + if ($request['version'] == 4 && is_a($request, '\Civi\Api4\Action\GetLinks') && CoreUtil::isContact($request->getEntityName())) { + $links = (array) $e->getResponse(); + $addLinkIndex = self::getActionIndex($links, 'add'); // Unset the generic "add" link and replace it with links per contact-type and sub-type - $addLinkIndex = self::getActionIndex($e->links, 'add'); - if ($addLinkIndex !== NULL) { - $addTemplate = $e->links[$addLinkIndex]; - unset($e->links[$addLinkIndex]); + if (isset($addLinkIndex) && $request->getExpandMultiple()) { + // Use the single add link from the schema as a template + $addTemplate = $links[$addLinkIndex]; + $newLinks = []; + $contactType = $request->getValue('contact_type') ?: $request->getEntityName(); // For contact entity, add links for every contact type - if ($e->entity === 'Contact') { - foreach (\CRM_Contact_BAO_ContactType::basicTypes() as $contactType) { - self::addLinks($contactType, $addTemplate, $e); + if ($contactType === 'Contact') { + foreach (\CRM_Contact_BAO_ContactType::basicTypes() as $type) { + self::addLinks($newLinks, $type, $addTemplate, $request->getEntityName()); } } // For Individual, Organization, Household entity else { - self::addLinks($e->entity, $addTemplate, $e); + self::addLinks($newLinks, $contactType, $addTemplate, $request->getEntityName()); } + array_splice($links, $addLinkIndex, 1, $newLinks); } + $e->getResponse()->exchangeArray(array_values($links)); } } - private static function addLinks(string $contactType, array $addTemplate, GenericHookEvent $e) { - $addTemplate['path'] = str_replace('[contact_type]', $contactType, $addTemplate['path']); + private static function addLinks(array &$newLinks, string $contactType, array $addTemplate, string $apiEntity) { + // Since this runs after api processing, not all fields may be returned, + // depending on the SELECT clause, so avoid undefined indexes. + if (!empty($addTemplate['path'])) { + $addTemplate['path'] = str_replace('[contact_type]', $contactType, $addTemplate['path']); + } + // Link to contact type + if ($apiEntity === 'Contact') { + $addTemplate['api_values'] = ['contact_type' => $contactType]; + } $link = $addTemplate; - if ($e->entity === 'Contact') { - $link['text'] = str_replace('%1', CoreUtil::getInfoItem($contactType, 'title'), $link['text']); - $link['api_values'] = ['contact_type' => $contactType]; + $link['text'] = ts('Add %1', [1 => \CRM_Contact_BAO_ContactType::getLabel($contactType)]); + if (array_key_exists('icon', $addTemplate)) { $link['icon'] = CoreUtil::getInfoItem($contactType, 'icon'); } - $e->links[] = $link; + $newLinks[] = $link; + // Links to contact sub-types $subTypes = \CRM_Contact_BAO_ContactType::subTypeInfo($contactType); $labels = array_column($subTypes, 'label'); array_multisort($labels, SORT_NATURAL, $subTypes); foreach ($subTypes as $subType) { - $addTemplate['weight']++; + if (isset($addTemplate['weight'])) { + $addTemplate['weight']++; + } $link = $addTemplate; - $link['path'] .= '&cst=' . $subType['name']; - $link['icon'] = $subType['icon'] ?? $link['icon']; - $link['text'] = str_replace('%1', $subType['label'], $link['text']); - $link['api_values'] = ['contact_sub_type' => $subType['name']]; - $e->links[] = $link; + if (!empty($link['path'])) { + $link['path'] .= '&cst=' . $subType['name']; + } + if (array_key_exists('icon', $addTemplate)) { + $link['icon'] = $subType['icon'] ?? $link['icon']; + } + $link['text'] = ts('Add %1', [1 => $subType['label']]); + $link['api_values']['contact_sub_type'] = $subType['name']; + $newLinks[] = $link; } } diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index 8dd1e9e126..cb59603c59 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -470,7 +470,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { private function formatFieldLinks($column, $data, $value): array { $links = []; foreach ((array) $value as $index => $val) { - $link = $this->formatLink($column['link'], $data, $val, $index); + $link = $this->formatLink($column['link'], $data, FALSE, $val, $index); if ($link) { // Style rules get appled to each link if (!empty($column['cssRules'])) { @@ -517,13 +517,13 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { * * @param array $link * @param array $data + * @param bool $allowMultiple * @param string|NULL $text - * @param $index + * @param int $index * @return array|null * @throws \CRM_Core_Exception - * @throws \Civi\API\Exception\NotImplementedException */ - protected function formatLink(array $link, array $data, string $text = NULL, $index = 0): ?array { + protected function formatLink(array $link, array $data, bool $allowMultiple = FALSE, string $text = NULL, $index = 0): ?array { $useApi = (!empty($link['entity']) && !empty($link['action'])); if (isset($index)) { foreach ($data as $key => $value) { @@ -539,11 +539,15 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { } // Hack to support links to relationships $linkEntity = ($link['entity'] === 'Relationship') ? 'RelationshipCache' : $link['entity']; - $apiInfo = civicrm_api4($linkEntity, 'getLinks', [ + $apiInfo = (array) civicrm_api4($linkEntity, 'getLinks', [ 'checkPermissions' => $checkPermissions, 'values' => $data, + 'expandMultiple' => $allowMultiple, 'where' => [['ui_action', '=', $link['action']]], ]); + if ($allowMultiple && count($apiInfo) > 1) { + return $this->formatMultiLink($link, $apiInfo, $data); + } $link['path'] = $apiInfo[0]['path'] ?? ''; } elseif (!$this->checkLinkAccess($link, $data)) { @@ -569,6 +573,28 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { }); } + /** + * Returns an array of multiple links for use as a dropdown-select when API::getLinks returns > 1 record + */ + private function formatMultiLink(array $link, array $apiInfo, array $data): array { + $dropdown = [ + 'text' => $this->replaceTokens($link['text'], $data, 'view') ?: $apiInfo[0]['text'], + 'icon' => $link['icon'] ?: $link[0]['icon'], + 'title' => $link['title'], + 'style' => $link['style'], + 'children' => [], + ]; + foreach ($apiInfo as $child) { + $dropdown['children'][] = [ + 'text' => $child['text'], + 'target' => $child['target'], + 'icon' => $child['icon'], + 'url' => $this->getUrl($child['path']), + ]; + } + return $dropdown; + } + /** * Check if a link should be visible to the user based on their permissions * @@ -585,7 +611,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { if (empty($link['path']) && empty($link['task'])) { return FALSE; } - if ($link['entity'] && !empty($link['action']) && !in_array($link['action'], ['view', 'preview'], TRUE) && $this->getCheckPermissions()) { + if (!empty($link['entity']) && !empty($link['action']) && !in_array($link['action'], ['view', 'preview'], TRUE) && $this->getCheckPermissions()) { $actionName = $this->getPermittedLinkAction($link['entity'], $link['action']); if (!$actionName) { return FALSE; @@ -1039,7 +1065,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { * @param string $format view|raw|url * @return string */ - private function replaceTokens($tokenExpr, $data, $format, $index = NULL) { + private function replaceTokens($tokenExpr, $data, $format) { foreach ($this->getTokens($tokenExpr ?? '') as $token) { $val = $data[$token] ?? NULL; if (isset($val) && $format === 'view') { diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php index 01d587fa22..79c5f7f416 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php @@ -162,7 +162,7 @@ class Run extends AbstractRunAction { if (!$this->checkLinkCondition($button, $data)) { continue; } - $button = $this->formatLink($button, $data); + $button = $this->formatLink($button, $data, TRUE); if ($button) { $toolbar[] = $button; } diff --git a/ext/search_kit/ang/crmSearchDisplay/toolbar.html b/ext/search_kit/ang/crmSearchDisplay/toolbar.html index f839f267bd..55b91e952f 100644 --- a/ext/search_kit/ang/crmSearchDisplay/toolbar.html +++ b/ext/search_kit/ang/crmSearchDisplay/toolbar.html @@ -1,4 +1,19 @@ - - - {{:: link.text }} - +
+ + + {{:: link.text }} + + + +
diff --git a/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.html b/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.html index 786caa43ef..53d52ce092 100644 --- a/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.html +++ b/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.html @@ -3,7 +3,7 @@
-
+
-
+
    diff --git a/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html b/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html index 5ca040d1f6..0c348ac3d1 100644 --- a/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html +++ b/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html @@ -4,7 +4,7 @@
    -
    +
    diff --git a/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php b/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php index b388174f9b..9814d53882 100644 --- a/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php +++ b/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php @@ -2055,11 +2055,12 @@ class SearchRunTest extends Api4TestBase implements TransactionalInterface { $result = civicrm_api4('SearchDisplay', 'run', $params); $this->assertCount(1, $result->toolbar); - $button = $result->toolbar[0]; - $this->assertEquals('crm-popup', $button['target']); - $this->assertEquals('fa-plus', $button['icon']); - $this->assertEquals('primary', $button['style']); - $this->assertEquals('Add Contact', $button['text']); + $menu = $result->toolbar[0]; + $this->assertEquals('Add Contact', $menu['text']); + $this->assertEquals('fa-plus', $menu['icon']); + $button = $menu['children'][0]; + $this->assertEquals('fa-user', $button['icon']); + $this->assertEquals('Add Individual', $button['text']); $this->assertStringContainsString('=Individual', $button['url']); // Try with pseudoconstant (for proper test the label needs to be different from the name) @@ -2068,9 +2069,14 @@ class SearchRunTest extends Api4TestBase implements TransactionalInterface { ->addWhere('name', '=', 'Organization') ->execute(); $params['filters'] = ['contact_type:label' => 'Disorganization']; + // Use default label this time + unset($params['display']['settings']['toolbar'][0]['text']); $result = civicrm_api4('SearchDisplay', 'run', $params); - $button = $result->toolbar[0]; + $menu = $result->toolbar[0]; + $this->assertEquals('Add Disorganization', $menu['text']); + $button = $menu['children'][0]; $this->assertStringContainsString('=Organization', $button['url']); + $this->assertEquals('Add Disorganization', $button['text']); // Test legacy 'addButton' setting $params['display']['settings']['toolbar'] = NULL; diff --git a/tests/phpunit/api/v4/Action/GetLinksTest.php b/tests/phpunit/api/v4/Action/GetLinksTest.php index fe7355e0e5..e51e79b7e0 100644 --- a/tests/phpunit/api/v4/Action/GetLinksTest.php +++ b/tests/phpunit/api/v4/Action/GetLinksTest.php @@ -34,6 +34,7 @@ class GetLinksTest extends Api4TestBase implements TransactionalInterface { public function testContactLinks(): void { $links = Contact::getLinks(FALSE) ->addWhere('api_action', '=', 'create') + ->setExpandMultiple(TRUE) ->execute()->indexBy('path'); $this->assertEquals('Add Individual', $links['civicrm/contact/add?reset=1&ct=Individual']['text']); $this->assertEquals('fa-user', $links['civicrm/contact/add?reset=1&ct=Individual']['icon']); @@ -68,6 +69,7 @@ class GetLinksTest extends Api4TestBase implements TransactionalInterface { ->execute(); $links = Individual::getLinks(FALSE) ->addWhere('api_action', '=', 'create') + ->setExpandMultiple(TRUE) ->execute(); $this->assertStringContainsString('ct=Individual', $links[0]['path']); $this->assertEquals('Add Individual', $links[0]['text']); -- 2.25.1