From 30d895a9fcea75216daf49121141110682972eb2 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Thu, 18 Feb 2021 16:02:25 -0500 Subject: [PATCH] SearchKit - Enable links for implicit joins Automatically create links for any implicitly joined entity linked through an fk field --- .../Civi/Api4/Action/SearchDisplay/Run.php | 17 +++++++++- ext/search/Civi/Search/Admin.php | 2 +- ext/search/ang/crmSearchAdmin.module.js | 4 +-- .../crmSearchAdmin.component.js | 33 ++++++++++++++----- .../crmSearchAdminLinkSelect.component.js | 22 +++++++++++++ 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php index 8028fc4d2e..1753ca099e 100644 --- a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php @@ -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'; diff --git a/ext/search/Civi/Search/Admin.php b/ext/search/Civi/Search/Admin.php index 3ef074c36b..4a58faf12b 100644 --- a/ext/search/Civi/Search/Admin.php +++ b/ext/search/Civi/Search/Admin.php @@ -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']) { diff --git a/ext/search/ang/crmSearchAdmin.module.js b/ext/search/ang/crmSearchAdmin.module.js index 6839000b48..6db8c6ba14 100644 --- a/ext/search/ang/crmSearchAdmin.module.js +++ b/ext/search/ang/crmSearchAdmin.module.js @@ -148,7 +148,7 @@ if (dotSplit.length === 2) { field = _.find(getEntity(entityName).fields, {name: dotSplit[0] + '.' + name}); if (field) { - field.entity = entityName; + field.baseEntity = entityName; return {field: field}; } } @@ -161,7 +161,7 @@ field = _.find(getEntity(join.bridge).fields, {name: name}); } if (field) { - field.entity = entityName; + field.baseEntity = entityName; return {field: field, join: join}; } } diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js b/ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js index b2c0e3ab25..e089a3141f 100644 --- a/ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js +++ b/ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js @@ -428,7 +428,19 @@ 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)) { @@ -635,7 +647,7 @@ 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 '' + formatFieldValue(info.field, value) + ''; @@ -652,12 +664,17 @@ 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) { diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js index 1a63886985..5a2a8ab853 100644 --- a/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js +++ b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js @@ -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]); @@ -24,6 +29,23 @@ 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; } -- 2.25.1