SearchKit - Improve icon handling
authorColeman Watts <coleman@civicrm.org>
Tue, 1 Nov 2022 02:10:33 +0000 (22:10 -0400)
committerColeman Watts <coleman@civicrm.org>
Tue, 1 Nov 2022 11:17:05 +0000 (07:17 -0400)
Gives the ability to have "fallback" icons, e.g. choosing the
icon for contact_sub_type with a fallback to contact_type.

Civi/Api4/Utils/FormattingUtil.php
ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php
ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php

index 8216a4c328ac39f9ca756c95094935943a814913..59c2f1e8df7feb13cfbc781aa6315b791c27904c 100644 (file)
@@ -243,6 +243,12 @@ class FormattingUtil {
           $fieldOptions = self::getPseudoconstantList($field, $fieldName, $result, $action);
           $dataType = NULL;
         }
+        // Store contact_type value before replacing pseudoconstant (e.g. transforming it to contact_type:label)
+        // Used by self::contactFieldsToRemove below
+        if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') {
+          $prefix = strrpos($fieldName, '.');
+          $contactTypePaths[$prefix ? substr($fieldName, 0, $prefix + 1) : ''] = $value;
+        }
         if ($fieldExpr->supportsExpansion) {
           if (!empty($field['serialize']) && is_string($value)) {
             $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']);
@@ -251,11 +257,6 @@ class FormattingUtil {
             $value = self::replacePseudoconstant($fieldOptions, $value);
           }
         }
-        // Keep track of contact types for self::contactFieldsToRemove
-        if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') {
-          $prefix = strrpos($fieldName, '.');
-          $contactTypePaths[$prefix ? substr($fieldName, 0, $prefix + 1) : ''] = $value;
-        }
         $result[$key] = self::convertDataType($value, $dataType);
       }
       // Remove inapplicable contact fields
index 25034eb1bea4e409f1060273a3b153e54aceb2e2..66cf56ba95cd19531081fdfdc9827a266b973d3a 100644 (file)
@@ -319,7 +319,11 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
   }
 
   /**
-   * Evaluates conditional style rules
+   * Add icons to a column
+   *
+   * Note: Only one icon is allowed per side (left/right).
+   * If more than one per side is given, latter icons are treated as fallbacks
+   * and only shown if prior ones are missing.
    *
    * @param array{icon: string, field: string, if: array, side: string}[] $icons
    * @param array $data
@@ -327,20 +331,23 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
    */
   protected function getColumnIcons(array $icons, array $data) {
     $result = [];
-    foreach ($icons as $icon) {
+    // Reverse order so latter icons become fallbacks and earlier ones take priority
+    foreach (array_reverse($icons) as $icon) {
       $iconClass = $icon['icon'] ?? NULL;
-      if (!$iconClass && !empty($icon['field'])) {
-        $iconClass = $data[$icon['field']] ?? NULL;
+      if (!$iconClass && !empty($icon['field']) && !empty($data[$icon['field']])) {
+        // Icon field may be multivalued e.g. contact_sub_type
+        $iconClass = \CRM_Utils_Array::first(array_filter((array) $data[$icon['field']]));
       }
       if ($iconClass) {
         $condition = $this->getRuleCondition($icon['if'] ?? []);
         if (!is_null($condition[0]) && !(self::filterCompare($data, $condition))) {
           continue;
         }
-        $result[] = ['class' => $iconClass, 'side' => $icon['side'] ?? 'left'];
+        $side = $icon['side'] ?? 'left';
+        $result[$side] = ['class' => $iconClass, 'side' => $side];
       }
     }
-    return $result;
+    return array_values($result);
   }
 
   /**
index 50556cafac83f6b3555e127ee4b51d31eb38c8dd..9791a96a7472cb6b23d2a87da646304ea43f0e10 100644 (file)
@@ -1066,7 +1066,7 @@ class SearchRunTest extends Api4TestBase implements TransactionalInterface {
     // Icon based on activity type
     $this->assertEquals([['class' => 'fa-slideshare', 'side' => 'left']], $result[0]['columns'][0]['icons']);
     // Activity type icon + conditional icon based on status
-    $this->assertEquals([['class' => 'fa-phone', 'side' => 'left'], ['class' => 'fa-star', 'side' => 'right']], $result[1]['columns'][0]['icons']);
+    $this->assertEquals([['class' => 'fa-star', 'side' => 'right'], ['class' => 'fa-phone', 'side' => 'left']], $result[1]['columns'][0]['icons']);
   }
 
   /**
@@ -1452,4 +1452,86 @@ class SearchRunTest extends Api4TestBase implements TransactionalInterface {
     $this->assertEquals(3, $result[2]['columns'][1]['val']);
   }
 
+  public function testContactTypeIcons(): void {
+    $this->createTestRecord('ContactType', [
+      'label' => 'Star',
+      'name' => 'Star',
+      'parent_id:name' => 'Individual',
+      'icon' => 'fa-star',
+    ]);
+    $this->createTestRecord('ContactType', [
+      'label' => 'None',
+      'name' => 'None',
+      'parent_id:name' => 'Individual',
+      'icon' => NULL,
+    ]);
+
+    $lastName = uniqid(__FUNCTION__);
+    $sampleData = [
+      [
+        'first_name' => 'Starry',
+        'contact_sub_type' => ['Star'],
+      ],
+      [
+        'first_name' => 'No icon',
+        'contact_sub_type' => ['None'],
+      ],
+      [
+        'first_name' => 'Both',
+        'contact_sub_type' => ['None', 'Star'],
+      ],
+    ];
+    $records = $this->saveTestRecords('Contact', [
+      'records' => $sampleData,
+      'defaults' => ['last_name' => $lastName],
+    ]);
+
+    $params = [
+      'checkPermissions' => FALSE,
+      'return' => 'page:1',
+      'savedSearch' => [
+        'api_entity' => 'Contact',
+        'api_params' => [
+          'version' => 4,
+          'select' => ['first_name', 'last_name'],
+        ],
+      ],
+      'display' => [
+        'type' => 'table',
+        'label' => '',
+        'settings' => [
+          'actions' => TRUE,
+          'pager' => [],
+          'columns' => [
+            [
+              'key' => 'first_name',
+              'label' => 'First',
+              'dataType' => 'String',
+              'type' => 'field',
+              'icons' => [
+                ['field' => 'contact_sub_type:icon'],
+                ['field' => 'contact_type:icon'],
+              ],
+            ],
+          ],
+          'sort' => [
+            ['sort_name', 'ASC'],
+          ],
+        ],
+      ],
+      'filters' => ['last_name' => $lastName],
+    ];
+
+    $result = civicrm_api4('SearchDisplay', 'run', $params);
+    $this->assertCount(3, $result);
+
+    // Contacts will be returned in order by sort_name
+    $this->assertEquals('Both', $result[0]['columns'][0]['val']);
+    $this->assertEquals('fa-star', $result[0]['columns'][0]['icons'][0]['class']);
+    $this->assertEquals('No icon', $result[1]['columns'][0]['val']);
+    $this->assertEquals('fa-user', $result[1]['columns'][0]['icons'][0]['class']);
+    $this->assertEquals('Starry', $result[2]['columns'][0]['val']);
+    $this->assertEquals('fa-star', $result[2]['columns'][0]['icons'][0]['class']);
+  }
+
 }