From: Coleman Watts Date: Tue, 3 Nov 2020 02:16:17 +0000 (-0500) Subject: Search ext: Add links to search admin and improve links in displays X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=f9cf879733996d4e847ac304cd945a4cd0770e44;p=civicrm-core.git Search ext: Add links to search admin and improve links in displays --- diff --git a/ext/search/Civi/Search/Admin.php b/ext/search/Civi/Search/Admin.php index 58f8d5e192..fab5cd7870 100644 --- a/ext/search/Civi/Search/Admin.php +++ b/ext/search/Civi/Search/Admin.php @@ -58,7 +58,7 @@ class Admin { public static function getSchema() { $schema = []; $entities = \Civi\Api4\Entity::get() - ->addSelect('name', 'title', 'title_plural', 'description', 'icon') + ->addSelect('name', 'title', 'title_plural', 'description', 'icon', 'paths') ->addWhere('name', '!=', 'Entity') ->addOrderBy('title_plural') ->setChain([ @@ -68,6 +68,31 @@ class Admin { 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 = ts('View %1', [1 => $entity['title']]); + break; + + case 'edit': + $title = ts('Edit %1', [1 => $entity['title']]); + break; + + case 'delete': + $title = ts('Delete %1', [1 => $entity['title']]); + break; + + default: + continue 2; + } + $entity['paths'][] = [ + 'path' => $path, + 'title' => $title, + 'action' => $action, + ]; + } $entity['fields'] = civicrm_api4($entity['name'], 'getFields', [ 'select' => $getFields, 'where' => [['name', 'NOT IN', ['api_key', 'hash']]], diff --git a/ext/search/ang/crmSearchAdmin.module.js b/ext/search/ang/crmSearchAdmin.module.js index af31489c9a..baa2cbecce 100644 --- a/ext/search/ang/crmSearchAdmin.module.js +++ b/ext/search/ang/crmSearchAdmin.module.js @@ -88,7 +88,8 @@ function getField(fieldName, entityName) { var dotSplit = fieldName.split('.'), joinEntity = dotSplit.length > 1 ? dotSplit[0] : null, - name = _.last(dotSplit).split(':')[0]; + name = _.last(dotSplit).split(':')[0], + field; // Custom fields contain a dot in their fieldname // If 3 segments, the first is the joinEntity and the last 2 are the custom field if (dotSplit.length === 3) { @@ -96,15 +97,20 @@ } // If 2 segments, it's ambiguous whether this is a custom field or joined field. Search the main entity first. if (dotSplit.length === 2) { - var field = _.find(getEntity(entityName).fields, {name: dotSplit[0] + '.' + name}); + field = _.find(getEntity(entityName).fields, {name: dotSplit[0] + '.' + name}); if (field) { + field.entity = entityName; return field; } } if (joinEntity) { entityName = _.find(CRM.vars.search.links[entityName], {alias: joinEntity}).entity; } - return _.find(getEntity(entityName).fields, {name: name}); + field = _.find(getEntity(entityName).fields, {name: name}); + if (field) { + field.entity = entityName; + return field; + } } return { getEntity: getEntity, diff --git a/ext/search/ang/crmSearchAdmin/compose/results.html b/ext/search/ang/crmSearchAdmin/compose/results.html index 049d037bb4..3cda603bc8 100644 --- a/ext/search/ang/crmSearchAdmin/compose/results.html +++ b/ext/search/ang/crmSearchAdmin/compose/results.html @@ -22,9 +22,7 @@ - - {{ formatResult(row, col) }} - + diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js b/ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js index b5a34a34e3..8e070741e9 100644 --- a/ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js +++ b/ext/search/ang/crmSearchAdmin/crmSearchAdmin.component.js @@ -114,8 +114,7 @@ this.addDisplay = function(type) { ctrl.savedSearch.displays.push({ type: type, - label: '', - settings: {} + label: '' }); $scope.selectTab('display_' + (ctrl.savedSearch.displays.length - 1)); }; @@ -285,7 +284,14 @@ // Debounced callback for loadResults function _loadResultsCallback() { // Multiply limit to read 2 pages at once & save ajax requests - var params = angular.merge({debug: true, limit: ctrl.limit * 2}, ctrl.savedSearch.api_params); + var params = _.merge(_.cloneDeep(ctrl.savedSearch.api_params), {debug: true, limit: ctrl.limit * 2}); + // Select the ids of 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)) { + params.select.push(idField); + } + }); lockTableHeight(); $scope.error = false; if (ctrl.stale) { @@ -321,7 +327,7 @@ }) .finally(function() { if (ctrl.debug) { - ctrl.debug.params = JSON.stringify(_.extend({version: 4}, ctrl.savedSearch.api_params), null, 2); + ctrl.debug.params = JSON.stringify(params, null, 2); if (ctrl.debug.timeIndex) { ctrl.debug.timeIndex = Number.parseFloat(ctrl.debug.timeIndex).toPrecision(2); } @@ -466,6 +472,10 @@ // Is a column eligible to use an aggregate function? this.canAggregate = function(col) { + // If the query does not use grouping, never + if (!ctrl.savedSearch.api_params.groupBy.length) { + return false; + } var info = searchMeta.parseExpr(col); // If the column is used for a groupBy, no if (ctrl.savedSearch.api_params.groupBy.indexOf(info.path) > -1) { @@ -475,33 +485,65 @@ return ctrl.savedSearch.api_params.groupBy.indexOf(info.prefix + 'id') < 0; }; - $scope.formatResult = function formatResult(row, col) { + $scope.formatResult = function(row, col) { var info = searchMeta.parseExpr(col), key = info.fn ? (info.fn.name + ':' + info.path + info.suffix) : col, value = row[key]; if (info.fn && info.fn.name === 'COUNT') { return value; } + // Output user-facing name/label fields as a link, if possible + if (info.field && _.includes(['display_name', 'title', 'label', 'subject'], info.field.name) && !info.fn && typeof value === 'string') { + var link = getEntityUrl(row, info); + if (link) { + return '' + formatFieldValue(info.field, value) + ''; + } + } return formatFieldValue(info.field, value); }; + // Attempts to construct a view url for a given entity + function getEntityUrl(row, info) { + var entity = searchMeta.getEntity(info.field.entity), + path = _.result(_.findWhere(entity.paths, {action: 'view'}), 'path'); + // Only proceed if the path metadata exists for this entity + 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]); + } + }); + // Only proceed if the row contains all the necessary data to resolve tokens + if (tokens.length === replacements.length) { + _.each(tokens, function(token, index) { + path = path.replace(token, replacements[index]); + }); + return {url: CRM.url(path), title: path.title}; + } + } + } + function formatFieldValue(field, value) { - var type = field.data_type; + var type = field.data_type, + result = value; if (_.isArray(value)) { return _.map(value, function(val) { return formatFieldValue(field, val); }).join(', '); } if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) { - return CRM.utils.formatDate(value, null, type === 'Timestamp'); + result = CRM.utils.formatDate(value, null, type === 'Timestamp'); } else if (type === 'Boolean' && typeof value === 'boolean') { - return value ? ts('Yes') : ts('No'); + result = value ? ts('Yes') : ts('No'); } else if (type === 'Money' && typeof value === 'number') { - return CRM.formatMoney(value); + result = CRM.formatMoney(value); } - return value; + return _.escape(result); } $scope.fieldsForGroupBy = function() { diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.directive.js b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.directive.js new file mode 100644 index 0000000000..6676055464 --- /dev/null +++ b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.directive.js @@ -0,0 +1,46 @@ +(function(angular, $, _) { + "use strict"; + + angular.module('crmSearchAdmin').component('crmSearchAdminLinkSelect', { + bindings: { + column: '<', + links: '<' + }, + templateUrl: '~/crmSearchAdmin/crmSearchAdminLinkSelect.html', + controller: function ($scope, $element, $timeout) { + var ts = $scope.ts = CRM.ts(), + ctrl = this; + + function onChange() { + var val = $('select', $element).val(); + if (val !== ctrl.column.link) { + var link = ctrl.getLink(val); + if (link) { + ctrl.column.link = link.path; + ctrl.column.title = link.title; + } else if (val === 'civicrm/') { + ctrl.column.link = val; + $timeout(function() { + $('input', $element).focus(); + }); + } else { + ctrl.column.link = ''; + ctrl.column.title = ''; + } + } + } + + this.$onInit = function() { + $('select', $element).on('change', function() { + $scope.$apply(onChange); + }); + }; + + this.getLink = function(path) { + return _.findWhere(ctrl.links, {path: path}); + }; + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html new file mode 100644 index 0000000000..47648e7aa4 --- /dev/null +++ b/ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html @@ -0,0 +1,10 @@ + + diff --git a/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js b/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js index 706366cc11..78fb6450da 100644 --- a/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js +++ b/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js @@ -39,12 +39,14 @@ ctrl.hiddenColumns.splice(index, 1); }; - this.toggleLink = function(col) { - col.link = col.link ? '' : (window.location.pathname + window.location.search).replace('civicrm/admin/search', 'civicrm/'); - }; - this.$onInit = function () { ctrl.getFieldLabel = ctrl.crmSearchAdmin.getFieldLabel; + if (!ctrl.display.settings) { + ctrl.display.settings = { + limit: 20, + pager: true + }; + } if (!ctrl.display.settings.columns) { ctrl.display.settings.columns = _.transform(ctrl.apiParams.select, function(columns, fieldExpr) { columns.push(fieldToColumn(fieldExpr)); @@ -63,6 +65,16 @@ } }); } + ctrl.links = _.cloneDeep(searchMeta.getEntity(ctrl.apiEntity).paths || []); + _.each(ctrl.apiParams.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] + '.'); + ctrl.links.push(link); + }); + }); }; } diff --git a/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html b/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html index 3777818fce..66a15a4286 100644 --- a/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html +++ b/ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html @@ -18,8 +18,12 @@
- - + + +
+
+ +
diff --git a/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js b/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js index 6e190bb755..c3d4e5e884 100644 --- a/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js +++ b/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.component.js @@ -36,6 +36,14 @@ if (_.isEmpty(params.where)) { params.where = []; } + // Select the ids of joined entities (helps with displaying links) + _.each(params.join, function(join) { + var joinEntity = join[0].split(' AS ')[1], + idField = joinEntity + '.id'; + if (!_.includes(params.select, idField) && !canAggregate('id', joinEntity + '.')) { + params.select.push(idField); + } + }); _.each(ctrl.filters, function(value, key) { if (value) { params.where.push([key, 'CONTAINS', value]); @@ -103,11 +111,19 @@ } result = _.escape(result); if (col.link) { - result = '' + result + ''; + result = '' + result + ''; } return result; } + function getUrl(link, row) { + var url = replaceTokens(link, row); + if (url.slice(0, 1) !== '/' && url.slice(0, 4) !== 'http') { + url = CRM.url(url); + } + return _.escape(url); + } + function replaceTokens(str, data) { _.each(data, function(value, key) { str = str.replace('[' + key + ']', value); @@ -115,6 +131,19 @@ return str; } + function canAggregate(fieldName, prefix) { + // If the query does not use grouping, never + if (!ctrl.apiParams.groupBy.length) { + return false; + } + // If the column is used for a groupBy, no + if (ctrl.apiParams.groupBy.indexOf(prefix + fieldName) > -1) { + return false; + } + // If the entity this column belongs to is being grouped by id, then also no + return ctrl.apiParams.groupBy.indexOf(prefix + 'id') < 0; + } + $scope.selectAllRows = function() { // Deselect all if (ctrl.allRowsSelected) { diff --git a/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html b/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html index e49fd6742f..6b086334fe 100644 --- a/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html +++ b/ext/search/ang/crmSearchDisplay/crmSearchDisplayTable.html @@ -18,13 +18,13 @@ - + -
+