Skip to content

Commit

Permalink
Merge pull request civicrm#20134 from colemanw/searchKitLinks
Browse files Browse the repository at this point in the history
Search Kit links improvements
  • Loading branch information
colemanw authored Apr 27, 2021
2 parents 5b90f13 + a320374 commit 76060cc
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 95 deletions.
71 changes: 41 additions & 30 deletions ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,36 +113,7 @@ public function _run(\Civi\Api4\Generic\Result $result) {
$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();
Expand Down Expand Up @@ -379,4 +350,44 @@ private function loadAfform() {
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'];
}
}
}

}
42 changes: 14 additions & 28 deletions ext/search_kit/Civi/Search/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,40 +87,28 @@ public static function getSchema() {
->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']) {
// Add paths (but only RUD actions) with translated titles
foreach ($entity['paths'] as $action => $path) {
unset($entity['paths'][$action]);
switch ($action) {
case 'view':
$title = E::ts('View %1', [1 => $entity['title']]);
break;

case 'update':
$title = E::ts('Edit %1', [1 => $entity['title']]);
break;

case 'delete':
$title = E::ts('Delete %1', [1 => $entity['title']]);
break;

default:
continue 2;
if (in_array($action, ['view', 'update', 'delete'], TRUE)) {
$entity['paths'][] = [
'path' => $path,
'action' => $action,
];
}
$entity['paths'][] = [
'path' => $path,
'title' => $title,
'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))) {
Expand All @@ -131,13 +119,13 @@ public static function getSchema() {
}
}
// 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');
}
Expand All @@ -147,9 +135,7 @@ public static function getSchema() {
}
// 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]);
}
Expand Down
17 changes: 9 additions & 8 deletions ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 '<a href="' + _.escape(link.url) + '" title="' + _.escape(link.title) + '">' + formatFieldValue(info.field, value) + '</a>';
Expand All @@ -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]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -177,7 +179,7 @@
if (column.link) {
ctrl.onChangeLink(column, column.link.path, '');
} else {
var defaultLink = ctrl.getLinks()[0];
var defaultLink = ctrl.getLinks(column.key)[0];
column.link = {path: defaultLink ? defaultLink.path : 'civicrm/'};
ctrl.onChangeLink(column, null, column.link.path);
}
Expand All @@ -198,51 +200,87 @@
}
};

