SearchKit - Enable links for implicit joins
authorColeman Watts <coleman@civicrm.org>
Thu, 18 Feb 2021 21:02:25 +0000 (16:02 -0500)
committerColeman Watts <coleman@civicrm.org>
Thu, 18 Feb 2021 21:02:25 +0000 (16:02 -0500)
Automatically create links for any implicitly joined entity linked through an fk field

ext/search/Civi/Api4/Action/SearchDisplay/Run.php
ext/search/Civi/Search/Admin.php
ext/search/ang/crmSearchAdmin.module.js
ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js
ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js

index 8028fc4d2edef8108fcfec83120bf8610bb69d2b..1753ca099e912d2869cb31586fd3f14f046393da 100644 (file)
@@ -94,7 +94,22 @@ class Run extends \Civi\Api4\Generic\AbstractAction {
         $apiParams['offset'] = $page ? $apiParams['limit'] * ($page - 1) : 0;
         $apiParams['orderBy'] = $this->getOrderByFromSort();
 
-        // Select the ids of joined entities (helps with displaying links)
+        // 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';
index 3ef074c36b7e4fe265b58751f5719ba6d3dbe52a..4a58faf12b85dbafdf4eeb0c63638744820650b8 100644 (file)
@@ -75,7 +75,7 @@ class Admin {
       ->setChain([
         'get' => ['$name', 'getActions', ['where' => [['name', '=', 'get']]], ['params']],
       ])->execute();
-    $getFields = ['name', 'title', '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', 'entity', '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']) {
index 6839000b48de52de0611799efc3af0bf66b78fdb..6db8c6ba143f115cc525537f6dbe070fe01bae96 100644 (file)
         if (dotSplit.length === 2) {
           field = _.find(getEntity(entityName).fields, {name: dotSplit[0] + '.' + name});
           if (field) {
-            field.entity = entityName;
+            field.baseEntity = entityName;
             return {field: field};
           }
         }
           field = _.find(getEntity(join.bridge).fields, {name: name});
         }
         if (field) {
-          field.entity = entityName;
+          field.baseEntity = entityName;
           return {field: field, join: join};
         }
       }
index b2c0e3ab2542dfde09e1caf9e77e8f4e7946ea61..e089a3141fdfd28a43e665768d5e621182c6e5da 100644 (file)
       function _loadResultsCallback() {
         // Multiply limit to read 2 pages at once & save ajax requests
         var params = _.merge(_.cloneDeep(ctrl.savedSearch.api_params), {debug: true, limit: ctrl.limit * 2});
-        // Select the ids of joined entities (helps with displaying links)
+        // Select the ids of implicitly joined entities (helps with displaying links)
+        _.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 (!_.includes(params.select, idField) && !ctrl.canAggregate(idField)) {
+                params.select.push(idField);
+              }
+            }
+          }
+        });
+        // Select the ids of explicitly joined entities (helps with displaying links)
         _.each(params.join, function(join) {
           var idField = join[0].split(' AS ')[1] + '.id';
           if (!_.includes(params.select, idField) && !ctrl.canAggregate(idField)) {
           return value;
         }
         // Output user-facing name/label fields as a link, if possible
-        if (info.field && info.field.name === searchMeta.getEntity(info.field.entity).label_field && !info.fn && typeof value === 'string') {
+        if (info.field && _.last(info.field.name.split('.')) === 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>';
         if (path) {
           // Replace tokens in the path (e.g. [id])
           var tokens = path.match(/\[\w*]/g) || [],
-            replacements = _.transform(tokens, function(replacements, token) {
-              var fieldName = info.prefix + token.slice(1, token.length - 1);
-              if (row[fieldName]) {
-                replacements.push(row[fieldName]);
-              }
-            });
+            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);
+            if (row[fieldName]) {
+              replacements.push(row[fieldName]);
+            }
+          });
           // Only proceed if the row contains all the necessary data to resolve tokens
           if (tokens.length === replacements.length) {
             _.each(tokens, function(token, index) {
index 1a63886985f6c450ec95505b3af0e3b9f7bf1645..5a2a8ab8533defe3e19146a23f68f8ad6fdd9a56 100644 (file)
@@ -7,6 +7,9 @@
       apiEntity: '<',
       apiParams: '<'
     },
+    require: {
+      crmSearchAdmin: '^^crmSearchAdmin'
+    },
     templateUrl: '~/crmSearchAdmin/crmSearchAdminLinkSelect.html',
     controller: function ($scope, $element, $timeout, searchMeta) {
       var ts = $scope.ts = CRM.ts(),
@@ -14,7 +17,9 @@
 
       // Return all possible links to main entity or join entities
       function getLinks() {
+        // Links to main entity
         var links = _.cloneDeep(searchMeta.getEntity(ctrl.apiEntity).paths || []);
+        // Links to explicitly joined entities
         _.each(ctrl.apiParams.join, function(join) {
           var joinName = join[0].split(' AS '),
             joinEntity = searchMeta.getEntity(joinName[0]);
             links.push(link);
           });
         });
+        // Links to implicit joins
+        _.each(ctrl.crmSearchAdmin.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 (!ctrl.crmSearchAdmin.canAggregate(idField)) {
+                var joinEntity = searchMeta.getEntity(info.field.fk_entity || info.field.entity);
+                _.each(joinEntity.paths, function(path) {
+                  var link = _.cloneDeep(path);
+                  link.path = link.path.replace(/\[id/g, '[' + idField);
+                  links.push(link);
+                });
+              }
+            }
+          }
+        });
         return links;
       }