From dbe1b6dadbfb81dee3e320911b2392eaa6ef1670 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Wed, 30 Mar 2022 19:42:41 -0400 Subject: [PATCH] SearchKit - Add UI for editing SearchSegments --- ext/search_kit/ang/crmSearchAdmin.module.js | 1 + .../crmSearchAdminLinkGroup.component.js | 2 +- .../crmSearchAdminSearchListing.component.js | 4 +- .../searchListing/searchList.html | 15 ++- .../crmSearchAdminSegment.component.js | 100 ++++++++++++++++++ .../searchSegment/crmSearchAdminSegment.html | 78 ++++++++++++++ .../searchSegment/editDialog.html | 3 + .../searchSegmentListing/buttons.html | 12 +++ .../crmSearchAdminSegmentListing.component.js | 97 +++++++++++++++++ .../searchSegmentListing/segments.html | 1 + .../traits/searchDisplayBaseTrait.service.js | 26 +++-- .../crmSearchDisplayGrid.component.js | 3 +- .../crmSearchDisplayList.component.js | 3 +- .../crmSearchDisplayTable.component.js | 4 +- ext/search_kit/css/crmSearchAdmin.css | 5 +- 15 files changed, 337 insertions(+), 17 deletions(-) create mode 100644 ext/search_kit/ang/crmSearchAdmin/searchSegment/crmSearchAdminSegment.component.js create mode 100644 ext/search_kit/ang/crmSearchAdmin/searchSegment/crmSearchAdminSegment.html create mode 100644 ext/search_kit/ang/crmSearchAdmin/searchSegment/editDialog.html create mode 100644 ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/buttons.html create mode 100644 ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/crmSearchAdminSegmentListing.component.js create mode 100644 ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/segments.html diff --git a/ext/search_kit/ang/crmSearchAdmin.module.js b/ext/search_kit/ang/crmSearchAdmin.module.js index 408bfab2fc..e03ace2311 100644 --- a/ext/search_kit/ang/crmSearchAdmin.module.js +++ b/ext/search_kit/ang/crmSearchAdmin.module.js @@ -72,6 +72,7 @@ if (!this.tab) { this.tab = this.tabs[0].name; } + this.searchSegmentCount = null; }) // Controller for creating a new search diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkGroup.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkGroup.component.js index 4956b302de..7d2c2be802 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkGroup.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkGroup.component.js @@ -50,7 +50,7 @@ this.sortableOptions = { containment: 'tbody', - direction: 'vertical', + axis: 'y', helper: function(e, ui) { // Prevent table row width from changing during drag ui.children().each(function() { diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js b/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js index ddff68cda3..e0bf1e62fc 100644 --- a/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/searchListing/crmSearchAdminSearchListing.component.js @@ -79,7 +79,7 @@ // Get the names of in-use filters function getActiveFilters() { return _.keys(_.pick(ctrl.filters, function(val) { - return val !== null && (val === true || val === false || val.length); + return val !== null && (_.includes(['boolean', 'number'], typeof val) || val.length); })); } @@ -190,7 +190,7 @@ searchMeta.fieldToColumn('label', { label: true, title: ts('Edit Label'), - editable: {entity: 'SavedSearch', id: 'id', name: 'label', value: 'label'} + editable: true }), searchMeta.fieldToColumn('api_entity:label', { label: ts('For'), diff --git a/ext/search_kit/ang/crmSearchAdmin/searchListing/searchList.html b/ext/search_kit/ang/crmSearchAdmin/searchListing/searchList.html index 770e7b7236..629efe7648 100644 --- a/ext/search_kit/ang/crmSearchAdmin/searchListing/searchList.html +++ b/ext/search_kit/ang/crmSearchAdmin/searchListing/searchList.html @@ -10,11 +10,21 @@ {{ tab.rowCount }} +
  • + + {{:: ts('Data Segmentation') }} + {{ $ctrl.searchSegmentCount }} + +
  • - + {{:: ts('New Search') }} + + + {{:: ts('New Segment') }} +
    @@ -35,4 +45,7 @@
    +
    + +
    diff --git a/ext/search_kit/ang/crmSearchAdmin/searchSegment/crmSearchAdminSegment.component.js b/ext/search_kit/ang/crmSearchAdmin/searchSegment/crmSearchAdminSegment.component.js new file mode 100644 index 0000000000..c98e16b9fc --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/searchSegment/crmSearchAdminSegment.component.js @@ -0,0 +1,100 @@ +(function(angular, $, _) { + "use strict"; + + angular.module('crmSearchAdmin').component('crmSearchAdminSegment', { + bindings: { + segmentId: '<', + }, + templateUrl: '~/crmSearchAdmin/searchSegment/crmSearchAdminSegment.html', + controller: function ($scope, searchMeta, dialogService, crmApi4, crmStatus, formatForSelect2) { + var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'), + ctrl = this; + + this.entitySelect = searchMeta.getPrimaryAndSecondaryEntitySelect(); + + ctrl.saving = false; + ctrl.segment = {items: []}; + + // Drag-n-drop settings for reordering items + this.sortableOptions = { + containment: 'fieldset', + axis: 'y', + handle: '.crm-draggable', + forcePlaceholderSize: true, + helper: function(e, ui) { + // Prevent table row width from changing during drag + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui; + } + }; + + this.$onInit = function() { + if (ctrl.segmentId) { + $('.ui-dialog:visible').block(); + crmApi4('SearchSegment', 'get', { + where: [['id', '=', ctrl.segmentId]] + }, 0).then(function(segment) { + ctrl.segment = segment; + searchMeta.loadFieldOptions([segment.entity_name]); + $('.ui-dialog:visible').unblock(); + }); + } + }; + + this.onChangeEntity = function() { + ctrl.segment.items.length = 0; + if (ctrl.segment.entity_name) { + searchMeta.loadFieldOptions([ctrl.segment.entity_name]); + ctrl.addItem(true); + } + }; + + function getDefaultField() { + var item = _.findLast(ctrl.segment.items, function(item) { + return item.when && item.when[0] && item.when[0][0]; + }); + return item ? item.when[0][0] : searchMeta.getEntity(ctrl.segment.entity_name).fields[0].name; + } + + this.addItem = function(addCondition) { + var item = {label: ''}; + if (addCondition) { + ctrl.addCondition(item); + } + ctrl.segment.items.push(item); + }; + + this.addCondition = function(item) { + var defaultField = getDefaultField(); + item.when = item.when || []; + item.when.push([defaultField, '=']); + }; + + this.hasDefault = function() { + return !!_.findLast(ctrl.segment.items, function(item) { + return !item.when || !item.when[0].length; + }); + }; + + this.getField = function(fieldName) { + return searchMeta.getField(fieldName, ctrl.segment.entity_name); + }; + + this.selectFields = function() { + return {results: formatForSelect2(searchMeta.getEntity(ctrl.segment.entity_name).fields, 'name', 'label', ['description'])}; + }; + + this.save = function() { + crmStatus({}, crmApi4('SearchSegment', 'save', { + records: [ctrl.segment] + })).then(function() { + dialogService.close('searchSegmentDialog'); + }); + }; + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/search_kit/ang/crmSearchAdmin/searchSegment/crmSearchAdminSegment.html b/ext/search_kit/ang/crmSearchAdmin/searchSegment/crmSearchAdminSegment.html new file mode 100644 index 0000000000..4ab34a597b --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/searchSegment/crmSearchAdminSegment.html @@ -0,0 +1,78 @@ +
    +
    + + +
    {{:: ts('Name of the new computed search field.') }}
    +
    + + +
    +
    + + +
    +
    +
    + {{:: ts('Items') }} + + + + + + + + + + + + + + + + + + + + + + +
    {{:: ts('Label') }}{{:: ts('Conditions') }}
    + + + + +
    + + + + + +
    +
    + +
    +
    + {{:: ts('Default Item') }} +
    +
    + +
    + + +
    +
    + + + +
    diff --git a/ext/search_kit/ang/crmSearchAdmin/searchSegment/editDialog.html b/ext/search_kit/ang/crmSearchAdmin/searchSegment/editDialog.html new file mode 100644 index 0000000000..4042add700 --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/searchSegment/editDialog.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/buttons.html b/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/buttons.html new file mode 100644 index 0000000000..64b87c6f1e --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/buttons.html @@ -0,0 +1,12 @@ +
    + +
    diff --git a/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/crmSearchAdminSegmentListing.component.js b/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/crmSearchAdminSegmentListing.component.js new file mode 100644 index 0000000000..828c4b9768 --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/crmSearchAdminSegmentListing.component.js @@ -0,0 +1,97 @@ +(function(angular, $, _) { + "use strict"; + + // Specialized searchDisplay, only used by Admins + angular.module('crmSearchAdmin').component('crmSearchAdminSegmentListing', { + bindings: { + filters: '<', + totalCount: '=' + }, + templateUrl: '~/crmSearchDisplayTable/crmSearchDisplayTable.html', + controller: function($scope, $element, crmApi4, searchMeta, searchDisplayBaseTrait, searchDisplaySortableTrait) { + var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'), + // Mix in traits to this controller + ctrl = angular.extend(this, searchDisplayBaseTrait, searchDisplaySortableTrait); + + this.apiEntity = 'SearchSegment'; + this.search = { + api_entity: 'SearchSegment', + api_params: { + version: 4, + select: [ + 'label', + 'description', + 'entity_name:label', + 'items' + ], + join: [], + where: [], + groupBy: [] + } + }; + + this.$onInit = function() { + buildDisplaySettings(); + this.initializeDisplay($scope, $element); + }; + + this.deleteSegment = function(row) { + ctrl.runSearch( + [['SearchSegment', 'delete', {where: [['id', '=', row.key]]}]], + {start: ts('Deleting...'), success: ts('Segment Deleted')}, + row + ); + }; + + function buildDisplaySettings() { + ctrl.display = { + type: 'table', + settings: { + limit: CRM.crmSearchAdmin.defaultPagerSize, + pager: {show_count: true, expose_limit: true}, + actions: false, + classes: ['table', 'table-striped'], + sort: [['label', 'ASC']], + columns: [ + { + key: 'label', + label: ts('Label'), + title: ts('Edit Label'), + type: 'field', + editable: true + }, + { + key: 'description', + label: ts('Description'), + type: 'field', + editable: true + }, + { + key: 'entity_name:label', + label: ts('For'), + type: 'field', + empty_value: ts('Missing'), + cssRules: [ + ['font-italic', 'entity_name:label', 'IS EMPTY'] + ] + }, + { + type: 'include', + label: ts('Items'), + path: '~/crmSearchAdmin/searchSegmentListing/segments.html' + }, + { + type: 'include', + label: '', + path: '~/crmSearchAdmin/searchSegmentListing/buttons.html' + } + ] + } + }; + ctrl.settings = ctrl.display.settings; + } + + } + }); + +})(angular, CRM.$, CRM._); diff --git a/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/segments.html b/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/segments.html new file mode 100644 index 0000000000..80fe0d2776 --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/searchSegmentListing/segments.html @@ -0,0 +1 @@ +{{:: item.label }}, diff --git a/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js b/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js index 11d0ab5050..bffde300df 100644 --- a/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js +++ b/ext/search_kit/ang/crmSearchDisplay/traits/searchDisplayBaseTrait.service.js @@ -28,13 +28,19 @@ }); }, 800); - // If search is embedded in contact summary tab, display count in tab-header + // Update totalCount variable if used. + // Integrations can pass in `total-count="somevar" to keep track of the number of results returned + // FIXME: Additional hack to directly update tabHeader for contact summary tab. It would be better to + // decouple the contactTab code into a separate directive that checks totalCount. var contactTab = $element.closest('.crm-contact-page .ui-tabs-panel').attr('id'); - if (contactTab) { - var unwatchCount = $scope.$watch('$ctrl.rowCount', function(rowCount) { - if (typeof rowCount === 'number') { - unwatchCount(); - CRM.tabHeader.updateCount(contactTab.replace('contact-', '#tab_'), rowCount); + if (contactTab || typeof ctrl.totalCount !== 'undefined') { + $scope.$watch('$ctrl.rowCount', function(rowCount) { + // Update totalCount only if no user filters are set + if (typeof rowCount === 'number' && angular.equals({}, ctrl.getAfformFilters())) { + ctrl.totalCount = rowCount; + if (contactTab) { + CRM.tabHeader.updateCount(contactTab.replace('contact-', '#tab_'), rowCount); + } } }); } @@ -76,6 +82,12 @@ $scope.$watch('$ctrl.filters', onChangeFilters, true); }, + getAfformFilters: function() { + return _.pick(this.afFieldset ? this.afFieldset.getFieldData() : {}, function(val) { + return val !== null && (_.includes(['boolean', 'number'], typeof val) || val.length); + }); + }, + // Generate params for the SearchDisplay.run api getApiParams: function(mode) { return { @@ -85,7 +97,7 @@ sort: this.sort, limit: this.limit, seed: this.seed, - filters: _.assign({}, (this.afFieldset ? this.afFieldset.getFieldData() : {}), this.filters), + filters: _.assign({}, this.getAfformFilters(), this.filters), afform: this.afFieldset ? this.afFieldset.getFormName() : null }; }, diff --git a/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.component.js b/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.component.js index d66e594382..300d861d0f 100644 --- a/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.component.js +++ b/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGrid.component.js @@ -8,7 +8,8 @@ display: '<', apiParams: '<', settings: '<', - filters: '<' + filters: '<', + totalCount: '=' }, require: { afFieldset: '?^^afFieldset' diff --git a/ext/search_kit/ang/crmSearchDisplayList/crmSearchDisplayList.component.js b/ext/search_kit/ang/crmSearchDisplayList/crmSearchDisplayList.component.js index e033c007d5..7982f36307 100644 --- a/ext/search_kit/ang/crmSearchDisplayList/crmSearchDisplayList.component.js +++ b/ext/search_kit/ang/crmSearchDisplayList/crmSearchDisplayList.component.js @@ -8,7 +8,8 @@ display: '<', apiParams: '<', settings: '<', - filters: '<' + filters: '<', + totalCount: '=' }, require: { afFieldset: '?^^afFieldset' diff --git a/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.component.js b/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.component.js index d1c859ef49..76e500910e 100644 --- a/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.component.js +++ b/ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.component.js @@ -7,7 +7,8 @@ search: '<', display: '<', settings: '<', - filters: '<' + filters: '<', + totalCount: '=' }, require: { afFieldset: '?^^afFieldset' @@ -18,7 +19,6 @@ // Mix in traits to this controller ctrl = angular.extend(this, searchDisplayBaseTrait, searchDisplayTasksTrait, searchDisplaySortableTrait); - this.$onInit = function() { this.initializeDisplay($scope, $element); diff --git a/ext/search_kit/css/crmSearchAdmin.css b/ext/search_kit/css/crmSearchAdmin.css index f422f42775..57f7ae53c0 100644 --- a/ext/search_kit/css/crmSearchAdmin.css +++ b/ext/search_kit/css/crmSearchAdmin.css @@ -108,10 +108,11 @@ display: inline-block; } -#bootstrap-theme.crm-search i.crm-search-move-icon { +#bootstrap-theme i.crm-i.crm-search-move-icon { opacity: .5; } -#bootstrap-theme.crm-search .crm-draggable:hover > * > i.crm-search-move-icon { +#bootstrap-theme .crm-draggable:hover > i.crm-i.crm-search-move-icon, +#bootstrap-theme .crm-draggable:hover > * > i.crm-i.crm-search-move-icon { opacity: 1; } -- 2.25.1