From 94d356ed24089393b6cb66ea9ef892e0ae6f3ceb Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sun, 5 Sep 2021 18:17:53 -0400 Subject: [PATCH] SearchKit - Enable tagging saved searches --- ext/search_kit/Civi/Search/Admin.php | 5 ++ ext/search_kit/ang/crmSearchAdmin.module.js | 20 ++++- .../crmSearchAdmin.component.js | 21 +++-- .../crmSearchAdminTags.component.js | 84 +++++++++++++++++++ .../crmSearchAdmin/crmSearchAdminTags.html | 29 +++++++ .../crmSearchAdminSearchListing.component.js | 26 +++++- .../crmSearchAdminSearchListing.html | 5 +- .../crmSearchAdmin/searchListing/tags.html | 1 + ext/search_kit/ang/crmSearchAdmin/tabs.html | 3 + ext/search_kit/css/crmSearchAdmin.css | 41 +++++++-- ext/search_kit/managed/TagUsedFor.mgd.php | 15 ++++ 11 files changed, 226 insertions(+), 24 deletions(-) create mode 100644 ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTags.component.js create mode 100644 ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTags.html create mode 100644 ext/search_kit/ang/crmSearchAdmin/searchListing/tags.html create mode 100644 ext/search_kit/managed/TagUsedFor.mgd.php diff --git a/ext/search_kit/Civi/Search/Admin.php b/ext/search_kit/Civi/Search/Admin.php index 963b02c44d..ff866722c1 100644 --- a/ext/search_kit/Civi/Search/Admin.php +++ b/ext/search_kit/Civi/Search/Admin.php @@ -11,6 +11,7 @@ namespace Civi\Search; +use Civi\Api4\Tag; use CRM_Search_ExtensionUtil as E; /** @@ -35,6 +36,10 @@ class Admin { 'defaultPagerSize' => \Civi::settings()->get('default_pager_size'), 'afformEnabled' => $extensions->isActiveModule('afform'), 'afformAdminEnabled' => $extensions->isActiveModule('afform_admin'), + 'tags' => Tag::get() + ->addSelect('id', 'name', 'color', 'is_selectable', 'description') + ->addWhere('used_for', 'CONTAINS', 'civicrm_saved_search') + ->execute(), ]; } diff --git a/ext/search_kit/ang/crmSearchAdmin.module.js b/ext/search_kit/ang/crmSearchAdmin.module.js index 265d5708ac..867d6952b9 100644 --- a/ext/search_kit/ang/crmSearchAdmin.module.js +++ b/ext/search_kit/ang/crmSearchAdmin.module.js @@ -29,7 +29,12 @@ savedSearch: function($route, crmApi4) { var params = $route.current.params; return crmApi4('SavedSearch', 'get', { + select: ['*', 'GROUP_CONCAT(DISTINCT entity_tag.tag_id) AS tag_id'], where: [['id', '=', params.id]], + join: [ + ['EntityTag AS entity_tag', 'LEFT', ['entity_tag.entity_table', '=', '"civicrm_saved_search"'], ['id', '=', 'entity_tag.entity_id']], + ], + groupBy: ['id'], chain: { groups: ['Group', 'get', {select: ['id', 'title', 'description', 'visibility', 'group_type', 'custom.*'], where: [['saved_search_id', '=', '$id']]}], displays: ['SearchDisplay', 'get', {where: [['saved_search_id', '=', '$id']]}] @@ -45,7 +50,7 @@ searchEntity = $routeParams.entity; $scope.$ctrl = this; this.savedSearch = { - api_entity: searchEntity, + api_entity: searchEntity }; // Changing entity will refresh the angular page $scope.$watch('$ctrl.savedSearch.api_entity', function(newEntity, oldEntity) { @@ -62,7 +67,7 @@ $scope.$ctrl = this; }) - .factory('searchMeta', function($q) { + .factory('searchMeta', function($q, formatForSelect2) { function getEntity(entityName) { if (entityName) { return _.find(CRM.crmSearchAdmin.schema, {name: entityName}); @@ -230,6 +235,17 @@ return info.prefix.replace('.', ''); } return ''; + }, + getPrimaryAndSecondaryEntitySelect: function() { + var primaryEntities = _.filter(CRM.crmSearchAdmin.schema, {searchable: 'primary'}), + secondaryEntities = _.filter(CRM.crmSearchAdmin.schema, {searchable: 'secondary'}), + select = formatForSelect2(primaryEntities, 'name', 'title_plural', ['description', 'icon']); + select.push({ + text: ts('More...'), + description: ts('Other less-commonly searched entities'), + children: formatForSelect2(secondaryEntities, 'name', 'title_plural', ['description', 'icon']) + }); + return select; } }; }) diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js index d8f13c449d..71707882ba 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js @@ -32,6 +32,7 @@ this.savedSearch.displays = this.savedSearch.displays || []; this.savedSearch.groups = this.savedSearch.groups || []; + this.savedSearch.tag_id = this.savedSearch.tag_id || []; this.groupExists = !!this.savedSearch.groups.length; if (!this.savedSearch.id) { @@ -48,14 +49,7 @@ }); } - var primaryEntities = _.filter(CRM.crmSearchAdmin.schema, {searchable: 'primary'}), - secondaryEntities = _.filter(CRM.crmSearchAdmin.schema, {searchable: 'secondary'}); - $scope.mainEntitySelect = formatForSelect2(primaryEntities, 'name', 'title_plural', ['description', 'icon']); - $scope.mainEntitySelect.push({ - text: ts('More...'), - description: ts('Other less-commonly searched entities'), - children: formatForSelect2(secondaryEntities, 'name', 'title_plural', ['description', 'icon']) - }); + $scope.mainEntitySelect = searchMeta.getPrimaryAndSecondaryEntitySelect(); $scope.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect); @@ -105,6 +99,17 @@ apiCalls.deleteDisplays = ['SearchDisplay', 'delete', {where: [['saved_search_id', '=', params.id]]}]; } delete params.displays; + if (params.tag_id && params.tag_id.length) { + chain.tag_id = ['EntityTag', 'replace', { + where: [['entity_id', '=', '$id'], ['entity_table', '=', 'civicrm_saved_search']], + records: _.transform(params.tag_id, function(records, id) {records.push({tag_id: id});}) + }]; + } else if (params.id) { + chain.tag_id = ['EntityTag', 'delete', { + where: [['entity_id', '=', '$id'], ['entity_table', '=', 'civicrm_saved_search']] + }]; + } + delete params.tag_id; apiCalls.saved = ['SavedSearch', 'save', {records: [params], chain: chain}, 0]; crmApi4(apiCalls).then(function(results) { // After saving a new search, redirect to the edit url diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTags.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTags.component.js new file mode 100644 index 0000000000..0527d1a1c4 --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTags.component.js @@ -0,0 +1,84 @@ +(function(angular, $, _) { + "use strict"; + + angular.module('crmSearchAdmin').component('crmSearchAdminTags', { + bindings: { + tagIds: '<', + savedSearchId: '<' + }, + templateUrl: '~/crmSearchAdmin/crmSearchAdminTags.html', + controller: function ($scope, $element, crmApi4, crmStatus) { + var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'), + ctrl = this; + this.allTags = CRM.crmSearchAdmin.tags; + + function reset() { + ctrl.menuOpen = false; + ctrl.search = ''; + } + + this.$onInit = function() { + ctrl.tagIds = ctrl.tagIds || []; + reset(); + $element.on('hidden.bs.dropdown', function() { + $scope.$apply(reset); + }); + }; + + this.openMenu = function() { + ctrl.menuOpen = true; + ctrl.color = getRandomColor(); + }; + + this.getTag = function(id) { + return _.findWhere(ctrl.allTags, {id: id}); + }; + + this.hasTag = function(tag) { + return _.includes(ctrl.tagIds, tag.id); + }; + + this.getStyle = function(id) { + var tag = ctrl.getTag(id); + if (tag && tag.color) { + return 'background-color: ' + tag.color + '; color: ' + CRM.utils.colorContrast(tag.color); + } + return ''; + }; + + this.toggleTag = function(tag) { + if (ctrl.hasTag(tag)) { + _.remove(ctrl.tagIds, function(id) {return id === tag.id;}); + if (ctrl.savedSearchId) { + crmStatus({}, crmApi4('EntityTag', 'delete', { + where: [['entity_id', '=', ctrl.savedSearchId], ['tag_id', '=', tag.id], ['entity_table', '=', 'civicrm_saved_search']] + })); + } + } else { + ctrl.tagIds.push(tag.id); + if (ctrl.savedSearchId) { + crmStatus({}, crmApi4('EntityTag', 'create', { + values: {entity_id: ctrl.savedSearchId, tag_id: tag.id, entity_table: 'civicrm_saved_search'} + })); + } + } + }; + + this.makeTag = function(name) { + crmApi4('Tag', 'create', { + values: {name: name, color: ctrl.color, is_selectable: true, used_for: ['civicrm_saved_search']} + }, 0).then(function(tag) { + ctrl.allTags.push(tag); + ctrl.toggleTag(tag); + }); + }; + + // TODO: Use https://github.com/davidmerfield/randomColor + function getRandomColor() { + return '#' + Math.floor(Math.random()*16777215).toString(16); + } + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTags.html b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTags.html new file mode 100644 index 0000000000..309ca3b4c2 --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminTags.html @@ -0,0 +1,29 @@ + + + + {{:: $ctrl.getTag(id).name }} + diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js b/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js index e6e8bb5077..933fa52889 100644 --- a/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js @@ -4,7 +4,7 @@ // Specialized searchDisplay, only used by Admins angular.module('crmSearchAdmin').component('crmSearchAdminSearchListing', { templateUrl: '~/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html', - controller: function($scope, crmApi4, crmStatus, searchMeta, searchDisplayBaseTrait, searchDisplaySortableTrait) { + controller: function($scope, crmApi4, crmStatus, searchMeta, searchDisplayBaseTrait, searchDisplaySortableTrait, formatForSelect2) { var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'), // Mix in traits to this controller ctrl = angular.extend(this, searchDisplayBaseTrait, searchDisplaySortableTrait); @@ -13,6 +13,7 @@ this.afformPath = CRM.url('civicrm/admin/afform'); this.afformEnabled = CRM.crmSearchAdmin.afformEnabled; this.afformAdminEnabled = CRM.crmSearchAdmin.afformAdminEnabled; + this.entitySelect = searchMeta.getPrimaryAndSecondaryEntitySelect(); this.apiEntity = 'SavedSearch'; this.search = { @@ -34,9 +35,15 @@ 'GROUP_CONCAT(display.label ORDER BY display.id) AS display_label', 'GROUP_CONCAT(display.type:icon ORDER BY display.id) AS display_icon', 'GROUP_CONCAT(display.acl_bypass ORDER BY display.id) AS display_acl_bypass', + 'tags', // Not a selectable field but this hacks around the requirement that filters be in the select clause + 'GROUP_CONCAT(DISTINCT entity_tag.tag_id) AS tag_id', 'GROUP_CONCAT(DISTINCT group.title) AS groups' ], - join: [['SearchDisplay AS display'], ['Group AS group']], + join: [ + ['SearchDisplay AS display', 'LEFT', ['id', '=', 'display.saved_search_id']], + ['Group AS group', 'LEFT', ['id', '=', 'group.saved_search_id']], + ['EntityTag AS entity_tag', 'LEFT', ['entity_tag.entity_table', '=', '"civicrm_saved_search"'], ['id', '=', 'entity_tag.entity_id']], + ], where: [['api_entity', 'IS NOT NULL']], groupBy: ['id'] } @@ -71,6 +78,14 @@ ); }; + this.getTags = function() { + return {results: formatForSelect2(CRM.crmSearchAdmin.tags, 'id', 'name', ['color', 'description'])}; + }; + + this.getEntities = function() { + return {results: formatForSelect2(CRM.crmSearchAdmin.tags, 'id', 'name', ['color', 'description'])}; + }; + function buildDisplaySettings() { ctrl.display = { type: 'table', @@ -88,6 +103,11 @@ searchMeta.fieldToColumn('api_entity:label', { label: ts('For'), }), + { + type: 'include', + label: ts('Tags'), + path: '~/crmSearchAdmin/searchListing/tags.html' + }, { type: 'include', label: ts('Displays'), @@ -115,7 +135,7 @@ } }; if (ctrl.afformEnabled) { - ctrl.display.settings.columns.splice(3, 0, { + ctrl.display.settings.columns.splice(4, 0, { type: 'include', label: ts('Forms'), path: '~/crmSearchAdmin/searchListing/afforms.html' diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html b/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html index b2e0db8b3f..eb888f0000 100644 --- a/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html +++ b/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.html @@ -1,8 +1,9 @@