SearchKit - Add pseudo-fields for row number and current user
authorColeman Watts <coleman@civicrm.org>
Sat, 11 Sep 2021 14:56:57 +0000 (10:56 -0400)
committerColeman Watts <coleman@civicrm.org>
Wed, 15 Sep 2021 19:52:49 +0000 (15:52 -0400)
ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php
ext/search_kit/Civi/Search/Admin.php
ext/search_kit/ang/crmSearchAdmin.module.js
ext/search_kit/ang/crmSearchAdmin/compose.html
ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js
ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTokenSelect.component.js
ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js
ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html

index f1578097d21ce47a4096c8fc22efbe12911d1fa1..9505e452f0664ea4589df50bb247dc27670d71e2 100644 (file)
@@ -121,20 +121,43 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
       $select[$expr->getAlias()] = $item;
     }
     $formatted = [];
-    foreach ($result as $data) {
+    foreach ($result as $index => $data) {
       $row = [];
       foreach ($select as $key => $item) {
-        $raw = $data[$key] ?? NULL;
-        $row[$key] = [
-          'raw' => $raw,
-          'view' => $this->formatViewValue($item['dataType'], $raw),
-        ];
+        $row[$key] = $this->getValue($key, $data, $item['dataType'], $index);
       }
       $formatted[] = $row;
     }
     return $formatted;
   }
 
+  /**
+   * @param $key
+   * @param $data
+   * @param $dataType
+   * @param $index
+   * @return array
+   */
+  private function getValue($key, $data, $dataType, $index) {
+    // Get value from api result unless this is a pseudo-field which gets a calculated value
+    switch ($key) {
+      case 'result_row_num':
+        $raw = $index + 1 + ($this->savedSearch['api_params']['offset'] ?? 0);
+        break;
+
+      case 'user_contact_id':
+        $raw = \CRM_Core_Session::getLoggedInContactID();
+        break;
+
+      default:
+        $raw = $data[$key] ?? NULL;
+    }
+    return [
+      'raw' => $raw,
+      'view' => $this->formatViewValue($dataType, $raw),
+    ];
+  }
+
   /**
    * Returns field definition for a given field or NULL if not found
    * @param $fieldName
@@ -467,4 +490,33 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
     return $this->_afform;
   }
 
+  /**
+   * Extra calculated fields provided by SearchKit
+   * @return array[]
+   */
+  public static function getPseudoFields(): array {
+    return [
+      [
+        'name' => 'result_row_num',
+        'fieldName' => 'result_row_num',
+        'title' => ts('Row Number'),
+        'label' => ts('Row Number'),
+        'description' => ts('Index of each row, starting from 1 on the first page'),
+        'type' => 'Pseudo',
+        'data_type' => 'Integer',
+        'readonly' => TRUE,
+      ],
+      [
+        'name' => 'user_contact_id',
+        'fieldName' => 'result_row_num',
+        'title' => ts('Current User ID'),
+        'label' => ts('Current User ID'),
+        'description' => ts('Contact ID of the current user if logged in'),
+        'type' => 'Pseudo',
+        'data_type' => 'Integer',
+        'readonly' => TRUE,
+      ],
+    ];
+  }
+
 }
index 9e557cbc855565b483c8d41ac7c4b05dfc8ac38a..9036b1c1b510be670a653a0999bb53a141d4125a 100644 (file)
@@ -11,6 +11,7 @@
 
 namespace Civi\Search;
 
+use Civi\Api4\Action\SearchDisplay\AbstractRunAction;
 use Civi\Api4\Tag;
 use CRM_Search_ExtensionUtil as E;
 
