From dc742067004cb2e47bed3d55387e2ed3fbfd72b6 Mon Sep 17 00:00:00 2001 From: colemanw Date: Wed, 2 Aug 2023 22:41:25 -0400 Subject: [PATCH] ScheduledReminders - Rewrite form to use metadata The complexity of the form lies in the fact that various fields have different meanings depending on the selected mapping type. For example the entity_value field might mean "Activity Type" or "Financial Account" or "Event Type" or "Event ID". The entity_status field also refers to different option lists, sometimes dependent on the selection of entity_value and it may or may not allow multiple values and it may or may not be required. The form also allows those fields to be pre-selected when embedded on an Event page, and the old Quickform HierSelect widget coped very poorly with all those scenarios. As a result the form employed a lot of javascript to handle specific hard-coded issues, and a few bespoke ajax callbacks. None of which worked perfectly. The postProcess & validate functions employed a sub-function called parseActionSchedule which was doing way too much work: 1. to detangle all the overcomplicated form values 2. preprocessing that really belonged in the BAO 3. to save a field-mapping (alarmingly this was happening during form validation AND postProcess!). This rewrite moves all of that logic into the BAO and getFields, allowing each mapping-type to set metadata appropriately (e.g. is a particular field required for this mapping type? does it support multiple values? does it have a different option list depending on another field selection?). This allowed me to delete the bespoke ajax callbacks and just call api4.getFields from the form. The new metadata also describes the relationship between fields (so when you update one field it knows which others to reload). --- CRM/Admin/Form.php | 25 +- CRM/Admin/Form/ScheduleReminders.php | 716 +++++------------- CRM/Admin/Page/AJAX.php | 47 -- CRM/Admin/Page/ScheduleReminders.php | 7 +- CRM/Contact/ActionMapping.php | 24 - CRM/Core/BAO/ActionSchedule.php | 62 +- CRM/Core/DAO.php | 29 +- CRM/Core/Form.php | 13 +- CRM/Core/Form/Renderer.php | 8 +- CRM/Core/Page/Basic.php | 12 + CRM/Core/xml/Menu/Admin.xml | 12 +- CRM/Event/ActionMapping/ByEvent.php | 2 +- .../Form/ManageEvent/ScheduleReminders.php | 8 +- CRM/SMS/BAO/Provider.php | 7 +- CRM/Utils/Date.php | 1 + Civi/ActionSchedule/MappingBase.php | 4 - Civi/ActionSchedule/MappingInterface.php | 11 - Civi/Api4/Generic/AbstractAction.php | 4 +- .../Provider/ActionScheduleSpecProvider.php | 12 + .../CRM/Admin/Form/ScheduleReminders.tpl | 579 +++++++------- .../CRM/Admin/Page/ScheduleReminders.tpl | 8 +- 21 files changed, 624 insertions(+), 967 deletions(-) diff --git a/CRM/Admin/Form.php b/CRM/Admin/Form.php index 1b85dbb986..873aa2961a 100644 --- a/CRM/Admin/Form.php +++ b/CRM/Admin/Form.php @@ -41,6 +41,12 @@ class CRM_Admin_Form extends CRM_Core_Form { */ protected $_BAOName; + /** + * Whether to use the legacy `retrieve` method or APIv4 to load values. + * @var string + */ + protected $retrieveMethod = 'retrieve'; + /** * Explicitly declare the form context. */ @@ -139,18 +145,25 @@ class CRM_Admin_Form extends CRM_Core_Form { } /** - * Retrieve entity from the database. - * - * TODO: Add flag to allow forms to opt-in to using API::get instead of BAO::retrieve + * Retrieve entity from the database using legacy retrieve method (default) or APIv4. * * @return array */ protected function retrieveValues(): array { $this->_values = []; if (isset($this->_id) && CRM_Utils_Rule::positiveInteger($this->_id)) { - $params = ['id' => $this->_id]; - // FIXME: `retrieve` function is deprecated :( - $this->_BAOName::retrieve($params, $this->_values); + if ($this->retrieveMethod === 'retrieve') { + $params = ['id' => $this->_id]; + $this->_BAOName::retrieve($params, $this->_values); + } + elseif ($this->retrieveMethod === 'api4') { + $this->_values = civicrm_api4($this->getDefaultEntity(), 'get', [ + 'where' => [['id', '=', $this->_id]], + ])->single(); + } + else { + throw new CRM_Core_Exception("Unknown retrieve method '$this->retrieveMethod' in " . get_class($this)); + } } return $this->_values; } diff --git a/CRM/Admin/Form/ScheduleReminders.php b/CRM/Admin/Form/ScheduleReminders.php index 972f606753..6b76ffa22c 100644 --- a/CRM/Admin/Form/ScheduleReminders.php +++ b/CRM/Admin/Form/ScheduleReminders.php @@ -18,36 +18,10 @@ use Civi\Token\TokenProcessor; /** - * This class generates form components for Scheduling Reminders. + * ActionSchedule (aka Scheduled Reminder) create/edit/delete form. */ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { - protected $_compId; - - /** - * @var CRM_Core_DAO_ActionSchedule - */ - private $_actionSchedule; - - /** - * @var int|string|null - */ - private $_mappingID; - - /** - * @return mixed - */ - public function getComponentID() { - return $this->_compId; - } - - /** - * @param mixed $compId - */ - public function setComponentID($compId): void { - $this->_compId = $compId; - } - /** * @return string */ @@ -56,236 +30,153 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { } /** - * Build the form object. - * - * @throws \CRM_Core_Exception + * Because `CRM_Mailing_BAO_Mailing::commonCompose` uses different fieldNames than `CRM_Core_DAO_ActionSchedule`. + * @var array */ - public function buildQuickForm(): void { - parent::buildQuickForm(); - $this->_mappingID = $mappingID = NULL; - $providersCount = CRM_SMS_BAO_Provider::activeProviderCount(); - $this->setContext(); - $isEvent = $this->getContext() === 'event'; + private static $messageFieldMap = [ + 'text_message' => 'body_text', + 'html_message' => 'body_html', + 'sms_text_message' => 'sms_body_text', + 'template' => 'msg_template_id', + 'SMStemplate' => 'sms_template_id', + ]; - if ($isEvent) { - $this->setComponentID(CRM_Utils_Request::retrieve('compId', 'Integer', $this)); - if (!CRM_Event_BAO_Event::checkPermission((int) $this->getComponentID(), CRM_Core_Permission::EDIT)) { + /** + * @throws \CRM_Core_Exception + */ + public function preProcess() { + parent::preProcess(); + // Pre-selected mapping_id and entity_value for embedded forms + if (CRM_Utils_Request::retrieve('mapping_id', 'Alphanumeric', $this, FALSE, NULL, 'GET')) { + $this->_values['mapping_id'] = $this->get('mapping_id'); + } + if (CRM_Utils_Request::retrieve('entity_value', 'CommaSeparatedIntegers', $this, FALSE, NULL, 'GET')) { + $this->_values['entity_value'] = explode(',', $this->get('entity_value')); + } + if (!empty($this->_values['mapping_id'])) { + $mapping = CRM_Core_BAO_ActionSchedule::getMapping($this->_values['mapping_id']); + $this->setPageTitle(ts('%1 Reminder', [1 => $mapping->getLabel()])); + } + // Allow pre-selected mapping to check its own permissions + if (!CRM_Core_Permission::check('administer CiviCRM data')) { + if (empty($mapping) || !$mapping->checkAccess($this->_values['entity_value'] ?? [])) { throw new CRM_Core_Exception(ts('You do not have permission to access this page.')); } } - elseif (!CRM_Core_Permission::check('administer CiviCRM')) { - throw new CRM_Core_Exception(ts('You do not have permission to access this page.')); - } + } - if ($this->_action & (CRM_Core_Action::DELETE)) { - $reminderName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_ActionSchedule', $this->_id, 'title'); - $this->assign('reminderName', $reminderName); + public function buildQuickForm(): void { + parent::buildQuickForm(); + + if ($this->getAction() == CRM_Core_Action::DELETE) { + $this->assign('reminderName', $this->_values['title']); return; } - elseif ($this->_action & (CRM_Core_Action::UPDATE)) { - $this->_mappingID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_ActionSchedule', $this->_id, 'mapping_id'); - } - if ($isEvent) { - $isTemplate = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $this->getComponentID(), 'is_template'); - $this->_mappingID = $isTemplate ? CRM_Event_ActionMapping::EVENT_TPL_MAPPING_ID : CRM_Event_ActionMapping::EVENT_NAME_MAPPING_ID; - } - if (!empty($_POST) && !empty($_POST['entity']) && empty($this->getContext())) { - $mappingID = $_POST['entity'][0]; - } - elseif ($this->_mappingID) { - $mappingID = $this->_mappingID; - if ($isEvent) { - $this->add('hidden', 'mappingID', $mappingID); + // Select fields whose option lists either control or are controlled by another field + $dynamicSelectFields = [ + 'mapping_id' => [], + 'entity_value' => [], + 'entity_status' => [], + 'recipient' => ['class' => 'twelve', 'placeholder' => ts('None')], + 'limit_to' => ['class' => 'twelve', 'placeholder' => ts('None')], + 'start_action_date' => ['class' => 'twelve'], + 'end_date' => ['class' => 'twelve'], + 'recipient_listing' => [], + ]; + // Load dynamic metadata based on current values + // Get values from submission (if rebuilding due to form errors) or saved reminder (in update mode) or preselected values from the preProcess fn. + $fieldMeta = Civi\Api4\ActionSchedule::getFields(FALSE) + ->setValues($this->_submitValues ?: $this->_values) + ->setAction('create') + ->setLoadOptions(['id', 'label', 'icon']) + ->addWhere('name', 'IN', array_keys($dynamicSelectFields)) + ->execute() + ->indexBy('name'); + $controlFields = []; + + // Add dynamic select fields + foreach ($fieldMeta as $field) { + $attr = $dynamicSelectFields[$field['name']] + ['multiple' => !empty($field['input_attrs']['multiple']), 'placeholder' => ts('Select')]; + if (!empty($field['input_attrs']['control_field'])) { + $controlFields[] = $attr['controlField'] = $field['input_attrs']['control_field']; } + $this->add('select2', $field['name'], $field['label'], $field['options'] ?: [], !empty($field['required']), $attr); } - $this->add( - 'text', - 'title', - ts('Title'), - CRM_Core_DAO::getAttribute('CRM_Core_DAO_ActionSchedule', 'title'), - TRUE - ); - - $mappings = CRM_Core_BAO_ActionSchedule::getMappings(); - $selectedMapping = $mappings[$mappingID ?: 1]; - $entityRecipientLabels = $selectedMapping::getRecipientTypes(); - $this->assign('entityMapping', json_encode( - CRM_Utils_Array::collectMethod('getEntityTable', $mappings) - )); - $this->assign('recipientMapping', json_encode( - array_combine(array_keys($entityRecipientLabels), array_keys($entityRecipientLabels)) - )); - - if (!$this->getContext()) { - $sel = &$this->add( - 'hierselect', - 'entity', - ts('Entity'), - [ - 'name' => 'entity[0]', - 'style' => 'vertical-align: top;', - ] - ); - $sel->setOptions([ - CRM_Utils_Array::collectMethod('getLabel', $mappings), - CRM_Core_BAO_ActionSchedule::getAllEntityValueLabels(), - CRM_Core_BAO_ActionSchedule::getAllEntityStatusLabels(), - ]); - - if (is_a($sel->_elements[1], 'HTML_QuickForm_select')) { - // make second selector a multi-select - - $sel->_elements[1]->setMultiple(TRUE); - $sel->_elements[1]->setSize(5); - } + // Javascript will reload metadata when these fields are changed + $this->assign('controlFields', array_values(array_unique($controlFields))); - if (is_a($sel->_elements[2], 'HTML_QuickForm_select')) { - // make third selector a multi-select - - $sel->_elements[2]->setMultiple(TRUE); - $sel->_elements[2]->setSize(5); - } + // These 2 are preselected if form is embedded + if ($this->get('entity_value')) { + $this->getElement('entity_value')->freeze(); } - else { - $mapping = CRM_Core_BAO_ActionSchedule::getMapping($this->_mappingID); - $options = $mapping->getStatusLabels($this->getComponentID()); - $attributes = ['multiple' => TRUE, 'class' => 'crm-select2 huge', 'placeholder' => $mapping->getStatusHeader()]; - $this->add('select', 'entity', ts('Recipient(s)'), $options, TRUE, $attributes); + if ($this->get('mapping_id') || $this->getAction() == CRM_Core_Action::UPDATE) { + $this->getElement('mapping_id')->freeze(); } - $this->assign('context', $this->getContext()); + // Pre-assigned mapping_id will cause the field to be hidden by the tpl + $this->assign('mappingId', $this->get('mapping_id')); - //reminder_interval - $this->add('number', 'start_action_offset', ts('When (trigger date)'), ['class' => 'six', 'min' => 0]); - $this->addRule('start_action_offset', ts('Value should be a positive number'), 'positiveInteger'); + // Units fields will be pluralized by javascript + $this->addField('start_action_unit', ['placeholder' => FALSE])->setAttribute('class', 'crm-form-select'); + $this->addField('repetition_frequency_unit', ['placeholder' => FALSE])->setAttribute('class', 'crm-form-select'); + $this->addField('end_frequency_unit', ['placeholder' => FALSE])->setAttribute('class', 'crm-form-select'); + // Data for js pluralization + $this->assign('recurringFrequencyOptions', [ + 'plural' => CRM_Utils_Array::makeNonAssociative(CRM_Core_SelectValues::getRecurringFrequencyUnits(2)), + 'single' => CRM_Utils_Array::makeNonAssociative(CRM_Core_SelectValues::getRecurringFrequencyUnits()), + ]); - $isActive = ts('Scheduled Reminder Active'); - $recordActivity = ts('Record activity for automated email'); + $this->addField('title', [], TRUE); + $this->addField('absolute_date', [], FALSE, FALSE); + $this->addField('start_action_offset', ['class' => 'four']); + $this->addField('start_action_condition', ['placeholder' => FALSE])->setAttribute('class', 'crm-form-select'); + $this->addField('record_activity', ['type' => 'advcheckbox']); + $this->addField('is_repeat', ['type' => 'advcheckbox']); + $this->addField('repetition_frequency_interval', ['label' => ts('Every'), 'class' => 'four']); + $this->addField('end_frequency_interval', ['label' => ts('Until'), 'class' => 'four']); + $this->addField('end_action', ['placeholder' => FALSE])->setAttribute('class', 'crm-form-select'); + $this->addField('effective_start_date', ['label' => ts('Effective From')], FALSE, FALSE); + $this->addField('effective_end_date', ['label' => ts('To')], FALSE, FALSE); + $this->addField('is_active', ['type' => 'advcheckbox']); + $this->addAutocomplete('recipient_manual', ts('Manual Recipients'), ['select' => ['multiple' => TRUE]]); + $this->addAutocomplete('group_id', ts('Group'), ['entity' => 'Group', 'select' => ['minimumInputLength' => 0]]); + + // From email address (optional, defaults to domain email) + $domainDefault = CRM_Core_BAO_Domain::getNameAndEmail(TRUE); + $this->addField('from_name', ['placeholder' => $domainDefault[0] ?? '', 'class' => 'big']); + $this->addField('from_email', ['placeholder' => $domainDefault[1] ?? '', 'class' => 'big']); + + // Relative/absolute date toggle (not a real field, just a widget for the form) + $this->add('select', 'absolute_or_relative_date', ts('When (trigger date)'), ['relative' => ts('Relative Date'), 'absolute' => ts('Choose Date')], TRUE); + + // SMS-only fields + $providersCount = CRM_SMS_BAO_Provider::activeProviderCount(); + $this->assign('sms', $providersCount); if ($providersCount) { - $this->assign('sms', $providersCount); - $recordActivity = ts('Record activity for automated email or SMS'); - $options = CRM_Core_OptionGroup::values('msg_mode'); - $this->add('select', 'mode', ts('Send as'), $options); - - $providers = CRM_SMS_BAO_Provider::getProviders(NULL, NULL, TRUE, 'is_default desc'); - - $providerSelect = []; - foreach ($providers as $provider) { - $providerSelect[$provider['id']] = $provider['title']; - } - $this->add('select', 'sms_provider_id', ts('SMS Provider'), $providerSelect, TRUE); - } - - foreach (CRM_Core_SelectValues::getRecurringFrequencyUnits() as $val => $label) { - $freqUnitsDisplay[$val] = ts('%1(s)', [1 => $label]); - } - - $this->add('datepicker', 'absolute_date', ts('Start Date'), [], FALSE, ['time' => FALSE]); - - //reminder_frequency - $this->add('select', 'start_action_unit', ts('Frequency'), $freqUnitsDisplay, TRUE); - - $condition = [ - 'before' => ts('before'), - 'after' => ts('after'), - ]; - //reminder_action - $this->add('select', 'start_action_condition', ts('Action Condition'), $condition); - - $this->add('select', 'start_action_date', ts('Date Field'), $selectedMapping->getDateFields(), TRUE); - - $this->addElement('checkbox', 'record_activity', $recordActivity); - - $this->addElement('checkbox', 'is_repeat', ts('Repeat'), - NULL, ['onchange' => "return showHideByValue('is_repeat',true,'repeatFields','table-row','radio',false);"] - ); - - $this->add('select', 'repetition_frequency_unit', ts('every'), $freqUnitsDisplay); - $this->add('number', 'repetition_frequency_interval', ts('every'), ['class' => 'six', 'min' => 0]); - $this->addRule('repetition_frequency_interval', ts('Value should be a positive number'), 'positiveInteger'); - - $this->add('select', 'end_frequency_unit', ts('until'), $freqUnitsDisplay); - $this->add('number', 'end_frequency_interval', ts('until'), ['class' => 'six', 'min' => 0]); - $this->addRule('end_frequency_interval', ts('Value should be a positive number'), 'positiveInteger'); - - $this->add('select', 'end_action', ts('Repetition Condition'), $condition, TRUE); - $this->add('select', 'end_date', ts('Date Field'), $selectedMapping->getDateFields(), TRUE); - - $this->add('text', 'from_name', ts('From Name')); - $this->add('text', 'from_email', ts('From Email')); - - $this->add('datepicker', 'effective_start_date', ts('Effective start date'), [], FALSE); - $this->add('datepicker', 'effective_end_date', ts('Effective end date'), [], FALSE); - - $recipientListingOptions = []; - - $limitOptions = ['' => ts('Neither')] + CRM_Core_BAO_ActionSchedule::buildOptions('limit_to'); - - $recipientLabels = ['activity' => ts('Recipients'), 'other' => ts('Limit or Add Recipients')]; - $this->assign('recipientLabels', $recipientLabels); - - $this->add('select', 'limit_to', ts('Limit Options'), $limitOptions, FALSE, ['onChange' => "showHideByValue('limit_to','','recipient', 'select','select',true);"]); - - $this->add('select', 'recipient', $recipientLabels['other'], $entityRecipientLabels, - FALSE, ['onchange' => "showHideByValue('recipient','manual','recipientManual','table-row','select',false); showHideByValue('recipient','group','recipientGroup','table-row','select',false);"] - ); - - if (!empty($this->_submitValues['recipient_listing'])) { - if ($this->getContext()) { - $recipientListingOptions = CRM_Core_BAO_ActionSchedule::getRecipientListing($this->_mappingID, $this->_submitValues['recipient']); - } - else { - $recipientListingOptions = CRM_Core_BAO_ActionSchedule::getRecipientListing($_POST['entity'][0], $_POST['recipient']); - } - } - elseif (!empty($this->_values['recipient_listing'])) { - $recipientListingOptions = CRM_Core_BAO_ActionSchedule::getRecipientListing($this->_values['mapping_id'], $this->_values['recipient']); + $this->addField('mode', ['placeholder' => FALSE, 'option_url' => FALSE], TRUE)->setAttribute('class', 'crm-form-select'); + $this->addField('sms_provider_id'); } - $this->add('select', 'recipient_listing', ts('Recipient Roles'), $recipientListingOptions, FALSE, - ['multiple' => TRUE, 'class' => 'crm-select2 huge', 'placeholder' => TRUE]); - - $this->addEntityRef('recipient_manual_id', ts('Manual Recipients'), ['multiple' => TRUE, 'create' => TRUE]); - - $this->add('select', 'group_id', ts('Group'), - CRM_Core_PseudoConstant::nestedGroup(), FALSE, ['class' => 'crm-select2 huge'] - ); - - // multilingual only options + // Multilingual-only fields $multilingual = CRM_Core_I18n::isMultilingual(); + $this->assign('multilingual', $multilingual); if ($multilingual) { - $smarty = CRM_Core_Smarty::singleton(); - $smarty->assign('multilingual', $multilingual); - - $languages = CRM_Core_I18n::languages(TRUE); - $languageFilter = $languages + [CRM_Core_I18n::NONE => ts('Contacts with no preferred language')]; - $element = $this->add('select', 'filter_contact_language', ts('Recipients language'), $languageFilter, FALSE, - ['multiple' => TRUE, 'class' => 'crm-select2', 'placeholder' => TRUE]); - - $communicationLanguage = [ - '' => ts('System default language'), - CRM_Core_I18n::AUTO => ts('Follow recipient preferred language'), - ]; - $communicationLanguage = $communicationLanguage + $languages; - $this->add('select', 'communication_language', ts('Communication language'), $communicationLanguage); + $this->addField('filter_contact_language', ['placeholder' => ts('Any language')]); + $this->addField('communication_language', ['placeholder' => 'System default language']); } + // Message fields + $this->addField('subject'); CRM_Mailing_BAO_Mailing::commonCompose($this); - $this->add('text', 'subject', ts('Subject'), - CRM_Core_DAO::getAttribute('CRM_Core_DAO_ActionSchedule', 'subject') - ); - - $this->add('checkbox', 'is_active', $isActive); - $this->addFormRule([__CLASS__, 'formRule'], $this); - - $this->setPageTitle(ts('Scheduled Reminder')); } /** * Global form rule. * - * @param array $fields + * @param array $values * The input form values. * @param array $files * @param CRM_Admin_Form_ScheduleReminders $self @@ -295,129 +186,70 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { * @throws \CRM_Core_Exception * @throws \Civi\API\Exception\UnauthorizedException */ - public static function formRule(array $fields, $files, $self) { + public static function formRule(array $values, $files, $self) { $errors = []; - if ((array_key_exists(1, $fields['entity']) && $fields['entity'][1][0] === 0) || - (array_key_exists(2, $fields['entity']) && $fields['entity'][2][0] == 0) - ) { - $errors['entity'] = ts('Please select appropriate value'); - } - - $mode = $fields['mode'] ?? FALSE; - if (!empty($fields['is_active']) && - CRM_Utils_System::isNull($fields['subject']) && (!$mode || $mode !== 'SMS') - ) { - $errors['subject'] = ts('Subject is a required field.'); - } - if (!empty($fields['is_active']) && - CRM_Utils_System::isNull(trim(strip_tags($fields['html_message']))) && (!$mode || $mode !== 'SMS') - ) { - $errors['html_message'] = ts('The HTML message is a required field.'); - } - - if (!empty($mode) && ($mode === 'SMS' || $mode === 'User_Preference') && !empty($fields['is_active']) && - CRM_Utils_System::isNull(trim(strip_tags($fields['sms_text_message']))) - ) { - $errors['sms_text_message'] = ts('The SMS message is a required field.'); - } - - if (empty($self->getContext()) && CRM_Utils_System::isNull($fields['entity'][1] ?? NULL)) { - $errors['entity'] = ts('Please select entity value'); - } - - if (!CRM_Utils_System::isNull($fields['absolute_date']) && !CRM_Utils_System::isNull($fields['start_action_offset'])) { - $errors['absolute_date'] = ts('Only an absolute date or a relative date or time can be entered, not both.'); - } - if (!CRM_Utils_System::isNull($fields['absolute_date'])) { - if ($fields['absolute_date'] < date('Y-m-d')) { - $errors['absolute_date'] = ts('Absolute date cannot be earlier than the current time.'); + $values = self::normalizeFormValues($values); + $fieldMeta = Civi\Api4\ActionSchedule::getFields(FALSE) + ->setValues($values) + ->setAction('create') + ->execute() + ->indexBy('name'); + + foreach ($fieldMeta as $fieldName => $fieldInfo) { + $fieldValue = $values[$fieldName] ?? NULL; + $formFieldName = array_search($fieldName, self::$messageFieldMap) ?: $fieldName; + // TODO: This snippet could be an api action e.g. `civicrm_api4('ActionSchedule', 'validate'...)` + if ($fieldValue === NULL || $fieldValue === '' || $fieldValue === []) { + if ( + (!empty($fieldInfo['required']) && !isset($fieldInfo['default_value'])) || + (!empty($fieldInfo['required_if']) && \Civi\Api4\Generic\AbstractAction::evaluateCondition($fieldInfo['required_if'], ['values' => $values])) + ) { + $errors[$formFieldName] = ts('%1 is a required field.', [1 => $fieldInfo['label']]); + } } - } - else { - if (CRM_Utils_System::isNull($fields['start_action_offset'])) { - $errors['start_action_offset'] = ts('Start Action Offset must be filled in or Absolute Date set'); + elseif (empty($fieldInfo['input_attrs']['multiple']) && is_array($fieldValue) && count($fieldValue) > 1) { + $errors[$formFieldName] = ts('Please select only 1 %1.', [1 => $fieldInfo['label']]); } } - if (!CRM_Utils_Rule::email($fields['from_email']) && (!$mode || $mode != 'SMS')) { - $errors['from_email'] = ts('Please enter a valid email address.'); - } - $recipientKind = [ - 'participant_role' => [ - 'name' => 'participant role', - 'target_id' => 'recipient_listing', - ], - 'manual' => [ - 'name' => 'recipient', - 'target_id' => 'recipient_manual_id', - ], - ]; - if ($fields['limit_to'] && array_key_exists($fields['recipient'], $recipientKind) && empty($fields[$recipientKind[$fields['recipient']]['target_id']])) { - $errors[$recipientKind[$fields['recipient']]['target_id']] = ts('If "Also include" or "Limit to" are selected, you must specify at least one %1', [1 => $recipientKind[$fields['recipient']]['name']]); - } - //CRM-21523 - if (!empty($fields['is_repeat']) && - (empty($fields['repetition_frequency_interval']) || ($fields['end_frequency_interval'] == NULL)) - ) { - $errors['is_repeat'] = ts('If you are enabling repetition you must indicate the frequency and ending term.'); + // Suppress irrelevant error messages depending on chosen date mode + if ($values['absolute_or_relative_date'] === 'absolute') { + unset($errors['start_action_offset'], $errors['start_action_unit'], $errors['start_action_condition'], $errors['start_action_date']); } - - $self->_actionSchedule = $self->parseActionSchedule($fields); - if ($self->_actionSchedule->mapping_id) { - $mapping = CRM_Core_BAO_ActionSchedule::getMapping($self->_actionSchedule->mapping_id); - CRM_Utils_Array::extend($errors, $mapping->validateSchedule($self->_actionSchedule)); + else { + unset($errors['absolute_date']); } - return empty($errors) ? TRUE : $errors; + return $errors ?: TRUE; } /** * @return array */ public function setDefaultValues() { + $defaults = $this->_values ?? []; if ($this->_action & CRM_Core_Action::ADD) { $defaults['is_active'] = 1; - $defaults['mode'] = 'Email'; $defaults['record_activity'] = 1; - $defaults['start_action_unit'] = 'hour'; } else { - $defaults = $this->_values; - $entityValue = explode(CRM_Core_DAO::VALUE_SEPARATOR, CRM_Utils_Array::value('entity_value', $defaults)); - $entityStatus = explode(CRM_Core_DAO::VALUE_SEPARATOR, CRM_Utils_Array::value('entity_status', $defaults)); - if (empty($this->getContext())) { - $defaults['entity'][0] = $defaults['mapping_id'] ?? NULL; - $defaults['entity'][1] = $entityValue; - $defaults['entity'][2] = $entityStatus; - } - else { - $defaults['entity'] = $entityStatus; + // Set values for the fields added by CRM_Mailing_BAO_Mailing::commonCompose + foreach (self::$messageFieldMap as $messageFieldName => $actionScheduleFieldName) { + $defaults[$messageFieldName] = $defaults[$actionScheduleFieldName] ?? NULL; } + $defaults['absolute_or_relative_date'] = !empty($defaults['absolute_date']) ? 'absolute' : 'relative'; - $recipientListing = $defaults['recipient_listing'] ?? NULL; - if ($recipientListing) { - $defaults['recipient_listing'] = explode(CRM_Core_DAO::VALUE_SEPARATOR, - $recipientListing - ); - } - $defaults['text_message'] = $defaults['body_text'] ?? NULL; - $defaults['html_message'] = $defaults['body_html'] ?? NULL; - $defaults['sms_text_message'] = $defaults['sms_body_text'] ?? NULL; - $defaults['template'] = $defaults['msg_template_id'] ?? NULL; - $defaults['SMStemplate'] = $defaults['sms_template_id'] ?? NULL; - if (!empty($defaults['group_id'])) { - $defaults['recipient'] = 'group'; - } - elseif (!empty($defaults['recipient_manual'])) { - $defaults['recipient'] = 'manual'; - $defaults['recipient_manual_id'] = $defaults['recipient_manual']; - } - $contactLanguage = $defaults['filter_contact_language'] ?? NULL; - if ($contactLanguage) { - $defaults['filter_contact_language'] = explode(CRM_Core_DAO::VALUE_SEPARATOR, $contactLanguage); + // This is weird - the form used to nullify `recipient` if it was 'group' or 'manual', + // but I see no good reason for doing that. + if (empty($defaults['recipient'])) { + if (!empty($defaults['group_id'])) { + $defaults['recipient'] = 'group'; + } + elseif (!empty($defaults['recipient_manual'])) { + $defaults['recipient'] = 'manual'; + } } } - return $defaults; } @@ -428,198 +260,74 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { if ($this->_action & CRM_Core_Action::DELETE) { CRM_Core_BAO_ActionSchedule::deleteRecord(['id' => $this->_id]); CRM_Core_Session::setStatus(ts('Selected Reminder has been deleted.'), ts('Record Deleted'), 'success'); - if ($this->getContext() === 'event' && $this->getComponentID()) { - $url = CRM_Utils_System::url('civicrm/event/manage/reminder', - "reset=1&action=browse&id=" . $this->getComponentID() . "&component=" . $this->getContext() . "&setTab=1" - ); - $session = CRM_Core_Session::singleton(); - $session->pushUserContext($url); - } return; } $values = $this->controller->exportValues($this->getName()); - if (empty($this->_actionSchedule)) { - $bao = $this->parseActionSchedule($values)->save(); - } - else { - $bao = $this->_actionSchedule->save(); + if ($this->_action & CRM_Core_Action::UPDATE) { + $values['id'] = $this->_id; } + $values = self::normalizeFormValues($values); + self::saveMessageTemplates($values); + + $bao = CRM_Core_BAO_ActionSchedule::writeRecord($values); + // we need to set this on the form so that hooks can identify the created entity $this->set('id', $bao->id); - $status = ts("Your new Reminder titled %1 has been saved.", - [1 => "{$values['title']}"] - ); + $status = ts('Reminder "%1" has been saved.', [1 => $values['title']]); - if ($this->_action) { - if ($this->_action & CRM_Core_Action::UPDATE) { - $status = ts("Your Reminder titled %1 has been updated.", - [1 => "{$values['title']}"] - ); - } - - if ($this->getContext() === 'event' && $this->getComponentID()) { - $url = CRM_Utils_System::url('civicrm/event/manage/reminder', "reset=1&action=browse&id=" . $this->getComponentID() . "&component=" . $this->getContext() . "&setTab=1"); - $session = CRM_Core_Session::singleton(); - $session->pushUserContext($url); - } - } CRM_Core_Session::setStatus($status, ts('Saved'), 'success'); } - /** - * FIXME: This function shouldn't exist. It takes an overcomplicated form - * and maps the wonky form values to the bao object to be saved. - * - * @param array $values - * - * @return CRM_Core_DAO_ActionSchedule - * @throws \CRM_Core_Exception - * @throws \Civi\API\Exception\UnauthorizedException - */ - public function parseActionSchedule($values) { - $params = []; - - $keys = [ - 'title', - 'subject', - 'absolute_date', - 'group_id', - 'limit_to', - 'mode', - 'sms_provider_id', - 'from_name', - 'from_email', - ]; - foreach ($keys as $key) { - $params[$key] = $values[$key] ?? NULL; - } - - // set boolean fields to false if not set. - foreach (['record_activity', 'is_repeat', 'is_active'] as $boolFieldName) { - $params[$boolFieldName] = $values[$boolFieldName] ?? 0; - } - - $moreKeys = [ - 'start_action_offset', - 'start_action_unit', - 'start_action_condition', - 'start_action_date', - 'repetition_frequency_unit', - 'repetition_frequency_interval', - 'end_frequency_unit', - 'end_frequency_interval', - 'end_action', - 'end_date', - 'effective_end_date', - 'effective_start_date', - ]; - - if (empty($params['absolute_date'])) { - $params['absolute_date'] = 'null'; - } - foreach ($moreKeys as $mkey) { - if ($params['absolute_date'] !== 'null' && CRM_Utils_String::startsWith($mkey, 'start_action')) { - $params[$mkey] = 'null'; - continue; + private static function normalizeFormValues(array $values): array { + // Ensure multivalued fields are formatted as an array + $serialized = \Civi\Api4\ActionSchedule::getFields(FALSE) + ->addWhere('serialize', 'IS NOT EMPTY') + ->execute()->column('name'); + foreach ($serialized as $fieldName) { + if (isset($values[$fieldName]) && is_string($values[$fieldName])) { + $values[$fieldName] = explode(',', $values[$fieldName]); } - $params[$mkey] = $values[$mkey] ?? NULL; } - $params['body_text'] = $values['text_message'] ?? NULL; - $params['sms_body_text'] = $values['sms_text_message'] ?? NULL; - $params['body_html'] = $values['html_message'] ?? NULL; - - if (($values['recipient'] ?? NULL) === 'manual') { - $params['recipient_manual'] = $values['recipient_manual_id'] ?? NULL; - $params['group_id'] = $params['recipient'] = $params['recipient_listing'] = 'null'; - } - elseif (($values['recipient'] ?? NULL) === 'group') { - $params['group_id'] = $values['group_id']; - $params['recipient_manual'] = $params['recipient'] = $params['recipient_listing'] = 'null'; - } - elseif (isset($values['recipient_listing']) && !empty($values['limit_to']) && !CRM_Utils_System::isNull($values['recipient_listing'])) { - $params['recipient'] = $values['recipient'] ?? NULL; - $params['recipient_listing'] = implode(CRM_Core_DAO::VALUE_SEPARATOR, - CRM_Utils_Array::value('recipient_listing', $values) - ); - $params['group_id'] = $params['recipient_manual'] = 'null'; + // Absolute or relative date + if ($values['absolute_or_relative_date'] === 'absolute') { + $values['start_action_offset'] = $values['start_action_unit'] = $values['start_action_condition'] = $values['start_action_date'] = NULL; } else { - $params['recipient'] = $values['recipient'] ?? NULL; - $params['group_id'] = $params['recipient_manual'] = $params['recipient_listing'] = 'null'; + $values['absolute_date'] = NULL; } - if (!empty($this->_mappingID) && !empty($this->getComponentID())) { - $params['mapping_id'] = $this->_mappingID; - $params['entity_value'] = $this->getComponentID(); - $params['entity_status'] = implode(CRM_Core_DAO::VALUE_SEPARATOR, $values['entity']); - } - else { - $params['mapping_id'] = $values['entity'][0]; - if ($params['mapping_id'] == 1) { - $params['limit_to'] = 1; - } - - $entity_value = CRM_Utils_Array::value(1, $values['entity'], []); - $entity_status = CRM_Utils_Array::value(2, $values['entity'], []); - $params['entity_value'] = implode(CRM_Core_DAO::VALUE_SEPARATOR, $entity_value); - $params['entity_status'] = implode(CRM_Core_DAO::VALUE_SEPARATOR, $entity_status); - } - - if (empty($values['is_repeat'])) { - $params['repetition_frequency_unit'] = 'null'; - $params['repetition_frequency_interval'] = 'null'; - $params['end_frequency_unit'] = 'null'; - $params['end_frequency_interval'] = 'null'; - $params['end_action'] = 'null'; - $params['end_date'] = 'null'; - } - - // multilingual options - $params['filter_contact_language'] = CRM_Utils_Array::value('filter_contact_language', $values, []); - $params['filter_contact_language'] = implode(CRM_Core_DAO::VALUE_SEPARATOR, $params['filter_contact_language']); - $params['communication_language'] = $values['communication_language'] ?? NULL; - - if ($this->_action & CRM_Core_Action::UPDATE) { - $params['id'] = $this->_id; - } - elseif ($this->_action & CRM_Core_Action::ADD) { - // we do this only once, so name never changes - $params['name'] = CRM_Utils_String::munge($params['title'], '_', 64); + // Convert values for the fields added by CRM_Mailing_BAO_Mailing::commonCompose + foreach (self::$messageFieldMap as $messageFieldName => $actionScheduleFieldName) { + $values[$actionScheduleFieldName] = $values[$messageFieldName] ?? NULL; } + return $values; + } - $modePrefixes = ['Mail' => NULL, 'SMS' => 'SMS']; + /** + * Add or update message templates (for both email & sms, according to mode) + * + * @param array $params + * @throws CRM_Core_Exception + */ + private static function saveMessageTemplates(array &$params): void { + $mode = $params['mode'] ?? 'Email'; + $modePrefixes = ['msg' => '', 'sms' => 'SMS']; - if ($params['mode'] === 'Email' || empty($params['sms_provider_id'])) { - unset($modePrefixes['SMS']); + if ($mode === 'Email' || empty($params['sms_provider_id'])) { + unset($modePrefixes['sms']); } - elseif ($params['mode'] === 'SMS') { - unset($modePrefixes['Mail']); + elseif ($mode === 'SMS') { + unset($modePrefixes['msg']); } - //TODO: handle postprocessing of SMS and/or Email info based on $modePrefixes - - $composeFields = [ - 'template', - 'saveTemplate', - 'updateTemplate', - 'saveTemplateName', - ]; $msgTemplate = NULL; - //mail template is composed - - foreach ($modePrefixes as $prefix) { - $composeParams = []; - foreach ($composeFields as $key) { - $key = $prefix . $key; - if (!empty($values[$key])) { - $composeParams[$key] = $values[$key]; - } - } - if (!empty($composeParams[$prefix . 'updateTemplate'])) { + foreach ($modePrefixes as $mode => $prefix) { + // Update existing template + if (!empty($params[$prefix . 'updateTemplate']) && !empty($params[$prefix . 'template'])) { $templateParams = ['is_active' => TRUE]; if ($prefix === 'SMS') { $templateParams += [ @@ -634,12 +342,13 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { 'msg_subject' => $params['subject'], ]; } - $templateParams['id'] = $values[$prefix . 'template']; + $templateParams['id'] = $params[$prefix . 'template']; $msgTemplate = CRM_Core_BAO_MessageTemplate::add($templateParams); } - if (!empty($composeParams[$prefix . 'saveTemplate'])) { + // Save new template + if (!empty($params[$prefix . 'saveTemplate'])) { $templateParams = ['is_active' => TRUE]; if ($prefix === 'SMS') { $templateParams += [ @@ -654,32 +363,15 @@ class CRM_Admin_Form_ScheduleReminders extends CRM_Admin_Form { 'msg_subject' => $params['subject'], ]; } - $templateParams['msg_title'] = $composeParams[$prefix . 'saveTemplateName']; + $templateParams['msg_title'] = $params[$prefix . 'saveTemplateName']; $msgTemplate = CRM_Core_BAO_MessageTemplate::add($templateParams); } - if ($prefix === 'SMS') { - if (isset($msgTemplate->id)) { - $params['sms_template_id'] = $msgTemplate->id; - } - else { - $params['sms_template_id'] = $values['SMStemplate'] ?? NULL; - } - } - else { - if (isset($msgTemplate->id)) { - $params['msg_template_id'] = $msgTemplate->id; - } - else { - $params['msg_template_id'] = $values['template'] ?? NULL; - } + if (isset($msgTemplate->id)) { + $params[$mode . '_template_id'] = $msgTemplate->id; } } - - $actionSchedule = new CRM_Core_DAO_ActionSchedule(); - $actionSchedule->copyValues($params); - return $actionSchedule; } /** diff --git a/CRM/Admin/Page/AJAX.php b/CRM/Admin/Page/AJAX.php index aa1ebccdd9..0cc9bbbb8c 100644 --- a/CRM/Admin/Page/AJAX.php +++ b/CRM/Admin/Page/AJAX.php @@ -279,53 +279,6 @@ class CRM_Admin_Page_AJAX { CRM_Core_Page_AJAX::returnJsonResponse($ret); } - /** - * Get a list of mappings. - * - * This appears to be only used by scheduled reminders. - */ - public static function mappingList() { - if (empty($_GET['mappingID'])) { - CRM_Utils_JSON::output(['status' => 'error', 'error_msg' => 'required params missing.']); - } - - $mapping = CRM_Core_BAO_ActionSchedule::getMapping($_GET['mappingID']); - $dateFieldLabels = $mapping ? $mapping->getDateFields() : []; - - $entityRecipientLabels = $mapping ? $mapping->getRecipientTypes() : []; - $recipientMapping = array_combine(array_keys($entityRecipientLabels), array_keys($entityRecipientLabels)); - - $output = [ - 'sel4' => CRM_Utils_Array::makeNonAssociative($dateFieldLabels), - 'sel5' => CRM_Utils_Array::makeNonAssociative($entityRecipientLabels), - 'recipientMapping' => $recipientMapping, - ]; - - CRM_Utils_JSON::output($output); - } - - /** - * (Scheduled Reminders) Get the list of possible recipient filters. - * - * Ex: GET /civicrm/ajax/recipientListing?mappingID=contribpage&recipientType= - */ - public static function recipientListing() { - $mappingID = filter_input(INPUT_GET, 'mappingID', FILTER_VALIDATE_REGEXP, [ - 'options' => [ - 'regexp' => '/^[a-zA-Z0-9_\-]+$/', - ], - ]); - $recipientType = filter_input(INPUT_GET, 'recipientType', FILTER_VALIDATE_REGEXP, [ - 'options' => [ - 'regexp' => '/^[a-zA-Z0-9_\-]+$/', - ], - ]); - - CRM_Utils_JSON::output([ - 'recipients' => CRM_Utils_Array::makeNonAssociative(CRM_Core_BAO_ActionSchedule::getRecipientListing($mappingID, $recipientType)), - ]); - } - /** * Outputs one branch in the tag tree * diff --git a/CRM/Admin/Page/ScheduleReminders.php b/CRM/Admin/Page/ScheduleReminders.php index c6a3415ebb..8952fc8ce5 100644 --- a/CRM/Admin/Page/ScheduleReminders.php +++ b/CRM/Admin/Page/ScheduleReminders.php @@ -72,12 +72,6 @@ class CRM_Admin_Page_ScheduleReminders extends CRM_Core_Page_Basic { * @throws \CRM_Core_Exception */ public function browse($action = NULL) { - //CRM-16777: Do not permit access to user, for page 'Administer->Communication->Schedule Reminder', - //when do not have 'administer CiviCRM' permission. - if (!CRM_Core_Permission::check('administer CiviCRM')) { - CRM_Core_Error::statusBounce(ts('You do not have permission to access this page.')); - } - // Get list of configured reminders $reminderList = CRM_Core_BAO_ActionSchedule::getList(); @@ -103,6 +97,7 @@ class CRM_Admin_Page_ScheduleReminders extends CRM_Core_Page_Basic { } $this->assign('rows', $reminderList); + $this->assign('addNewLink', $this->getLinkPath('add')); } } diff --git a/CRM/Contact/ActionMapping.php b/CRM/Contact/ActionMapping.php index d8d0ce0b22..52dea49e97 100644 --- a/CRM/Contact/ActionMapping.php +++ b/CRM/Contact/ActionMapping.php @@ -77,30 +77,6 @@ class CRM_Contact_ActionMapping extends \Civi\ActionSchedule\MappingBase { 'modified_date', ]; - /** - * Determine whether a schedule based on this mapping is sufficiently - * complete. - * - * @param \CRM_Core_DAO_ActionSchedule $schedule - * @return array - * Array (string $code => string $message). - * List of error messages. - */ - public function validateSchedule($schedule): array { - $errors = []; - if (CRM_Utils_System::isNull($schedule->entity_value) || $schedule->entity_value === '0') { - $errors['entity'] = ts('Please select a specific date field.'); - } - elseif (count(CRM_Utils_Array::explodePadded($schedule->entity_value)) > 1) { - $errors['entity'] = ts('You may only select one contact field per reminder'); - } - elseif (CRM_Utils_System::isNull($schedule->entity_status) || $schedule->entity_status === '0') { - $errors['entity'] = ts('Please select whether the reminder is sent each year.'); - } - - return $errors; - } - /** * Generate a query to locate recipients who match the given * schedule. diff --git a/CRM/Core/BAO/ActionSchedule.php b/CRM/Core/BAO/ActionSchedule.php index c38607f793..ae0783a7f1 100644 --- a/CRM/Core/BAO/ActionSchedule.php +++ b/CRM/Core/BAO/ActionSchedule.php @@ -130,7 +130,7 @@ class CRM_Core_BAO_ActionSchedule extends CRM_Core_DAO_ActionSchedule implements if (!$values['mapping_id']) { return []; } - return self::getMapping($values['mapping_id'])->getStatusLabels((array) $values['entity_value']); + return self::getMapping($values['mapping_id'])->getStatusLabels($values['entity_value']); } /** @@ -174,42 +174,6 @@ class CRM_Core_BAO_ActionSchedule extends CRM_Core_DAO_ActionSchedule implements return [CRM_Core_I18n::AUTO => ts('Follow recipient preferred language')] + $languages; } - /** - * For each entity, get a list of entity-value labels. - * - * @return array - * Ex: $entityValueLabels[$mappingId][$valueId] = $valueLabel. - * @throws CRM_Core_Exception - */ - public static function getAllEntityValueLabels() { - $entityValueLabels = []; - foreach (CRM_Core_BAO_ActionSchedule::getMappings() as $mapping) { - $entityValueLabels[$mapping->getId()] = $mapping->getValueLabels(); - $valueLabel = ['- ' . strtolower($mapping->getValueHeader()) . ' -']; - $entityValueLabels[$mapping->getId()] = $valueLabel + $entityValueLabels[$mapping->getId()]; - } - return $entityValueLabels; - } - - /** - * For each entity, get a list of entity-status labels. - * - * @return array - * Ex: $entityValueLabels[$mappingId][$valueId][$statusId] = $statusLabel. - */ - public static function getAllEntityStatusLabels() { - $entityValueLabels = self::getAllEntityValueLabels(); - $entityStatusLabels = []; - foreach (CRM_Core_BAO_ActionSchedule::getMappings() as $mapping) { - $statusLabel = ['- ' . strtolower($mapping->getStatusHeader()) . ' -']; - $entityStatusLabels[$mapping->getId()] = $entityValueLabels[$mapping->getId()]; - foreach ($entityStatusLabels[$mapping->getId()] as $kkey => & $vval) { - $vval = $statusLabel + $mapping->getStatusLabels($kkey); - } - } - return $entityStatusLabels; - } - /** * Retrieve list of Scheduled Reminders. * @@ -300,9 +264,29 @@ FROM civicrm_action_schedule cas */ public static function self_hook_civicrm_pre(\Civi\Core\Event\PreEvent $event) { if (in_array($event->action, ['create', 'edit'])) { - if (isset($event->params['limit_to']) && in_array($event->params['limit_to'], [0, '0', FALSE], TRUE)) { + $values =& $event->params; + if (isset($values['limit_to']) && in_array($values['limit_to'], [0, '0', FALSE], TRUE)) { CRM_Core_Error::deprecatedWarning('Deprecated value "0" is no longer a valid option for ActionSchedule.limit_to; changed to "2".'); - $event->params['limit_to'] = 2; + $values['limit_to'] = 2; + } + $recipient = $values['recipient'] ?? NULL; + if ($recipient && $recipient !== 'group') { + $values['group_id'] = ''; + } + elseif ($recipient && $recipient !== 'manual') { + $values['recipient_manual'] = ''; + } + if ($recipient === 'group' || $recipient === 'manual') { + $values['recipient_listing'] = ''; + } + // When repeat is disabled, wipe all related fields + if (isset($values['is_repeat']) && !$values['is_repeat']) { + $values['repetition_frequency_unit'] = ''; + $values['repetition_frequency_interval'] = ''; + $values['end_frequency_unit'] = ''; + $values['end_frequency_interval'] = ''; + $values['end_action'] = ''; + $values['end_date'] = ''; } } } diff --git a/CRM/Core/DAO.php b/CRM/Core/DAO.php index 27cb3b417c..ad06001e74 100644 --- a/CRM/Core/DAO.php +++ b/CRM/Core/DAO.php @@ -3365,6 +3365,7 @@ SELECT contact_id * Given an incomplete record, attempt to fill missing field values from the database */ public static function fillValues(array $existingValues, $fieldsToRetrieve): array { + $entityFields = static::getSupportedFields(); $idField = static::$_primaryKey[0]; // Ensure primary key is set $existingValues += [$idField => NULL]; @@ -3378,25 +3379,25 @@ SELECT contact_id } $idValue = $existingValues[$idField] ?? NULL; foreach ($fieldsToRetrieve as $fieldName) { + $fieldMeta = $entityFields[$fieldName] ?? ['type' => NULL]; if (!array_key_exists($fieldName, $existingValues)) { - $fieldMeta = static::getSupportedFields()[$fieldName] ?? ['type' => NULL]; $existingValues[$fieldName] = NULL; if ($idValue) { $existingValues[$fieldName] = self::getFieldValue(static::class, $idValue, $fieldName, $idField); } - if (isset($existingValues[$fieldName])) { - if (!empty($fieldMeta['serialize'])) { - self::unSerializeField($existingValues[$fieldName], $fieldMeta['serialize']); - } - elseif ($fieldMeta['type'] === CRM_Utils_Type::T_BOOLEAN) { - $existingValues[$fieldName] = (bool) $existingValues[$fieldName]; - } - elseif ($fieldMeta['type'] === CRM_Utils_Type::T_INT) { - $existingValues[$fieldName] = (int) $existingValues[$fieldName]; - } - elseif ($fieldMeta['type'] === CRM_Utils_Type::T_FLOAT) { - $existingValues[$fieldName] = (float) $existingValues[$fieldName]; - } + } + if (isset($existingValues[$fieldName])) { + if (!empty($fieldMeta['serialize']) && !is_array($existingValues[$fieldName])) { + $existingValues[$fieldName] = self::unSerializeField($existingValues[$fieldName], $fieldMeta['serialize']); + } + elseif ($fieldMeta['type'] === CRM_Utils_Type::T_BOOLEAN) { + $existingValues[$fieldName] = (bool) $existingValues[$fieldName]; + } + elseif ($fieldMeta['type'] === CRM_Utils_Type::T_INT) { + $existingValues[$fieldName] = (int) $existingValues[$fieldName]; + } + elseif ($fieldMeta['type'] === CRM_Utils_Type::T_FLOAT) { + $existingValues[$fieldName] = (float) $existingValues[$fieldName]; } } } diff --git a/CRM/Core/Form.php b/CRM/Core/Form.php index e671b3f8b6..edc2b4060b 100644 --- a/CRM/Core/Form.php +++ b/CRM/Core/Form.php @@ -490,8 +490,14 @@ class CRM_Core_Form extends HTML_QuickForm_Page { // Like select but accepts rich array data (with nesting, colors, icons, etc) as option list. if ($inputType == 'select2') { $type = 'text'; - $options = $attributes; - $attributes = ($extra ? $extra : []) + ['class' => '']; + $options = []; + foreach ($attributes as $option) { + // Transform options from api4.getFields format + $option['text'] = $option['text'] ?? $option['label']; + unset($option['label']); + $options[] = $option; + } + $attributes = ($extra ?: []) + ['class' => '']; $attributes['class'] = ltrim($attributes['class'] . " crm-select2 crm-form-select2"); $attributes['data-select-params'] = json_encode(['data' => $options, 'multiple' => !empty($attributes['multiple'])]); unset($attributes['multiple']); @@ -1825,6 +1831,9 @@ class CRM_Core_Form extends HTML_QuickForm_Page { $widget = $widget == 'Select2' ? $widget : 'Select'; $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE); } + elseif (!empty($fieldSpec['serialize'])) { + $props['multiple'] = TRUE; + } // Add data for popup link. $canEditOptions = CRM_Core_Permission::check('administer CiviCRM'); diff --git a/CRM/Core/Form/Renderer.php b/CRM/Core/Form/Renderer.php index f85323feea..92e23e7a50 100644 --- a/CRM/Core/Form/Renderer.php +++ b/CRM/Core/Form/Renderer.php @@ -165,16 +165,20 @@ class CRM_Core_Form_Renderer extends HTML_QuickForm_Renderer_ArraySmarty { } } - $class = $element->getAttribute('class'); + $class = $element->getAttribute('class') ?? ''; $type = $element->getType(); if (!$class) { if ($type == 'text' || $type == 'password') { $size = $element->getAttribute('size'); if (!empty($size)) { - $class = self::$_sizeMapper[$size] ?? NULL; + $class = self::$_sizeMapper[$size] ?? ''; } } } + // When select2 is an it requires comma-separated values instead of an array + if (in_array($type, ['text', 'hidden']) && str_contains($class, 'crm-select2') && is_array($element->getValue())) { + $element->setValue(implode(',', $element->getValue())); + } if ($type == 'select' && $element->getAttribute('multiple')) { $type = 'multiselect'; diff --git a/CRM/Core/Page/Basic.php b/CRM/Core/Page/Basic.php index 93a6294074..355e764841 100644 --- a/CRM/Core/Page/Basic.php +++ b/CRM/Core/Page/Basic.php @@ -451,4 +451,16 @@ abstract class CRM_Core_Page_Basic extends CRM_Core_Page { return []; } + /** + * Get the menu path corresponding to an action on this entity + * + * @param string $linkAction + * e.g. "view" + * @return string|null + * e.g. "civicrm/activity?reset=1&action=view&id=[id]" + */ + public function getLinkPath(string $linkAction): ?string { + return $this->getBAOName()::getEntityPaths()[$linkAction] ?? NULL; + } + } diff --git a/CRM/Core/xml/Menu/Admin.xml b/CRM/Core/xml/Menu/Admin.xml index 6eb998424a..d3e95cb89b 100644 --- a/CRM/Core/xml/Menu/Admin.xml +++ b/CRM/Core/xml/Menu/Admin.xml @@ -295,7 +295,7 @@ Schedule Reminders. CRM_Admin_Page_ScheduleReminders 1 - administer CiviCRM data;edit all events + administer CiviCRM data Communications 40 @@ -705,16 +705,6 @@ Price Field Options CRM_Price_Page_Option - - civicrm/ajax/mapping - CRM_Admin_Page_AJAX::mappingList - administer CiviCRM,access CiviCRM - - - civicrm/ajax/recipientListing - CRM_Admin_Page_AJAX::recipientListing - access CiviEvent,access CiviCRM - civicrm/admin/sms/provider Sms Providers diff --git a/CRM/Event/ActionMapping/ByEvent.php b/CRM/Event/ActionMapping/ByEvent.php index 1143b23865..32b48d7f10 100644 --- a/CRM/Event/ActionMapping/ByEvent.php +++ b/CRM/Event/ActionMapping/ByEvent.php @@ -24,7 +24,7 @@ class CRM_Event_ActionMapping_ByEvent extends CRM_Event_ActionMapping { } public function getLabel(): string { - return ts('Event Name'); + return ts('Event'); } public function getValueLabels(): array { diff --git a/CRM/Event/Form/ManageEvent/ScheduleReminders.php b/CRM/Event/Form/ManageEvent/ScheduleReminders.php index 1af6314f12..f0f7ad5595 100644 --- a/CRM/Event/Form/ManageEvent/ScheduleReminders.php +++ b/CRM/Event/Form/ManageEvent/ScheduleReminders.php @@ -34,6 +34,7 @@ class CRM_Event_Form_ManageEvent_ScheduleReminders extends CRM_Event_Form_Manage $mapping = CRM_Core_BAO_ActionSchedule::getMapping($this->_isTemplate ? CRM_Event_ActionMapping::EVENT_TPL_MAPPING_ID : CRM_Event_ActionMapping::EVENT_NAME_MAPPING_ID); $reminderList = CRM_Core_BAO_ActionSchedule::getList($mapping, $this->_id); + $scheduleReminder = new CRM_Admin_Page_ScheduleReminders(); // Add action links to each of the reminders foreach ($reminderList as & $format) { $action = CRM_Core_Action::UPDATE + CRM_Core_Action::DELETE; @@ -43,10 +44,9 @@ class CRM_Event_Form_ManageEvent_ScheduleReminders extends CRM_Event_Form_Manage else { $action += CRM_Core_Action::ENABLE; } - $scheduleReminder = new CRM_Admin_Page_ScheduleReminders(); $links = $scheduleReminder->links(); - $links[CRM_Core_Action::DELETE]['qs'] .= "&context=event&compId={$this->_id}"; - $links[CRM_Core_Action::UPDATE]['qs'] .= "&context=event&compId={$this->_id}"; + $links[CRM_Core_Action::DELETE]['qs'] .= "&mapping_id={$mapping->getId()}&entity_value={$this->_id}"; + $links[CRM_Core_Action::UPDATE]['qs'] .= "&mapping_id={$mapping->getId()}&entity_value={$this->_id}"; $format['action'] = CRM_Core_Action::formLink( $links, $action, @@ -61,7 +61,7 @@ class CRM_Event_Form_ManageEvent_ScheduleReminders extends CRM_Event_Form_Manage $this->assign('rows', $reminderList); $this->assign('setTab', $setTab); - $this->assign('component', 'event'); + $this->assign('addNewLink', $scheduleReminder->getLinkPath('add') . "&mapping_id={$mapping->getId()}&entity_value={$this->_id}"); // Update tab "disabled" css class $this->ajaxResponse['tabValid'] = is_array($reminderList) && (count($reminderList) > 0); diff --git a/CRM/SMS/BAO/Provider.php b/CRM/SMS/BAO/Provider.php index 11b16e97fb..72158e34c2 100644 --- a/CRM/SMS/BAO/Provider.php +++ b/CRM/SMS/BAO/Provider.php @@ -17,12 +17,11 @@ class CRM_SMS_BAO_Provider extends CRM_SMS_DAO_Provider { /** - * @return null|string + * @return int */ - public static function activeProviderCount() { - $activeProviders = CRM_Core_DAO::singleValueQuery('SELECT count(id) FROM civicrm_sms_provider WHERE is_active = 1 AND (domain_id = %1 OR domain_id IS NULL)', + public static function activeProviderCount(): int { + return (int) CRM_Core_DAO::singleValueQuery('SELECT count(id) FROM civicrm_sms_provider WHERE is_active = 1 AND (domain_id = %1 OR domain_id IS NULL)', [1 => [CRM_Core_Config::domainID(), 'Positive']]); - return $activeProviders; } /** diff --git a/CRM/Utils/Date.php b/CRM/Utils/Date.php index 0159874e7d..2edbfdb0ea 100644 --- a/CRM/Utils/Date.php +++ b/CRM/Utils/Date.php @@ -1987,6 +1987,7 @@ class CRM_Utils_Date { $field['smarty_view_format'] = $dateAttributes['smarty_view_format']; } $field['datepicker']['extra'] = self::getDatePickerExtra($field); + $field['datepicker']['extra']['time'] = $fieldMetaData['type'] == CRM_Utils_Type::T_TIMESTAMP; $field['datepicker']['attributes'] = self::getDatePickerAttributes($field); } } diff --git a/Civi/ActionSchedule/MappingBase.php b/Civi/ActionSchedule/MappingBase.php index 63c5cbe748..f4d078333c 100644 --- a/Civi/ActionSchedule/MappingBase.php +++ b/Civi/ActionSchedule/MappingBase.php @@ -89,10 +89,6 @@ abstract class MappingBase extends AutoSubscriber implements MappingInterface { return FALSE; } - public function validateSchedule($schedule): array { - return []; - } - public function getDateFields(): array { return []; } diff --git a/Civi/ActionSchedule/MappingInterface.php b/Civi/ActionSchedule/MappingInterface.php index 509a51ddb0..2f6f75b721 100644 --- a/Civi/ActionSchedule/MappingInterface.php +++ b/Civi/ActionSchedule/MappingInterface.php @@ -121,17 +121,6 @@ interface MappingInterface extends SpecProviderInterface { */ public function checkAccess(array $entityValue): bool; - /** - * Determine whether a schedule based on this mapping is sufficiently - * complete. - * - * @param \CRM_Core_DAO_ActionSchedule $schedule - * @return array - * Array (string $code => string $message). - * List of error messages. - */ - public function validateSchedule($schedule): array; - /** * Generate a query to locate contacts who match the given * schedule. diff --git a/Civi/Api4/Generic/AbstractAction.php b/Civi/Api4/Generic/AbstractAction.php index bef7843610..6e2d48ace8 100644 --- a/Civi/Api4/Generic/AbstractAction.php +++ b/Civi/Api4/Generic/AbstractAction.php @@ -485,7 +485,7 @@ abstract class AbstractAction implements \ArrayAccess { $unmatched[] = $fieldName; } elseif (!empty($fieldInfo['required_if'])) { - if ($this->evaluateCondition($fieldInfo['required_if'], ['values' => $values])) { + if (self::evaluateCondition($fieldInfo['required_if'], ['values' => $values])) { $unmatched[] = $fieldName; } } @@ -549,7 +549,7 @@ abstract class AbstractAction implements \ArrayAccess { * @throws \CRM_Core_Exception * @throws \Exception */ - protected function evaluateCondition($expr, $vars) { + public static function evaluateCondition($expr, $vars) { if (strpos($expr, '}') !== FALSE || strpos($expr, '{') !== FALSE) { throw new \CRM_Core_Exception('Illegal character in expression'); } diff --git a/Civi/Api4/Service/Spec/Provider/ActionScheduleSpecProvider.php b/Civi/Api4/Service/Spec/Provider/ActionScheduleSpecProvider.php index d0f13c162e..df7bc7c583 100644 --- a/Civi/Api4/Service/Spec/Provider/ActionScheduleSpecProvider.php +++ b/Civi/Api4/Service/Spec/Provider/ActionScheduleSpecProvider.php @@ -26,10 +26,22 @@ class ActionScheduleSpecProvider extends \Civi\Core\Service\AutoService implemen public function modifySpec(RequestSpec $spec) { if ($spec->getAction() === 'create') { $spec->getFieldByName('title')->setRequired(TRUE); + $spec->getFieldByName('name')->setRequired(FALSE); $spec->getFieldByName('mapping_id')->setRequired(TRUE); $spec->getFieldByName('entity_value')->setRequired(TRUE); + $spec->getFieldByName('start_action_offset')->setRequiredIf('empty($values.absolute_date)'); + $spec->getFieldByName('start_action_unit')->setRequiredIf('empty($values.absolute_date)'); + $spec->getFieldByName('start_action_condition')->setRequiredIf('empty($values.absolute_date)'); $spec->getFieldByName('start_action_date')->setRequiredIf('empty($values.absolute_date)'); $spec->getFieldByName('absolute_date')->setRequiredIf('empty($values.start_action_date)'); + $spec->getFieldByName('group_id')->setRequiredIf('!empty($values.limit_to) && !empty($values.recipient) && $values.recipient === "group"'); + $spec->getFieldByName('recipient_manual')->setRequiredIf('!empty($values.limit_to) && !empty($values.recipient) && $values.recipient === "manual"'); + $spec->getFieldByName('subject')->setRequiredIf('!empty($values.is_active) && (empty($values.mode) || $values.mode !== "SMS")'); + $spec->getFieldByName('body_html')->setRequiredIf('!empty($values.is_active) && (empty($values.mode) || $values.mode !== "SMS")'); + $spec->getFieldByName('sms_body_text')->setRequiredIf('!empty($values.is_active) && !empty($values.mode) && $values.mode !== "Email"'); + $spec->getFieldByName('sms_provider_id')->setRequiredIf('!empty($values.is_active) && !empty($values.mode) && $values.mode !== "Email"'); + $spec->getFieldByName('repetition_frequency_interval')->setRequiredIf('!empty($values.is_repeat)'); + $spec->getFieldByName('end_frequency_interval')->setRequiredIf('!empty($values.is_repeat)'); } $spec->getFieldByName('mapping_id')->setSuffixes(['name', 'label', 'icon']); } diff --git a/templates/CRM/Admin/Form/ScheduleReminders.tpl b/templates/CRM/Admin/Form/ScheduleReminders.tpl index ec8a9d2fe0..f9db4a0f72 100644 --- a/templates/CRM/Admin/Form/ScheduleReminders.tpl +++ b/templates/CRM/Admin/Form/ScheduleReminders.tpl @@ -9,300 +9,337 @@ *} {* This template is used for adding/scheduling reminders. *}
+ {if $action eq 8} +
+ {icon icon="fa-info-circle"}{/icon} + {ts 1=$reminderName}WARNING: You are about to delete the Reminder titled %1.{/ts} {ts}Do you want to continue?{/ts} +
+ {else} + + + + + + + + + + + + + + + + + -{if $action eq 8} -
- {icon icon="fa-info-circle"}{/icon} - {ts 1=$reminderName}WARNING: You are about to delete the Reminder titled %1.{/ts} {ts}Do you want to continue?{/ts} -
-{else} -
{$form.title.label}{$form.title.html}
{$form.mapping_id.label}{$form.mapping_id.html}
{$form.entity_value.label}{$form.entity_value.html}
{$form.entity_status.label}{$form.entity_status.html}
- - - - - - - + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {if !empty($form.mode)} - - - - - {/if} - {if !empty($multilingual)} - - - - - - - - - {/if} - - - - -
{$form.title.label}{$form.title.html}
{$form.entity.label}{$form.entity.html}
{$form.absolute_or_relative_date.label} + {$form.absolute_or_relative_date.html} + {help id="relative_absolute_schedule_dates"} + {$form.absolute_date.html} +
{$form.start_action_offset.label}{$form.absolute_date.html} {ts}OR{/ts}
+ {$form.start_action_offset.html} + {$form.start_action_unit.html} + {$form.start_action_condition.html} + {$form.start_action_date.html} +
{$form.effective_start_date.label} + {$form.effective_start_date.html} + {$form.effective_end_date.label} + {$form.effective_end_date.html} +
{ts}Earliest and latest trigger dates to include.{/ts}
+
{$form.is_repeat.label}{$form.is_repeat.html}
{$form.repetition_frequency_interval.label} *{$form.repetition_frequency_interval.html} {$form.repetition_frequency_unit.html}
{$form.end_frequency_interval.label} *{$form.end_frequency_interval.html} {$form.end_frequency_unit.html} {$form.end_action.html} {$form.end_date.html}
{$form.record_activity.label}{$form.record_activity.html}
{$form.recipient.label} + + {$form.limit_to.html} {help id="limit_to" class="limit_to" title=$form.recipient.label} + + + {$form.recipient.html} + +
{$form.recipient_listing.label}{$form.recipient_listing.html}
{$form.recipient_manual.label} *{$form.recipient_manual.html}
- {$form.start_action_offset.html}   {$form.start_action_unit.html}   {$form.start_action_condition.html}   {$form.start_action_date.html} - {if $context === "event"} {help id="relative_absolute_schedule_dates"}{/if} -
{$form.record_activity.label}{$form.record_activity.html}
{$form.is_repeat.label}{$form.is_repeat.html}
- - - - + + + + + {if $sms} + + + - - + {/if} + {if $multilingual} + + + -
{$form.repetition_frequency_interval.label} *  {$form.repetition_frequency_interval.html}{$form.repetition_frequency_unit.html}
{$form.group_id.label} *{$form.group_id.html}
{$form.mode.label}{$form.mode.html}
{$form.end_frequency_interval.label} *  {$form.end_frequency_interval.html} - {$form.end_frequency_unit.html}   {$form.end_action.html}   {$form.end_date.html}
{$form.filter_contact_language.label}{$form.filter_contact_language.html} {help id="filter_contact_language"}
-
{$form.effective_start_date.label}{$form.effective_start_date.html}
{ts}Earliest trigger date to include.{/ts}
{$form.effective_end_date.label}{$form.effective_end_date.html}
{ts}Earliest trigger date to exclude.{/ts}
{$form.from_name.label}{$form.from_name.html}  {help id="id-from_name_email"}
{$form.from_email.label}{$form.from_email.html}  
{$form.recipient.label}{$form.limit_to.html} {help id="limit_to" class="limit_to" title=$form.recipient.label}{$form.recipient.html} {help id="recipient" class="recipient" title=$recipientLabels.activity}
{$form.recipient_listing.label}{$form.recipient_listing.html}
{$form.recipient_manual_id.label}{$form.recipient_manual_id.html}
{$form.group_id.label}{$form.group_id.html}
{$form.mode.label}{$form.mode.html}
{$form.filter_contact_language.label}{$form.filter_contact_language.html} {help id="filter_contact_language"}
{$form.communication_language.label}{$form.communication_language.html} {help id="communication_language"}
{$form.is_active.label}{$form.is_active.html}
-
- {ts}Email Screen{/ts} -
- - - - - - - - - -
{$form.template.label}{$form.template.html}
{$form.subject.label} - {$form.subject.html|crmAddClass:huge} - - {help id="id-token-subject" file="CRM/Contact/Form/Task/Email.hlp"} -
- {include file="CRM/Contact/Form/Task/EmailCommon.tpl" upload=1 noAttach=1} -
-
- {if !empty($sms)} -
{ts}SMS Screen{/ts} -
- - - - + + + - - - - -
{$form.sms_provider_id.label}{$form.sms_provider_id.html}
{$form.communication_language.label}{$form.communication_language.html} {help id="communication_language"}
{$form.SMStemplate.label}{$form.SMStemplate.html}
- {include file="CRM/Contact/Form/Task/SMSCommon.tpl" upload=1 noAttach=1} -
-
- {/if} - -{include file="CRM/common/showHideByFieldValue.tpl" - trigger_field_id = "is_repeat" - trigger_value = "true" - target_element_id = "repeatFields" - target_element_type = "table-row" - field_type = "radio" - invert = "false" -} - -{include file="CRM/common/showHideByFieldValue.tpl" - trigger_field_id ="recipient" - trigger_value = 'manual' - target_element_id ="recipientManual" - target_element_type ="table-row" - field_type ="select" - invert = 0 -} - -{include file="CRM/common/showHideByFieldValue.tpl" - trigger_field_id ="recipient" - trigger_value = 'group' - target_element_id ="recipientGroup" - target_element_type ="table-row" - field_type ="select" - invert = 0 -} + {/if} + + {$form.is_active.label} + {$form.is_active.html} + + +
+ {ts}Email{/ts} +
+ + + + + + + + + + + + + +
{$form.from_name.label} + {$form.from_name.html} + {$form.from_email.label} + {$form.from_email.html} + {help id="id-from_name_email"} +
{$form.template.label}{$form.template.html}
{$form.subject.label} + {$form.subject.html|crmAddClass:huge} + + {help id="id-token-subject" file="CRM/Contact/Form/Task/Email.hlp"} +
+ {include file="CRM/Contact/Form/Task/EmailCommon.tpl" upload=1 noAttach=1} +
+
+ {if $sms} +
{ts}SMS{/ts} +
+ + + + + + + + + +
{$form.sms_provider_id.label} *{$form.sms_provider_id.html}
{$form.SMStemplate.label}{$form.SMStemplate.html}
+ {include file="CRM/Contact/Form/Task/SMSCommon.tpl" upload=1 noAttach=1} +
+
+ {/if} -{literal} - -{/literal} + // Wait for widgets (select2, datepicker) to be initialized + window.setTimeout(function() { + toggleLimitTo(); + toggleRecipientManualGroup(); + toggleAbsoluteRelativeDate(); + toggleRepeatSection(); + toggleEmailOrSms(); + toggleRecipient(); + }); + }); + })(CRM.$, CRM._); + + {/literal} -{/if} + {/if} -
{include file="CRM/common/formButtons.tpl" location="bottom"}
+
{include file="CRM/common/formButtons.tpl" location="bottom"}
diff --git a/templates/CRM/Admin/Page/ScheduleReminders.tpl b/templates/CRM/Admin/Page/ScheduleReminders.tpl index c5ed999c95..c58cc5e0de 100644 --- a/templates/CRM/Admin/Page/ScheduleReminders.tpl +++ b/templates/CRM/Admin/Page/ScheduleReminders.tpl @@ -34,13 +34,7 @@ {/if} {/if} -- 2.25.1