this.getLinks = function() {
this.getLinks = function(columnKey) {
if (!ctrl.links) {
ctrl.links = buildLinks();
ctrl.links = {'*': buildLinks()};
}
return ctrl.links;
if (!columnKey) {
return ctrl.links['*'];
}
var expr = ctrl.getExprFromSelect(columnKey),
info = searchMeta.parseExpr(expr),
joinEntity = '';
if (info.field.fk_entity || info.field.name !== info.field.fieldName) {
joinEntity = info.prefix + (info.field.fk_entity ? info.field.name : info.field.name.substr(0, info.field.name.lastIndexOf('.')));
} else if (info.prefix) {
joinEntity = info.prefix.replace('.', '');
}
if (!ctrl.links[joinEntity]) {
ctrl.links[joinEntity] = _.filter(ctrl.links['*'], function(link) {
return joinEntity === (link.join || '');
});
}
return ctrl.links[joinEntity];
};

// Build a list of all possible links to main entity or join entities
function buildLinks() {
function addTitle(link, entityName) {
switch (link.action) {
case 'view':
link.title = ts('View %1', {1: entityName});
break;

case 'update':
link.title = ts('Edit %1', {1: entityName});
break;

case 'delete':
link.title = ts('Delete %1', {1: entityName});
break;
}
}

// Links to main entity
var links = _.cloneDeep(searchMeta.getEntity(ctrl.savedSearch.api_entity).paths || []),
entityCount = {};
entityCount[ctrl.savedSearch.api_entity] = 1;
var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
links = _.cloneDeep(mainEntity.paths || []);
_.each(links, function(link) {
addTitle(link, mainEntity.title);
});
// Links to explicitly joined entities
_.each(ctrl.savedSearch.api_params.join, function(join) {
var joinName = join[0].split(' AS '),
joinEntity = searchMeta.getEntity(joinName[0]);
entityCount[joinEntity.name] = (entityCount[joinEntity.name] || 0) + 1;
_.each(ctrl.savedSearch.api_params.join, function(joinClause) {
var join = searchMeta.getJoin(joinClause[0]),
joinEntity = searchMeta.getEntity(join.entity);
_.each(joinEntity.paths, function(path) {
var link = _.cloneDeep(path);
link.path = link.path.replace(/\[/g, '[' + joinName[1] + '.');
if (entityCount[joinEntity.name] > 1) {
link.title += ' ' + entityCount[joinEntity.name];
}
link.path = link.path.replace(/\[/g, '[' + join.alias + '.');
link.join = join.alias;
addTitle(link, join.label);
links.push(link);
});
});
// Links to implicit joins
_.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 (!ctrl.crmSearchAdmin.canAggregate(idField)) {
var joinEntity = searchMeta.getEntity(info.field.fk_entity || info.field.entity);
if (info.field && !info.suffix && !info.fn && (info.field.fk_entity || info.field.name !== info.field.fieldName)) {
var idFieldName = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')),
idField = searchMeta.parseExpr(idFieldName).field;
if (!ctrl.crmSearchAdmin.canAggregate(idFieldName)) {
var joinEntity = searchMeta.getEntity(idField.fk_entity),
label = (idField.join ? idField.join.label + ': ' : '') + (idField.input_attrs && idField.input_attrs.label || idField.label);
_.each((joinEntity || {}).paths, function(path) {
var link = _.cloneDeep(path);
link.path = link.path.replace(/\[id/g, '[' + idField);
link.path = link.path.replace(/\[id/g, '[' + idFieldName);
link.join = idFieldName;
addTitle(link, label);
links.push(link);
});
}
}
}
});
return links;
return _.uniq(links, 'path');
}

this.pickIcon = function(model, key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,27 @@
ctrl.group.push({
path: path,
style: link && defaultStyles[link.action] || 'default',
text: link ? link.title : '',
text: link ? link.title : ts('Link'),
icon: link && defaultIcons[link.action] || 'fa-external-link'
});
};

this.onChangeLink = function(item, before, after) {
var beforeLink = before && ctrl.getLink(before),
beforeTitle = beforeLink ? beforeLink.title : ts('Link'),
afterLink = after && ctrl.getLink(after);
if (afterLink && (!item.text || beforeTitle === item.text)) {
item.text = afterLink.title;
}
};

this.$onInit = function() {
var defaultLinks = _.filter(ctrl.links, function(link) {
return !link.join;
});
if (!ctrl.group.length) {
if (ctrl.links.length) {
_.each(_.pluck(ctrl.links, 'path'), ctrl.addItem);
if (defaultLinks.length) {
_.each(_.pluck(defaultLinks, 'path'), ctrl.addItem);
} else {
ctrl.addItem('civicrm/');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<input type="text" class="form-control" ng-model="item.text">
</td>
<td class="form-inline">
<crm-search-admin-link-select api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" link="item" links="$ctrl.links"></crm-search-admin-link-select>
<crm-search-admin-link-select api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" link="item" links="$ctrl.links" on-change="$ctrl.onChangeLink(item, before, after)"></crm-search-admin-link-select>
</td>
<td>
<div class="btn-group">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<option value="_blank">{{:: ts('New tab') }}</option>
<option value="crm-popup">{{:: ts('Popup dialog') }}</option>
</select>
<crm-search-admin-link-select ng-if="col.link" link="col.link" on-change="$ctrl.parent.onChangeLink(col, before, after)" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" links=":: $ctrl.parent.getLinks()">
<crm-search-admin-link-select ng-if="col.link" link="col.link" on-change="$ctrl.parent.onChangeLink(col, before, after)" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" links=":: $ctrl.parent.getLinks(col.key)">
</crm-search-admin-link-select>
</div>
<div class="form-inline crm-search-admin-flex-row">
Expand Down

0 comments on commit 76060cc

Please sign in to comment.