@@ -29,6 +30,7 @@ class Admin {
     return [
       'schema' => self::addImplicitFKFields($schema),
       'joins' => self::getJoins($schema),
+      'pseudoFields' => AbstractRunAction::getPseudoFields(),
       'operators' => \CRM_Utils_Array::makeNonAssociative(self::getOperators()),
       'functions' => \CRM_Api4_Page_Api4Explorer::getSqlFunctions(),
       'displayTypes' => Display::getDisplayTypes(['id', 'name', 'label', 'description', 'icon']),
index 867d6952b98bb8ff222532347d76f2dcc22f4db4..207c10670d300b9aa8d0ce74de8cea505703f69c 100644 (file)
         if (!field && join && join.bridge) {
           field = _.find(getEntity(join.bridge).fields, {name: name});
         }
+        // Might be a pseudoField
+        if (!field) {
+          field = _.cloneDeep(_.find(CRM.crmSearchAdmin.pseudoFields, {name: name}));
+        }
         if (field) {
           field.baseEntity = entityName;
           return {field: field, join: join};
index 64b1694b268f8821245b2ea1f0a327bd81f0a91c..6bcf443b9148e60baf06b4b84bd4ff792bc354aa 100644 (file)
@@ -41,7 +41,7 @@
           {{:: ts('Field Transformations') }}
         </legend>
         <div ng-if="!!controls.showFunctions">
-          <fieldset ng-repeat="col in $ctrl.savedSearch.api_params.select">
+          <fieldset ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-if="!$ctrl.isPseudoField(col)">
             <crm-search-function expr="$ctrl.savedSearch.api_params.select[$index]"></crm-search-function>
           </fieldset>
         </div>
index ebc00da41f5d8b3ba599b446aaa6d9d64f0298dd..f06792b4dd88098c96470f4ebf7d70001ef09d50 100644 (file)
@@ -6,7 +6,7 @@
       savedSearch: '<'
     },
     templateUrl: '~/crmSearchAdmin/crmSearchAdmin.html',
-    controller: function($scope, $element, $location, $timeout, crmApi4, dialogService, searchMeta, formatForSelect2) {
+    controller: function($scope, $element, $location, $timeout, crmApi4, dialogService, searchMeta) {
       var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
         ctrl = this,
         fieldsForJoinGetters = {};
 
       this.getAllFields = function(suffix, allowedTypes, disabledIf, topJoin) {
         disabledIf = disabledIf || _.noop;
-        function formatFields(entityName, join) {
+
+        function formatEntityFields(entityName, join) {
           var prefix = join ? join.alias + '.' : '',
             result = [];
 
-          function addFields(fields) {
-            _.each(fields, function(field) {
-              var item = {
-                id: prefix + field.name + (field.options ? suffix : ''),
-                text: field.label,
-                description: field.description
-              };
-              if (disabledIf(item.id)) {
-                item.disabled = true;
-              }
-              if (!allowedTypes || _.includes(allowedTypes, field.type)) {
-                result.push(item);
-              }
-            });
-          }
-
           // Add extra searchable fields from bridge entity
           if (join && join.bridge) {
-            addFields(_.filter(searchMeta.getEntity(join.bridge).fields, function(field) {
+            formatFields(_.filter(searchMeta.getEntity(join.bridge).fields, function(field) {
               return (field.name !== 'id' && field.name !== 'entity_id' && field.name !== 'entity_table' && !field.fk_entity);
-            }));
+            }), result, prefix);
           }
 
-          addFields(searchMeta.getEntity(entityName).fields);
+          formatFields(searchMeta.getEntity(entityName).fields, result, prefix);
+          return result;
+        }
+
+        function formatFields(fields, result, prefix) {
+          prefix = typeof prefix === 'undefined' ? '' : prefix;
+          _.each(fields, function(field) {
+            var item = {
+              id: prefix + field.name + (field.options ? suffix : ''),
+              text: field.label,
+              description: field.description
+            };
+            if (disabledIf(item.id)) {
+              item.disabled = true;
+            }
+            if (!allowedTypes || _.includes(allowedTypes, field.type)) {
+              result.push(item);
+            }
+          });
           return result;
         }
 
             text: joinInfo.label,
             description: joinInfo.description,
             icon: joinEntity.icon,
-            children: formatFields(joinEntity.name, joinInfo)
+            children: formatEntityFields(joinEntity.name, joinInfo)
           });
         }
 
         result.push({
           text: mainEntity.title_plural,
           icon: mainEntity.icon,
-          children: formatFields(ctrl.savedSearch.api_entity)
+          children: formatEntityFields(ctrl.savedSearch.api_entity)
         });
+
+        // Include SearchKit's pseudo-fields if specifically requested
+        if (allowedTypes && _.includes(allowedTypes, 'Pseudo')) {
+          result.push({
+            text: ts('Extra'),
+            icon: 'fa-gear',
+            children: formatFields(CRM.crmSearchAdmin.pseudoFields, [])
+          });
+        }
+
         _.each(joinEntities, addJoin);
         return result;
       };
         });
       };
 
+      this.isPseudoField = function(name) {
+        return _.findIndex(CRM.crmSearchAdmin.pseudoFields, {name: name}) >= 0;
+      };
+
       /**
        * Fetch pseudoconstants for main entity + joined entities
        *
         _.each(ctrl.savedSearch.api_params.select, function(fieldName) {
           if (!_.includes(fieldName, ' AS ')) {
             var info = searchMeta.parseExpr(fieldName);
-            if (info.field && !info.suffix && !info.fn && (info.field.fk_entity || info.field.name !== info.field.fieldName)) {
+            if (info.field && !info.suffix && !info.fn && info.field.type === 'Field' && (info.field.fk_entity || info.field.name !== info.field.fieldName)) {
               var idFieldName = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')),
                 idField = searchMeta.parseExpr(idFieldName).field;
               if (!ctrl.canAggregate(idFieldName)) {
index f364ebf66df1a2722716366819cd65a6c827e7e7..b1f69ba2f2e13e92bc3d26a2db4ccbb51c3e855d 100644 (file)
@@ -27,7 +27,7 @@
       };
 
       this.getTokens = function() {
-        var allFields = ctrl.admin.getAllFields(ctrl.suffix || '', ['Field', 'Custom', 'Extra']);
+        var allFields = ctrl.admin.getAllFields(ctrl.suffix || '', ['Field', 'Custom', 'Extra', 'Pseudo']);
         _.eachRight(ctrl.admin.savedSearch.api_params.select, function(fieldName) {
           allFields.unshift({
             id: fieldName,
index 0a1f994fc75627c4092233f923e985883fedc84e..2329ade4eaa8cf56c4d3f336f25c2166dc193e53 100644 (file)
       };
 
       $scope.fieldsForSelect = function() {
-        return {results: ctrl.crmSearchAdmin.getAllFields(':label', ['Field', 'Custom', 'Extra'], function(key) {
+        return {results: ctrl.crmSearchAdmin.getAllFields(':label', ['Field', 'Custom', 'Extra', 'Pseudo'], function(key) {
             return _.contains(ctrl.search.api_params.select, key);
           })
         };
index 56614f2c3d456d5dbbaf6b5bfc67fa9304d57699..ab98ae853453eaed0b92f48cfaa51b7977c99da4 100644 (file)
@@ -19,7 +19,7 @@
           </span>
         </th>
         <th class="form-inline text-right">
-          <input class="form-control crm-action-menu fa-plus"
+          <input class="form-control crm-action-menu fa-plus collapsible-optgroups"
                  crm-ui-select="::{data: fieldsForSelect, placeholder: ts('Add'), width: '80px', containerCss: {minWidth: '80px'}, dropdownCss: {width: '300px'}}"
                  on-crm-ui-select="addColumn(selection)" >
         </th>