SearchKit - Add @searchFields metadata to specify default search display fields per...
authorcolemanw <coleman@civicrm.org>
Mon, 21 Aug 2023 13:14:58 +0000 (09:14 -0400)
committercolemanw <coleman@civicrm.org>
Tue, 22 Aug 2023 19:06:35 +0000 (15:06 -0400)
- Use Contact.sort_name instead of display_name for searches and Autocompletes
- Adds more useful fields by default to a new SearchKit display
- Makes use of new metadata to improve generated default Autocomplete display

30 files changed:
CRM/Pledge/DAO/Pledge.php
Civi/Api4/Contact.php
Civi/Api4/Entity.php
Civi/Api4/Generic/AbstractEntity.php
Civi/Api4/Relationship.php
Civi/Api4/RelationshipCache.php
Civi/Api4/Service/Autocomplete/ActivityAutocompleteProvider.php
Civi/Api4/Service/Autocomplete/CaseAutocompleteProvider.php
Civi/Api4/Service/Autocomplete/ContactAutocompleteProvider.php
Civi/Api4/Service/Autocomplete/ContributionAutocompleteProvider.php
Civi/Api4/Service/Autocomplete/ContributionRecurAutocompleteProvider.php
Civi/Api4/Service/Autocomplete/ParticipantAutocompleteProvider.php [deleted file]
Civi/Api4/Service/Autocomplete/PledgeAutocompleteProvider.php
Civi/Api4/Service/Autocomplete/RelationshipAutocompleteProvider.php
Civi/Api4/Utils/CoreUtil.php
ext/afform/mock/tests/phpunit/api/v4/AfformAutocompleteUsageTest.php
ext/civi_contribute/Civi/Api4/Contribution.php
ext/civi_event/Civi/Api4/Participant.php
ext/civi_member/Civi/Api4/Membership.php
ext/civi_pledge/Civi/Api4/Pledge.php
ext/civigrant/Civi/Api4/Grant.php
ext/civigrant/Civi/Api4/Service/Autocomplete/GrantAutocompleteProvider.php [deleted file]
ext/search_kit/Civi/Api4/Action/SearchDisplay/GetDefault.php
ext/search_kit/Civi/Api4/Event/Subscriber/DefaultDisplaySubscriber.php
ext/search_kit/Civi/Search/Admin.php
ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js
ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayAutocomplete.component.js
tests/phpunit/api/v4/Action/AutocompleteTest.php
tests/phpunit/api/v4/Entity/EntityTest.php
xml/schema/Pledge/Pledge.xml

index f38304665194e62314467cfa2653014430a48eed..e8d5012217d050b82670ea157c56e113235c8780 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Pledge/Pledge.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:de226da85d80eda7f8ffef798215ad2a)
+ * (GenCodeChecksum:91fbbe8481e26df491af63fd07594f20)
  */
 
 /**
@@ -819,6 +819,7 @@ class CRM_Pledge_DAO_Pledge extends CRM_Core_DAO {
           'localizable' => 0,
           'html' => [
             'type' => 'Select',
+            'label' => ts("Status"),
           ],
           'pseudoconstant' => [
             'optionGroupName' => 'pledge_status',
index b8ddca6aa0aa0959d0fec4f9e24e9ea9eee14c4a..eeb05eb9a10200c07448654210f51a97b1be7574 100644 (file)
@@ -21,6 +21,7 @@ namespace Civi\Api4;
  * @see https://docs.civicrm.org/user/en/latest/organising-your-data/contacts/
  * @searchable primary
  * @orderBy sort_name
+ * @searchFields sort_name
  * @iconField contact_sub_type:icon,contact_type:icon
  * @since 5.19
  * @package Civi\Api4
index 5f36793aa7fcce1da0e44b32b6dd29cf6bd60ed5..fb2b4780e9841152334c0af0dac47a0022f0fa64 100644 (file)
@@ -74,6 +74,11 @@ class Entity extends Generic\AbstractEntity {
       'name' => 'label_field',
       'description' => 'Field to show when displaying a record',
     ],
+    [
+      'name' => 'search_fields',
+      'data_type' => 'Array',
+      'description' => 'Fields to show in search context',
+    ],
     [
       'name' => 'icon_field',
       'data_type' => 'Array',
index 083e3359e2cc4fdda0210ae8ffb6e4a0a658ca9d..6e5a8e0cce7eac56d25a507bdcbf76baf6eca953 100644 (file)
@@ -177,7 +177,10 @@ abstract class AbstractEntity {
         $info[$field['name']] = $val;
       }
     }
-
+    // search_fields defaults to label_field
+    if (empty($info['search_fields']) && !empty($info['label_field'])) {
+      $info['search_fields'] = [$info['label_field']];
+    }
     if ($dao) {
       $info['description'] = $dao::getEntityDescription() ?? $info['description'] ?? NULL;
     }
index bed75391c4c230e01aa1d9617036d956aed80996..2532d3a65cce112fd98cfba60c961b3ad6caddfb 100644 (file)
@@ -15,6 +15,7 @@ namespace Civi\Api4;
  *
  * @see https://docs.civicrm.org/user/en/latest/organising-your-data/relationships/
  * @searchable none
+ * @searchFields contact_id_a.sort_name,relationship_type_id.label_a_b,contact_id_b.sort_name
  * @since 5.19
  * @package Civi\Api4
  */
