From c507688988cc0c17222f8cf8ab484caae3b0ec7b Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 8 Jun 2021 12:33:00 -0400 Subject: [PATCH] SearchKit - Enable search for relationships as base entity This makes it possible to use a bridge entity for the base of a search (only if it is annotated @searchable primary|secondary) It also ensures that fields needed for links are available in search displays (previously it ensured ID was returned but some links require other fields as well) --- CRM/Contact/DAO/Relationship.php | 12 ++++- CRM/Contact/DAO/RelationshipCache.php | 13 ++++- Civi/Api4/ActivityContact.php | 1 + Civi/Api4/CaseActivity.php | 1 + Civi/Api4/CaseContact.php | 1 + Civi/Api4/DashboardContact.php | 1 + Civi/Api4/Entity.php | 1 + Civi/Api4/EntityFinancialAccount.php | 1 + Civi/Api4/EntityFinancialTrxn.php | 1 + Civi/Api4/EntityTag.php | 2 +- Civi/Api4/GroupContact.php | 1 + Civi/Api4/RelationshipCache.php | 1 + .../Civi/Api4/Action/SearchDisplay/Run.php | 48 +++++++++++++++---- ext/search_kit/Civi/Search/Admin.php | 2 +- .../crmSearchAdmin.component.js | 10 ++-- .../phpunit/api/v4/Entity/ConformanceTest.php | 2 +- xml/schema/Contact/Relationship.xml | 5 ++ xml/schema/Contact/RelationshipCache.xml | 6 +++ 18 files changed, 89 insertions(+), 20 deletions(-) diff --git a/CRM/Contact/DAO/Relationship.php b/CRM/Contact/DAO/Relationship.php index 1f4ef79592..211536c505 100644 --- a/CRM/Contact/DAO/Relationship.php +++ b/CRM/Contact/DAO/Relationship.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Contact/Relationship.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:787e0139d4a6b8b587b8d0d607e25ff0) + * (GenCodeChecksum:a10cc7576dc2353519a6c572435fb10a) */ /** @@ -37,6 +37,16 @@ class CRM_Contact_DAO_Relationship extends CRM_Core_DAO { */ public static $_log = TRUE; + /** + * Paths for accessing this entity in the UI. + * + * @var string[] + */ + protected static $_paths = [ + 'view' => 'civicrm/contact/view/rel?action=view&reset=1&cid=[contact_id_a]&id=[id]', + 'delete' => 'civicrm/contact/view/rel?action=delete&reset=1&cid=[contact_id_a]&id=[id]', + ]; + /** * Relationship ID * diff --git a/CRM/Contact/DAO/RelationshipCache.php b/CRM/Contact/DAO/RelationshipCache.php index 3583f377a7..e61073232a 100644 --- a/CRM/Contact/DAO/RelationshipCache.php +++ b/CRM/Contact/DAO/RelationshipCache.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Contact/RelationshipCache.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:3376402e2a249b7004b40df6aeac5df9) + * (GenCodeChecksum:dd52d37d1350a679b727c906ea37661b) */ /** @@ -37,6 +37,17 @@ class CRM_Contact_DAO_RelationshipCache extends CRM_Core_DAO { */ public static $_log = FALSE; + /** + * Paths for accessing this entity in the UI. + * + * @var string[] + */ + protected static $_paths = [ + 'view' => 'civicrm/contact/view/rel?action=view&reset=1&cid=[near_contact_id]&id=[relationship_id]', + 'update' => 'civicrm/contact/view/rel?action=update&reset=1&cid=[near_contact_id]&id=[relationship_id]&rtype=[orientation]', + 'delete' => 'civicrm/contact/view/rel?action=delete&reset=1&cid=[near_contact_id]&id=[relationship_id]', + ]; + /** * Relationship Cache ID * diff --git a/Civi/Api4/ActivityContact.php b/Civi/Api4/ActivityContact.php index 39ec1b3b95..767595de70 100644 --- a/Civi/Api4/ActivityContact.php +++ b/Civi/Api4/ActivityContact.php @@ -27,6 +27,7 @@ namespace Civi\Api4; * The record_type_id field determines the contact's role in the activity (source, target, or assignee). * @ui_join_filters record_type_id * + * @searchable bridge * @see \Civi\Api4\Activity * @package Civi\Api4 */ diff --git a/Civi/Api4/CaseActivity.php b/Civi/Api4/CaseActivity.php index 95be921960..f38ce21ee9 100644 --- a/Civi/Api4/CaseActivity.php +++ b/Civi/Api4/CaseActivity.php @@ -24,6 +24,7 @@ namespace Civi\Api4; * * This connects an activity to one or more cases. * + * @searchable bridge * @see \Civi\Api4\Case * @package Civi\Api4 */ diff --git a/Civi/Api4/CaseContact.php b/Civi/Api4/CaseContact.php index 4d8ae66130..a9351a9798 100644 --- a/Civi/Api4/CaseContact.php +++ b/Civi/Api4/CaseContact.php @@ -24,6 +24,7 @@ namespace Civi\Api4; * * This connects a client to a case. * + * @searchable bridge * @see \Civi\Api4\Case * @package Civi\Api4 */ diff --git a/Civi/Api4/DashboardContact.php b/Civi/Api4/DashboardContact.php index d029f388d5..91eae6be2f 100644 --- a/Civi/Api4/DashboardContact.php +++ b/Civi/Api4/DashboardContact.php @@ -23,6 +23,7 @@ namespace Civi\Api4; * * This places a dashboard item on a user's home screen. * + * @searchable bridge * @see \Civi\Api4\Dashboard * @searchable none * @package Civi\Api4 diff --git a/Civi/Api4/Entity.php b/Civi/Api4/Entity.php index 00a99f6b33..e66a9bec70 100644 --- a/Civi/Api4/Entity.php +++ b/Civi/Api4/Entity.php @@ -100,6 +100,7 @@ class Entity extends Generic\AbstractEntity { 'options' => [ 'primary' => ts('Primary'), 'secondary' => ts('Secondary'), + 'bridge' => ts('Bridge'), 'none' => ts('None'), ], ], diff --git a/Civi/Api4/EntityFinancialAccount.php b/Civi/Api4/EntityFinancialAccount.php index 4b9b3d537b..e8f4750ce9 100644 --- a/Civi/Api4/EntityFinancialAccount.php +++ b/Civi/Api4/EntityFinancialAccount.php @@ -25,6 +25,7 @@ namespace Civi\Api4; * * @ui_join_filters account_relationship * + * @searchable bridge * @package Civi\Api4 */ class EntityFinancialAccount extends Generic\DAOEntity { diff --git a/Civi/Api4/EntityFinancialTrxn.php b/Civi/Api4/EntityFinancialTrxn.php index 6fd015de30..f63803793a 100644 --- a/Civi/Api4/EntityFinancialTrxn.php +++ b/Civi/Api4/EntityFinancialTrxn.php @@ -24,6 +24,7 @@ namespace Civi\Api4; * * @see https://docs.civicrm.org/dev/en/latest/financial/financialentities/ * + * @searchable bridge * @package Civi\Api4 */ class EntityFinancialTrxn extends Generic\DAOEntity { diff --git a/Civi/Api4/EntityTag.php b/Civi/Api4/EntityTag.php index 2febe72254..62fe8b6870 100644 --- a/Civi/Api4/EntityTag.php +++ b/Civi/Api4/EntityTag.php @@ -22,7 +22,7 @@ namespace Civi\Api4; * EntityTag - links tags to contacts, activities, etc. * * @see \Civi\Api4\Tag - * + * @searchable bridge * @package Civi\Api4 */ class EntityTag extends Generic\DAOEntity { diff --git a/Civi/Api4/GroupContact.php b/Civi/Api4/GroupContact.php index fd988878bd..850fe78b44 100644 --- a/Civi/Api4/GroupContact.php +++ b/Civi/Api4/GroupContact.php @@ -26,6 +26,7 @@ namespace Civi\Api4; * * @ui_join_filters status * + * @searchable bridge * @see \Civi\Api4\Group * @package Civi\Api4 */ diff --git a/Civi/Api4/RelationshipCache.php b/Civi/Api4/RelationshipCache.php index c878b8056e..7a4b774fbc 100644 --- a/Civi/Api4/RelationshipCache.php +++ b/Civi/Api4/RelationshipCache.php @@ -22,6 +22,7 @@ namespace Civi\Api4; /** * RelationshipCache - readonly table to facilitate joining and finding contacts by relationship. * + * @searchable secondary * @see \Civi\Api4\Relationship * @ui_join_filters near_relation * @package Civi\Api4 diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php index 4bc54ae629..dcf36703c5 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php @@ -63,6 +63,11 @@ class Run extends \Civi\Api4\Generic\AbstractAction { */ private $_afform; + /** + * @var array + */ + private $_extraEntityFields = []; + /** * @param \Civi\Api4\Generic\Result $result * @throws UnauthorizedException @@ -270,11 +275,11 @@ class Run extends \Civi\Api4\Generic\AbstractAction { /** * Determines if a column is eligible to use an aggregate function - * @param $fieldName - * @param $prefix + * @param string $fieldName + * @param string $prefix * @return bool */ - private function canAggregate($fieldName, $prefix) { + private function canAggregate($fieldName, $prefix = '') { $apiParams = $this->savedSearch['api_params']; // If the query does not use grouping, never @@ -357,13 +362,21 @@ class Run extends \Civi\Api4\Generic\AbstractAction { * @param array $apiParams */ private function augmentSelectClause(&$apiParams): void { + foreach ($this->getExtraEntityFields($this->savedSearch['api_entity']) as $extraFieldName) { + if (!in_array($extraFieldName, $apiParams['select']) && !$this->canAggregate($extraFieldName)) { + $apiParams['select'][] = $extraFieldName; + } + } $joinAliases = []; - // Select the ids of explicitly joined entities (helps with displaying links) + // Select the ids, etc. of explicitly joined entities (helps with displaying links) foreach ($apiParams['join'] ?? [] as $join) { - $joinAliases[] = $joinAlias = explode(' AS ', $join[0])[1]; - $idFieldName = $joinAlias . '.id'; - if (!in_array($idFieldName, $apiParams['select']) && !$this->canAggregate('id', $joinAlias . '.')) { - $apiParams['select'][] = $idFieldName; + [$joinEntity, $joinAlias] = explode(' AS ', $join[0]); + $joinAliases[] = $joinAlias; + foreach ($this->getExtraEntityFields($joinEntity) as $extraField) { + $extraFieldName = $joinAlias . '.' . $extraField; + if (!in_array($extraFieldName, $apiParams['select']) && !$this->canAggregate($extraField, $joinAlias . '.')) { + $apiParams['select'][] = $extraFieldName; + } } } // Select the ids of implicitly joined entities (helps with displaying links) @@ -391,4 +404,23 @@ class Run extends \Civi\Api4\Generic\AbstractAction { } } + /** + * Get list of extra fields needed for displaying links for a given entity + * + * @param string $entityName + * @return array + */ + private function getExtraEntityFields(string $entityName): array { + if (!isset($this->_extraEntityFields[$entityName])) { + $info = CoreUtil::getApiClass($entityName)::getInfo(); + $this->_extraEntityFields[$entityName] = [$info['id_field']]; + foreach ($info['paths'] ?? [] as $path) { + $matches = []; + preg_match_all('#\[(\w+)]#', $path, $matches); + $this->_extraEntityFields[$entityName] = array_unique(array_merge($this->_extraEntityFields[$entityName], $matches[1] ?? [])); + } + } + return $this->_extraEntityFields[$entityName]; + } + } diff --git a/ext/search_kit/Civi/Search/Admin.php b/ext/search_kit/Civi/Search/Admin.php index 367deb9a80..05ae166e76 100644 --- a/ext/search_kit/Civi/Search/Admin.php +++ b/ext/search_kit/Civi/Search/Admin.php @@ -122,7 +122,7 @@ class Admin { // Add in FK fields for implicit joins // For example, add a `campaign_id.title` field to the Contribution entity foreach ($schema as &$entity) { - if (in_array('DAOEntity', $entity['type'], TRUE) && !in_array('EntityBridge', $entity['type'], TRUE)) { + if ($entity['searchable'] !== 'bridge') { foreach (array_reverse($entity['fields'], TRUE) as $index => $field) { if (!empty($field['fk_entity']) && !$field['options'] && empty($field['serialize']) && !empty($schema[$field['fk_entity']]['label_field'])) { $isCustom = strpos($field['name'], '.'); diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js index 6bcb11213b..1d56e21cda 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js @@ -58,12 +58,8 @@ }); } - var primaryEntities = _.filter(CRM.crmSearchAdmin.schema, function(entity) { - return entity.searchable === 'primary' && !_.includes(entity.type, 'EntityBridge'); - }); - var secondaryEntities = _.filter(CRM.crmSearchAdmin.schema, function(entity) { - return entity.searchable === 'secondary' && !_.includes(entity.type, 'EntityBridge'); - }); + var primaryEntities = _.filter(CRM.crmSearchAdmin.schema, {searchable: 'primary'}), + secondaryEntities = _.filter(CRM.crmSearchAdmin.schema, {searchable: 'secondary'}); $scope.mainEntitySelect = formatForSelect2(primaryEntities, 'name', 'title_plural', ['description', 'icon']); $scope.mainEntitySelect.push({ text: ts('More...'), @@ -814,7 +810,7 @@ // Add extra searchable fields from bridge entity if (join && join.bridge) { addFields(_.filter(searchMeta.getEntity(join.bridge).fields, function(field) { - return (field.name !== 'id' && field.name !== 'entity_id' && field.name !== 'entity_table' && !field.fk_entity); + return (field.name !== 'id' && field.name !== 'entity_id' && field.name !== 'entity_table' && !field.fk_entity && !_.includes(field.name, '.')); })); } diff --git a/tests/phpunit/api/v4/Entity/ConformanceTest.php b/tests/phpunit/api/v4/Entity/ConformanceTest.php index 5ad31a132f..34ca6c1066 100644 --- a/tests/phpunit/api/v4/Entity/ConformanceTest.php +++ b/tests/phpunit/api/v4/Entity/ConformanceTest.php @@ -156,7 +156,7 @@ class ConformanceTest extends UnitTestCase { $this->assertNotEmpty($info['title_plural']); $this->assertNotEmpty($info['type']); $this->assertNotEmpty($info['description']); - $this->assertContains($info['searchable'], ['primary', 'secondary', 'none']); + $this->assertContains($info['searchable'], ['primary', 'secondary', 'bridge', 'none']); // Bridge must be between exactly 2 entities if (in_array('EntityBridge', $info['type'], TRUE)) { $this->assertCount(2, $info['bridge']); diff --git a/xml/schema/Contact/Relationship.xml b/xml/schema/Contact/Relationship.xml index 8eb0f66c8f..9b8e55b313 100644 --- a/xml/schema/Contact/Relationship.xml +++ b/xml/schema/Contact/Relationship.xml @@ -8,6 +8,11 @@ 1.1 true fa-handshake-o + + civicrm/contact/view/rel?action=view&reset=1&cid=[contact_id_a]&id=[id] + civicrm/contact/view/rel?action=delete&reset=1&cid=[contact_id_a]&id=[id] + + id int unsigned diff --git a/xml/schema/Contact/RelationshipCache.xml b/xml/schema/Contact/RelationshipCache.xml index e24998f55e..0487942ed4 100644 --- a/xml/schema/Contact/RelationshipCache.xml +++ b/xml/schema/Contact/RelationshipCache.xml @@ -9,6 +9,12 @@ false fa-handshake-o Related Contact + + civicrm/contact/view/rel?action=view&reset=1&cid=[near_contact_id]&id=[relationship_id] + civicrm/contact/view/rel?action=update&reset=1&cid=[near_contact_id]&id=[relationship_id]&rtype=[orientation] + civicrm/contact/view/rel?action=delete&reset=1&cid=[near_contact_id]&id=[relationship_id] + + id int unsigned -- 2.25.1