diff --git a/CRM/Contact/BAO/ContactType.php b/CRM/Contact/BAO/ContactType.php index 477bb09d06f8..cec5abe03006 100644 --- a/CRM/Contact/BAO/ContactType.php +++ b/CRM/Contact/BAO/ContactType.php @@ -849,9 +849,10 @@ public static function getAllContactTypes() { $contactTypes[$name]['parent_label'] = $contactType['parent_id'] ? $parents[$contactType['parent_id']]['label'] : NULL; // Cast int/bool types. $contactTypes[$name]['id'] = (int) $contactType['id']; - $contactTypes[$name]['parent_id'] = $contactType['parent_id'] ? (int) $contactType['parent_id'] : NULL; + $contactTypes[$name]['parent_id'] = ((int) $contactType['parent_id']) ?: NULL; $contactTypes[$name]['is_active'] = (bool) $contactType['is_active']; $contactTypes[$name]['is_reserved'] = (bool) $contactType['is_reserved']; + $contactTypes[$name]['icon'] = $contactType['icon'] ?? $parents[$contactType['parent_id']]['icon'] ?? NULL; } $cache->set($cacheKey, $contactTypes); } diff --git a/CRM/Core/BAO/CustomGroup.php b/CRM/Core/BAO/CustomGroup.php index e7b561a07c8d..b9b82c590053 100644 --- a/CRM/Core/BAO/CustomGroup.php +++ b/CRM/Core/BAO/CustomGroup.php @@ -2061,7 +2061,7 @@ public static function getExtendedObjectTypes(&$types = []) { * @param array $params * @return array */ - public static function getExtendsEntityColumnValueOptions($context, $params) { + public static function getExtendsEntityColumnValueOptions($context, $params): array { $props = $params['values'] ?? []; // Requesting this option list only makes sense if the value of 'extends' is known or can be looked up if (!empty($props['id']) || !empty($props['name']) || !empty($props['extends']) || !empty($props['extends_entity_column_id'])) { @@ -2397,6 +2397,39 @@ public static function checkGroupAccess($groupId, $operation = CRM_Core_Permissi return in_array($groupId, $allowedGroups); } + /** + * Given the name of a custom group, gets the name of the API entity the group extends. + * + * Sort of the inverse of this function: + * @see \Civi\Api4\Utils\CoreUtil::getCustomGroupExtends + * + * @param string $groupName + * @return string + * @throws \CRM_Core_Exception + */ + public static function getEntityForGroup(string $groupName): string { + $extends = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $groupName, 'extends', 'name'); + if (!$extends) { + throw new \CRM_Core_Exception("Custom group $groupName not found"); + } + return self::getEntityFromExtends($extends); + } + + /** + * Translate CustomGroup.extends to entity name. + * + * CustomGroup.extends pretty much maps 1-1 with entity names, except for Individual, Organization & Household. + * @param string $extends + * @return string + * @see self::getCustomGroupExtendsOptions + */ + public static function getEntityFromExtends(string $extends): string { + if ($extends === 'Contact' || in_array($extends, \CRM_Contact_BAO_ContactType::basicTypes(TRUE))) { + return 'Contact'; + } + return $extends; + } + /** * List all possible values for `CustomGroup.extends`. * @@ -2404,11 +2437,12 @@ public static function checkGroupAccess($groupId, $operation = CRM_Core_Permissi * but not the extra options from `custom_data_type` used on the form ("ParticipantStatus", etc). * * Returns a mix of hard-coded array and `cg_extend_objects` OptionValues. - * The 'id' return key maps to the 'value' in `cg_extend_objects`. - * The 'grouping' key refers to the entity field used to select a sub-type. - * The 'table_name' key is for internal use (is not returned by getFields.loadOptions), and - * maps to the 'name' in `cg_extend_objects`. We don't return it as the 'name' in getFields because - * it is not always unique (since contact types are pseudo-entities in this list). + * - 'id' return key (maps to `cg_extend_objects.value`). + * - 'grouping' key refers to the entity field used to select a sub-type. + * - 'is_multiple' (@internal, not returned by getFields.loadOptions) (maps to `cg_extend_objects.filter`) + * controls whether the entity supports multi-record custom groups. + * - 'table_name' (@internal, not returned by getFields.loadOptions) (maps to `cg_extend_objects.name`). + * We don't return it as the 'name' in getFields because it is not always unique (since contact types are pseudo-entities). * * @return array{id: string, label: string, grouping: string, table_name: string}[] */ @@ -2419,81 +2453,100 @@ public static function getCustomGroupExtendsOptions() { 'label' => ts('Activities'), 'grouping' => 'activity_type_id', 'table_name' => 'civicrm_activity', + 'is_multiple' => FALSE, ], [ 'id' => 'Relationship', 'label' => ts('Relationships'), 'grouping' => 'relationship_type_id', 'table_name' => 'civicrm_relationship', + 'is_multiple' => FALSE, ], + // TODO: Move to civi_contribute extension (example: OptionValue_cg_extends_objects_grant.mgd.php) [ 'id' => 'Contribution', 'label' => ts('Contributions'), 'grouping' => 'financial_type_id', 'table_name' => 'civicrm_contribution', + 'is_multiple' => FALSE, ], [ 'id' => 'ContributionRecur', 'label' => ts('Recurring Contributions'), 'grouping' => NULL, 'table_name' => 'civicrm_contribution_recur', + 'is_multiple' => FALSE, ], [ 'id' => 'Group', 'label' => ts('Groups'), 'grouping' => NULL, 'table_name' => 'civicrm_group', + 'is_multiple' => FALSE, ], + // TODO: Move to civi_member extension (example: OptionValue_cg_extends_objects_grant.mgd.php) [ 'id' => 'Membership', 'label' => ts('Memberships'), 'grouping' => 'membership_type_id', 'table_name' => 'civicrm_membership', + 'is_multiple' => FALSE, ], + // TODO: Move to civi_event extension (example: OptionValue_cg_extends_objects_grant.mgd.php) [ 'id' => 'Event', 'label' => ts('Events'), 'grouping' => 'event_type_id', 'table_name' => 'civicrm_event', + 'is_multiple' => FALSE, ], [ 'id' => 'Participant', 'label' => ts('Participants'), 'grouping' => NULL, 'table_name' => 'civicrm_participant', + 'is_multiple' => FALSE, ], + // TODO: Move to civi_pledge extension (example: OptionValue_cg_extends_objects_grant.mgd.php) [ 'id' => 'Pledge', 'label' => ts('Pledges'), 'grouping' => NULL, 'table_name' => 'civicrm_pledge', + 'is_multiple' => FALSE, ], [ 'id' => 'Address', 'label' => ts('Addresses'), 'grouping' => NULL, 'table_name' => 'civicrm_address', + 'is_multiple' => FALSE, ], + // TODO: Move to civi_campaign extension (example: OptionValue_cg_extends_objects_grant.mgd.php) [ 'id' => 'Campaign', 'label' => ts('Campaigns'), 'grouping' => 'campaign_type_id', 'table_name' => 'civicrm_campaign', + 'is_multiple' => FALSE, ], [ 'id' => 'Contact', 'label' => ts('Contacts'), 'grouping' => NULL, 'table_name' => 'civicrm_contact', + 'is_multiple' => TRUE, ], ]; // `CustomGroup.extends` stores contact type as if it were an entity. - foreach (CRM_Contact_BAO_ContactType::basicTypePairs(TRUE) as $contactType => $contactTypeLabel) { + foreach (CRM_Contact_BAO_ContactType::basicTypeInfo(TRUE) as $contactType => $contactInfo) { $options[] = [ 'id' => $contactType, - 'label' => $contactTypeLabel, + 'label' => $contactInfo['label'], 'grouping' => 'contact_sub_type', 'table_name' => 'civicrm_contact', + 'is_multiple' => TRUE, + 'icon' => $contactInfo['icon'], ]; } $ogId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', 'cg_extend_objects', 'id', 'name'); @@ -2504,8 +2557,12 @@ public static function getCustomGroupExtendsOptions() { 'label' => $ogValue['label'], 'grouping' => $ogValue['grouping'] ?? NULL, 'table_name' => $ogValue['name'], + 'is_multiple' => !empty($ogValue['filter']), ]; } + foreach ($options as &$option) { + $option['icon'] = $option['icon'] ?? \Civi\Api4\Utils\CoreUtil::getInfoItem($option['id'], 'icon'); + } return $options; } diff --git a/CRM/Core/BAO/CustomValue.php b/CRM/Core/BAO/CustomValue.php index f801e1296fb8..485b231cc4ba 100644 --- a/CRM/Core/BAO/CustomValue.php +++ b/CRM/Core/BAO/CustomValue.php @@ -211,18 +211,19 @@ public static function deleteCustomValue($customValueID, $customGroupID) { } /** - * ACL clause for an APIv4 custom pseudo-entity (aka multi-record custom group extending Contact). + * ACL clause for an APIv4 custom pseudo-entity (aka multi-record custom group). * @param string|null $entityName * @param int|null $userId * @param array $conditions * @return array */ public function addSelectWhereClause(string $entityName = NULL, int $userId = NULL, array $conditions = []): array { - // To-date, custom-value-based entities are only supported for contacts. - // If this changes, $entityName variable contains the name of this custom group, - // and could be used to lookup the type of entity this custom group joins to. + // Some legacy code omits $entityName, in which case fall-back on 'Contact' which until 2023 + // was the only type of entity that could be extended by multi-record custom groups. + $groupName = \Civi\Api4\Utils\CoreUtil::getCustomGroupName((string) $entityName); + $joinEntity = $groupName ? CRM_Core_BAO_CustomGroup::getEntityForGroup($groupName) : 'Contact'; $clauses = [ - 'entity_id' => CRM_Utils_SQL::mergeSubquery('Contact'), + 'entity_id' => CRM_Utils_SQL::mergeSubquery($joinEntity), ]; CRM_Utils_Hook::selectWhereClause($entityName ?? $this, $clauses); return $clauses; @@ -232,7 +233,7 @@ public function addSelectWhereClause(string $entityName = NULL, int $userId = NU * Special checkAccess function for multi-record custom pseudo-entities * * @param string $entityName - * Ex: 'Contact' or 'Custom_Foobar' + * APIv4-style entity name e.g. 'Custom_Foobar' * @param string $action * @param array $record * @param int $userID @@ -243,8 +244,9 @@ public function addSelectWhereClause(string $entityName = NULL, int $userId = NU public static function _checkAccess(string $entityName, string $action, array $record, int $userID): ?bool { // This check implements two rules: you must have access to the specific custom-data-group - and to the underlying record (e.g. Contact). - $groupName = substr($entityName, 0, 7) === 'Custom_' ? substr($entityName, 7) : NULL; - $extends = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $groupName, 'extends', 'name'); + // Expecting APIv4-style entity name + $groupName = \Civi\Api4\Utils\CoreUtil::getCustomGroupName($entityName); + $extends = \CRM_Core_BAO_CustomGroup::getEntityForGroup($groupName); $id = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $groupName, 'id', 'name'); if (!$groupName) { // $groupName is required but the function signature has to match the parent. @@ -267,17 +269,8 @@ public static function _checkAccess(string $entityName, string $action, array $r } // Do we have access to the target record? - if ($extends === 'Contact' || in_array($extends, CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE)) { - return \Civi\Api4\Utils\CoreUtil::checkAccessDelegated('Contact', 'update', ['id' => $eid], $userID); - } - elseif (\Civi\Api4\Utils\CoreUtil::getApiClass($extends)) { - // For most entities (Activity, Relationship, Contribution, ad nauseum), we acn just use an eponymous API. - return \Civi\Api4\Utils\CoreUtil::checkAccessDelegated($extends, 'update', ['id' => $eid], $userID); - } - else { - // Do you need to add a special case for some oddball custom-group type? - throw new CRM_Core_Exception("Cannot assess delegated permissions for group {$groupName}."); - } + $delegatedAction = $action === 'get' ? 'get' : 'update'; + return \Civi\Api4\Utils\CoreUtil::checkAccessDelegated($extends, $delegatedAction, ['id' => $eid], $userID); } } diff --git a/CRM/Core/DAO/CustomGroup.php b/CRM/Core/DAO/CustomGroup.php index 17c0bd892208..f1efcc5ca554 100644 --- a/CRM/Core/DAO/CustomGroup.php +++ b/CRM/Core/DAO/CustomGroup.php @@ -6,7 +6,7 @@ * * Generated from xml/schema/CRM/Core/CustomGroup.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:7a53822fedde32eeb1089f09654a2d88) + * (GenCodeChecksum:05aa9cfdd47acd56178a0a33a2f6ffc1) */ /** @@ -87,7 +87,7 @@ class CRM_Core_DAO_CustomGroup extends CRM_Core_DAO { public $extends; /** - * FK to civicrm_option_value.id (for option group custom_data_type.) + * FK to civicrm_option_value.value (for option group custom_data_type) * * @var int|string|null * (SQL type: int unsigned) @@ -384,6 +384,7 @@ public static function &fields() { 'name', 'label', 'grouping', + 'icon', ], ], 'add' => '1.1', @@ -392,7 +393,7 @@ public static function &fields() { 'name' => 'extends_entity_column_id', 'type' => CRM_Utils_Type::T_INT, 'title' => ts('Custom Group Subtype List'), - 'description' => ts('FK to civicrm_option_value.id (for option group custom_data_type.)'), + 'description' => ts('FK to civicrm_option_value.value (for option group custom_data_type)'), 'usage' => [ 'import' => FALSE, 'export' => FALSE, diff --git a/CRM/Core/Form.php b/CRM/Core/Form.php index d0f17d883d0e..07e15afea710 100644 --- a/CRM/Core/Form.php +++ b/CRM/Core/Form.php @@ -478,13 +478,9 @@ public function &add( // Like select but accepts rich array data (with nesting, colors, icons, etc) as option list. if ($inputType === 'select2') { $type = 'text'; - $options = []; - foreach ($attributes as $option) { - // Transform options from api4.getFields format - $option['text'] = $option['text'] ?? $option['label']; - unset($option['label']); - $options[] = $option; - } + // Options stored in $attributes. Transform from api4.getFields format if needed. + $options = CRM_Utils_Array::formatForSelect2($attributes ?: []); + // Attributes stored in $extra $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'])]); diff --git a/CRM/Custom/Form/Group.php b/CRM/Custom/Form/Group.php index 4f9baa454402..0c1dd26f6c24 100644 --- a/CRM/Custom/Form/Group.php +++ b/CRM/Custom/Form/Group.php @@ -18,61 +18,42 @@ /** * form to process actions on the set aspect of Custom Data */ -class CRM_Custom_Form_Group extends CRM_Core_Form { +class CRM_Custom_Form_Group extends CRM_Admin_Form { /** - * The set id saved to the session for an update. - * - * @var int - */ - public $_id; - - /** - * set is empty or not. + * Have any custom data records been saved yet? + * If not we can be more lenient about making changes. * * @var bool */ protected $_isGroupEmpty = TRUE; /** - * Array of existing subtypes set for a custom set. - * - * @var array + * Use APIv4 to load values. + * @var string */ - protected $_subtypes = []; + protected $retrieveMethod = 'api4'; /** * Set variables up before form is built. * - * * @return void */ public function preProcess() { $this->preventAjaxSubmit(); - Civi::resources()->addScriptFile('civicrm', 'js/jquery/jquery.crmIconPicker.js'); + parent::preProcess(); - // current set id - $this->_id = CRM_Utils_Request::retrieve('id', 'Positive', $this); $this->setAction($this->_id ? CRM_Core_Action::UPDATE : CRM_Core_Action::ADD); - if ($this->_id && CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $this->_id, 'is_reserved', 'id')) { - CRM_Core_Error::statusBounce("You cannot edit the settings of a reserved custom field-set."); - } - if ($this->_id) { - $title = CRM_Core_BAO_CustomGroup::getTitle($this->_id); - $this->setTitle(ts('Edit %1', [1 => $title])); - $params = ['id' => $this->_id]; - CRM_Core_BAO_CustomGroup::retrieve($params, $this->_defaults); - - $subExtends = $this->_defaults['extends_entity_column_value'] ?? NULL; - if (!empty($subExtends)) { - $this->_subtypes = explode(CRM_Core_DAO::VALUE_SEPARATOR, substr($subExtends, 1, -1)); + if ($this->_values['is_reserved']) { + CRM_Core_Error::statusBounce("You cannot edit the settings of a reserved custom field-set."); } + $this->_isGroupEmpty = CRM_Core_BAO_CustomGroup::isGroupEmpty($this->_id); + $this->setTitle(ts('Edit %1', [1 => $this->_values['title']])); } - else { - $this->setTitle(ts('New Custom Field Set')); - } + // Used by I18n/Dialog + $this->assign('gid', $this->_id); } /** @@ -103,26 +84,6 @@ public static function formRule($fields, $files, $self) { $errors['title'] = ts('Custom group \'%1\' already exists in Database.', [1 => $title]); } - if (!empty($fields['extends'][1])) { - if (in_array('', $fields['extends'][1]) && count($fields['extends'][1]) > 1) { - $errors['extends'] = ts("Cannot combine other option with 'Any'."); - } - } - - if (empty($fields['extends'][0])) { - $errors['extends'] = ts("You need to select the type of record that this set of custom fields is applicable for."); - } - - $extends = ['Activity', 'Relationship', 'Group', 'Contribution', 'Membership', 'Event', 'Participant']; - if (in_array($fields['extends'][0], $extends) && $fields['style'] == 'Tab') { - $errors['style'] = ts("Display Style should be Inline for this Class"); - $self->assign('showStyle', TRUE); - } - - if (!empty($fields['is_multiple'])) { - $self->assign('showMultiple', TRUE); - } - if (empty($fields['is_multiple']) && $fields['style'] == 'Tab with table') { $errors['style'] = ts("Display Style 'Tab with table' is only supported for multiple-record custom field sets."); } @@ -160,74 +121,67 @@ public function addRules() { */ public function buildQuickForm() { $this->applyFilter('__ALL__', 'trim'); - $attributes = CRM_Core_DAO::getAttribute('CRM_Core_DAO_CustomGroup'); - //title - $this->add('text', 'title', ts('Set Name'), $attributes['title'], TRUE); + // This form is largely driven by a trio of related fields: + // 1. `extends` - entity name e.g. Activity, Contact, (plus contact types pretending to be entities e.g. Individual, Organization) + // 2. `extends_entity_column_id` - "category" of sub_type (usually null as most entities only have one category of sub_type) + // 3. `extends_entity_column_value` - sub_type value(s) e.g. options from `activity_type_id` + // Most entities have no options for field 2. For them, it will be hidden from the form, and + // the pair of fields 1 & 3 will act like a normal chain-select, (value of 1 controls the options shown in 3). + // For extra-complex entities like Participant, fields 1 + 2 will act like a compound key to + // control the options in field 3. + + // Get options for the `extends` field. + $extendsOptions = CRM_Core_BAO_CustomGroup::getCustomGroupExtendsOptions(); + // Sort by label + $labels = array_column($extendsOptions, 'label'); + array_multisort($labels, SORT_NATURAL, $extendsOptions); + + // Get options for `extends_entity_column_id` (rarely used except for participants) + // Format as an array keyed by entity to match with 'extends' values, e.g. + // [ + // 'Participant' => [['id' => 'ParticipantRole', 'text' => 'Participants (Role)'], ...]], + // ] + $entityColumnIdOptions = []; + foreach (CRM_Core_BAO_CustomGroup::getExtendsEntityColumnIdOptions() as $idOption) { + $entityColumnIdOptions[$idOption['extends']][] = [ + 'id' => $idOption['id'], + 'text' => $idOption['label'], + ]; + } - //Fix for code alignment, CRM-3058 - $contactTypes = array_merge(['Contact'], CRM_Contact_BAO_ContactType::basicTypes()); - $this->assign('contactTypes', json_encode($contactTypes)); + $extendsValue = $this->_values['extends'] ?? NULL; + $initialEntityColumnIdOptions = $entityColumnIdOptions[$extendsValue] ?? []; + + $initialEntityColumnValueOptions = []; + if ($extendsValue) { + $initialEntityColumnValueOptions = civicrm_api4('CustomGroup', 'getFields', [ + 'where' => [['name', '=', 'extends_entity_column_value']], + 'action' => 'create', + 'loadOptions' => ['id', 'label'], + 'values' => $this->_values, + ], 0)['options']; + } - $sel1 = ["" => ts("- select -")] + CRM_Core_SelectValues::customGroupExtends(); - ksort($sel1); - $sel2 = CRM_Core_BAO_CustomGroup::getSubTypes(); + // Assign data for use by js chain-selects + $this->assign('entityColumnIdOptions', $entityColumnIdOptions); + // List of entities that allow `is_multiple` + $this->assign('allowMultiple', array_column($extendsOptions, 'is_multiple', 'id')); + // Used by warnDataLoss + $this->assign('defaultSubtypes', $this->_values['extends_entity_column_value'] ?? []); + // Used to initially hide selects with no options + $this->assign('emptyEntityColumnId', empty($initialEntityColumnIdOptions)); + $this->assign('emptyEntityColumnValue', empty($initialEntityColumnValueOptions)); + + // Add form fields + $this->add('text', 'title', ts('Set Name'), $attributes['title'], TRUE); - foreach ($sel2 as $main => $sub) { - if (!empty($sel2[$main])) { - $sel2[$main] = [ - '' => ts("- Any -"), - ] + $sel2[$main]; - } - } + $this->add('select2', 'extends', ts('Used For'), $extendsOptions, TRUE, ['placeholder' => ts('Select')]); - $sel = &$this->add('hierselect', - 'extends', - ts('Used For'), - [ - 'name' => 'extends[0]', - 'style' => 'vertical-align: top;', - ], - TRUE - ); - $sel->setOptions([$sel1, $sel2]); - 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); - } - if ($this->_action == CRM_Core_Action::UPDATE) { - $subName = $this->_defaults['extends_entity_column_id'] ?? NULL; - if ($this->_defaults['extends'] == 'Participant') { - if ($subName == 1) { - $this->_defaults['extends'] = 'ParticipantRole'; - } - elseif ($subName == 2) { - $this->_defaults['extends'] = 'ParticipantEventName'; - } - elseif ($subName == 3) { - $this->_defaults['extends'] = 'ParticipantEventType'; - } - } + $this->add('select2', 'extends_entity_column_id', ts('Type'), $initialEntityColumnIdOptions, FALSE, ['placeholder' => ts('Any')]); - //allow to edit settings if custom set is empty CRM-5258 - $this->_isGroupEmpty = CRM_Core_BAO_CustomGroup::isGroupEmpty($this->_id); - if (!$this->_isGroupEmpty) { - if (!empty($this->_subtypes)) { - // we want to allow adding / updating subtypes for this case, - // and therefore freeze the first selector only. - $sel->_elements[0]->freeze(); - } - else { - // freeze both the selectors - $sel->freeze(); - } - } - $this->assign('isCustomGroupEmpty', $this->_isGroupEmpty); - $this->assign('gid', $this->_id); - } - $this->assign('defaultSubtypes', json_encode($this->_subtypes)); + $this->add('select2', 'extends_entity_column_value', ts('Sub Type'), $initialEntityColumnValueOptions, FALSE, ['multiple' => TRUE, 'placeholder' => ts('Any')]); // help text $this->add('wysiwyg', 'help_pre', ts('Pre-form Help'), $attributes['help_pre']); @@ -254,26 +208,21 @@ public function buildQuickForm() { //Is this set visible on public pages? $this->addElement('advcheckbox', 'is_public', ts('Is this Custom Data Set public?')); - // does this set have multiple record? - $multiple = $this->addElement('advcheckbox', 'is_multiple', + $this->addElement('advcheckbox', 'is_multiple', ts('Does this Custom Field Set allow multiple records?'), NULL); - // $min_multiple = $this->add('text', 'min_multiple', ts('Minimum number of multiple records'), $attributes['min_multiple'] ); - // $this->addRule('min_multiple', ts('is a numeric field') , 'numeric'); - - $max_multiple = $this->add('number', 'max_multiple', ts('Maximum number of multiple records'), $attributes['max_multiple']); + $this->add('number', 'max_multiple', ts('Maximum number of multiple records'), $attributes['max_multiple']); $this->addRule('max_multiple', ts('is a numeric field'), 'numeric'); - //allow to edit settings if custom set is empty CRM-5258 - $this->assign('isGroupEmpty', $this->_isGroupEmpty); + // Once data exists, certain options cannot be changed if (!$this->_isGroupEmpty) { - $multiple->freeze(); - //$min_multiple->freeze(); - $max_multiple->freeze(); + $this->getElement('extends')->freeze(); + $this->getElement('extends_entity_column_id')->freeze(); + $this->getElement('is_multiple')->freeze(); + // Don't allow max to be lowered if data already exists + $this->getElement('max_multiple')->setAttribute('min', $this->_values['max_multiple'] ?? '0'); } - $this->assign('showStyle', FALSE); - $this->assign('showMultiple', FALSE); $buttons = [ [ 'type' => 'next', @@ -286,77 +235,41 @@ public function buildQuickForm() { 'name' => ts('Cancel'), ], ]; - if (!$this->_isGroupEmpty && !empty($this->_subtypes)) { + if (!$this->_isGroupEmpty && !empty($this->_values['extends_entity_column_value'])) { $buttons[0]['class'] = 'crm-warnDataLoss'; } $this->addButtons($buttons); } /** - * Set default values for the form. Note that in edit/view mode - * the default values are retrieved from the database - * - * + * Set default values for the form. * @return array - * array of default values */ - public function setDefaultValues() { - $defaults = &$this->_defaults; - $this->assign('showMaxMultiple', TRUE); + public function setDefaultValues(): array { + $defaults = &$this->_values; if ($this->_action == CRM_Core_Action::ADD) { $defaults['weight'] = CRM_Utils_Weight::getDefaultWeight('CRM_Core_DAO_CustomGroup'); - $defaults['is_multiple'] = $defaults['min_multiple'] = 0; $defaults['is_active'] = $defaults['is_public'] = $defaults['collapse_adv_display'] = 1; $defaults['style'] = 'Inline'; } - elseif (empty($defaults['max_multiple']) && !$this->_isGroupEmpty) { - $this->assign('showMaxMultiple', FALSE); - } - - if (($this->_action & CRM_Core_Action::UPDATE) && !empty($defaults['is_multiple'])) { - $defaults['collapse_display'] = 0; - } - - if (isset($defaults['extends'])) { - $extends = $defaults['extends']; - unset($defaults['extends']); - - $defaults['extends'][0] = $extends; - - if (!empty($this->_subtypes)) { - $defaults['extends'][1] = $this->_subtypes; - } - else { - $defaults['extends'][1] = [0 => '']; - } - - if ($extends == 'Relationship' && !empty($this->_subtypes)) { - $relationshipDefaults = []; - foreach ($defaults['extends'][1] as $donCare => $rel_type_id) { - $relationshipDefaults[] = $rel_type_id; - } - - $defaults['extends'][1] = $relationshipDefaults; - } - } - return $defaults; } /** - * Process the form. - * - * * @return void */ public function postProcess() { // get the submitted form values. $params = $this->controller->exportValues('Group'); + if (!empty($params['extends_entity_column_value']) && is_string($params['extends_entity_column_value'])) { + // Because select2 + $params['extends_entity_column_value'] = explode(',', $params['extends_entity_column_value']); + } $params['overrideFKConstraint'] = 0; if ($this->_action & CRM_Core_Action::UPDATE) { $params['id'] = $this->_id; - if ($this->_defaults['extends'][0] != $params['extends'][0]) { + if ($this->_values['extends'] != $params['extends']) { $params['overrideFKConstraint'] = 1; } @@ -423,11 +336,14 @@ public function postProcess() { } } + public function getDefaultEntity(): string { + return 'CustomGroup'; + } + /** - * Return a formatted list of relationship labels. + * Function that's only ever called by another deprecated function. * - * @return array - * Array (int $id => string $label). + * @deprecated */ public static function getRelationshipTypes() { // Note: We include inactive reltypes because we don't want to break custom-data diff --git a/CRM/Utils/Array.php b/CRM/Utils/Array.php index da71c1197120..f1e2fd0adaed 100644 --- a/CRM/Utils/Array.php +++ b/CRM/Utils/Array.php @@ -1413,4 +1413,28 @@ public static function filterByPrefix(array &$collection, string $prefix): array return $filtered; } + /** + * Changes array keys to meet the expectations of select2.js + * + * @param array $options + * @param string $label + * @param string $id + * @return array + */ + public static function formatForSelect2(array $options, string $label = 'label', string $id = 'id'): array { + foreach ($options as &$option) { + if (isset($option[$label])) { + $option['text'] = (string) $option[$label]; + } + if (isset($option[$id])) { + $option['id'] = (string) $option[$id]; + } + if (!empty($option['children'])) { + $option['children'] = self::formatForSelect2($option['children'], $label, $id); + } + $option = array_intersect_key($option, array_flip(['id', 'text', 'children', 'color', 'icon', 'description'])); + } + return $options; + } + } diff --git a/Civi/Api4/Generic/Traits/DAOActionTrait.php b/Civi/Api4/Generic/Traits/DAOActionTrait.php index 3de18daa7ca6..0f6c5cf8cdb8 100644 --- a/Civi/Api4/Generic/Traits/DAOActionTrait.php +++ b/Civi/Api4/Generic/Traits/DAOActionTrait.php @@ -13,7 +13,6 @@ namespace Civi\Api4\Generic\Traits; use Civi\Api4\CustomField; -use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable; use Civi\Api4\Utils\FormattingUtil; use Civi\Api4\Utils\CoreUtil; use Civi\Api4\Utils\ReflectionUtils; @@ -311,7 +310,7 @@ protected function getCustomFieldInfo(string $fieldExpr) { $field['table_name'] = $field['custom_group_id.table_name']; unset($field['custom_group_id.table_name']); $field['name'] = $groupName . '.' . $name; - $field['entity'] = CustomGroupJoinable::getEntityFromExtends($field['custom_group_id.extends']); + $field['entity'] = \CRM_Core_BAO_CustomGroup::getEntityFromExtends($field['custom_group_id.extends']); $info[$name] = $field; } \Civi::cache('metadata')->set($cacheKey, $info); diff --git a/Civi/Api4/Provider/CustomEntityProvider.php b/Civi/Api4/Provider/CustomEntityProvider.php index b454a55eedf3..41f40971596c 100644 --- a/Civi/Api4/Provider/CustomEntityProvider.php +++ b/Civi/Api4/Provider/CustomEntityProvider.php @@ -12,9 +12,9 @@ namespace Civi\Api4\Provider; use Civi\Api4\CustomValue; -use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable; use Civi\Core\Event\GenericHookEvent; use Civi\Core\Service\AutoService; +use CRM_Core_BAO_CustomGroup; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -44,7 +44,7 @@ public static function addCustomEntities(GenericHookEvent $e) { $group = \CRM_Core_DAO::executeQuery($select); while ($group->fetch()) { $entityName = 'Custom_' . $group->name; - $baseEntity = CustomGroupJoinable::getEntityFromExtends($group->extends); + $baseEntity = CRM_Core_BAO_CustomGroup::getEntityFromExtends($group->extends); // Lookup base entity info using DAO methods not CoreUtil to avoid early-bootstrap issues $baseEntityDao = \CRM_Core_DAO_AllCoreTables::getFullName($baseEntity); $baseEntityTitle = $baseEntityDao ? $baseEntityDao::getEntityTitle(TRUE) : $baseEntity; diff --git a/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php b/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php index 4ca50f079554..1a10075f7674 100644 --- a/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php +++ b/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php @@ -89,25 +89,4 @@ public function getSqlColumn($fieldName) { return $this->columns[$fieldName]; } - /** - * Translate custom_group.extends to entity name. - * - * Custom_group.extends pretty much maps 1-1 with entity names, except for a couple oddballs. - * @see \CRM_Core_SelectValues::customGroupExtends - * - * @param $extends - * @return string - * @throws \CRM_Core_Exception - * @throws \Civi\API\Exception\UnauthorizedException - */ - public static function getEntityFromExtends($extends) { - if (strpos($extends, 'Participant') === 0) { - return 'Participant'; - } - if ($extends === 'Contact' || in_array($extends, \CRM_Contact_BAO_ContactType::basicTypes(TRUE))) { - return 'Contact'; - } - return $extends; - } - } diff --git a/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php b/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php index 1da2127af664..5ac7c1e6def8 100644 --- a/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php +++ b/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php @@ -14,6 +14,7 @@ use Civi\Api4\Service\Spec\FieldSpec; use Civi\Api4\Service\Spec\RequestSpec; +use Civi\Api4\Utils\CoreUtil; /** * @service @@ -31,19 +32,26 @@ public function modifySpec(RequestSpec $spec) { $idField->setType('Field'); $idField->setInputType('Number'); $idField->setColumnName('id'); - $idField->setNullable('false'); + $idField->setNullable(FALSE); $idField->setTitle(ts('Custom Value ID')); $idField->setReadonly(TRUE); $idField->setNullable(FALSE); $spec->addFieldSpec($idField); + // Check which entity this group extends + $groupName = CoreUtil::getCustomGroupName($spec->getEntity()); + $baseEntity = \CRM_Core_BAO_CustomGroup::getEntityForGroup($groupName); + // Lookup base entity info using DAO methods not CoreUtil to avoid early-bootstrap issues + $baseEntityDao = \CRM_Core_DAO_AllCoreTables::getFullName($baseEntity); + $baseEntityTitle = $baseEntityDao ? $baseEntityDao::getEntityTitle() : $baseEntity; + $entityField = new FieldSpec('entity_id', $spec->getEntity(), 'Integer'); $entityField->setType('Field'); $entityField->setColumnName('entity_id'); $entityField->setTitle(ts('Entity ID')); - $entityField->setLabel(ts('Contact')); + $entityField->setLabel($baseEntityTitle); $entityField->setRequired($action === 'create'); - $entityField->setFkEntity('Contact'); + $entityField->setFkEntity($baseEntity); $entityField->setReadonly(TRUE); $entityField->setNullable(FALSE); $entityField->setInputType('EntityRef'); @@ -54,7 +62,7 @@ public function modifySpec(RequestSpec $spec) { * @inheritDoc */ public function applies($entity, $action) { - return strstr($entity, 'Custom_'); + return str_starts_with($entity, 'Custom_'); } } diff --git a/Civi/Api4/Utils/CoreUtil.php b/Civi/Api4/Utils/CoreUtil.php index 473c6ff80321..fb5df8e6d3c3 100644 --- a/Civi/Api4/Utils/CoreUtil.php +++ b/Civi/Api4/Utils/CoreUtil.php @@ -117,6 +117,10 @@ public static function getApiNameFromTableName($tableName) { return NULL; } + public static function getCustomGroupName(string $entityName): ?string { + return str_starts_with($entityName, 'Custom_') ? substr($entityName, 7) : NULL; + } + /** * @return string[] */ @@ -134,6 +138,9 @@ public static function getOperators() { /** * For a given API Entity, return the types of custom fields it supports and the column they join to. * + * Sort of the inverse of this function: + * @see \CRM_Core_BAO_CustomGroup::getEntityForGroup + * * @param string $entityName * @return array{extends: array, column: string, grouping: mixed}|null */ diff --git a/api/v3/CustomGroup.php b/api/v3/CustomGroup.php index 55d5f51774ed..6eaf8f60186b 100644 --- a/api/v3/CustomGroup.php +++ b/api/v3/CustomGroup.php @@ -16,19 +16,17 @@ */ /** - * Create or modify a custom field group. + * This entire function consists of legacy handling, probably for a form that no longer exists. + * APIv3 is where code like this goes to die... * * @param array $params * For legacy reasons, 'extends' can be passed as an array (for setting Participant column_value) * * @return array - * @todo $params['extends'] is array format - is that std compatible */ function civicrm_api3_custom_group_create($params) { if (isset($params['extends']) && is_string($params['extends'])) { - $extends = explode(",", $params['extends']); - unset($params['extends']); - $params['extends'] = $extends; + $params['extends'] = explode(',', $params['extends']); } if (!isset($params['id']) && (!isset($params['extends'][0]) || !trim($params['extends'][0]))) { diff --git a/sql/civicrm_data/civicrm_option_group/custom_data_type.sqldata.php b/sql/civicrm_data/civicrm_option_group/custom_data_type.sqldata.php index 0ecea058f3de..13f200ae6c47 100644 --- a/sql/civicrm_data/civicrm_option_group/custom_data_type.sqldata.php +++ b/sql/civicrm_data/civicrm_option_group/custom_data_type.sqldata.php @@ -3,21 +3,25 @@ ->addMetadata([ 'title' => ts('Custom Data Type'), ]) + // Note: When adding options to this group, the 'name' *must* begin with the exact name of the base entity, + // as that's the (very lo-tech) way these options are matched with their base entity. + // Wrong: 'name' => 'ActivitiesByStatus' + // Right: 'name' => 'ActivityByStatus' ->addValues([ [ - 'label' => ts('Participants (Role)'), + 'label' => ts('Role'), 'value' => 1, 'name' => 'ParticipantRole', 'grouping' => 'role_id', ], [ - 'label' => ts('Participants (Event Name)'), + 'label' => ts('Event Name'), 'value' => 2, 'name' => 'ParticipantEventName', 'grouping' => 'event_id', ], [ - 'label' => ts('Participants (Event Type)'), + 'label' => ts('Event Type'), 'value' => 3, 'name' => 'ParticipantEventType', 'grouping' => 'event_id.event_type_id', diff --git a/templates/CRM/Custom/Form/Group.tpl b/templates/CRM/Custom/Form/Group.tpl index ed4367d63f15..cfde76ea8720 100644 --- a/templates/CRM/Custom/Form/Group.tpl +++ b/templates/CRM/Custom/Form/Group.tpl @@ -12,55 +12,59 @@
- - + + - - + + - - + + - {* This section shown only when Used For = Contact, Individ, Org or Household. *} - - + + + - - - + + + - - - + + + - + - - - + + + - - + + - - + + - + - +
{$form.title.label} {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_custom_group' field='title' id=$gid}{/if}{$form.title.html} {help id="id-title"}{$form.title.label} {help id="id-title"}{if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_custom_group' field='title' id=$gid}{/if}{$form.title.html}
{$form.extends.label}{$form.extends.html|smarty:nodefaults} {help id="id-extends"}{$form.extends.label} {help id="id-extends"} + {$form.extends.html} + {$form.extends_entity_column_id.html} + {$form.extends_entity_column_value.html} +
{$form.weight.label}{$form.weight.html} {help id="id-weight"}{$form.weight.label} {help id="id-weight"}{$form.weight.html}
{$form.is_multiple.html} {$form.is_multiple.label} {help id="id-is_multiple"}
{help id="id-is_multiple"}{$form.is_multiple.html} {$form.is_multiple.label}
{$form.max_multiple.label}{$form.max_multiple.html} {help id="id-max_multiple"}
{$form.max_multiple.label} {help id="id-max_multiple"}{$form.max_multiple.html}
{$form.style.label}{$form.style.html} {help id="id-display_style"}
{$form.style.label} {help id="id-display_style"}{$form.style.html}
{$form.icon.label} {$form.icon.html}
 {$form.collapse_display.html} {$form.collapse_display.label} {help id="id-collapse"}
{help id="id-collapse"}{$form.collapse_display.html} {$form.collapse_display.label}
 {$form.collapse_adv_display.html} {$form.collapse_adv_display.label} {help id="id-collapse-adv"}{help id="id-collapse-adv"}{$form.collapse_adv_display.html} {$form.collapse_adv_display.label}
  {$form.is_active.html} {$form.is_active.label}
 {$form.is_public.html} {$form.is_public.label} {help id="id-is-public"}{help id="id-is-public"}{$form.is_public.html} {$form.is_public.label}
{$form.help_pre.label} {help id="id-help_pre"}{$form.help_pre.label} {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_custom_group' field='help_pre' id=$gid}{/if} {help id="id-help_pre"} {$form.help_pre.html}
{$form.help_post.label} {help id="id-help_post"}{$form.help_post.label} {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_custom_group' field='help_post' id=$gid}{/if} {help id="id-help_post"} {$form.help_post.html}
@@ -74,104 +78,132 @@ {/if} {literal} -