Skip to content

Commit

Permalink
SearchKit - Support all fields as tokens
Browse files Browse the repository at this point in the history
Previously, only fields present in the SELECT clause could be tokens.
Now the SearchDisplay::Run api will add any fields used as tokens to the SELECT automatically.
  • Loading branch information
colemanw committed Jul 17, 2021
1 parent 65ef287 commit 6e2d7f8
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 121 deletions.
89 changes: 13 additions & 76 deletions ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php
Original file line number Diff line number Diff line change
Expand Up @@ -281,27 +281,6 @@ private function getSelectAliases() {
return $result;
}

/**
* Determines if a column is eligible to use an aggregate function
* @param string $fieldName
* @param string $prefix
* @return bool
*/
private function canAggregate($fieldName, $prefix = '') {
$apiParams = $this->savedSearch['api_params'];

// If the query does not use grouping, never
if (empty($apiParams['groupBy'])) {
return FALSE;
}
// If the column is used for a groupBy, no
if (in_array($prefix . $fieldName, $apiParams['groupBy'])) {
return FALSE;
}
// If the entity this column belongs to is being grouped by id, then also no
return !in_array($prefix . 'id', $apiParams['groupBy']);
}

/**
* Returns field definition for a given field or NULL if not found
* @param $fieldName
Expand Down Expand Up @@ -376,70 +355,28 @@ private function loadAfform() {
}

/**
* Adds additional useful fields to the select clause
* Adds additional fields to the select clause required to render the display
*
* @param array $apiParams
*/
private function augmentSelectClause(&$apiParams): void {
foreach ($this->getExtraEntityFields($this->savedSearch['api_entity']) as $extraFieldName) {
if (!in_array($extraFieldName, $apiParams['select']) && !$this->canAggregate($extraFieldName)) {
$apiParams['select'][] = $extraFieldName;
}
}
$joinAliases = [];
// Select the ids, etc. of explicitly joined entities (helps with displaying links)
foreach ($apiParams['join'] ?? [] as $join) {
[$joinEntity, $joinAlias] = explode(' AS ', $join[0]);
$joinAliases[] = $joinAlias;
foreach ($this->getExtraEntityFields($joinEntity) as $extraField) {
$extraFieldName = $joinAlias . '.' . $extraField;
if (!in_array($extraFieldName, $apiParams['select']) && !$this->canAggregate($extraField, $joinAlias . '.')) {
$apiParams['select'][] = $extraFieldName;
}
}
}
// 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
$possibleTokens = '';
foreach ($this->display['settings']['columns'] ?? [] as $column) {
if (isset($column['editable']['value']) && !in_array($column['editable']['value'], $apiParams['select'])) {
$apiParams['select'][] = $column['editable']['value'];
// Collect display values in which a token is allowed
$possibleTokens .= ($column['rewrite'] ?? '') . ($column['link']['path'] ?? '');
if (!empty($column['links'])) {
$possibleTokens .= implode('', array_column($column['links'], 'path'));
}
}
}

/**
* Get list of extra fields needed for displaying links for a given entity
*
* @param string $entityName
* @return array
*/
private function getExtraEntityFields(string $entityName): array {
if (!isset($this->_extraEntityFields[$entityName])) {
$id = CoreUtil::getInfoItem($entityName, 'primary_key');
$this->_extraEntityFields[$entityName] = $id;
foreach (CoreUtil::getInfoItem($entityName, 'paths') ?? [] as $path) {
$matches = [];
preg_match_all('#\[(\w+)]#', $path, $matches);
$this->_extraEntityFields[$entityName] = array_unique(array_merge($this->_extraEntityFields[$entityName], $matches[1] ?? []));
// Select value fields for in-place editing
if (isset($column['editable']['value']) && !in_array($column['editable']['value'], $apiParams['select'])) {
$apiParams['select'][] = $column['editable']['value'];
}
}
return $this->_extraEntityFields[$entityName];
// Add fields referenced via token
$tokens = [];
preg_match_all('/\\[([^]]+)\\]/', $possibleTokens, $tokens);
$apiParams['select'] = array_unique(array_merge($apiParams['select'], $tokens[1]));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
</ul>
</div>
</div>
<crm-search-admin-token-select ng-if="!$ctrl.getLink($ctrl.link.path)" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="$ctrl.link" field="path"></crm-search-admin-token-select>
<crm-search-admin-token-select ng-if="!$ctrl.getLink($ctrl.link.path)" model="$ctrl.link" field="path"></crm-search-admin-token-select>
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,34 @@

angular.module('crmSearchAdmin').component('crmSearchAdminTokenSelect', {
bindings: {
apiEntity: '<',
apiParams: '<',
model: '<',
field: '@'
field: '@',
suffix: '@'
},
require: {
admin: '^crmSearchAdmin'
},
templateUrl: '~/crmSearchAdmin/crmSearchAdminTokenSelect.html',
controller: function ($scope, $element, searchMeta) {
var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
ctrl = this;

this.initTokens = function() {
ctrl.tokens = ctrl.tokens || getTokens();
};

this.insertToken = function(key) {
ctrl.model[ctrl.field] = (ctrl.model[ctrl.field] || '') + ctrl.tokens[key].token;
ctrl.model[ctrl.field] = (ctrl.model[ctrl.field] || '') + '[' + key + ']';
};

function getTokens() {
var tokens = {
id: {
token: '[id]',
label: searchMeta.getField('id', ctrl.apiEntity).label
}
};
_.each(ctrl.apiParams.join, function(joinParams) {
var info = searchMeta.parseExpr(joinParams[0].split(' AS ')[1] + '.id');
tokens[info.alias] = {
token: '[' + info.alias + ']',
label: info.field ? info.field.label : info.alias
};
this.getTokens = function() {
var allFields = ctrl.admin.getAllFields(ctrl.suffix || '', ['Field', 'Custom', 'Extra']);
_.eachRight(ctrl.admin.savedSearch.api_params.select, function(fieldName) {
allFields.unshift({
id: fieldName,
text: searchMeta.getDefaultLabel(fieldName)
});
});
_.each(ctrl.apiParams.select, function(expr) {
var info = searchMeta.parseExpr(expr);
tokens[info.alias] = {
token: '[' + info.alias + ']',
label: info.field ? info.field.label : info.alias
};
});
return tokens;
}
return {
results: allFields
};
};

}
});
Expand Down
14 changes: 4 additions & 10 deletions ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTokenSelect.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
<div class="btn-group btn-group-xs">
<button type="button" class="btn btn-default-outline dropdown-toggle" ng-click="$ctrl.initTokens()" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{:: ts('Tokens') }} <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li ng-repeat="(id, token) in $ctrl.tokens" >
<a href ng-click="$ctrl.insertToken(id)">{{ token.label }}</a>
</li>
</ul>
</div>
<input class="form-control crm-action-menu fa-code collapsible-optgroups"
crm-ui-select="{placeholder: ts('Tokens'), data: $ctrl.getTokens, width: '12em'}"
on-crm-ui-select="$ctrl.insertToken(selection)"
/>
4 changes: 2 additions & 2 deletions ext/search_kit/ang/crmSearchAdmin/displays/colType/field.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
{{:: ts('Tooltip') }}
</label>
<input class="form-control crm-flex-1" type="text" ng-model="col.title" ng-if="col.title" ng-model-options="{updateOn: 'blur'}" />
<crm-search-admin-token-select ng-if="col.title" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="title"></crm-search-admin-token-select>
<crm-search-admin-token-select ng-if="col.title" model="col" field="title" suffix=":label"></crm-search-admin-token-select>
</div>
<div class="form-inline crm-search-admin-flex-row">
<label title="{{ ts('Change the contents of this field, or combine multiple field values.') }}">
<input type="checkbox" ng-checked="col.rewrite" ng-click="$ctrl.parent.toggleRewrite(col)" >
{{:: ts('Rewrite') }}
</label>
<input type="text" class="form-control crm-flex-1" ng-if="col.rewrite" ng-model="col.rewrite" ng-model-options="{updateOn: 'blur'}">
<crm-search-admin-token-select ng-if="col.rewrite" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="rewrite"></crm-search-admin-token-select>
<crm-search-admin-token-select ng-if="col.rewrite" model="col" field="rewrite" suffix=":label"></crm-search-admin-token-select>
</div>
<div class="form-inline">
<label ng-if="$ctrl.parent.isEditable(col)" title="{{:: ts('Users will be able to click to edit this field.') }}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
{{:: ts('Label') }}
</label>
<input ng-if="col.label" class="form-control crm-flex-1" type="text" ng-model="col.label" ng-model-options="{updateOn: 'blur'}">
<crm-search-admin-token-select ng-if="col.label" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="label"></crm-search-admin-token-select>
<crm-search-admin-token-select ng-if="col.label" model="col" field="label" suffix=":label"></crm-search-admin-token-select>
</div>
<div class="form-inline" ng-if="col.label">
<label style="visibility: hidden"><input type="checkbox" disabled></label><!--To indent by 1 checkbox-width-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function setUpHeadless() {
/**
* Test running a searchDisplay with various filters.
*/
public function testRunDisplay() {
public function testRunWithFilters() {
foreach (['Tester', 'Bot'] as $type) {
ContactType::create(FALSE)
->addValue('parent_id.name', 'Individual')
Expand Down Expand Up @@ -122,6 +122,84 @@ public function testRunDisplay() {
$this->assertCount(2, $result);
}

/**
* Test return values are augmented by tokens.
*/
public function testWithTokens() {
$lastName = uniqid(__FUNCTION__);
$sampleData = [
['first_name' => 'One', 'last_name' => $lastName, 'source' => 'Unit test'],
['first_name' => 'Two', 'last_name' => $lastName, 'source' => 'Unit test'],
];
Contact::save(FALSE)->setRecords($sampleData)->execute();

$params = [
'checkPermissions' => FALSE,
'return' => 'page:1',
'savedSearch' => [
'api_entity' => 'Contact',
'api_params' => [
'version' => 4,
'select' => ['id', 'display_name'],
'where' => [['last_name', '=', $lastName]],
],
],
'display' => [
'type' => 'table',
'label' => '',
'settings' => [
'limit' => 20,
'pager' => TRUE,
'columns' => [
[
'key' => 'id',
'label' => 'Contact ID',
'dataType' => 'Integer',
'type' => 'field',
],
[
'key' => 'display_name',
'label' => 'Display Name',
'dataType' => 'String',
'type' => 'field',
'link' => [
'path' => 'civicrm/test/token-[sort_name]',
],
],
],
'sort' => [
['id', 'ASC'],
],
],
],
];

$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(2, $result);
$this->assertNotEmpty($result->first()['display_name']);
// Assert that display name was added to the search due to the link token
$this->assertNotEmpty($result->first()['sort_name']);

// These items are not part of the search, but will be added via links
$this->assertArrayNotHasKey('contact_type', $result->first());
$this->assertArrayNotHasKey('source', $result->first());
$this->assertArrayNotHasKey('last_name', $result->first());

// Add links
$params['display']['settings']['columns'][] = [
'type' => 'links',
'label' => 'Links',
'links' => [
['path' => 'civicrm/test-[source]-[contact_type]'],
['path' => 'civicrm/test-[last_name]'],
],
];
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertEquals('Individual', $result->first()['contact_type']);
$this->assertEquals('Unit test', $result->first()['source']);
$this->assertEquals($lastName, $result->first()['last_name']);
}

/**
* Test running a searchDisplay as a restricted user.
*/
Expand Down

0 comments on commit 6e2d7f8

Please sign in to comment.