From 9b85cd33d84cbd8ca53fe346c8b1fc1cf8c90fe0 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Wed, 9 Sep 2020 09:47:23 -0400 Subject: [PATCH] Custom field admin form reform This overhauls the custom field administration form: - Gets rid of the difficult-to-use hierarchcal select - Removes changeFieldType as a separate form - Allows changing field type on the main form, with improved validation - Fixes up some metadata - Improves choosing default values --- CRM/Core/BAO/CustomField.php | 4 +- CRM/Core/DAO/CustomField.php | 7 +- CRM/Core/SelectValues.php | 1 - CRM/Core/xml/Menu/Admin.xml | 5 - CRM/Custom/Form/ChangeFieldType.php | 309 ---------------- CRM/Custom/Form/Field.php | 344 ++++++------------ js/Common.js | 11 + templates/CRM/Custom/Form/ChangeFieldType.tpl | 46 --- templates/CRM/Custom/Form/Field.tpl | 157 +++++--- tests/phpunit/api/v3/CustomFieldTest.php | 15 +- tests/phpunit/api/v3/CustomValueTest.php | 12 +- xml/schema/Core/CustomField.xml | 5 + 12 files changed, 247 insertions(+), 669 deletions(-) delete mode 100644 CRM/Custom/Form/ChangeFieldType.php delete mode 100644 templates/CRM/Custom/Form/ChangeFieldType.tpl diff --git a/CRM/Core/BAO/CustomField.php b/CRM/Core/BAO/CustomField.php index c67b60f7a9..55e1b786bb 100644 --- a/CRM/Core/BAO/CustomField.php +++ b/CRM/Core/BAO/CustomField.php @@ -157,13 +157,13 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { * Fetch object based on array of properties. * * @param array $params - * (reference ) an assoc array of name/value pairs. + * An assoc array of name/value pairs. * @param array $defaults * (reference ) an assoc array to hold the flattened values. * * @return CRM_Core_DAO_CustomField */ - public static function retrieve(&$params, &$defaults) { + public static function retrieve($params, &$defaults) { return CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_CustomField', $params, $defaults); } diff --git a/CRM/Core/DAO/CustomField.php b/CRM/Core/DAO/CustomField.php index 698d34a495..5edafdf5c6 100644 --- a/CRM/Core/DAO/CustomField.php +++ b/CRM/Core/DAO/CustomField.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Core/CustomField.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:4ded3c0d1a8e34502a5957ee74c4480a) + * (GenCodeChecksum:b74179ea5553c544931562d6aac5641e) */ /** @@ -365,6 +365,7 @@ class CRM_Core_DAO_CustomField extends CRM_Core_DAO { 'localizable' => 0, 'html' => [ 'type' => 'Select', + 'label' => ts("Data Type"), ], 'pseudoconstant' => [ 'callback' => 'CRM_Core_BAO_CustomField::dataType', @@ -384,6 +385,10 @@ class CRM_Core_DAO_CustomField extends CRM_Core_DAO { 'entity' => 'CustomField', 'bao' => 'CRM_Core_BAO_CustomField', 'localizable' => 0, + 'html' => [ + 'type' => 'Select', + 'label' => ts("Field Input Type"), + ], 'pseudoconstant' => [ 'callback' => 'CRM_Core_SelectValues::customHtmlType', ], diff --git a/CRM/Core/SelectValues.php b/CRM/Core/SelectValues.php index 7adcd178c5..b81422698f 100644 --- a/CRM/Core/SelectValues.php +++ b/CRM/Core/SelectValues.php @@ -177,7 +177,6 @@ class CRM_Core_SelectValues { 'RichTextEditor' => ts('Rich Text Editor'), 'Autocomplete-Select' => ts('Autocomplete-Select'), 'Link' => ts('Link'), - 'ContactReference' => ts('Autocomplete-Select'), ]; } diff --git a/CRM/Core/xml/Menu/Admin.xml b/CRM/Core/xml/Menu/Admin.xml index df4d108a1d..245b7ae549 100644 --- a/CRM/Core/xml/Menu/Admin.xml +++ b/CRM/Core/xml/Menu/Admin.xml @@ -36,11 +36,6 @@ Custom Field - Move CRM_Custom_Form_MoveField - - civicrm/admin/custom/group/field/changetype - Custom Field - Change Type - CRM_Custom_Form_ChangeFieldType - civicrm/admin/uf/group Profiles diff --git a/CRM/Custom/Form/ChangeFieldType.php b/CRM/Custom/Form/ChangeFieldType.php deleted file mode 100644 index 54f9197ea8..0000000000 --- a/CRM/Custom/Form/ChangeFieldType.php +++ /dev/null @@ -1,309 +0,0 @@ -_id = CRM_Utils_Request::retrieve('id', 'Positive', - $this, TRUE - ); - - $this->_values = []; - $params = ['id' => $this->_id]; - CRM_Core_BAO_CustomField::retrieve($params, $this->_values); - - if ($this->_values['html_type'] == 'Select' && $this->_values['serialize']) { - $this->_values['html_type'] = 'Multi-Select'; - } - $this->_htmlTypeTransitions = self::fieldTypeTransitions(CRM_Utils_Array::value('data_type', $this->_values), - CRM_Utils_Array::value('html_type', $this->_values) - ); - - if (empty($this->_values) || empty($this->_htmlTypeTransitions)) { - CRM_Core_Error::statusBounce(ts("Invalid custom field or can't change input type of this custom field.")); - } - - $url = CRM_Utils_System::url('civicrm/admin/custom/group/field/update', - "action=update&reset=1&gid={$this->_values['custom_group_id']}&id={$this->_id}" - ); - $session = CRM_Core_Session::singleton(); - $session->pushUserContext($url); - - CRM_Utils_System::setTitle(ts('Change Field Type: %1', - [1 => $this->_values['label']] - )); - } - - /** - * Build the form object. - * - * @return void - */ - public function buildQuickForm() { - - $srcHtmlType = $this->add('select', - 'src_html_type', - ts('Current HTML Type'), - [$this->_values['html_type'] => $this->_values['html_type']], - TRUE - ); - - $srcHtmlType->setValue($this->_values['html_type']); - $srcHtmlType->freeze(); - - $this->assign('srcHtmlType', $this->_values['html_type']); - - $dstHtmlType = $this->add('select', - 'dst_html_type', - ts('New HTML Type'), - [ - '' => ts('- select -'), - ] + $this->_htmlTypeTransitions, - TRUE - ); - - $this->addButtons([ - [ - 'type' => 'next', - 'name' => ts('Change Field Type'), - 'isDefault' => TRUE, - 'js' => ['onclick' => 'return checkCustomDataField();'], - ], - [ - 'type' => 'cancel', - 'name' => ts('Cancel'), - ], - ]); - } - - /** - * Process the form when submitted. - * - * @return void - */ - public function postProcess() { - $params = $this->controller->exportValues($this->_name); - - $tableName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', - $this->_values['custom_group_id'], - 'table_name' - ); - - $singleValueOps = [ - 'Text', - 'Select', - 'Radio', - 'Autocomplete-Select', - ]; - - $mutliValueOps = [ - 'CheckBox', - 'Multi-Select', - ]; - - $srcHtmlType = $this->_values['html_type']; - $dstHtmlType = $params['dst_html_type']; - - $customField = new CRM_Core_DAO_CustomField(); - $customField->id = $this->_id; - $customField->find(TRUE); - $customField->serialize = in_array($dstHtmlType, $mutliValueOps, TRUE); - - if ($dstHtmlType == 'Text' && in_array($srcHtmlType, [ - 'Select', - 'Radio', - 'Autocomplete-Select', - ])) { - $customField->option_group_id = 'NULL'; - CRM_Core_BAO_CustomField::checkOptionGroup($this->_values['option_group_id']); - } - - if (in_array($srcHtmlType, $mutliValueOps) && - in_array($dstHtmlType, $singleValueOps)) { - $this->flattenToFirstValue($tableName, $this->_values['column_name']); - } - elseif (in_array($srcHtmlType, $singleValueOps) && - in_array($dstHtmlType, $mutliValueOps)) { - $this->firstValueToFlatten($tableName, $this->_values['column_name']); - } - - $customField->html_type = ($dstHtmlType === 'Multi-Select') ? 'Select' : $dstHtmlType; - $customField->save(); - - // Reset cache for custom fields - Civi::cache('fields')->flush(); - // reset ACL and system caches. - CRM_Core_BAO_Cache::resetCaches(); - - CRM_Core_Session::setStatus(ts('Input type of custom field \'%1\' has been successfully changed to \'%2\'.', - [1 => $this->_values['label'], 2 => $dstHtmlType] - ), ts('Field Type Changed'), 'success'); - } - - /** - * @param $dataType - * @param $htmlType - * - * @return array|null - */ - public static function fieldTypeTransitions($dataType, $htmlType) { - // Text field is single value field, - // can not be change to other single value option which contains option group - if ($htmlType == 'Text') { - return NULL; - } - - $singleValueOps = [ - 'Text' => 'Text', - 'Select' => 'Select', - 'Radio' => 'Radio', - 'Autocomplete-Select' => 'Autocomplete-Select', - ]; - - $mutliValueOps = [ - 'CheckBox' => 'CheckBox', - 'Multi-Select' => 'Multi-Select', - ]; - - switch ($dataType) { - case 'String': - if (in_array($htmlType, array_keys($singleValueOps))) { - unset($singleValueOps[$htmlType]); - return array_merge($singleValueOps, $mutliValueOps); - } - elseif (in_array($htmlType, array_keys($mutliValueOps))) { - unset($singleValueOps['Text']); - foreach ($singleValueOps as $type => $label) { - $singleValueOps[$type] = "{$label} ( " . ts('Not Safe') . " )"; - } - unset($mutliValueOps[$htmlType]); - return array_merge($mutliValueOps, $singleValueOps); - } - break; - - case 'Int': - case 'Float': - case 'Money': - if (in_array($htmlType, array_keys($singleValueOps))) { - unset($singleValueOps[$htmlType]); - return $singleValueOps; - } - break; - - case 'Memo': - $ops = [ - 'TextArea' => 'TextArea', - 'RichTextEditor' => 'RichTextEditor', - ]; - if (in_array($htmlType, array_keys($ops))) { - unset($ops[$htmlType]); - return $ops; - } - break; - } - - return NULL; - } - - /** - * Take a single-value column (eg: a Radio or Select etc ) and convert - * value to the multi listed value (eg:"^Foo^") - * - * @param string $table - * @param string $column - */ - public function firstValueToFlatten($table, $column) { - $selectSql = "SELECT id, $column FROM $table WHERE $column IS NOT NULL"; - $updateSql = "UPDATE $table SET $column = %1 WHERE id = %2"; - $dao = CRM_Core_DAO::executeQuery($selectSql); - while ($dao->fetch()) { - if (!$dao->{$column}) { - continue; - } - $value = CRM_Core_DAO::VALUE_SEPARATOR . $dao->{$column} . CRM_Core_DAO::VALUE_SEPARATOR; - $params = [ - 1 => [(string) $value, 'String'], - 2 => [$dao->id, 'Integer'], - ]; - CRM_Core_DAO::executeQuery($updateSql, $params); - } - } - - /** - * Take a multi-value column (e.g. a Multi-Select or CheckBox column), and convert - * all values (of the form "^^" or "^Foo^" or "^Foo^Bar^") to the first listed value ("Foo") - * - * @param string $table - * @param string $column - */ - public function flattenToFirstValue($table, $column) { - $selectSql = "SELECT id, $column FROM $table WHERE $column IS NOT NULL"; - $updateSql = "UPDATE $table SET $column = %1 WHERE id = %2"; - $dao = CRM_Core_DAO::executeQuery($selectSql); - while ($dao->fetch()) { - $values = self::explode($dao->{$column}); - $params = [ - 1 => [(string) array_shift($values), 'String'], - 2 => [$dao->id, 'Integer'], - ]; - CRM_Core_DAO::executeQuery($updateSql, $params); - } - } - - /** - * @param $str - * - * @return array - */ - public static function explode($str) { - if (empty($str) || $str == CRM_Core_DAO::VALUE_SEPARATOR . CRM_Core_DAO::VALUE_SEPARATOR) { - return []; - } - else { - return explode(CRM_Core_DAO::VALUE_SEPARATOR, trim($str, CRM_Core_DAO::VALUE_SEPARATOR)); - } - } - -} diff --git a/CRM/Custom/Form/Field.php b/CRM/Custom/Form/Field.php index 2bbc8e0c07..ba68cc706a 100644 --- a/CRM/Custom/Form/Field.php +++ b/CRM/Custom/Form/Field.php @@ -39,13 +39,6 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { */ protected $_id; - /** - * The default custom data/input types, when editing the field - * - * @var array - */ - protected $_defaultDataType; - /** * Array of custom field values if update mode. * @var array @@ -57,36 +50,26 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { * * @var array */ - private static $_dataTypeValues = NULL; - private static $_dataTypeKeys = NULL; - - private static $_dataToLabels = NULL; + public static $htmlTypesWithOptions = ['Select', 'Radio', 'CheckBox', 'Autocomplete-Select']; /** - * Used for mapping data types to html type options. + * Maps each data_type to allowed html_type options * - * Each item in this array corresponds to the same index in the dataType array - * @var array + * @var array[] */ public static $_dataToHTML = [ - [ - 'Text' => 'Text', - 'Select' => 'Select', - 'Radio' => 'Radio', - 'CheckBox' => 'CheckBox', - 'Autocomplete-Select' => 'Autocomplete-Select', - ], - ['Text' => 'Text', 'Select' => 'Select', 'Radio' => 'Radio'], - ['Text' => 'Text', 'Select' => 'Select', 'Radio' => 'Radio'], - ['Text' => 'Text', 'Select' => 'Select', 'Radio' => 'Radio'], - ['TextArea' => 'TextArea', 'RichTextEditor' => 'RichTextEditor'], - ['Date' => 'Select Date'], - ['Radio' => 'Radio'], - ['StateProvince' => 'Select'], - ['Country' => 'Select'], - ['File' => 'File'], - ['Link' => 'Link'], - ['ContactReference' => 'Autocomplete-Select'], + 'String' => ['Text', 'Select', 'Radio', 'CheckBox', 'Autocomplete-Select'], + 'Int' => ['Text', 'Select', 'Radio'], + 'Float' => ['Text', 'Select', 'Radio'], + 'Money' => ['Text', 'Select', 'Radio'], + 'Memo' => ['TextArea', 'RichTextEditor'], + 'Date' => ['Select Date'], + 'Boolean' => ['Radio'], + 'StateProvince' => ['Select'], + 'Country' => ['Select'], + 'File' => ['File'], + 'Link' => ['Link'], + 'ContactReference' => ['Autocomplete-Select'], ]; /** @@ -95,19 +78,15 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { * @return void */ public function preProcess() { - if (!(self::$_dataTypeKeys)) { - self::$_dataTypeKeys = array_keys(CRM_Core_BAO_CustomField::dataType()); - self::$_dataTypeValues = array_values(CRM_Core_BAO_CustomField::dataType()); - } - //custom field id $this->_id = CRM_Utils_Request::retrieve('id', 'Positive', $this); + $this->assign('dataToHTML', self::$_dataToHTML); + $this->_values = []; //get the values form db if update. if ($this->_id) { - $params = ['id' => $this->_id]; - CRM_Core_BAO_CustomField::retrieve($params, $this->_values); + CRM_Core_BAO_CustomField::retrieve(['id' => $this->_id], $this->_values); // note_length is an alias for the text_length field $this->_values['note_length'] = $this->_values['text_length'] ?? NULL; // custom group id @@ -130,41 +109,6 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { $session = CRM_Core_Session::singleton(); $session->pushUserContext($url); } - - if (self::$_dataToLabels == NULL) { - self::$_dataToLabels = [ - [ - 'Text' => ts('Text'), - 'Select' => ts('Select'), - 'Radio' => ts('Radio'), - 'CheckBox' => ts('CheckBox'), - 'Autocomplete-Select' => ts('Autocomplete-Select'), - ], - [ - 'Text' => ts('Text'), - 'Select' => ts('Select'), - 'Radio' => ts('Radio'), - ], - [ - 'Text' => ts('Text'), - 'Select' => ts('Select'), - 'Radio' => ts('Radio'), - ], - [ - 'Text' => ts('Text'), - 'Select' => ts('Select'), - 'Radio' => ts('Radio'), - ], - ['TextArea' => ts('TextArea'), 'RichTextEditor' => ts('Rich Text Editor')], - ['Date' => ts('Select Date')], - ['Radio' => ts('Radio')], - ['Select' => ts('Select')], - ['Select' => ts('Select')], - ['File' => ts('Select File')], - ['Link' => ts('Link')], - ['ContactReference' => ts('Autocomplete-Select')], - ]; - } } /** @@ -177,19 +121,12 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { public function setDefaultValues() { $defaults = $this->_values; + // Defaults for update mode if ($this->_id) { $this->assign('id', $this->_id); $this->_gid = $defaults['custom_group_id']; $defaultValue = $defaults['default_value'] ?? NULL; - //get the value for state or country - if ($defaults['data_type'] == 'StateProvince' && $defaultValue) { - $defaults['default_value'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_StateProvince', $defaultValue); - } - elseif ($defaults['data_type'] == 'Country' && $defaultValue) { - $defaults['default_value'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Country', $defaultValue); - } - if ($defaults['data_type'] == 'ContactReference' && !empty($defaults['filter'])) { $contactRefFilter = 'Advance'; if (strpos($defaults['filter'], 'action=lookup') !== FALSE && @@ -215,51 +152,32 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { $defaults['filter_selected'] = $contactRefFilter; } - if (!empty($defaults['data_type'])) { - $defaultDataType = array_search($defaults['data_type'], - self::$_dataTypeKeys - ); - $defaultHTMLType = array_search($defaults['html_type'], - self::$_dataToHTML[$defaultDataType] - ); - $defaults['data_type'] = [ - '0' => $defaultDataType, - '1' => $defaultHTMLType, - ]; - $this->_defaultDataType = $defaults['data_type']; - } - $defaults['option_type'] = 2; - - $this->assign('changeFieldType', CRM_Custom_Form_ChangeFieldType::fieldTypeTransitions($this->_values['data_type'], $this->_values['html_type'])); } - else { + + // Defaults for create mode + if ($this->_action & CRM_Core_Action::ADD) { + $defaults['data_type'] = 'String'; + $defaults['html_type'] = 'Text'; $defaults['is_active'] = 1; $defaults['option_type'] = 1; $defaults['is_search_range'] = 1; - } - - // set defaults for weight. - for ($i = 1; $i <= self::NUM_OPTION; $i++) { - $defaults['option_status[' . $i . ']'] = 1; - $defaults['option_weight[' . $i . ']'] = $i; - $defaults['option_value[' . $i . ']'] = $i; - } - - if ($this->_action & CRM_Core_Action::ADD) { - $fieldValues = ['custom_group_id' => $this->_gid]; - $defaults['weight'] = CRM_Utils_Weight::getDefaultWeight('CRM_Core_DAO_CustomField', $fieldValues); - + $defaults['weight'] = CRM_Utils_Weight::getDefaultWeight('CRM_Core_DAO_CustomField', ['custom_group_id' => $this->_gid]); $defaults['text_length'] = 255; $defaults['note_columns'] = 60; $defaults['note_rows'] = 4; $defaults['is_view'] = 0; + + if (CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $this->_gid, 'is_multiple')) { + $defaults['in_selector'] = 1; + } } - if ($this->_action & CRM_Core_Action::ADD && - CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $this->_gid, 'is_multiple', 'id') - ) { - $defaults['in_selector'] = 1; + // Set defaults for option values. + for ($i = 1; $i <= self::NUM_OPTION; $i++) { + $defaults['option_status[' . $i . ']'] = 1; + $defaults['option_weight[' . $i . ']'] = $i; + $defaults['option_value[' . $i . ']'] = $i; } return $defaults; @@ -277,8 +195,6 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { $this->assign('gid', $this->_gid); } - $this->assign('dataTypeKeys', self::$_dataTypeKeys); - // lets trim all the whitespace $this->applyFilter('__ALL__', 'trim'); @@ -292,17 +208,11 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { TRUE ); - $dt = &self::$_dataTypeValues; - $it = []; - foreach ($dt as $key => $value) { - $it[$key] = self::$_dataToLabels[$key]; - } - $sel = &$this->addElement('hierselect', - 'data_type', - ts('Data and Input Field Type'), - '   ' - ); - $sel->setOptions([$dt, $it]); + // FIXME: Switch addField to use APIv4 so we don't get those legacy options from v3 + $htmlOptions = CRM_Core_BAO_CustomField::buildOptions('html_type', 'create'); + + $this->addField('data_type', ['class' => 'twenty'], TRUE); + $this->addField('html_type', ['class' => 'twenty', 'options' => $htmlOptions], TRUE); if (CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $this->_gid, 'is_multiple')) { $this->add('checkbox', 'in_selector', ts('Display in Table?')); @@ -320,12 +230,17 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { if ($this->_action == CRM_Core_Action::UPDATE) { $this->freeze('data_type'); if (!empty($this->_values['option_group_id'])) { - $this->assign('hasOptionGroup', TRUE); + $this->assign('hasOptionGroup', in_array($this->_values['html_type'], self::$htmlTypesWithOptions)); // Before dev/core#155 we didn't set the is_reserved flag properly, which should be handled by the upgrade script... // but it is still possible that existing installs may have optiongroups linked to custom fields that are marked reserved. $optionGroupParams['id'] = $this->_values['option_group_id']; $optionGroupParams['options']['or'] = [["is_reserved", "id"]]; } + $this->assign('originalHtmlType', $this->_values['html_type']); + $this->assign('originalSerialize', $this->_values['serialize']); + if (!empty($this->_values['serialize'])) { + $this->assign('existingMultiValueCount', $this->getMultiValueCount()); + } } // Retrieve optiongroups for selection list @@ -531,8 +446,7 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { // is searchable by range? $this->addRadio('is_search_range', ts('Search by Range?'), [ts('No'), ts('Yes')]); - // add buttons - $this->addButtons([ + $buttons = [ [ 'type' => 'done', 'name' => ts('Save'), @@ -547,7 +461,14 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { 'type' => 'cancel', 'name' => ts('Cancel'), ], - ]); + ]; + // Save & new only applies to adding a field + if ($this->_id) { + unset($buttons[1]); + } + + // add buttons + $this->addButtons($buttons); // add a form rule to check default value $this->addFormRule(['CRM_Custom_Form_Field', 'formRule'], $this); @@ -617,11 +538,7 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { $errors['label'] = ts("You cannot use 'id' as a field label."); } - if (!isset($fields['data_type'][0]) || !isset($fields['data_type'][1])) { - $errors['_qf_default'] = ts('Please enter valid - Data and Input Field Type.'); - } - - $dataType = self::$_dataTypeKeys[$fields['data_type'][0]]; + $dataType = $fields['data_type']; if ($default || $dataType == 'ContactReference') { switch ($dataType) { @@ -663,8 +580,8 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { case 'Country': if (!empty($default)) { - $query = "SELECT count(*) FROM civicrm_country WHERE name = %1 OR iso_code = %1"; - $params = [1 => [$fields['default_value'], 'String']]; + $query = "SELECT count(*) FROM civicrm_country WHERE id = %1"; + $params = [1 => [$fields['default_value'], 'Int']]; if (CRM_Core_DAO::singleValueQuery($query, $params) <= 0) { $errors['default_value'] = ts('Invalid default value for country.'); } @@ -676,9 +593,8 @@ class CRM_Custom_Form_Field extends CRM_Core_Form { $query = " SELECT count(*) FROM civicrm_state_province - WHERE name = %1 - OR abbreviation = %1"; - $params = [1 => [$fields['default_value'], 'String']]; + WHERE id = %1"; + $params = [1 => [$fields['default_value'], 'Int']]; if (CRM_Core_DAO::singleValueQuery($query, $params) <= 0) { $errors['default_value'] = ts('The invalid default value for State/Province data type'); } @@ -699,7 +615,7 @@ SELECT count(*) } } - if (self::$_dataTypeKeys[$fields['data_type'][0]] == 'Date') { + if ($dataType == 'Date') { if (!$fields['date_format']) { $errors['date_format'] = ts('Please select a date format.'); } @@ -711,11 +627,7 @@ SELECT count(*) */ $_flagOption = $_rowError = 0; $_showHide = new CRM_Core_ShowHideBlocks('', ''); - $dataType = self::$_dataTypeKeys[$fields['data_type'][0]]; - if (isset($fields['data_type'][1])) { - $dataField = $fields['data_type'][1]; - } - $optionFields = ['Select', 'CheckBox', 'Radio']; + $htmlType = $fields['html_type']; if (isset($fields['option_type']) && $fields['option_type'] == 1) { //capture duplicate Custom option values @@ -827,8 +739,7 @@ SELECT count(*) $_flagOption = $_emptyRow = 0; } } - elseif (isset($dataField) && - in_array($dataField, $optionFields) && + elseif (in_array($htmlType, self::$htmlTypesWithOptions) && !in_array($dataType, ['Boolean', 'Country', 'StateProvince']) ) { if (!$fields['option_group_id']) { @@ -841,10 +752,7 @@ FROM civicrm_custom_field WHERE data_type != %1 AND option_group_id = %2"; $params = [ - 1 => [ - self::$_dataTypeKeys[$fields['data_type'][0]], - 'String', - ], + 1 => [$dataType, 'String'], 2 => [$fields['option_group_id'], 'Integer'], ]; $count = CRM_Core_DAO::singleValueQuery($query, $params); @@ -860,18 +768,10 @@ AND option_group_id = %2"; $assignError->assign('optionRowError', $_rowError); } else { - if (isset($fields['data_type'][1])) { - switch (self::$_dataToHTML[$fields['data_type'][0]][$fields['data_type'][1]]) { + if (isset($htmlType)) { + switch ($htmlType) { case 'Radio': - $_fieldError = 1; - $assignError->assign('fieldError', $_fieldError); - break; - - case 'Checkbox': - $_fieldError = 1; - $assignError->assign('fieldError', $_fieldError); - break; - + case 'CheckBox': case 'Select': $_fieldError = 1; $assignError->assign('fieldError', $_fieldError); @@ -900,6 +800,27 @@ AND option_group_id = %2"; $errors['is_view'] = ts('Can not set this field Required and View Only at the same time.'); } + // If switching to a new option list, validate existing data + if (empty($errors) && $self->_id && in_array($htmlType, self::$htmlTypesWithOptions)) { + $oldHtmlType = $self->_values['html_type']; + $oldOptionGroup = $self->_values['option_group_id']; + if ($oldHtmlType === 'Text' || $oldOptionGroup != $fields['option_group_id'] || $fields['option_type'] == 1) { + if ($fields['option_type'] == 2) { + $optionQuery = "SELECT value FROM civicrm_option_value WHERE option_group_id = " . (int) $fields['option_group_id']; + } + else { + $options = array_map(['CRM_Core_DAO', 'escapeString'], array_filter($fields['option_value'], 'strlen')); + $optionQuery = '"' . implode('","', $options) . '"'; + } + $table = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $self->_gid, 'table_name'); + $column = $self->_values['column_name']; + $invalid = CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM `$table` WHERE `$column` NOT IN ($optionQuery)"); + if ($invalid) { + $errors['html_type'] = ts('Cannot impose option list because there is existing data which does not match the options.'); + } + } + } + return empty($errors) ? TRUE : $errors; } @@ -912,24 +833,9 @@ AND option_group_id = %2"; // store the submitted values in an array $params = $this->controller->exportValues($this->_name); self::clearEmptyOptions($params); - if ($this->_action == CRM_Core_Action::UPDATE) { - $dataTypeKey = $this->_defaultDataType[0]; - $params['data_type'] = self::$_dataTypeKeys[$this->_defaultDataType[0]]; - $params['html_type'] = self::$_dataToHTML[$this->_defaultDataType[0]][$this->_defaultDataType[1]]; - } - else { - $dataTypeKey = $params['data_type'][0]; - $params['html_type'] = self::$_dataToHTML[$params['data_type'][0]][$params['data_type'][1]]; - $params['data_type'] = self::$_dataTypeKeys[$params['data_type'][0]]; - } //fix for 'is_search_range' field. - if (in_array($dataTypeKey, [ - 1, - 2, - 3, - 5, - ])) { + if (in_array($params['data_type'], ['Int', 'Float', 'Money', 'Date'])) { if (empty($params['is_searchable'])) { $params['is_search_range'] = 0; } @@ -938,11 +844,7 @@ AND option_group_id = %2"; $params['is_search_range'] = 0; } - // Serialization cannot be changed on update - if ($this->_id) { - unset($params['serialize']); - } - elseif (strpos($params['html_type'], 'Select') === 0) { + if ($params['html_type'] === 'Select') { $params['serialize'] = $params['serialize'] ? CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND : 'null'; } else { @@ -950,12 +852,11 @@ AND option_group_id = %2"; } $filter = 'null'; - if ($dataTypeKey == 11 && !empty($params['filter_selected'])) { + if ($params['data_type'] == 'ContactReference' && !empty($params['filter_selected'])) { if ($params['filter_selected'] == 'Advance' && trim(CRM_Utils_Array::value('filter', $params))) { $filter = trim($params['filter']); } elseif ($params['filter_selected'] == 'Group' && !empty($params['group_id'])) { - $filter = 'action=lookup&group=' . implode(',', $params['group_id']); } } @@ -971,43 +872,6 @@ AND option_group_id = %2"; $params['weight'] = CRM_Utils_Weight::updateOtherWeights('CRM_Core_DAO_CustomField', $oldWeight, $params['weight'], $fieldValues); } - $strtolower = function_exists('mb_strtolower') ? 'mb_strtolower' : 'strtolower'; - - //store the primary key for State/Province or Country as default value. - if (strlen(trim($params['default_value']))) { - switch ($params['data_type']) { - case 'StateProvince': - $fieldStateProvince = $strtolower($params['default_value']); - - // LOWER in query below roughly translates to 'hurt my database without deriving any benefit' See CRM-19811. - $query = " -SELECT id - FROM civicrm_state_province - WHERE LOWER(name) = '$fieldStateProvince' - OR abbreviation = '$fieldStateProvince'"; - $dao = CRM_Core_DAO::executeQuery($query); - if ($dao->fetch()) { - $params['default_value'] = $dao->id; - } - break; - - case 'Country': - $fieldCountry = $strtolower($params['default_value']); - - // LOWER in query below roughly translates to 'hurt my database without deriving any benefit' See CRM-19811. - $query = " -SELECT id - FROM civicrm_country - WHERE LOWER(name) = '$fieldCountry' - OR iso_code = '$fieldCountry'"; - $dao = CRM_Core_DAO::executeQuery($query); - if ($dao->fetch()) { - $params['default_value'] = $dao->id; - } - break; - } - } - // The text_length attribute for Memo fields is in a different input as there // are different label, help text and default value than for other type fields if ($params['data_type'] == "Memo") { @@ -1062,4 +926,32 @@ SELECT id } } + /** + * Get number of existing records for this field that contain more than one serialized value. + * + * @return int + * @throws CRM_Core_Exception + */ + public function getMultiValueCount() { + $table = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $this->_gid, 'table_name'); + $column = $this->_values['column_name']; + $sp = CRM_Core_DAO::VALUE_SEPARATOR; + $sql = "SELECT COUNT(*) FROM `$table` WHERE `$column` LIKE '{$sp}%{$sp}%{$sp}'"; + return (int) CRM_Core_DAO::singleValueQuery($sql); + } + + /** + * @return string + */ + public function getDefaultContext() { + return 'create'; + } + + /** + * @return string + */ + public function getDefaultEntity() { + return 'CustomField'; + } + } diff --git a/js/Common.js b/js/Common.js index f42314f6ed..881dda124d 100644 --- a/js/Common.js +++ b/js/Common.js @@ -289,6 +289,17 @@ if (!CRM.vars) CRM.vars = {}; return rendered; }; + CRM.utils.getOptions = function(select) { + var options = []; + $('option', select).each(function() { + var option = {key: $(this).attr('value'), value: $(this).text()}; + if (option.key !== '') { + options.push(option); + } + }); + return options; + }; + function chainSelect() { var $form = $(this).closest('form'), $target = $('select[data-name="' + $(this).data('target') + '"]', $form), diff --git a/templates/CRM/Custom/Form/ChangeFieldType.tpl b/templates/CRM/Custom/Form/ChangeFieldType.tpl deleted file mode 100644 index e87067ea84..0000000000 --- a/templates/CRM/Custom/Form/ChangeFieldType.tpl +++ /dev/null @@ -1,46 +0,0 @@ -{* - +--------------------------------------------------------------------+ - | Copyright CiviCRM LLC. All rights reserved. | - | | - | This work is published under the GNU AGPLv3 license with some | - | permitted exceptions and without any warranty. For full license | - | and copyright information, see https://civicrm.org/licensing | - +--------------------------------------------------------------------+ -*} -
-
{include file="CRM/common/formButtons.tpl" location="top"}
-
{icon icon="fa-info-circle"}{/icon} -  {ts}Warning: This functionality is currently in beta stage. Consider backing up your database before using it. Click "Cancel" to return to the "edit custom field" form without making changes.{/ts} -
- - - - - - - - - -
{$form.src_html_type.label}{$form.src_html_type.html}
{$form.dst_html_type.label}{$form.dst_html_type.html}
-
{include file="CRM/common/formButtons.tpl" location="bottom"}
-
-{literal} - -{/literal} - diff --git a/templates/CRM/Custom/Form/Field.tpl b/templates/CRM/Custom/Form/Field.tpl index 5dd62613b9..aeb56d695b 100644 --- a/templates/CRM/Custom/Form/Field.tpl +++ b/templates/CRM/Custom/Form/Field.tpl @@ -20,29 +20,16 @@ {$form.data_type.label} - {$form.data_type.html} - {if $action neq 1 && $form.data_type.value[1][0] eq "Select" && $form.serialize.value} - ({ts}Multi-Select{/ts}) - {/if} - {if $action neq 4 and $action neq 2} -
{ts}Select the type of data you want to collect and store for this contact. Then select from the available HTML input field types (choices are based on the type of data being collected).{/ts} - {/if} - {if $action eq 2 and $changeFieldType} -
- - - {ts}Change Input Field Type{/ts} - -
- {/if} - + {$form.data_type.html} + + + {$form.html_type.label} + {$form.html_type.html} + + + {$form.serialize.label} + {$form.serialize.html} - {if $action eq 1} - - {$form.serialize.label} - {$form.serialize.html} - - {/if} {if $form.in_selector} {$form.in_selector.label} @@ -176,19 +163,45 @@ {literal} {/literal} diff --git a/tests/phpunit/api/v3/CustomFieldTest.php b/tests/phpunit/api/v3/CustomFieldTest.php index e5c900dd24..cfa7894fe4 100644 --- a/tests/phpunit/api/v3/CustomFieldTest.php +++ b/tests/phpunit/api/v3/CustomFieldTest.php @@ -108,22 +108,13 @@ class api_v3_CustomFieldTest extends CiviUnitTestCase { $htype = CRM_Custom_Form_Field::$_dataToHTML; // Legacy html types returned by v3 - foreach ($htype as &$item) { - if (isset($item['StateProvince'])) { - $item['StateProvince'] = 'Select State/Province'; - } - if (isset($item['Country'])) { - $item['Country'] = 'Select Country'; - } - } + $htype['StateProvince'] = ['Select State/Province']; + $htype['Country'] = ['Select Country']; - $n = 0; foreach ($dtype as $dkey => $dvalue) { - foreach ($htype[$n] as $hkey => $hvalue) { - //echo $dkey."][".$hvalue."\n"; + foreach ($htype[$dkey] as $hvalue) { $this->_loopingCustomFieldCreateTest($this->_buildParams($gid['id'], $hvalue, $dkey)); } - $n++; } } diff --git a/tests/phpunit/api/v3/CustomValueTest.php b/tests/phpunit/api/v3/CustomValueTest.php index 02c16401c4..96ce2e42c0 100644 --- a/tests/phpunit/api/v3/CustomValueTest.php +++ b/tests/phpunit/api/v3/CustomValueTest.php @@ -95,11 +95,11 @@ class api_v3_CustomValueTest extends CiviUnitTestCase { $customFieldDataType = CRM_Core_BAO_CustomField::dataType(); $dataToHtmlTypes = CRM_Custom_Form_Field::$_dataToHTML; - $count = 0; - $optionSupportingHTMLTypes = ['Select', 'Radio', 'CheckBox', 'Autocomplete-Select', 'Multi-Select']; + $optionSupportingHTMLTypes = CRM_Custom_Form_Field::$htmlTypesWithOptions; foreach ($customFieldDataType as $dataType => $label) { switch ($dataType) { + // skipping File data-type & state province due to caching issues // case 'Country': // case 'StateProvince': case 'String': @@ -139,7 +139,7 @@ class api_v3_CustomValueTest extends CiviUnitTestCase { } //Create custom field of $dataType and html-type $html - foreach ($dataToHtmlTypes[$count] as $html) { + foreach ($dataToHtmlTypes[$dataType] as $html) { // per CRM-18568 the like operator does not currently work for fields with options. // the LIKE operator could potentially bypass ACLs (as could IS NOT NULL) and some thought needs to be given // to it. @@ -160,13 +160,7 @@ class api_v3_CustomValueTest extends CiviUnitTestCase { //Now test with $validSQLOperator SQL operators against its custom value(s) $this->_testCustomValue($customField['values'][$customField['id']], $validSQLOperators, $type); } - $count++; - break; - default: - // skipping File data-type & state province due to caching issues - $count++; - break; } } } diff --git a/xml/schema/Core/CustomField.xml b/xml/schema/Core/CustomField.xml index 44c6322560..54cd5c5d74 100644 --- a/xml/schema/Core/CustomField.xml +++ b/xml/schema/Core/CustomField.xml @@ -73,6 +73,7 @@ 1.1 Select + @@ -85,6 +86,10 @@ CRM_Core_SelectValues::customHtmlType + + Select + + 1.1 -- 2.25.1