$activityLinks = \Civi\Api4\Activity::getLinks()
->addValue('target_contact_id', $contactId)
->addWhere('ui_action', '=', 'add')
+ ->setExpandMultiple(TRUE)
->execute();
foreach ($activityLinks as $activityLink) {
$activityTypes[] = [
* 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;
*/
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
namespace Civi\Api4\Service\Links;
+use Civi\API\Event\RespondEvent;
use Civi\Api4\Utils\CoreUtil;
use Civi\Core\Event\GenericHookEvent;
public static function getSubscribedEvents(): array {
return [
'civi.api4.getLinks' => 'alterContactLinks',
+ 'civi.api.respond' => 'alterContactLinksResult',
];
}
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;
}
}
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'])) {
*
* @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) {
}
// 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)) {
});
}
+ /**
+ * 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
*
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;
* @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') {
if (!$this->checkLinkCondition($button, $data)) {
continue;
}
- $button = $this->formatLink($button, $data);
+ $button = $this->formatLink($button, $data, TRUE);
if ($button) {
$toolbar[] = $button;
}
-<a ng-repeat="link in $ctrl.toolbar" ng-href="{{:: link.url }}" class="btn btn-{{:: link.style }}" target="{{:: link.target }}">
- <i ng-if="link.icon" class="crm-i {{:: link.icon }}"></i>
- {{:: link.text }}
-</a>
+<div class="btn-group" ng-repeat="link in $ctrl.toolbar">
+ <a ng-if="link.url" ng-href="{{:: link.url }}" class="btn btn-{{:: link.style }}" target="{{:: link.target }}">
+ <i ng-if="link.icon" class="crm-i {{:: link.icon }}"></i>
+ {{:: link.text }}
+ </a>
+ <button ng-if="link.children" type="button" class="btn btn-{{:: link.style }} dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ <i ng-if="link.icon" class="crm-i {{:: link.icon }}"></i>
+ {{:: link.text }}
+ <span class="caret"></span>
+ </button>
+ <ul ng-if="link.children" class="dropdown-menu dropdown-menu-right">
+ <li ng-repeat="child in link.children">
+ <a ng-href="{{:: child.url }}" target="{{:: child.target }}">
+ <i ng-if="child.icon" class="crm-i {{:: child.icon }}"></i>
+ {{:: child.text }}
+ </a>
+ </li>
+ </ul>
+</div>
<div class="form-inline">
<div class="btn-group" ng-include="'~/crmSearchDisplay/SearchButton.html'" ng-if="$ctrl.settings.button"></div>
<span ng-if="$ctrl.settings.headerCount" ng-include="'~/crmSearchDisplay/ResultCount.html'"></span>
- <div class="btn-group pull-right" ng-include="'~/crmSearchDisplay/toolbar.html'" ng-if="$ctrl.toolbar"></div>
+ <div class="form-group pull-right" ng-include="'~/crmSearchDisplay/toolbar.html'" ng-if="$ctrl.toolbar"></div>
</div>
<div
class="crm-search-display-grid-container crm-search-display-grid-layout-{{$ctrl.settings.colno}}"
<div class="form-inline">
<div class="btn-group" ng-include="'~/crmSearchDisplay/SearchButton.html'" ng-if="$ctrl.settings.button"></div>
<span ng-if="$ctrl.settings.headerCount" ng-include="'~/crmSearchDisplay/ResultCount.html'"></span>
- <div class="btn-group pull-right" ng-include="'~/crmSearchDisplay/toolbar.html'" ng-if="$ctrl.toolbar"></div>
+ <div class="form-group pull-right" ng-include="'~/crmSearchDisplay/toolbar.html'" ng-if="$ctrl.toolbar"></div>
</div>
<ol ng-if=":: $ctrl.settings.style === 'ol'" ng-include="'~/crmSearchDisplayList/crmSearchDisplayList' + ($ctrl.loading ? 'Loading' : 'Items') + '.html'" ng-style="{'list-style': $ctrl.settings.symbol}"></ol>
<ul ng-if=":: $ctrl.settings.style !== 'ol'" ng-include="'~/crmSearchDisplayList/crmSearchDisplayList' + ($ctrl.loading ? 'Loading' : 'Items') + '.html'" ng-style="{'list-style': $ctrl.settings.symbol}"></ul>
<div class="btn-group" ng-include="'~/crmSearchDisplay/SearchButton.html'" ng-if="$ctrl.settings.button"></div>
<crm-search-tasks-menu ng-if="$ctrl.settings.actions && $ctrl.taskManager" ids="$ctrl.selectedRows" task-manager="$ctrl.taskManager"></crm-search-tasks-menu>
<span ng-if="$ctrl.settings.headerCount" ng-include="'~/crmSearchDisplay/ResultCount.html'"></span>
- <div class="btn-group pull-right" ng-include="'~/crmSearchDisplay/toolbar.html'" ng-if="$ctrl.toolbar"></div>
+ <div class="form-group pull-right" ng-include="'~/crmSearchDisplay/toolbar.html'" ng-if="$ctrl.toolbar"></div>
</div>
<table class="{{:: $ctrl.settings.classes.join(' ') }}">
<thead>
$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)
->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;
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']);
->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']);