index 3beee1ab4303e2af9907f93566ba534a3047e469..1b3e350fcf46436b3f4bf0b2211acc56eb0fd23d 100644 (file)
@@ -14,6 +14,7 @@ namespace Civi\Api4;
  * RelationshipCache - readonly table to facilitate joining and finding contacts by relationship.
  *
  * @searchable secondary
+ * @searchFields near_contact_id.sort_name,near_relation:label,far_contact_id.sort_name
  * @see \Civi\Api4\Relationship
  * @ui_join_filters near_relation
  * @since 5.29
index 144035c90a7f9a411a930e7cfa26ed27a1b16340..843ef5fea36120fa6113a5e1356ed243e5c70827 100644 (file)
@@ -36,7 +36,7 @@ class ActivityAutocompleteProvider extends \Civi\Core\Service\AutoService implem
         'id',
         'subject',
         'activity_date_time',
-        'Activity_ActivityContact_Contact_01.display_name',
+        'Activity_ActivityContact_Contact_01.sort_name',
         'activity_type_id:label',
       ],
       'orderBy' => [],
@@ -97,8 +97,8 @@ class ActivityAutocompleteProvider extends \Civi\Core\Service\AutoService implem
       [$entity, $contactAlias] = explode(' AS ', $join[0]);
       if ($entity === 'Contact') {
         array_unshift($e->display['settings']['sort'], ["$contactAlias.sort_name", 'ASC']);
-        $e->display['settings']['columns'][0]['rewrite'] = "[$contactAlias.display_name] - [subject]";
-        $e->display['settings']['columns'][0]['empty_value'] = "[$contactAlias.display_name] (" . ts('no subject') . ')';
+        $e->display['settings']['columns'][0]['rewrite'] = "[$contactAlias.sort_name] - [subject]";
+        $e->display['settings']['columns'][0]['empty_value'] = "[$contactAlias.sort_name] (" . ts('no subject') . ')';
         break;
       }
     }
index ed13b5c951ad82f03b4974c8a285b549d4e6d8e3..e28e6fbaf960652e693dbb14324a7e0b09724cb6 100644 (file)
@@ -35,7 +35,7 @@ class CaseAutocompleteProvider extends \Civi\Core\Service\AutoService implements
       'select' => [
         'id',
         'subject',
-        'Case_CaseContact_Contact_01.display_name',
+        'Case_CaseContact_Contact_01.sort_name',
         'case_type_id:label',
         'status_id:label',
         'start_date',
@@ -94,8 +94,8 @@ class CaseAutocompleteProvider extends \Civi\Core\Service\AutoService implements
       [$entity, $contactAlias] = explode(' AS ', $join[0]);
       if ($entity === 'Contact') {
         array_unshift($e->display['settings']['sort'], ["$contactAlias.sort_name", 'ASC']);
-        $e->display['settings']['columns'][0]['rewrite'] = "[$contactAlias.display_name] - [subject]";
-        $e->display['settings']['columns'][0]['empty_value'] = "[$contactAlias.display_name] (" . ts('no subject') . ')';
+        $e->display['settings']['columns'][0]['rewrite'] = "[$contactAlias.sort_name] - [subject]";
+        $e->display['settings']['columns'][0]['empty_value'] = "[$contactAlias.sort_name] (" . ts('no subject') . ')';
         break;
       }
     }
