From 0458a6137e9588b5d8a249a09f915a1a4cb13275 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Fri, 23 Apr 2021 14:23:20 -0400 Subject: [PATCH] SearchKit - Fix links to implicitly joined entities This improves link construction to propery replace tokens in explicit joins and implicit joins. --- .../Civi/Api4/Action/SearchDisplay/Run.php | 71 +++++++++++-------- ext/search_kit/Civi/Search/Admin.php | 17 ++--- .../crmSearchAdmin.component.js | 17 ++--- .../crmSearchAdminDisplay.component.js | 14 ++-- 4 files changed, 67 insertions(+), 52 deletions(-) diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php index 22725aee4d..9bc29e7f4f 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php @@ -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']; + } + } + } + } diff --git a/ext/search_kit/Civi/Search/Admin.php b/ext/search_kit/Civi/Search/Admin.php index 5a28f6d6f0..99fd42f74c 100644 --- a/ext/search_kit/Civi/Search/Admin.php +++ b/ext/search_kit/Civi/Search/Admin.php @@ -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]); } diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js index 8c1934f6b4..84eef3d1e8 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js @@ -451,8 +451,8 @@ _.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); } @@ -670,7 +670,7 @@ 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 '' + formatFieldValue(info.field, value) + ''; @@ -688,12 +688,13 @@ // 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]); } diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js index 635af8e49c..b19d59e447 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js @@ -67,9 +67,11 @@ }, }; + // 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; @@ -137,8 +139,8 @@ 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 = { @@ -229,8 +231,8 @@ _.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) { @@ -242,7 +244,7 @@ } } }); - return links; + return _.uniq(links, 'path'); } this.pickIcon = function(model, key) { -- 2.25.1