SearchKit - Enable search for relationships as base entity
authorColeman Watts <coleman@civicrm.org>
Tue, 8 Jun 2021 16:33:00 +0000 (12:33 -0400)
committerColeman Watts <coleman@civicrm.org>
Tue, 8 Jun 2021 18:56:44 +0000 (14:56 -0400)
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)

18 files changed:
CRM/Contact/DAO/Relationship.php
CRM/Contact/DAO/RelationshipCache.php
Civi/Api4/ActivityContact.php
Civi/Api4/CaseActivity.php
Civi/Api4/CaseContact.php
Civi/Api4/DashboardContact.php
Civi/Api4/Entity.php
Civi/Api4/EntityFinancialAccount.php
Civi/Api4/EntityFinancialTrxn.php
Civi/Api4/EntityTag.php
Civi/Api4/GroupContact.php
Civi/Api4/RelationshipCache.php
ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php
ext/search_kit/Civi/Search/Admin.php
ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js
tests/phpunit/api/v4/Entity/ConformanceTest.php
xml/schema/Contact/Relationship.xml
xml/schema/Contact/RelationshipCache.xml

index 1f4ef79592d26053632444498f30ddf2fc332395..211536c505bba9013c9d55ed813c39349a252ebe 100644 (file)
@@ -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
    *
index 3583f377a74b384ecdf84baa230ae28c03ee4a0e..e61073232a58cd242e35ed36401436916352e5a0 100644 (file)
@@ -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
    *
index 39ec1b3b95df5db0679952e79f99230283011669..767595de702887669bca83679ca643533e79069d 100644 (file)
@@ -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
  */
index 95be92196087626d3a4915ecbc5b061af233ce99..f38ce21ee93c6ab9c489310e78254ea9530ed3bb 100644 (file)
@@ -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
  */
index 4d8ae66130839dbd329513655da1d7fadfeaed05..a9351a9798f5e822dea67db419f1a6b1c1fed20a 100644 (file)
@@ -24,6 +24,7 @@ namespace Civi\Api4;
  *
  * This connects a client to a case.
  *
+ * @searchable bridge
  * @see \Civi\Api4\Case
  * @package Civi\Api4
  */
index d029f388d5cc43061be496c50d59f2e4fe4b6382..91eae6be2f05e6d3e0c27ffd7f5697856ec53c83 100644 (file)
@@ -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
index 00a99f6b331aeb49cba3e44bcd1966ed739bf95c..e66a9bec701b3f9cb70bd6db438642115d492ca7 100644 (file)
@@ -100,6 +100,7 @@ class Entity extends Generic\AbstractEntity {
           'options' => [
             'primary' => ts('Primary'),
             'secondary' => ts('Secondary'),
+            'bridge' => ts('Bridge'),
             'none' => ts('None'),
           ],
         ],
index 4b9b3d537bf7d2a8b5861fd286ddf4020a64ab8c..e8f4750ce94982b7210dfdaf4ee0286502c269de 100644 (file)
@@ -25,6 +25,7 @@ namespace Civi\Api4;
  *
  * @ui_join_filters account_relationship
  *
+ * @searchable bridge
  * @package Civi\Api4
  */
 class EntityFinancialAccount extends Generic\DAOEntity {
index 6fd015de30c776733728f914d46ee34a88a63f53..f63803793a66ec0599a414deb36672068e894e1a 100644 (file)
@@ -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 {
index 2febe72254e348cf20fe1a6d2c88101b1047454d..62fe8b68704cebfe451598de40789668392512b5 100644 (file)
@@ -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 {
index fd988878bd99764138c498f3a9e4adff3bc2e4b6..850fe78b440604af997232c8f75fab0e43579254 100644 (file)
@@ -26,6 +26,7 @@ namespace Civi\Api4;
  *
  * @ui_join_filters status
  *
+ * @searchable bridge
  * @see \Civi\Api4\Group
  * @package Civi\Api4
  */
index c878b8056ea035b56e774715ff55d43f6fff0e78..7a4b774fbc536a602c406c06d36a4b1feee931f9 100644 (file)
@@ -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
index 4bc54ae62944a2de177d56081ee7dc12a93485f5..dcf36703c5bb8684927e3b7e0305750be2db3321 100644 (file)
@@ -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];
+  }
+
 }
index 367deb9a80eca14499ce148bf03560be5d452605..05ae166e76c980d7bdb49b9fd70eb2e4f735a034 100644 (file)
@@ -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'], '.');
index 6bcb11213b22c5672ce4d82a9d255f61260508e9..1d56e21cdab9f2c96e8be1f99397d276acaf4b60 100644 (file)
           });
         }
 
-        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...'),
           // 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, '.'));
             }));
           }
 
index 5ad31a132f9bcaac7e26fc35e2d5c8246f5d16c0..34ca6c1066310b7a651108b9a0489e2cd5abd21d 100644 (file)
@@ -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']);
index 8eb0f66c8f93e078dc28cc02f0bf98110c73db27..9b8e55b3133235ea1da961e8d490105a777409dd 100644 (file)
@@ -8,6 +8,11 @@
   <add>1.1</add>
   <log>true</log>
   <icon>fa-handshake-o</icon>
+  <paths>
+    <view>civicrm/contact/view/rel?action=view&amp;reset=1&amp;cid=[contact_id_a]&amp;id=[id]</view>
+    <delete>civicrm/contact/view/rel?action=delete&amp;reset=1&amp;cid=[contact_id_a]&amp;id=[id]</delete>
+  </paths>
+
   <field>
     <name>id</name>
     <type>int unsigned</type>
index e24998f55ef02e30589234643d371d7f3a83d8b5..0487942ed4757a98ef9e8de16c2ec3c29017c56f 100644 (file)
@@ -9,6 +9,12 @@
   <log>false</log>
   <icon>fa-handshake-o</icon>
   <title>Related Contact</title>
+  <paths>
+    <view>civicrm/contact/view/rel?action=view&amp;reset=1&amp;cid=[near_contact_id]&amp;id=[relationship_id]</view>
+    <update>civicrm/contact/view/rel?action=update&amp;reset=1&amp;cid=[near_contact_id]&amp;id=[relationship_id]&amp;rtype=[orientation]</update>
+    <delete>civicrm/contact/view/rel?action=delete&amp;reset=1&amp;cid=[near_contact_id]&amp;id=[relationship_id]</delete>
+  </paths>
+
   <field>
     <name>id</name>
     <type>int unsigned</type>