SearchKit - Fix links to implicitly joined entities
authorColeman Watts <coleman@civicrm.org>
Fri, 23 Apr 2021 18:23:20 +0000 (14:23 -0400)
committerColeman Watts <coleman@civicrm.org>
Sat, 24 Apr 2021 22:27:18 +0000 (18:27 -0400)
This improves link construction to propery replace tokens in explicit joins and implicit joins.

ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php
ext/search_kit/Civi/Search/Admin.php
ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js
ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js

index 22725aee4d6c325c91daaf804080575a65fec95e..9bc29e7f4fcadc3b12d7eb2c600466892f53e09e 100644 (file)
@@ -113,36 +113,7 @@ class Run extends \Civi\Api4\Generic\AbstractAction {
         $apiParams['limit'] = $settings['limit'] ?? NULL;
         $apiParams['offset'] = $page ? $apiParams['limit'] * ($page - 1) : 0;
         $apiParams['orderBy'] = $this->getOrderByFromSort();
-
-        // Select the ids of implicitly joined entities (helps with displaying links)
-        foreach ($apiParams['select'] as $fieldName) {
-          if (strstr($fieldName, '.') && !strstr($fieldName, ' AS ') && !strstr($fieldName, ':')) {
-            $idField = substr($fieldName, 0, strrpos($fieldName, '.')) . '_id';
-            $prefix = '';
-            $id = $idField;
-            if (strstr($id, '.')) {
-              [$prefix, $idField] = explode(',', $id);
-              $prefix .= '.';
-            }
-            if (!in_array($idField, $apiParams['select']) && !empty($this->getField($idField)['fk_entity']) && !$this->canAggregate($id, $prefix)) {
-              $apiParams['select'][] = $idField;
-            }
-          }
-        }
-        // Select the ids of explicitly joined entities (helps with displaying links)
-        foreach ($apiParams['join'] ?? [] as $join) {
-          $joinEntity = explode(' AS ', $join[0])[1];
-          $idField = $joinEntity . '.id';
-          if (!in_array($idField, $apiParams['select']) && !$this->canAggregate('id', $joinEntity . '.')) {
-            $apiParams['select'][] = $idField;
-          }
-        }
-        // Select value fields for in-place editing
-        foreach ($settings['columns'] ?? [] as $column) {
-          if (isset($column['editable']['value']) && !in_array($column['editable']['value'], $apiParams['select'])) {
-            $apiParams['select'][] = $column['editable']['value'];
-          }
-        }
+        $this->augmentSelectClause($apiParams);
     }
 
     $this->applyFilters();
@@ -379,4 +350,44 @@ class Run extends \Civi\Api4\Generic\AbstractAction {
     return $this->_afform;
   }
 
+  /**
+   * Adds additional useful fields to the select clause
+   *
+   * @param array $apiParams
+   */
+  private function augmentSelectClause(&$apiParams): void {
+    $joinAliases = [];
+    // Select the ids 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;
+      }
+    }
+    // Select the ids of implicitly joined entities (helps with displaying links)
+    foreach ($apiParams['select'] as $fieldName) {
+      if (strstr($fieldName, '.') && !strstr($fieldName, ' AS ') && !strstr($fieldName, ':')) {
+        $idFieldName = $fieldNameWithoutPrefix = substr($fieldName, 0, strrpos($fieldName, '.'));
+        $idField = $this->getField($idFieldName);
+        $explicitJoin = '';
+        if (strstr($idFieldName, '.')) {
+          [$prefix, $fieldNameWithoutPrefix] = explode('.', $idFieldName, 2);
+          if (in_array($prefix, $joinAliases, TRUE)) {
+            $explicitJoin = $prefix . '.';
+          }
+        }
+        if (!in_array($idFieldName, $apiParams['select']) && !empty($idField['fk_entity']) && !$this->canAggregate($fieldNameWithoutPrefix, $explicitJoin)) {
+          $apiParams['select'][] = $idFieldName;
+        }
+      }
+    }
+    // Select value fields for in-place editing
+    foreach ($this->display['settings']['columns'] ?? [] as $column) {
+      if (isset($column['editable']['value']) && !in_array($column['editable']['value'], $apiParams['select'])) {
+        $apiParams['select'][] = $column['editable']['value'];
+      }
+    }
+  }
+
 }
index 5a28f6d6f054caea5f90eddc86250112017ef9be..99fd42f74c89f9e4330341d2f35c6feaf1373dfb 100644 (file)
@@ -87,7 +87,6 @@ class Admin {
       ->setChain([
         'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']],
       ])->execute();
-    $getFields = ['name', 'title', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity', 'readonly'];
     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']) {
@@ -116,11 +115,15 @@ class Admin {
             'action' => $action,
           ];
         }
-        $entity['fields'] = (array) civicrm_api4($entity['name'], 'getFields', [
-          'select' => $getFields,
+        $getFields = civicrm_api4($entity['name'], 'getFields', [
+          'select' => ['name', 'title', 'label', 'description', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity', 'readonly'],
           'where' => [['name', 'NOT IN', ['api_key', 'hash']]],
           'orderBy' => ['label'],
         ]);
+        foreach ($getFields as $field) {
+          $field['fieldName'] = $field['name'];
+          $entity['fields'][] = $field;
+        }
         $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))) {
@@ -131,13 +134,13 @@ class Admin {
       }
     }
     // Add in FK fields for implicit joins