index 1aadc6446404c2958fa3277af2335e10fa9e8b0e..0d292aa0db32793f59ae6fd1368860d7d7cf40ff 100644 (file)
@@ -37,7 +37,7 @@ class ContactAutocompleteProvider extends \Civi\Core\Service\AutoService impleme
       'columns' => [
         [
           'type' => 'field',
-          'key' => 'display_name',
+          'key' => 'sort_name',
           'icons' => [
             ['field' => 'contact_sub_type:icon'],
             ['field' => 'contact_type:icon'],
index 916a6c56d6a351e2d9a29687ad89879cee1d71e5..114b3f4b29c8806533cd7daba5bec44bfded6fa8 100644 (file)
@@ -34,7 +34,7 @@ class ContributionAutocompleteProvider extends \Civi\Core\Service\AutoService im
       'version' => 4,
       'select' => [
         'id',
-        'contact_id.display_name',
+        'contact_id.sort_name',
         'total_amount',
         'receive_date',
         'financial_type_id:label',
@@ -66,8 +66,8 @@ class ContributionAutocompleteProvider extends \Civi\Core\Service\AutoService im
       'columns' => [
         [
           'type' => 'field',
-          'key' => 'contact_id.display_name',
-          'rewrite' => '[contact_id.display_name] - [total_amount]',
+          'key' => 'contact_id.sort_name',
+          'rewrite' => '[contact_id.sort_name] - [total_amount]',
         ],
         [
           'type' => 'field',
index 4f845248cbf88e07fc564d000f9613f3cba4207f..7d3ddae797604ae09a5e32532a96f7eacf4f13ce 100644 (file)
@@ -34,7 +34,7 @@ class ContributionRecurAutocompleteProvider extends \Civi\Core\Service\AutoServi
       'version' => 4,
       'select' => [
         'id',
-        'contact_id.display_name',
+        'contact_id.sort_name',
         'frequency_unit:label',
         'frequency_interval',
         'amount',
@@ -68,8 +68,8 @@ class ContributionRecurAutocompleteProvider extends \Civi\Core\Service\AutoServi
       'columns' => [
         [
           'type' => 'field',
-          'key' => 'contact_id.display_name',
-          'rewrite' => '[contact_id.display_name] - [amount]',
+          'key' => 'contact_id.sort_name',
+          'rewrite' => '[contact_id.sort_name] - [amount]',
         ],
         [
           'type' => 'field',
diff --git a/Civi/Api4/Service/Autocomplete/ParticipantAutocompleteProvider.php b/Civi/Api4/Service/Autocomplete/ParticipantAutocompleteProvider.php
deleted file mode 100644 (file)
index 2131e3c..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-/*
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC. All rights reserved.                        |
- |                                                                    |
- | This work is published under the GNU AGPLv3 license with some      |
- | permitted exceptions and without any warranty. For full license    |
- | and copyright information, see https://civicrm.org/licensing       |
- +--------------------------------------------------------------------+
- */
-
-namespace Civi\Api4\Service\Autocomplete;
-
-use Civi\Core\Event\GenericHookEvent;
-use Civi\Core\HookInterface;
-
-/**
- * @service
- * @internal
- */
-class ParticipantAutocompleteProvider extends \Civi\Core\Service\AutoService implements HookInterface {
-
-  /**
-   * Provide default SearchDisplay for Participant autocompletes
-   *
-   * @param \Civi\Core\Event\GenericHookEvent $e
-   */
-  public static function on_civi_search_defaultDisplay(GenericHookEvent $e) {
-    if ($e->display['settings'] || $e->display['type'] !== 'autocomplete' || $e->savedSearch['api_entity'] !== 'Participant') {
-      return;
-    }
-    $e->display['settings'] = [
-      'sort' => [
-        ['contact_id.sort_name', 'ASC'],
-        ['event_id.title', 'ASC'],
-      ],
-      'columns' => [
-        [
-          'type' => 'field',
-          'key' => 'contact_id.display_name',
-          'rewrite' => '[contact_id.display_name] - [event_id.title]',
-        ],
-        [
-          'type' => 'field',
-          'key' => 'role_id:label',
-          'rewrite' => '#[id] [role_id:label]',
-        ],
-        [
-          'type' => 'field',
-          'key' => 'status_id:label',
-        ],
-      ],
-    ];
-  }
-
-}
index 7e7e22964b141b2b0493072cde90263091764256..3a38d539565d69c8f5cc779ae0b58aaf655806af 100644 (file)
@@ -34,7 +34,7 @@ class PledgeAutocompleteProvider extends \Civi\Core\Service\AutoService implemen
       'version' => 4,
       'select' => [
         'id',
-        'contact_id.display_name',
+        'contact_id.sort_name',
         'amount',
         'start_date',
         'end_date',
@@ -66,8 +66,8 @@ class PledgeAutocompleteProvider extends \Civi\Core\Service\AutoService implemen
       'columns' => [
         [
           'type' => 'field',
-          'key' => 'contact_id.display_name',
-          'rewrite' => '[contact_id.display_name] - [amount]',
+          'key' => 'contact_id.sort_name',
+          'rewrite' => '[contact_id.sort_name] - [amount]',
         ],
         [
           'type' => 'field',
index f86bcbaae6821a386a5f91970f2c11d26df1bee5..bbae3255cf7cea994361d3c8670552dfb3b6fdcb 100644 (file)
@@ -38,7 +38,7 @@ class RelationshipAutocompleteProvider extends \Civi\Core\Service\AutoService im
         [
           'type' => 'field',
           'key' => 'relationship_type_id.label_a_b',
-          'rewrite' => '[contact_id_a.display_name] [relationship_type_id.label_a_b] [contact_id_b.display_name]',
+          'rewrite' => '[contact_id_a.sort_name] [relationship_type_id.label_a_b] [contact_id_b.sort_name]',
         ],
         [
           'type' => 'field',
index 865aa7be14aebb13808134d2b639759a5b542f3b..76db7022f718ebdfa8d8425c9fc0d10eb0b11f57 100644 (file)
@@ -81,6 +81,15 @@ class CoreUtil {
     return self::getInfoItem($entityName, 'primary_key')[0] ?? 'id';
   }
 
+  /**
+   * Get name of field(s) to display in search context
+   * @param string $entityName
+   * @return array
+   */
+  public static function getSearchFields(string $entityName): array {
+    return self::getInfoItem($entityName, 'search_fields') ?: [];
+  }
+
   /**
    * Get table name of given entity
    *
index 43b88fcf5ab14422d672f93452ddfe72a34e32c6..ade9f0c09cf54598f391fb24bf4213db73e5b6bf 100644 (file)
@@ -68,8 +68,8 @@ EOHTML;
       ->execute();
 
     $this->assertCount(2, $result);
-    $this->assertEquals('A ' . $lastName, $result[0]['label']);
-    $this->assertEquals('B ' . $lastName, $result[1]['label']);
+    $this->assertEquals($lastName . ', A', $result[0]['label']);
+    $this->assertEquals($lastName . ', B', $result[1]['label']);
 
     // Ensure form validates submission, restricting it to contacts A & B
     $values = [
@@ -168,8 +168,8 @@ EOHTML;
       ->execute();
 
     $this->assertCount(2, $result);
-    $this->assertEquals('A ' . $lastName, $result[0]['label']);
-    $this->assertEquals('B ' . $lastName, $result[1]['label']);
+    $this->assertEquals($lastName . ', A', $result[0]['label']);
+    $this->assertEquals($lastName . ', B', $result[1]['label']);
 
     // Ensure form validates submission, restricting it to contacts A & B
     $values = [
@@ -262,8 +262,8 @@ EOHTML;
       ->execute();
 
     $this->assertCount(2, $result);
-    $this->assertEquals('A ' . $lastName, $result[0]['label']);
-    $this->assertEquals('C ' . $lastName, $result[1]['label']);
+    $this->assertEquals($lastName . ', A', $result[0]['label']);
+    $this->assertEquals($lastName . ', C', $result[1]['label']);
 
     // Ensure form validates submission, restricting it to contacts A & C
     $values = [
index 67d61be1f7b6af624e6451ffe8d37b8d09bbc0a7..b2a275d3536fee0f34e3ebb40f02bc663e693173 100644 (file)
@@ -14,6 +14,7 @@ namespace Civi\Api4;
  * Contribution entity.
  *
  * @searchable primary
+ * @searchFields contact_id.sort_name,total_amount
  * @since 5.19
  * @package Civi\Api4
  */
index 425020067ebd2906593d24b0689f77d4ba29bfd9..3224d94bff40efa93f7d55ab1cbaddb2aff98e76 100644 (file)
@@ -14,6 +14,7 @@ namespace Civi\Api4;
  * Participant entity, stores the participation record of a contact in an event.
  *
  * @searchable primary
+ * @searchFields contact_id.sort_name,event_id.title
  * @since 5.19
  * @package Civi\Api4
  */
index 751cd65e32a8a1a1ec7a02019f67001ea00d3676..1a8eda1d217da6da8f62ae56fb80491a06487288 100644 (file)
@@ -14,6 +14,7 @@ namespace Civi\Api4;
  * Membership entity.
  *
  * @searchable primary
+ * @searchFields contact_id.sort_name
  * @since 5.42
  * @package Civi\Api4
  */
index c571f9d464fdd2836c93048387980b9fe80be3a4..df39ede1efd12392be11acbfecae0f1d8f561f6d 100644 (file)
@@ -15,6 +15,7 @@ namespace Civi\Api4;
  *
  * @see https://docs.civicrm.org/user/en/latest/pledges/what-is-civipledge/
  * @searchable primary
+ * @searchFields contact_id.display_name,amount
  * @since 5.35
  * @package Civi\Api4
  */
index 96caf2375e86db49f63cba0ffab1ce221adb134b..83ad0f87d5d414449e90178ed18e0180bf2c57b6 100644 (file)
@@ -18,6 +18,7 @@ namespace Civi\Api4;
  * @see https://docs.civicrm.org/user/en/latest/grants/what-is-civigrant/
  *
  * @searchable primary
+ * @searchFields contact_id.sort_name,grant_type_id:label
  * @since 5.33
  * @package Civi\Api4
  */
diff --git a/ext/civigrant/Civi/Api4/Service/Autocomplete/GrantAutocompleteProvider.php b/ext/civigrant/Civi/Api4/Service/Autocomplete/GrantAutocompleteProvider.php
deleted file mode 100644 (file)
index e5c0388..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-/*
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC. All rights reserved.                        |
- |                                                                    |
- | This work is published under the GNU AGPLv3 license with some      |
- | permitted exceptions and without any warranty. For full license    |
- | and copyright information, see https://civicrm.org/licensing       |
- +--------------------------------------------------------------------+
- */
-
-namespace Civi\Api4\Service\Autocomplete;
-
-use Civi\Core\Event\GenericHookEvent;
-use Civi\Core\HookInterface;
-
-/**
- * @service
- * @internal
- */
-class GrantAutocompleteProvider extends \Civi\Core\Service\AutoService implements HookInterface {
-
-  /**
-   * Provide default SavedSearch for Grant autocompletes
-   *
-   * @param \Civi\Core\Event\GenericHookEvent $e
-   */
-  public static function on_civi_search_autocompleteDefault(GenericHookEvent $e) {
-    if (!is_array($e->savedSearch) || $e->savedSearch['api_entity'] !== 'Grant') {
-      return;
-    }
-    $e->savedSearch['api_params'] = [
-      'version' => 4,
-      'select' => [
-        'id',
-        'contact_id.display_name',
-        'grant_type_id:label',
-        'financial_type_id:label',
-        'status_id:label',
-      ],
-      'orderBy' => [],
-      'where' => [],
-      'groupBy' => [],
-      'join' => [],
-      'having' => [],
-    ];
-  }
-
-  /**
-   * Provide default SearchDisplay for Grant autocompletes
-   *
-   * @param \Civi\Core\Event\GenericHookEvent $e
-   */
-  public static function on_civi_search_defaultDisplay(GenericHookEvent $e) {
-    if ($e->display['settings'] || $e->display['type'] !== 'autocomplete' || $e->savedSearch['api_entity'] !== 'Grant') {
-      return;
-    }
-    $e->display['settings'] = [
-      'sort' => [
-        ['contact_id.sort_name', 'ASC'],
-        ['application_received_date', 'DESC'],
-      ],
-      'columns' => [
-        [
-          'type' => 'field',
-          'key' => 'contact_id.display_name',
-          'rewrite' => '[contact_id.display_name] - [grant_type_id:label]',
-        ],
-        [
-          'type' => 'field',
-          'key' => 'financial_type_id:label',
-          'rewrite' => '#[id] [status_id:label]',
-        ],
-        [
-          'type' => 'field',
-          'key' => 'financial_type_id:label',
-        ],
-      ],
-    ];
-  }
-
-}
index 994e2018ec65cf0bc4e3451a40cebafbbd5a36c9..407b6ba1c11f9da8bf7a5c6dc27974f2067f3216 100644 (file)
@@ -195,7 +195,7 @@ class GetDefault extends \Civi\Api4\Generic\AbstractAction {
     if ($clause['expr'] instanceof SqlField || $clause['expr'] instanceof SqlFunctionGROUP_CONCAT) {
       $field = \CRM_Utils_Array::first($clause['fields'] ?? []);
       if ($field &&
-        CoreUtil::getInfoItem($field['entity'], 'label_field') === $field['name'] &&
+        in_array($field['name'], array_merge(CoreUtil::getSearchFields($field['entity']), [CoreUtil::getInfoItem($field['entity'], 'label_field')]), TRUE) &&
         !empty(CoreUtil::getInfoItem($field['entity'], 'paths')['view'])
       ) {
         $col['link'] = [
index 20fa233a4b44be7f0ca165db13a99ba4203ad327..03649969ba12b98386ddeadbce72439b5fb491dd 100644 (file)
@@ -59,8 +59,8 @@ class DefaultDisplaySubscriber extends \Civi\Core\Service\AutoService implements
       throw new \CRM_Core_Exception("Entity name is required to get autocomplete default display.");
     }
     $idField = CoreUtil::getIdFieldName($entityName);
-    $labelField = CoreUtil::getInfoItem($entityName, 'label_field');
-    if (!$labelField) {
+    $searchFields = CoreUtil::getSearchFields($entityName);
+    if (!$searchFields) {
       throw new \CRM_Core_Exception("Entity $entityName has no default label field.");
     }
 
@@ -69,11 +69,17 @@ class DefaultDisplaySubscriber extends \Civi\Core\Service\AutoService implements
 
     $apiGet = Request::create($entityName, 'get', ['version' => 4]);
     $fields = $apiGet->entityFields();
-    $columns = [$labelField];
+    $columns = array_slice($searchFields, 0, 1);
     // Add grouping fields like "event_type_id" in the description
-    $grouping = (array) (CoreUtil::getCustomGroupExtends($entityName)['grouping'] ?? []);
+    $grouping = (array) (CoreUtil::getCustomGroupExtends($entityName)['grouping'] ?? ['financial_type_id']);
     foreach ($grouping as $fieldName) {
-      $columns[] = "$fieldName:label";
+      if (!empty($fields[$fieldName]['options']) && !in_array("$fieldName:label", $searchFields)) {
+        $columns[] = "$fieldName:label";
+      }
+    }
+    $statusField = $fields['status_id'] ?? $fields[strtolower($entityName) . '_status_id'] ?? NULL;
+    if (!empty($statusField['options']) && !in_array("{$statusField['name']}:label", $searchFields)) {
+      $columns[] = "{$statusField['name']}:label";
     }
     if (isset($fields['description'])) {
       $columns[] = 'description';
@@ -86,11 +92,15 @@ class DefaultDisplaySubscriber extends \Civi\Core\Service\AutoService implements
         'key' => $columnField,
       ];
     }
+    if (count($searchFields) > 1) {
+      $e->display['settings']['columns'][0]['rewrite'] = '[' . implode('] - [', $searchFields) . ']';
+    }
     // Include entity id on the second line
     $e->display['settings']['columns'][1] = [
       'type' => 'field',
-      'key' => $idField,
+      'key' => $columns[1] ?? $idField,
       'rewrite' => "#[$idField]" . (isset($columns[1]) ? " [$columns[1]]" : ''),
+      'empty_value' => "#[$idField]",
     ];
 
     // Default icons
@@ -152,8 +162,12 @@ class DefaultDisplaySubscriber extends \Civi\Core\Service\AutoService implements
    * @return array
    */
   protected static function getDefaultSort($entityName) {
-    $sortField = CoreUtil::getInfoItem($entityName, 'order_by') ?: CoreUtil::getInfoItem($entityName, 'label_field');
-    return $sortField ? [[$sortField, 'ASC']] : [];
+    $result = [];
+    $sortFields = (array) (CoreUtil::getInfoItem($entityName, 'order_by') ?: CoreUtil::getSearchFields($entityName));
+    foreach ($sortFields as $sortField) {
+      $result[] = [$sortField, 'ASC'];
+    }
+    return $result;
   }
 
 }
index 1ebf8660166fa45f5409fb2da5b276909eef5485..90f317a75a14e9aafa0ac765ce9385e802febf41 100644 (file)
@@ -131,7 +131,7 @@ class Admin {
   public static function getSchema(): array {
     $schema = [];
     $entities = Entity::get()
-      ->addSelect('name', 'title', 'title_plural', 'bridge_title', 'type', 'primary_key', 'description', 'label_field', 'icon', 'dao', 'bridge', 'ui_join_filters', 'searchable', 'order_by')
+      ->addSelect('name', 'title', 'title_plural', 'bridge_title', 'type', 'primary_key', 'description', 'label_field', 'search_fields', 'icon', 'dao', 'bridge', 'ui_join_filters', 'searchable', 'order_by')
       ->addWhere('searchable', '!=', 'none')
       ->addOrderBy('title_plural')
       ->setChain([
@@ -154,7 +154,7 @@ class Admin {
             'select' => ['name', 'title', 'label', 'description', 'type', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity', 'readonly', 'operators', 'suffixes', 'nullable'],
             'where' => [['deprecated', '=', FALSE], ['name', 'NOT IN', ['api_key', 'hash']]],
             'orderBy' => ['label'],
-          ]);
+          ])->indexBy('name');
         }
         catch (\CRM_Core_Exception $e) {
           \Civi::log()->warning('Entity could not be loaded', ['entity' => $entity['name']]);
@@ -170,6 +170,22 @@ class Admin {
           }
           $entity['fields'][] = $field;
         }
+        $defaultColumns = CoreUtil::getSearchFields($entity['name']);
+        // Add grouping fields like "event_type_id" + status_id + description if available
+        $grouping = (array) (CoreUtil::getCustomGroupExtends($entity['name'])['grouping'] ?? ['financial_type_id']);
+        foreach ($grouping as $fieldName) {
+          if (!empty($getFields[$fieldName]['options'])) {
+            $defaultColumns[] = "$fieldName:label";
+          }
+        }
+        $statusField = $getFields['status_id'] ?? $getFields[strtolower($entity['name']) . '_status_id'] ?? NULL;
+        if (!empty($statusField['options'])) {
+          $defaultColumns[] = $statusField['name'] . ':label';
+        }
+        if (isset($getFields['description'])) {
+          $defaultColumns[] = 'description';
+        }
+        $entity['default_columns'] = array_values(array_unique($defaultColumns));
         $params = $entity['get'][0];
         // Entity must support at least these params or it is too weird for search kit
         if (!array_diff(['select', 'where', 'orderBy', 'limit', 'offset'], array_keys($params))) {
@@ -194,22 +210,27 @@ class Admin {
     foreach ($schema as &$entity) {
       if ($entity['searchable'] !== 'bridge') {
         foreach (array_reverse($entity['fields'] ?? [], TRUE) as $index => $field) {
-          if (!empty($field['fk_entity']) && !$field['options'] && !$field['suffixes'] && !empty($schema[$field['fk_entity']]['label_field'])) {
-            $isCustom = strpos($field['name'], '.');
-            // Custom fields: append "Contact ID" etc. to original field label
-            if ($isCustom) {
-              $idField = array_column($schema[$field['fk_entity']]['fields'], NULL, 'name')['id'];
-              $entity['fields'][$index]['label'] .= ' ' . $idField['title'];
-            }
-            // DAO fields: use title instead of label since it represents the id (title usually ends in ID but label does not)
-            else {
-              $entity['fields'][$index]['label'] = $field['title'];
+          if (!empty($field['fk_entity']) && !$field['options'] && !$field['suffixes'] && !empty($schema[$field['fk_entity']]['search_fields'])) {
+            $labelFields = array_unique(array_merge($schema[$field['fk_entity']]['search_fields'], (array) ($schema[$field['fk_entity']]['label_field'] ?? [])));
+            foreach ($labelFields as $labelField) {
+              $isCustom = strpos($field['name'], '.');
+              // Custom fields: append "Contact ID" etc. to original field label
+              if ($isCustom) {
+                $idField = array_column($schema[$field['fk_entity']]['fields'], NULL, 'name')['id'];
+                $entity['fields'][$index]['label'] .= ' ' . $idField['title'];
+              }
+              // DAO fields: use title instead of label since it represents the id (title usually ends in ID but label does not)
+              else {
+                $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' => $labelField])[0] ?? NULL;
+              if ($newField) {
+                $newField['name'] = $field['name'] . '.' . $labelField;
+                $newField['label'] = $field['label'] . ' ' . $newField['label'];
+                array_splice($entity['fields'], $index + 1, 0, [$newField]);
+              }
             }
-            // 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'] = $field['name'] . '.' . $schema[$field['fk_entity']]['label_field'];
-            $newField['label'] = $field['label'] . ' ' . $newField['label'];
-            array_splice($entity['fields'], $index, 0, [$newField]);
           }
         }
         // Useful address fields (see ContactSchemaMapSubscriber)
index 34633bed32f6688d89117db3ddce11d4984bd39b..970a83a86d6078eef67db5e2271814a444a0d016 100644 (file)
@@ -61,7 +61,7 @@
       if (!this.savedSearch.id) {
         var defaults = {
           version: 4,
-          select: getDefaultSelect(),
+          select: searchMeta.getEntity(ctrl.savedSearch.api_entity).default_columns,
           orderBy: {},
           where: [],
         };
           params.push(condition);
         });
         ctrl.savedSearch.api_params.join.push(params);
-        if (entity.label_field && $scope.controls.joinType !== 'EXCLUDE') {
-          ctrl.savedSearch.api_params.select.push(join.alias + '.' + entity.label_field);
+        if (entity.search_fields && $scope.controls.joinType !== 'EXCLUDE') {
+          // Add columns for newly-joined entity
+          entity.search_fields.forEach((fieldName) => {
+            // Try to avoid adding duplicate columns
+            const simpleName = _.last(fieldName.split('.'));
+            if (!ctrl.savedSearch.api_params.select.join(',').includes(simpleName)) {
+              ctrl.savedSearch.api_params.select.push(join.alias + '.' + fieldName);
+            }
+          });
         }
         loadFieldOptions();
       }
       return {results: ctrl.getSelectFields()};
     };
 
-    // Sets the default select clause based on commonly-named fields
-    function getDefaultSelect() {
-      var entity = searchMeta.getEntity(ctrl.savedSearch.api_entity);
-      return _.transform(entity.fields, function(defaultSelect, field) {
-        if (field.name === 'id' || field.name === entity.label_field) {
-          defaultSelect.push(field.name);
-        }
-      });
-    }
-
     this.getAllFields = function(suffix, allowedTypes, disabledIf, topJoin) {
       disabledIf = disabledIf || _.noop;
       allowedTypes = allowedTypes || ['Field', 'Custom', 'Extra', 'Filter'];
index df55c2e731e6250d11a932a882c5ca29919b34c5..c8803ef76d138182b254412c0aff14dede89d0f1 100644 (file)
@@ -26,8 +26,9 @@
             sort: ctrl.parent.getDefaultSort(),
             columns: []
           };
-          var labelField = searchMeta.getEntity(ctrl.apiEntity).label_field;
-          _.each([labelField, 'description'], function(field) {
+          var searchFields = searchMeta.getEntity(ctrl.apiEntity).search_fields || [];
+          searchFields.push('description');
+          searchFields.forEach((field) => {
             if (_.includes(ctrl.parent.savedSearch.api_params.select, field)) {
               ctrl.display.settings.columns.push(searchMeta.fieldToColumn(field, {}));
             }
index 415b7919de818ee4801df28d0ebcb31db89c7071..d4fef2a47400f96104ebc8c93b7a82411cdecdfe 100644 (file)
@@ -135,11 +135,11 @@ class AutocompleteTest extends Api4TestBase implements HookInterface, Transactio
       ->execute();
 
     // Contacts will be returned in order by sort_name
-    $this->assertStringStartsWith('Both', $result[0]['label']);
+    $this->assertStringEndsWith('Both', $result[0]['label']);
     $this->assertEquals('fa-star', $result[0]['icon']);
-    $this->assertStringStartsWith('No icon', $result[1]['label']);
+    $this->assertStringEndsWith('No icon', $result[1]['label']);
     $this->assertEquals('fa-user', $result[1]['icon']);
-    $this->assertStringStartsWith('Starry', $result[2]['label']);
+    $this->assertStringEndsWith('Starry', $result[2]['label']);
     $this->assertEquals('fa-star', $result[2]['icon']);
   }
 
index 271e94cfcaab6fdf25851837ba1069dc9ea0c670..6a65f5fbb62f3e731ce2c55b901c2069057e41ae 100644 (file)
@@ -28,17 +28,24 @@ use api\v4\Api4TestBase;
 class EntityTest extends Api4TestBase {
 
   public function testEntityGet() {
+    \CRM_Core_BAO_ConfigSetting::enableAllComponents();
     $result = Entity::get(FALSE)
       ->execute()
       ->indexBy('name');
-    $this->assertArrayHasKey('Entity', $result,
-      "Entity::get missing itself");
+    $this->assertArrayHasKey('Entity', $result, "Entity::get missing itself");
 
     $this->assertEquals('CRM_Contact_DAO_Contact', $result['Contact']['dao']);
     $this->assertEquals(['DAOEntity'], $result['Contact']['type']);
     $this->assertEquals(['id'], $result['Contact']['primary_key']);
     // Contact icon fields
     $this->assertEquals(['contact_sub_type:icon', 'contact_type:icon'], $result['Contact']['icon_field']);
+    // Label fields
+    $this->assertEquals('display_name', $result['Contact']['label_field']);
+    $this->assertEquals('title', $result['Event']['label_field']);
+    // Search fields
+    $this->assertEquals(['sort_name'], $result['Contact']['search_fields']);
+    $this->assertEquals(['title'], $result['Event']['search_fields']);
+    $this->assertEquals(['contact_id.sort_name', 'event_id.title'], $result['Participant']['search_fields']);
   }
 
   public function testEntity() {
index 32dfa1a2d730e27dde722d6d6d783ac4b5d86b11..df479d0e6c3732404d02d68702c082c27ad0b089 100644 (file)
     <type>int unsigned</type>
     <html>
       <type>Select</type>
+      <label>Status</label>
     </html>
     <pseudoconstant>
       <optionGroupName>pledge_status</optionGroupName>