Skip to content

Commit

Permalink
Merge pull request #19673 from colemanw/searchKitLinks
Browse files Browse the repository at this point in the history
SearchKit - Add links/menus/buttons to search displays
  • Loading branch information
eileenmcnaughton authored Feb 26, 2021
2 parents e291659 + daa4e55 commit 5c6ea43
Show file tree
Hide file tree
Showing 28 changed files with 517 additions and 126 deletions.
1 change: 1 addition & 0 deletions ext/search/CRM/Search/Upgrader.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public function upgrade_1001() {
$key = $newAliases[$column['expr']] ?? $column['expr'];
unset($display['settings']['columns'][$c]['expr']);
$display['settings']['columns'][$c]['key'] = explode(' AS ', $key)[1] ?? $key;
$display['settings']['columns'][$c]['type'] = 'field';
}
\Civi\Api4\SearchDisplay::update(FALSE)
->setValues($display)
Expand Down
45 changes: 31 additions & 14 deletions ext/search/Civi/Search/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Civi\Search;

use CRM_Search_ExtensionUtil as E;

/**
* Class Admin
* @package Civi\Search
Expand All @@ -28,6 +30,7 @@ public static function getAdminSettings():array {
'operators' => \CRM_Utils_Array::makeNonAssociative(self::getOperators()),
'functions' => \CRM_Api4_Page_Api4Explorer::getSqlFunctions(),
'displayTypes' => Display::getDisplayTypes(['id', 'name', 'label', 'description', 'icon']),
'styles' => \CRM_Utils_Array::makeNonAssociative(self::getStyles()),
'afformEnabled' => (bool) \CRM_Utils_Array::findAll(
\CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles(),
['fullName' => 'org.civicrm.afform']
Expand All @@ -50,15 +53,29 @@ public static function getOperators():array {
'<' => '<',
'>=' => '',
'<=' => '',
'CONTAINS' => ts('Contains'),
'IN' => ts('Is One Of'),
'NOT IN' => ts('Not One Of'),
'LIKE' => ts('Is Like'),
'NOT LIKE' => ts('Not Like'),
'BETWEEN' => ts('Is Between'),
'NOT BETWEEN' => ts('Not Between'),
'IS NULL' => ts('Is Null'),
'IS NOT NULL' => ts('Not Null'),
'CONTAINS' => E::ts('Contains'),
'IN' => E::ts('Is One Of'),
'NOT IN' => E::ts('Not One Of'),
'LIKE' => E::ts('Is Like'),
'NOT LIKE' => E::ts('Not Like'),
'BETWEEN' => E::ts('Is Between'),
'NOT BETWEEN' => E::ts('Not Between'),
'IS NULL' => E::ts('Is Null'),
'IS NOT NULL' => E::ts('Not Null'),
];
}

/**
* @return string[]
*/
public static function getStyles():array {
return [
'default' => E::ts('Default'),
'primary' => E::ts('Primary'),
'success' => E::ts('Success'),
'info' => E::ts('Info'),
'warning' => E::ts('Warning'),
'danger' => E::ts('Danger'),
];
}

Expand All @@ -84,15 +101,15 @@ public static function getSchema() {
unset($entity['paths'][$action]);
switch ($action) {
case 'view':
$title = ts('View %1', [1 => $entity['title']]);
$title = E::ts('View %1', [1 => $entity['title']]);
break;

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

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

default:
Expand Down Expand Up @@ -239,7 +256,7 @@ public static function getJoins(array $allowedEntities) {
$alias = $baseEntity['name'] . "_{$bridge}_" . $targetEntityName;
$joins[$baseEntity['name']][] = [
'label' => $baseEntity['title'] . ' ' . $targetsTitle,
'description' => ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
'description' => E::ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
'entity' => $targetEntityName,
'conditions' => array_merge(
[$bridge],
Expand All @@ -254,7 +271,7 @@ public static function getJoins(array $allowedEntities) {
$alias = $targetEntityName . "_{$bridge}_" . $baseEntity['name'];
$joins[$targetEntityName][] = [
'label' => $targetEntity['title'] . ' ' . $baseEntity['title_plural'],
'description' => ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]),
'description' => E::ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]),
'entity' => $baseEntity['name'],
'conditions' => array_merge(
[$bridge],
Expand Down
34 changes: 33 additions & 1 deletion ext/search/ang/crmSearchAdmin.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
$scope.$ctrl = this;
})

.factory('searchMeta', function() {
.factory('searchMeta', function($q) {
function getEntity(entityName) {
if (entityName) {
return _.find(CRM.crmSearchAdmin.schema, {name: entityName});
Expand Down Expand Up @@ -227,8 +227,40 @@
}
});
});
},
pickIcon: function() {
var deferred = $q.defer();
$('#crm-search-admin-icon-picker').off('change').siblings('.crm-icon-picker-button').click();
$('#crm-search-admin-icon-picker').on('change', function() {
deferred.resolve($(this).val());
});
return deferred.promise;
}
};
})
.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
// view -> model
elm.on('blur', function() {
ctrl.$setViewValue(elm.html());
});

// model -> view
ctrl.$render = function() {
elm.html(ctrl.$viewValue);
};
}
};
});

// Shoehorn in a non-angular widget for picking icons
$(function() {
$('#crm-container').append('<div style="display:none"><input id="crm-search-admin-icon-picker"></div>');
CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').done(function() {
$('#crm-search-admin-icon-picker').crmIconPicker();
});
});

})(angular, CRM.$, CRM._);
100 changes: 98 additions & 2 deletions ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,55 @@

this.preview = this.stale = false;

