From 65a6ae6b9228ad945b0f2dc9b80886468098555f Mon Sep 17 00:00:00 2001 From: colemanw Date: Mon, 12 Jun 2023 21:21:36 -0400 Subject: [PATCH 1/4] crmUi - more reliable method of checking whether autocomplete element needs updating --- ang/crmUi.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ang/crmUi.js b/ang/crmUi.js index 462d547a8df..0f3019eb0a2 100644 --- a/ang/crmUi.js +++ b/ang/crmUi.js @@ -755,15 +755,12 @@ }; if (ctrl.ngModel) { - var oldValue; // Ensure widget is updated when model changes ctrl.ngModel.$render = function() { - element.val(ctrl.ngModel.$viewValue || ''); // Trigger change so the Select2 renders the current value, // but only if the value has actually changed (to avoid recursion) - if (!angular.equals(ctrl.ngModel.$viewValue, oldValue)) { - oldValue = ctrl.ngModel.$viewValue; - element.change(); + if (!angular.equals(ctrl.ngModel.$viewValue || '', element.val())) { + element.val(ctrl.ngModel.$viewValue || '').change(); } }; From 4752454a6254a5f18c406fd522b31ac476eef18f Mon Sep 17 00:00:00 2001 From: colemanw Date: Sun, 11 Jun 2023 17:39:12 -0400 Subject: [PATCH 2/4] CiviMail - Create searchDisplay to autocomplete Groups + Mailings Generally improves handling of Autocomplete searchDisplays based on EntitySets --- Civi/Api4/EntitySet.php | 9 + Civi/Api4/Generic/AutocompleteAction.php | 43 +++-- Civi/Api4/Query/SqlFunctionIF.php | 6 +- .../MailingRecipientsAutocompleteProvider.php | 165 ++++++++++++++++++ .../SearchDisplay/AbstractRunAction.php | 4 + .../Subscriber/DefaultDisplaySubscriber.php | 2 +- 6 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 Civi/Api4/Service/Autocomplete/MailingRecipientsAutocompleteProvider.php diff --git a/Civi/Api4/EntitySet.php b/Civi/Api4/EntitySet.php index 03ac7566502..d8a82fcb051 100644 --- a/Civi/Api4/EntitySet.php +++ b/Civi/Api4/EntitySet.php @@ -29,6 +29,15 @@ public static function get($checkPermissions = TRUE) { ->setCheckPermissions($checkPermissions); } + /** + * @param bool $checkPermissions + * @return Generic\AutocompleteAction + */ + public static function autocomplete($checkPermissions = TRUE) { + return (new Generic\AutocompleteAction('EntitySet', __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + /** * @return \Civi\Api4\Generic\BasicGetFieldsAction */ diff --git a/Civi/Api4/Generic/AutocompleteAction.php b/Civi/Api4/Generic/AutocompleteAction.php index 874e9654201..8842355c9d8 100644 --- a/Civi/Api4/Generic/AutocompleteAction.php +++ b/Civi/Api4/Generic/AutocompleteAction.php @@ -29,6 +29,8 @@ * @method string getFormName() * @method $this setFieldName(string $fieldName) Set fieldName. * @method string getFieldName() + * @method $this setKey(string $key) Set keyField used as unique identifier. + * @method string getKey() * @method $this setFilters(array $filters) * @method array getFilters() */ @@ -123,6 +125,8 @@ public function _run(Result $result) { // Allow the default search to be modified \Civi::dispatcher()->dispatch('civi.search.autocompleteDefault', GenericHookEvent::create([ 'savedSearch' => &$this->savedSearch, + 'formName' => $this->formName, + 'fieldName' => $this->fieldName, ])); } $this->loadSavedSearch(); @@ -137,7 +141,7 @@ public function _run(Result $result) { // Render mode: fetch by id if ($this->ids) { - $this->savedSearch['api_params']['where'][] = [$keyField, 'IN', $this->ids]; + $this->addFilter($keyField, ['IN' => $this->ids]); unset($this->display['settings']['pager']); $return = NULL; } @@ -188,8 +192,8 @@ public function _run(Result $result) { foreach (array_slice($row['columns'], 1) as $col) { $item['description'][] = $col['val']; } - if (!empty($this->display['settings']['color'])) { - $item['color'] = $row['data'][$this->display['settings']['color']] ?? NULL; + foreach ($this->display['settings']['extra'] ?? [] as $name => $key) { + $item[$key] = $row['data'][$name] ?? $item[$key] ?? NULL; } $result[] = $item; } @@ -232,16 +236,33 @@ private function getDisplayFields() { * @param array $displayFields */ private function augmentSelectClause(string $idField, array $displayFields) { - $select = array_merge([$idField], $displayFields); + // Don't mess with aggregated queries + if ($this->savedSearch['api_entity'] === 'EntitySet' || !empty($this->savedSearch['api_params']['groupBy'])) { + return; + } + // Original select params. Key by alias to avoid duplication. + $originalSelect = []; + foreach ($this->savedSearch['api_params']['select'] ?? [] as $item) { + $alias = explode(' AS ', $item)[1] ?? $item; + $originalSelect[$alias] = $item; + } + // Add any missing fields which should be selected + $additions = array_merge([$idField], $displayFields); // Add trustedFilters to the SELECT clause so that SearchDisplay::run will trust them foreach ($this->trustedFilters as $fields => $val) { - $select = array_merge($select, explode(',', $fields)); - } - if (!empty($this->display['settings']['color'])) { - $select[] = $this->display['settings']['color']; + $additions = array_merge($additions, explode(',', $fields)); } - $select = array_merge($select, array_column($this->display['settings']['sort'] ?? [], 0)); - $this->savedSearch['api_params']['select'] = array_unique(array_merge($this->savedSearch['api_params']['select'], $select)); + // Add 'extra' fields defined by the display + $additions = array_merge($additions, array_keys($this->display['settings']['extra'] ?? [])); + // Add 'sort' fields + $additions = array_merge($additions, array_column($this->display['settings']['sort'] ?? [], 0)); + + // Key by field name and combine with original SELECT + $additions = array_unique($additions); + $additions = array_combine($additions, $additions); + + // Maintain original order (important when using UNIONs in the query) + $this->savedSearch['api_params']['select'] = array_values($originalSelect + $additions); } /** @@ -265,7 +286,7 @@ private function getKeyField() { } } } - return CoreUtil::getIdFieldName($entityName); + return $this->display['settings']['keyField'] ?? CoreUtil::getIdFieldName($entityName); } /** diff --git a/Civi/Api4/Query/SqlFunctionIF.php b/Civi/Api4/Query/SqlFunctionIF.php index 31187b5f39b..77f26a03726 100644 --- a/Civi/Api4/Query/SqlFunctionIF.php +++ b/Civi/Api4/Query/SqlFunctionIF.php @@ -24,17 +24,17 @@ protected static function params(): array { return [ [ 'optional' => FALSE, - 'must_be' => ['SqlEquation', 'SqlField'], + 'must_be' => ['SqlEquation', 'SqlField', 'SqlFunction'], 'label' => ts('If'), ], [ 'optional' => FALSE, - 'must_be' => ['SqlField', 'SqlString', 'SqlNumber', 'SqlNull'], + 'must_be' => ['SqlField', 'SqlString', 'SqlNumber', 'SqlNull', 'SqlFunction'], 'label' => ts('Then'), ], [ 'optional' => FALSE, - 'must_be' => ['SqlField', 'SqlString', 'SqlNumber', 'SqlNull'], + 'must_be' => ['SqlField', 'SqlString', 'SqlNumber', 'SqlNull', 'SqlFunction'], 'label' => ts('Else'), ], ]; diff --git a/Civi/Api4/Service/Autocomplete/MailingRecipientsAutocompleteProvider.php b/Civi/Api4/Service/Autocomplete/MailingRecipientsAutocompleteProvider.php new file mode 100644 index 00000000000..6e687e15828 --- /dev/null +++ b/Civi/Api4/Service/Autocomplete/MailingRecipientsAutocompleteProvider.php @@ -0,0 +1,165 @@ + ['mailingAutocompleteDefaultSearch', 50], + 'civi.search.defaultDisplay' => ['mailingAutocompleteDefaultDisplay', 50], + ]; + } + + /** + * Construct a special-purpose SavedSearch for the Mailing.recipients autocomplete + * + * It uses a UNION to combine groups with mailings + * + * @param \Civi\Core\Event\GenericHookEvent $e + * @return void + */ + public function mailingAutocompleteDefaultSearch(GenericHookEvent $e) { + if ( + !is_array($e->savedSearch) || + $e->savedSearch['api_entity'] !== 'EntitySet' || + ($e->fieldName !== 'Mailing.recipients_include' && $e->fieldName !== 'Mailing.recipients_exclude') || + strpos($e->formName ?? '', 'crmMailing.') !== 0 + ) { + return; + } + $mailingId = (int) (explode('.', $e->formName)[1] ?? 0); + // Mode is "include" or "exclude" + $mode = explode('_', $e->fieldName)[1]; + $e->savedSearch['api_params'] = [ + 'version' => 4, + 'select' => ['key', 'label', 'description', 'type', 'icon', 'date', '(is_hidden = 1) AS locked'], + 'sets' => [ + [ + 'UNION ALL', 'Group', 'get', [ + 'select' => [ + 'CONCAT("groups_", id) AS key', + 'IF(is_hidden, "' . ts('Search Results') . '", title) AS label', + 'description', + '"group" AS entity', + 'NULL AS type', + 'IF(saved_search_id, "fa-lightbulb-o", "fa-group") AS icon', + 'DATE(saved_search_id.created_date) AS date', + 'is_hidden', + ], + 'join' => [], + 'where' => [ + ['group_type:name', 'CONTAINS', 'Mailing List'], + ['OR', [['saved_search_id.expires_date', 'IS NULL'], ['saved_search_id.expires_date', '>', 'NOW()', TRUE]]], + ['OR', [['is_hidden', '=', FALSE], [($mode === 'include' ? 'mailing_group.id' : '(NULL)'), 'IS NOT NULL']]], + ], + ], + ], + [ + 'UNION ALL', 'Mailing', 'get', [ + 'select' => [ + 'CONCAT("mailings_", id) AS key', + 'name', + 'subject', + '"mailing" AS entity', + 'IF(is_archived, "' . ts('Archived Mailing') . '", IF(is_completed, "' . ts('Sent Mailing') . '", "' . ts('Unsent Mailing') . '")) AS type', + 'IF(is_archived, "fa-archive", IF(is_completed, "fa-envelope", "fa-file-o")) AS icon', + 'COALESCE(DATE(scheduled_date), DATE(created_date))', + '0', + ], + 'where' => [ + ['id', '!=', $mailingId], + ['domain_id', '=', 'current_domain'], + ], + ], + ], + ], + ]; + // Join is only needed for "include" mode to fetch the hidden search group if any + if ($mode === 'include') { + $e->savedSearch['api_params']['sets'][0][3]['join'][] = ['MailingGroup AS mailing_group', 'LEFT', + ['id', '=', 'mailing_group.entity_id'], + ['mailing_group.group_type', '=', '"Include"'], + ['mailing_group.entity_table', '=', '"civicrm_group"'], + ['is_hidden', '=', TRUE], + ['mailing_group.mailing_id', '=', $mailingId], + ]; + } + } + + /** + * Construct a SearchDisplay for the above SavedSearch + * + * @param \Civi\Core\Event\GenericHookEvent $e + * @return void + */ + public function mailingAutocompleteDefaultDisplay(GenericHookEvent $e) { + if ( + // Early return if display has already been overridden + $e->display['settings'] || + // Check display type + $e->display['type'] !== 'autocomplete' + // Check entity + || $e->savedSearch['api_entity'] !== 'EntitySet' || + // Check that this is the correct SavedSearch + $e->savedSearch['api_params']['sets'][0][3]['select'][0] !== 'CONCAT("groups_", id) AS key' + ) { + return; + } + $e->display['settings'] = [ + 'sort' => [ + ['entity', 'ASC'], + ['label', 'ASC'], + ], + 'keyField' => 'key', + 'extra' => [ + 'locked' => 'locked', + ], + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'label', + 'empty_value' => '(' . ts('no name') . ')', + 'icons' => [ + [ + 'field' => 'icon', + 'side' => 'left', + ], + ], + ], + [ + 'type' => 'field', + 'key' => 'type', + 'rewrite' => '[type] ([date])', + 'empty_value' => '', + ], + [ + 'type' => 'field', + 'key' => 'description', + ], + ], + ]; + } + +} diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index 73dfc4c92ea..547f7304669 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -894,6 +894,10 @@ protected function getOrderByFromSort() { * @param array $apiParams */ protected function augmentSelectClause(&$apiParams): void { + // Don't mess with EntitySets + if ($this->savedSearch['api_entity'] === 'EntitySet') { + return; + } // Add primary key field if actions are enabled // (only needed for non-dao entities, as Api4SelectQuery will auto-add the id) if (!in_array('DAOEntity', CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'type')) && diff --git a/ext/search_kit/Civi/Api4/Event/Subscriber/DefaultDisplaySubscriber.php b/ext/search_kit/Civi/Api4/Event/Subscriber/DefaultDisplaySubscriber.php index f6711dd7245..20fa233a4b4 100644 --- a/ext/search_kit/Civi/Api4/Event/Subscriber/DefaultDisplaySubscriber.php +++ b/ext/search_kit/Civi/Api4/Event/Subscriber/DefaultDisplaySubscriber.php @@ -101,7 +101,7 @@ public static function autocompleteDefault(GenericHookEvent $e) { // Color field if (isset($fields['color'])) { - $e->display['settings']['color'] = 'color'; + $e->display['settings']['extra']['color'] = 'color'; } } From fb04bcdc11b1ea82a6b24168633f3aafde1280fa Mon Sep 17 00:00:00 2001 From: colemanw Date: Tue, 13 Jun 2023 09:56:08 -0400 Subject: [PATCH 3/4] CiviMail - Switch to APIv4 autocomplete to select recipients This uses an autocomplete callback with UNIONs to select from both groups and mailings, but instead of a single widget for incluce/exclude it provides two widgets for better usability. --- ang/crmMailing.css | 20 +- ang/crmMailing/BlockRecipients.html | 12 +- ang/crmMailing/EditRecipCtrl.js | 7 +- ang/crmMailing/Recipients.js | 345 ------------------ ...MailingRecipientsAutocomplete.component.js | 61 ++++ 5 files changed, 69 insertions(+), 376 deletions(-) delete mode 100644 ang/crmMailing/Recipients.js create mode 100644 ang/crmMailing/crmMailingRecipientsAutocomplete.component.js diff --git a/ang/crmMailing.css b/ang/crmMailing.css index 4dc0a0ed246..a04e9305013 100644 --- a/ang/crmMailing.css +++ b/ang/crmMailing.css @@ -23,17 +23,6 @@ text-align: center; } -span.crmMailing-include { - color: #060; -} -span.crmMailing-exclude { - color: #600; - text-decoration: line-through; -} -span.crmMailing-mandatory { - color: #866304; -} - .crmMailing input[name=preview_test_email], .crmMailing-preview select[name=preview_test_group] { width: 80%; @@ -53,12 +42,9 @@ span.crmMailing-mandatory { .crmMailing .preview-contact { border-right: 1px solid black; } -.crmMailing .preview-group, -.crmMailing .preview-contact { -} .crmMailing .crmMailing-schedule-outer { - width: 98% + width: 98%; } .crmMailing .crmMailing-schedule-inner { width: 40em; @@ -79,8 +65,8 @@ input[name=preview_test_email]::-webkit-input-placeholder { input[name=preview_test_email]:-ms-input-placeholder { text-align: center; } -.crmMailing-active { -} + +/* .crmMailing-active {} */ .crmMailing-inactive { text-decoration: line-through; } diff --git a/ang/crmMailing/BlockRecipients.html b/ang/crmMailing/BlockRecipients.html index 8803868453a..90be26287c0 100644 --- a/ang/crmMailing/BlockRecipients.html +++ b/ang/crmMailing/BlockRecipients.html @@ -1,12 +1,8 @@
- + + + +
diff --git a/ang/crmMailing/EditRecipCtrl.js b/ang/crmMailing/EditRecipCtrl.js index 304b56959ea..cc02c4dea25 100644 --- a/ang/crmMailing/EditRecipCtrl.js +++ b/ang/crmMailing/EditRecipCtrl.js @@ -11,12 +11,7 @@ var SETTING_DEBOUNCE_MS = 5000; var RECIPIENTS_PREVIEW_LIMIT = 50; - var ts = $scope.ts = CRM.ts(null); - - $scope.isMailingList = function isMailingList(group) { - var GROUP_TYPE_MAILING_LIST = '2'; - return _.contains(group.group_type, GROUP_TYPE_MAILING_LIST); - }; + var ts = $scope.ts = CRM.ts(); $scope.recipients = null; $scope.outdated = null; diff --git a/ang/crmMailing/Recipients.js b/ang/crmMailing/Recipients.js deleted file mode 100644 index 133efacdcac..00000000000 --- a/ang/crmMailing/Recipients.js +++ /dev/null @@ -1,345 +0,0 @@ -(function(angular, $, _) { - // example: - // FIXME: participate in ngModel's validation cycle - angular.module('crmMailing').directive('crmMailingRecipients', function(crmUiAlert) { - return { - restrict: 'AE', - require: 'ngModel', - scope: { - ngRequired: '@' - }, - link: function(scope, element, attrs, ngModel) { - scope.recips = ngModel.$viewValue; - scope.groups = scope.$parent.$eval(attrs.crmAvailGroups); - scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings); - refreshMandatory(); - - var ts = scope.ts = CRM.ts(null); - - /// Convert MySQL date ("yyyy-mm-dd hh:mm:ss") to JS date object - scope.parseDate = function(date) { - if (!angular.isString(date)) { - return date; - } - var p = date.split(/[\- :]/); - return new Date(parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2]), parseInt(p[3]), parseInt(p[4]), parseInt(p[5])); - }; - - /// Remove {value} from {array} - function arrayRemove(array, value) { - var idx = array.indexOf(value); - if (idx >= 0) { - array.splice(idx, 1); - } - } - - // @param string id an encoded string like "4 civicrm_mailing include" - // @return Object keys: entity_id, entity_type, mode - function convertValueToObj(id) { - var a = id.split(" "); - return {entity_id: parseInt(a[0]), entity_type: a[1], mode: a[2]}; - } - - // @param Object mailing - // @return array list of values like "4 civicrm_mailing include" - function convertMailingToValues(recipients) { - var r = []; - angular.forEach(recipients.groups.include, function(v) { - r.push(v + " civicrm_group include"); - }); - angular.forEach(recipients.groups.exclude, function(v) { - r.push(v + " civicrm_group exclude"); - }); - angular.forEach(recipients.mailings.include, function(v) { - r.push(v + " civicrm_mailing include"); - }); - angular.forEach(recipients.mailings.exclude, function(v) { - r.push(v + " civicrm_mailing exclude"); - }); - return r; - } - - function refreshMandatory() { - if (ngModel.$viewValue && ngModel.$viewValue.groups) { - scope.mandatoryGroups = _.filter(scope.$parent.$eval(attrs.crmMandatoryGroups), function(grp) { - return _.contains(ngModel.$viewValue.groups.include, parseInt(grp.id)); - }); - scope.mandatoryIds = _.map(_.pluck(scope.$parent.$eval(attrs.crmMandatoryGroups), 'id'), function(n) { - return parseInt(n); - }); - } - else { - scope.mandatoryGroups = []; - scope.mandatoryIds = []; - } - } - - function isMandatory(grpId) { - return _.contains(scope.mandatoryIds, parseInt(grpId)); - } - - var refreshUI = ngModel.$render = function refresuhUI() { - scope.recips = ngModel.$viewValue; - if (ngModel.$viewValue) { - $(element).select2('val', convertMailingToValues(ngModel.$viewValue)); - validate(); - refreshMandatory(); - } - }; - - // @return string HTML representing an option - function formatItem(item) { - if (!item.id) { - // return `text` for optgroup - return item.text; - } - var option = convertValueToObj(item.id); - var icon = (option.entity_type === 'civicrm_mailing') ? 'fa-envelope' : 'fa-users'; - var smartGroupMarker = item.is_smart ? '* ' : ''; - var spanClass = (option.mode == 'exclude') ? 'crmMailing-exclude' : 'crmMailing-include'; - if (option.entity_type != 'civicrm_mailing' && isMandatory(option.entity_id)) { - spanClass = 'crmMailing-mandatory'; - } - return ' ' + smartGroupMarker + item.text + ''; - } - - function validate() { - if (scope.$parent.$eval(attrs.ngRequired)) { - var empty = (_.isEmpty(ngModel.$viewValue.groups.include) && _.isEmpty(ngModel.$viewValue.mailings.include)); - ngModel.$setValidity('empty', !empty); - } - else { - ngModel.$setValidity('empty', true); - } - } - - var rcpAjaxState = { - input: '', - entity: 'civicrm_group', - type: 'include', - page_n: 0, - page_i: 0, - }; - - $(element).select2({ - width: '36em', - dropdownAutoWidth: true, - placeholder: "Groups or Past Recipients", - formatResult: formatItem, - formatSelection: formatItem, - escapeMarkup: function(m) { - return m; - }, - multiple: true, - initSelection: function(el, cb) { - var values = el.val().split(','); - - var gids = []; - var mids = []; - - for (var i = 0; i < values.length; i++) { - var dv = convertValueToObj(values[i]); - if (dv.entity_type == 'civicrm_group') { - gids.push(dv.entity_id); - } - else if (dv.entity_type == 'civicrm_mailing') { - mids.push(dv.entity_id); - } - } - // push non existant 0 group/mailing id in order when no recipents group or prior mailing is selected - // this will allow to resuse the below code to handle datamap - if (gids.length === 0) { - gids.push(0); - } - if (mids.length === 0) { - mids.push(0); - } - - CRM.api3('Group', 'getlist', { params: { id: { IN: gids }, options: { limit: 0 } }, extra: ["is_hidden"] }).then( - function(glist) { - CRM.api3('Mailing', 'getlist', { params: { id: { IN: mids }, options: { limit: 0 } } }).then( - function(mlist) { - var datamap = []; - - var groupNames = []; - var civiMails = []; - - $(glist.values).each(function (idx, group) { - var key = group.id + ' civicrm_group include'; - - groupNames.push({id: parseInt(group.id), title: group.label, is_hidden: group.extra.is_hidden}); - if (values.indexOf(key) >= 0) { - datamap.push({id: key, text: group.label}); - } - - key = group.id + ' civicrm_group exclude'; - if (values.indexOf(key) >= 0) { - datamap.push({id: key, text: group.label}); - } - }); - - $(mlist.values).each(function (idx, group) { - var key = group.id + ' civicrm_mailing include'; - civiMails.push({id: parseInt(group.id), name: group.label}); - - if (values.indexOf(key) >= 0) { - datamap.push({id: key, text: group.label}); - } - - key = group.id + ' civicrm_mailing exclude'; - if (values.indexOf(key) >= 0) { - datamap.push({id: key, text: group.label}); - } - }); - - scope.$parent.crmMailingConst.groupNames = groupNames; - scope.$parent.crmMailingConst.civiMails = civiMails; - - refreshMandatory(); - - cb(datamap); - }); - }); - }, - ajax: { - url: CRM.url('civicrm/ajax/rest'), - quietMillis: 300, - data: function(input, page_num) { - if (page_num <= 1) { - rcpAjaxState = { - input: input, - entity: 'civicrm_group', - type: 'include', - page_n: 0, - }; - } - - rcpAjaxState.page_i = page_num - rcpAjaxState.page_n; - var filterParams = {}; - switch(rcpAjaxState.entity) { - case 'civicrm_group': - filterParams = { is_hidden: 0, is_active: 1, group_type: {"LIKE": "%2%"} }; - break; - - case 'civicrm_mailing': - filterParams = { is_hidden: 0, is_active: 1, id: {"!=": scope.$parent.mailing.id} }; - break; - } - var params = { - input: input, - page_num: rcpAjaxState.page_i, - params: filterParams, - }; - - if('civicrm_mailing' === rcpAjaxState.entity) { - params["api.MailingRecipients.getcount"] = {}; - } - else if ('civicrm_group' === rcpAjaxState.entity) { - params.extra = ["saved_search_id"]; - } - - return params; - }, - transport: function(params) { - switch(rcpAjaxState.entity) { - case 'civicrm_group': - CRM.api3('Group', 'getlist', params.data).then(params.success, params.error); - break; - - case 'civicrm_mailing': - params.data.params.options = { sort: "is_archived asc, scheduled_date desc" }; - CRM.api3('Mailing', 'getlist', params.data).then(params.success, params.error); - break; - } - }, - results: function(data) { - var results = { - children: $.map(data.values, function(obj) { - if('civicrm_mailing' === rcpAjaxState.entity) { - return obj["api.MailingRecipients.getcount"] > 0 ? { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type, - text: obj.label } : ''; - } - else { - return { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type, text: obj.label, - is_smart: (!_.isEmpty(obj.extra.saved_search_id)) }; - } - }) - }; - - if (rcpAjaxState.page_i == 1 && data.count && results.children.length > 0) { - results.text = ts((rcpAjaxState.type == 'include'? 'Include ' : 'Exclude ') + - (rcpAjaxState.entity == 'civicrm_group'? 'Group' : 'Mailing')); - } - - var more = data.more_results || !(rcpAjaxState.entity == 'civicrm_mailing' && rcpAjaxState.type == 'exclude'); - - if (more && !data.more_results) { - if (rcpAjaxState.type == 'include') { - rcpAjaxState.type = 'exclude'; - } else { - rcpAjaxState.type = 'include'; - rcpAjaxState.entity = 'civicrm_mailing'; - } - rcpAjaxState.page_n += rcpAjaxState.page_i; - } - - return { more: more, results: [ results ] }; - }, - }, - }); - - $(element).on('select2-selecting', function(e) { - var option = convertValueToObj(e.val); - var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups'; - if (option.mode == 'exclude') { - ngModel.$viewValue[typeKey].exclude.push(option.entity_id); - arrayRemove(ngModel.$viewValue[typeKey].include, option.entity_id); - } - else { - ngModel.$viewValue[typeKey].include.push(option.entity_id); - arrayRemove(ngModel.$viewValue[typeKey].exclude, option.entity_id); - } - scope.$apply(); - $(element).select2('close'); - validate(); - e.preventDefault(); - }); - - $(element).on("select2-removing", function(e) { - var option = convertValueToObj(e.val); - var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups'; - if (typeKey == 'groups' && isMandatory(option.entity_id)) { - crmUiAlert({ - text: ts('This mailing was generated based on search results. The search results cannot be removed.'), - title: ts('Required') - }); - e.preventDefault(); - return; - } - scope.$parent.$apply(function() { - arrayRemove(ngModel.$viewValue[typeKey][option.mode], option.entity_id); - }); - validate(); - e.preventDefault(); - }); - - scope.$watchCollection("recips.groups.include", refreshUI); - scope.$watchCollection("recips.groups.exclude", refreshUI); - scope.$watchCollection("recips.mailings.include", refreshUI); - scope.$watchCollection("recips.mailings.exclude", refreshUI); - setTimeout(refreshUI, 50); - - scope.$watchCollection(attrs.crmAvailGroups, function() { - scope.groups = scope.$parent.$eval(attrs.crmAvailGroups); - }); - scope.$watchCollection(attrs.crmAvailMailings, function() { - scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings); - }); - scope.$watchCollection(attrs.crmMandatoryGroups, function() { - refreshMandatory(); - }); - } - }; - }); - -})(angular, CRM.$, CRM._); diff --git a/ang/crmMailing/crmMailingRecipientsAutocomplete.component.js b/ang/crmMailing/crmMailingRecipientsAutocomplete.component.js new file mode 100644 index 00000000000..4869ade8864 --- /dev/null +++ b/ang/crmMailing/crmMailingRecipientsAutocomplete.component.js @@ -0,0 +1,61 @@ +(function(angular, $, _) { + // Ex: + angular.module('crmMailing').component('crmMailingRecipientsAutocomplete', { + bindings: { + recipients: '', + controller: function($timeout) { + var ctrl = this; + + this.$onInit = function() { + this.placeholder = this.mode === 'include' ? ts('Include Groups & Mailings') : ts('Exclude Groups & Mailings'); + this.title = this.mode === 'include' ? ts('Include recipents from groups and past mailings.') : ts('Exclude recipents from groups and past mailings.'); + ctrl.autocompleteParams = { + formName: 'crmMailing.' + ctrl.mailingId, + fieldName: 'Mailing.recipients_' + ctrl.mode + }; + }; + + // Getter/setter for the select's ng-model + // Converts between a munged string e.g. 'mailings_3,groups_2,groups_5' + // and the mailing.recipients object e.g. {groups: {include: [2,5]}, mailings: {include: [3]}} + this.getSetValue = function(val) { + var selectValues = ''; + if (arguments.length) { + ctrl.recipients.groups[ctrl.mode].length = 0; + ctrl.recipients.mailings[ctrl.mode].length = 0; + _.each(val, function(munged) { + var entityType = munged.split('_')[0], + id = parseInt(munged.split('_')[1], 10), + oppositeMode = ctrl.mode === 'include' ? 'exclude' : 'include'; + ctrl.recipients[entityType][ctrl.mode].push(id); + // Items cannot be both include and exclude so remove from opposite collection + _.pull(ctrl.recipients[entityType][oppositeMode], id); + }); + } + else { + _.each(ctrl.recipients, function (items, entityType) { + _.each(items[ctrl.mode], function (id) { + selectValues += (selectValues.length ? ',' : '') + entityType + '_' + id; + }); + }); + + } + return selectValues; + }; + } + }); + +})(angular, CRM.$, CRM._); From 0a13fe50cf2e59be739bcaa2f75a76c75f1199d7 Mon Sep 17 00:00:00 2001 From: colemanw Date: Tue, 20 Jun 2023 19:14:44 -0400 Subject: [PATCH 4/4] CiviMail - Move include/exclude recipients onto separate lines --- ang/crmMailing/BlockMailing.html | 4 +--- ang/crmMailing/BlockRecipientsMultiline.html | 13 +++++++++++++ ang/crmMailing/BlockRecipientsMultiline.js | 5 +++++ .../crmMailingRecipientsAutocomplete.component.js | 2 +- 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 ang/crmMailing/BlockRecipientsMultiline.html create mode 100644 ang/crmMailing/BlockRecipientsMultiline.js diff --git a/ang/crmMailing/BlockMailing.html b/ang/crmMailing/BlockMailing.html index 3f6c7ca44aa..8b87a3dd0cd 100644 --- a/ang/crmMailing/BlockMailing.html +++ b/ang/crmMailing/BlockMailing.html @@ -36,9 +36,7 @@
-
-
-
+
+
+ +
+ + {{getRecipientCount()}} +
+
+ +
diff --git a/ang/crmMailing/BlockRecipientsMultiline.js b/ang/crmMailing/BlockRecipientsMultiline.js new file mode 100644 index 00000000000..f4431e48e92 --- /dev/null +++ b/ang/crmMailing/BlockRecipientsMultiline.js @@ -0,0 +1,5 @@ +(function(angular, $, _) { + angular.module('crmMailing').directive('crmMailingBlockRecipientsMultiline', function(crmMailingSimpleDirective) { + return crmMailingSimpleDirective('crmMailingBlockRecipientsMultiline', '~/crmMailing/BlockRecipientsMultiline.html'); + }); +})(angular, CRM.$, CRM._); diff --git a/ang/crmMailing/crmMailingRecipientsAutocomplete.component.js b/ang/crmMailing/crmMailingRecipientsAutocomplete.component.js index 4869ade8864..5e7424a0288 100644 --- a/ang/crmMailing/crmMailingRecipientsAutocomplete.component.js +++ b/ang/crmMailing/crmMailingRecipientsAutocomplete.component.js @@ -6,7 +6,7 @@ mailingId: '