-    // For example, add a `campaign.title` field to the Contribution entity
+    // 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)) {
         foreach (array_reverse($entity['fields'], TRUE) as $index => $field) {
           if (!empty($field['fk_entity']) && !$field['options'] && !empty($schema[$field['fk_entity']]['label_field'])) {
             $isCustom = strpos($field['name'], '.');
-            // Custom fields: append "ID" to original field label
+            // Custom fields: append "Contact ID" to original field label
             if ($isCustom) {
               $entity['fields'][$index]['label'] .= ' ' . E::ts('Contact ID');
             }
@@ -147,9 +150,7 @@ class Admin {
             }
             // 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];
-            // Due to string manipulation in \Civi\Api4\Service\Schema\SchemaMapBuilder::addJoins()
-            $alias = $isCustom ? $field['name'] : str_replace('_id', '', $field['name']);
-            $newField['name'] = $alias . '.' . $schema[$field['fk_entity']]['label_field'];
+            $newField['name'] = $field['name'] . '.' . $schema[$field['fk_entity']]['label_field'];
             $newField['label'] = $field['label'] . ' ' . $newField['label'];
             array_splice($entity['fields'], $index, 0, [$newField]);
           }
index 8c1934f6b4e24b1a446109abbac5e65ea08a1dc9..84eef3d1e8951d04dd06962b46a903968da7895f 100644 (file)
         _.each(params.select, function(fieldName) {
           if (_.includes(fieldName, '.') && !_.includes(fieldName, ' AS ')) {
             var info = searchMeta.parseExpr(fieldName);
-            if (info.field && !info.suffix && !info.fn && (info.field.entity !== info.field.baseEntity)) {
-              var idField = fieldName.substr(0, fieldName.lastIndexOf('.')) + '_id';
+            if (info.field && !info.suffix && !info.fn && (info.field.name !== info.field.fieldName)) {
+              var idField = fieldName.substr(0, fieldName.lastIndexOf('.'));
               if (!_.includes(params.select, idField) && !ctrl.canAggregate(idField)) {
                 params.select.push(idField);
               }
           return value;
         }
         // Output user-facing name/label fields as a link, if possible
-        if (info.field && _.last(info.field.name.split('.')) === searchMeta.getEntity(info.field.entity).label_field && !info.fn && typeof value === 'string') {
+        if (info.field && info.field.fieldName === searchMeta.getEntity(info.field.entity).label_field && !info.fn && typeof value === 'string') {
           var link = getEntityUrl(row, info);
           if (link) {
             return '<a href="' + _.escape(link.url) + '" title="' + _.escape(link.title) + '">' + formatFieldValue(info.field, value) + '</a>';
           // Replace tokens in the path (e.g. [id])
           var tokens = path.match(/\[\w*]/g) || [],
             prefix = info.prefix;
-          // For implicit join fields
-          if (info.field.name.split('.').length > 1) {
-            prefix += info.field.name.split('.')[0] + '_';
-          }
           var replacements = _.transform(tokens, function(replacements, token) {
-            var fieldName = prefix + token.slice(1, token.length - 1);
+            var fieldName = token.slice(1, token.length - 1);
+            // For implicit join fields
+            if (fieldName === 'id' && info.field.name !== info.field.fieldName) {
+              fieldName = info.field.name.substr(0, info.field.name.lastIndexOf('.'));
+            }
+            fieldName = prefix + fieldName;
             if (row[fieldName]) {
               replacements.push(row[fieldName]);
             }
index 635af8e49c660fcf85c0534e7cba41f2f72398b6..b19d59e4472f191458eb7af1a011589fa7235b6c 100644 (file)
         },
       };
 
+      // Drag-n-drop settings for reordering columns
       this.sortableOptions = {
         connectWith: '.crm-search-admin-edit-columns',
-        containment: '.crm-search-admin-edit-columns-wrapper'
+        containment: '.crm-search-admin-edit-columns-wrapper',
+        cancel: 'input,textarea,button,select,option,a,label'
       };
 
       this.styles = CRM.crmSearchAdmin.styles;
         var info = searchMeta.parseExpr(col.key),
           value = col.key.split(':')[0];
         // If field is an implicit join, use the original fk field
-        if (info.field.entity !== info.field.baseEntity) {
-          value = value.substr(0, value.indexOf('.')) + '_id';
+        if (info.field.name !== info.field.fieldName) {
+          value = value.substr(0, value.lastIndexOf('.'));
           info = searchMeta.parseExpr(value);
         }
         col.editable = {
         _.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.entity !== info.field.baseEntity)) {
-              var idField = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')) + '_id';
+            if (info.field && !info.suffix && !info.fn && (info.field.fk_entity || info.field.name !== info.field.fieldName)) {
+              var idField = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.'));
               if (!ctrl.crmSearchAdmin.canAggregate(idField)) {
                 var joinEntity = searchMeta.getEntity(info.field.fk_entity || info.field.entity);
                 _.each((joinEntity || {}).paths, function(path) {
             }
           }
         });
-        return links;
+        return _.uniq(links, 'path');
       }
 
       this.pickIcon = function(model, key) {