this.colTypes = {
links: {
label: ts('Links'),
icon: 'fa-link',
defaults: {
links: []
}
},
buttons: {
label: ts('Buttons'),
icon: 'fa-square-o',
defaults: {
size: 'btn-sm',
links: []
}
},
menu: {
label: ts('Menu'),
icon: 'fa-bars',
defaults: {
text: ts('Actions'),
style: 'default',
size: 'btn-sm',
icon: 'fa-bars',
links: []
}
},
};

this.sortableOptions = {
connectWith: '.crm-search-admin-edit-columns',
containment: '.crm-search-admin-edit-columns-wrapper'
};

this.styles = CRM.crmSearchAdmin.styles;

this.addCol = function(type) {
var col = _.cloneDeep(this.colTypes[type].defaults);
col.type = type;
if (this.display.type === 'table') {
col.alignment = 'text-right';
}
ctrl.display.settings.columns.push(col);
};

this.removeCol = function(index) {
ctrl.hiddenColumns.push(ctrl.display.settings.columns[index]);
if (ctrl.display.settings.columns[index].type === 'field') {
ctrl.hiddenColumns.push(ctrl.display.settings.columns[index]);
}
ctrl.display.settings.columns.splice(index, 1);
};

Expand All @@ -70,6 +112,13 @@
return searchMeta.getDefaultLabel(expr);
};

this.getColLabel = function(col) {
if (col.type === 'field') {
return ctrl.getFieldLabel(col.key);
}
return ctrl.colTypes[col.type].label;
};

function fieldToColumn(fieldExpr, defaults) {
var info = searchMeta.parseExpr(fieldExpr),
values = _.cloneDeep(defaults);
Expand All @@ -85,6 +134,53 @@
return values;
}

this.getLinks = function() {
if (!ctrl.links) {
ctrl.links = buildLinks();
}
return ctrl.links;
};

// Build a list of all possible links to main entity or join entities
function buildLinks() {
// Links to main entity
var links = _.cloneDeep(searchMeta.getEntity(ctrl.savedSearch.api_entity).paths || []);
// Links to explicitly joined entities
_.each(ctrl.savedSearch.api_params.join, function(join) {
var joinName = join[0].split(' AS '),
joinEntity = searchMeta.getEntity(joinName[0]);
_.each(joinEntity.paths, function(path) {
var link = _.cloneDeep(path);
link.path = link.path.replace(/\[/g, '[' + joinName[1] + '.');
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);
_.each(joinEntity.paths, function(path) {
var link = _.cloneDeep(path);
link.path = link.path.replace(/\[id/g, '[' + idField);
links.push(link);
});
}
}
}
});
return links;
}

this.pickIcon = function(model, key) {
searchMeta.pickIcon().then(function(icon) {
model[key] = icon;
});
};

// Helper function to sort active from hidden columns and initialize each column with defaults
this.initColumns = function(defaults) {
if (!ctrl.display.settings.columns) {
Expand All @@ -104,7 +200,7 @@
}
});
_.eachRight(activeColumns, function(key, index) {
if (!_.includes(selectAliases, key)) {
if (key && !_.includes(selectAliases, key)) {
ctrl.display.settings.columns.splice(index, 1);
}
});
Expand Down
101 changes: 101 additions & 0 deletions ext/search/ang/crmSearchAdmin/crmSearchAdminLinkGroup.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
(function(angular, $, _) {
"use strict";

angular.module('crmSearchAdmin').component('crmSearchAdminLinkGroup', {
bindings: {
group: '<',
apiEntity: '<',
apiParams: '<',
links: '<'
},
templateUrl: '~/crmSearchAdmin/crmSearchAdminLinkGroup.html',
controller: function ($scope, $element, $timeout, searchMeta) {
var ts = $scope.ts = CRM.ts(),
ctrl = this;

this.styles = CRM.crmSearchAdmin.styles;

this.setValue = function(val, index) {
var link = ctrl.getLink(val),
item = ctrl.group[index];
if (item.path === val) {
return;
}
item.path = val;
item.icon = link ? defaultIcons[link.action] : 'fa-external-link';
if (val === 'civicrm/') {
$timeout(function () {
$('tr:eq(' + index + ') input[type=text]', $element).focus();
});
}
};

this.sortableOptions = {
containment: 'tbody',
direction: 'vertical',
helper: function(e, ui) {
// Prevent table row width from changing during drag
ui.children().each(function() {
$(this).width($(this).width());
});
return ui;
}
};

var defaultIcons = {
view: 'fa-external-link',
update: 'fa-pencil',
delete: 'fa-trash'
};

var defaultStyles = {
view: 'primary',
update: 'warning',
delete: 'danger'
};

$scope.pickIcon = function(index) {
searchMeta.pickIcon().then(function(icon) {
ctrl.group[index].icon = icon;
});
};

this.addItem = function(path) {
var link = ctrl.getLink(path);
ctrl.group.push({
path: path,
style: link && defaultStyles[link.action] || 'default',
text: link ? link.title : '',
icon: link && defaultIcons[link.action] || 'fa-external-link'
});
};

this.$onInit = function() {
if (!ctrl.group.length) {
if (ctrl.links.length) {
_.each(_.pluck(ctrl.links, 'path'), ctrl.addItem);
} else {
ctrl.addItem('civicrm/');
}
}
$element.on('change', 'select.crm-search-admin-select-path', function() {
var $select = $(this);
$scope.$apply(function() {
if ($select.closest('tfoot').length) {
ctrl.addItem($select.val());
$select.val('');
} else {
ctrl.setValue($select.val(), $select.closest('tr').index());
}
});
});
};

this.getLink = function(path) {
return _.findWhere(ctrl.links, {path: path});
};

}
});

})(angular, CRM.$, CRM._);
Loading

0 comments on commit 5c6ea43

Please sign in to comment.