SearchKit - Add in FK fields for implicit joins
[civicrm-core.git] / ext / search / Civi / Search / Admin.php
index b223b321f09b9d2307e3c9d413b43999c933a1fa..3ef074c36b7e4fe265b58751f5719ba6d3dbe52a 100644 (file)
@@ -27,7 +27,15 @@ class Admin {
       'joins' => self::getJoins(array_column($schema, NULL, 'name')),
       'operators' => \CRM_Utils_Array::makeNonAssociative(self::getOperators()),
       'functions' => \CRM_Api4_Page_Api4Explorer::getSqlFunctions(),
-      'displayTypes' => Display::getDisplayTypes(['name', 'label', 'description', 'icon']),
+      'displayTypes' => Display::getDisplayTypes(['id', 'name', 'label', 'description', 'icon']),
+      'afformEnabled' => (bool) \CRM_Utils_Array::findAll(
+        \CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(),
+        ['fullName' => 'org.civicrm.afform']
+      ),
+      'afformAdminEnabled' => (bool) \CRM_Utils_Array::findAll(
+        \CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(),
+        ['fullName' => 'org.civicrm.afform_admin']
+      ),
     ];
   }
 
@@ -43,8 +51,8 @@ class Admin {
       '>=' => '≥',
       '<=' => '≤',
       'CONTAINS' => ts('Contains'),
-      'IN' => ts('Is In'),
-      'NOT IN' => ts('Not In'),
+      'IN' => ts('Is One Of'),
+      'NOT IN' => ts('Not One Of'),
       'LIKE' => ts('Is Like'),
       'NOT LIKE' => ts('Not Like'),
       'BETWEEN' => ts('Is Between'),
@@ -61,13 +69,13 @@ class Admin {
   public static function getSchema() {
     $schema = [];
     $entities = \Civi\Api4\Entity::get()
-      ->addSelect('name', 'title', 'type', 'title_plural', 'description', 'icon', 'paths', 'dao', 'bridge')
+      ->addSelect('name', 'title', 'type', 'title_plural', 'description', 'label_field', 'icon', 'paths', 'dao', 'bridge', 'ui_join_filters')
       ->addWhere('searchable', '=', TRUE)
       ->addOrderBy('title_plural')
       ->setChain([
         'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']],
       ])->execute();
-    $getFields = ['name', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'fk_entity'];
+    $getFields = ['name', 'title', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'fk_entity'];
     foreach ($entities as $entity) {
       // Skip if entity doesn't have a 'get' action or the user doesn't have permission to use get
       if ($entity['get']) {
@@ -106,11 +114,26 @@ class Admin {
         if (!array_diff(['select', 'where', 'orderBy', 'limit', 'offset'], array_keys($params))) {
           \CRM_Utils_Array::remove($params, 'checkPermissions', 'debug', 'chain', 'language', 'select', 'where', 'orderBy', 'limit', 'offset');
           unset($entity['get']);
-          $schema[] = ['params' => array_keys($params)] + array_filter($entity);
+          $schema[$entity['name']] = ['params' => array_keys($params)] + array_filter($entity);
         }
       }
     }
-    return $schema;
+    // Add in FK fields for implicit joins
+    // For example, add a `campaign.title` field to the Contribution entity
+    foreach ($schema as &$entity) {
+      foreach (array_reverse($entity['fields'], TRUE) as $index => $field) {
+        if (!empty($field['fk_entity']) && !$field['options'] && !empty($schema[$field['fk_entity']]['label_field'])) {
+          // The original field will get title instead of label since it represents the id (title usually ends in ID but label does not)
+          $entity['fields'][$index]['label'] = $field['title'];
+          // Add the label field from the other entity to this entity's list of fields
+          $newField = \CRM_Utils_Array::findAll($schema[$field['fk_entity']]['fields'], ['name' => $schema[$field['fk_entity']]['label_field']])[0];
+          $newField['name'] = str_replace('_id', '', $field['name']) . '.' . $schema[$field['fk_entity']]['label_field'];
+          $newField['label'] = $field['label'] . ' ' . $newField['label'];
+          array_splice($entity['fields'], $index, 0, [$newField]);
+        }
+      }
+    }
+    return array_values($schema);
   }
 
   /**
@@ -120,7 +143,34 @@ class Admin {
   public static function getJoins(array $allowedEntities) {
     $joins = [];
     foreach ($allowedEntities as $entity) {
-      if (!empty($entity['dao'])) {
+      // Multi-record custom field groups (to-date only the contact entity supports these)
+      if (in_array('CustomValue', $entity['type'])) {
+        $targetEntity = $allowedEntities['Contact'];
+        // Join from Custom group to Contact (n-1)
+        $alias = $entity['name'] . '_Contact_entity_id';
+        $joins[$entity['name']][] = [
+          'label' => $entity['title'] . ' ' . $targetEntity['title'],
+          'description' => '',
+          'entity' => 'Contact',
+          'conditions' => self::getJoinConditions('entity_id', $alias . '.id'),
+          'defaults' => self::getJoinDefaults($alias, $targetEntity),
+          'alias' => $alias,
+          'multi' => FALSE,
+        ];
+        // Join from Contact to Custom group (n-n)
+        $alias = 'Contact_' . $entity['name'] . '_entity_id';
+        $joins['Contact'][] = [
+          'label' => $entity['title_plural'],
+          'description' => '',
+          'entity' => $entity['name'],
+          'conditions' => self::getJoinConditions('id', $alias . '.entity_id'),
+          'defaults' => self::getJoinDefaults($alias, $entity),
+          'alias' => $alias,
+          'multi' => TRUE,
+        ];
+      }
+      // Non-custom DAO entities
+      elseif (!empty($entity['dao'])) {
         /* @var \CRM_Core_DAO $daoClass */
         $daoClass = $entity['dao'];
         $references = $daoClass::getReferenceColumns();
@@ -142,21 +192,10 @@ class Admin {
           ) {
             continue;
           }
-          // Dynamic references use a column like "entity_table"
+          // Dynamic references use a column like "entity_table" (for normal joins this value will be null)
           $dynamicCol = $reference->getTypeColumn();
-          if ($dynamicCol) {
-            $targetTables = $daoClass::buildOptions($dynamicCol);
-            if (!$targetTables) {
-              continue;
-            }
-            $targetTables = array_keys($targetTables);
-          }
-          else {
-            $targetTables = [$reference->getTargetTable()];
-          }
-          foreach ($targetTables as $targetTable) {
-            $targetDao = \CRM_Core_DAO_AllCoreTables::getClassForTable($targetTable);
-            $targetEntityName = \CRM_Core_DAO_AllCoreTables::getBriefName($targetDao);
+          // For dynamic references getTargetEntities will return multiple targets; for normal joins this loop will only run once
+          foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) {
             if (!isset($allowedEntities[$targetEntityName]) || $targetEntityName === $entity['name']) {
               continue;
             }
@@ -170,6 +209,7 @@ class Admin {
                 'description' => $dynamicCol ? '' : $keyField['label'],
                 'entity' => $targetEntityName,
                 'conditions' => self::getJoinConditions($keyField['name'], $alias . '.' . $reference->getTargetKey(), $targetTable, $dynamicCol),
+                'defaults' => self::getJoinDefaults($alias, $targetEntity),
                 'alias' => $alias,
                 'multi' => FALSE,
               ];
@@ -180,6 +220,7 @@ class Admin {
                 'description' => $dynamicCol ? '' : $keyField['label'],
                 'entity' => $entity['name'],
                 'conditions' => self::getJoinConditions($reference->getTargetKey(), $alias . '.' . $keyField['name'], $targetTable, $dynamicCol ? $alias . '.' . $dynamicCol : NULL),
+                'defaults' => self::getJoinDefaults($alias, $entity),
                 'alias' => $alias,
                 'multi' => TRUE,
               ];
@@ -204,6 +245,7 @@ class Admin {
                   [$bridge],
                   self::getJoinConditions('id', $alias . '.' . $baseKey, NULL, NULL)
                 ),
+                'defaults' => self::getJoinDefaults($alias, $targetEntity, $entity),
                 'bridge' => $bridge,
                 'alias' => $alias,
                 'multi' => TRUE,
@@ -218,6 +260,7 @@ class Admin {
                     [$bridge],
                     self::getJoinConditions($reference->getTargetKey(), $alias . '.' . $keyField['name'], $targetTable, $dynamicCol ? $alias . '.' . $dynamicCol : NULL)
                   ),
+                  'defaults' => self::getJoinDefaults($alias, $baseEntity, $entity),
                   'bridge' => $bridge,
                   'alias' => $alias,
                   'multi' => TRUE,
@@ -240,7 +283,7 @@ class Admin {
    * @param string|null $dynamicCol
    * @return array[]
    */
-  private static function getJoinConditions($nearCol, $farCol, $targetTable, $dynamicCol) {
+  private static function getJoinConditions($nearCol, $farCol, $targetTable = NULL, $dynamicCol = NULL) {
     $conditions = [
       [
         $nearCol,
@@ -258,4 +301,29 @@ class Admin {
     return $conditions;
   }
 
+  /**
+   * @param $alias
+   * @param array ...$entities
+   * @return array
+   */
+  private static function getJoinDefaults($alias, ...$entities):array {
+    $conditions = [];
+    foreach ($entities as $entity) {
+      foreach ($entity['ui_join_filters'] ?? [] as $fieldName) {
+        $field = civicrm_api4($entity['name'], 'getFields', [
+          'select' => ['options'],
+          'where' => [['name', '=', $fieldName]],
+          'loadOptions' => ['name'],
+        ])->first();
+        $value = isset($field['options'][0]) ? json_encode($field['options'][0]['name']) : '';
+        $conditions[] = [
+          $alias . '.' . $fieldName . ($value ? ':name' : ''),
+          '=',
+          $value,
+        ];
+      }
+    }
+    return $conditions;
+  }
+
 }