Skip to content

Commit

Permalink
Merge pull request #31606 from ufundo/custom-group-tabsets
Browse files Browse the repository at this point in the history
dev/core#5603 Support multi-record CustomGroups for Events using Afform tab display
  • Loading branch information
colemanw authored Dec 31, 2024
2 parents b250ffa + b985d82 commit 10a132b
Show file tree
Hide file tree
Showing 25 changed files with 219 additions and 59 deletions.
30 changes: 15 additions & 15 deletions CRM/Core/BAO/CustomGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -2137,7 +2137,7 @@ public static function getEntityFromExtends(string $extends): string {
* Returns a mix of hard-coded array and `cg_extend_objects` OptionValues.
* - '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`)
* - 'allow_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).
Expand All @@ -2151,89 +2151,89 @@ public static function getCustomGroupExtendsOptions() {
'label' => ts('Activities'),
'grouping' => 'activity_type_id',
'table_name' => 'civicrm_activity',
'is_multiple' => FALSE,
'allow_is_multiple' => FALSE,
],
[
'id' => 'Relationship',
'label' => ts('Relationships'),
'grouping' => 'relationship_type_id',
'table_name' => 'civicrm_relationship',
'is_multiple' => FALSE,
'allow_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,
'allow_is_multiple' => FALSE,
],
[
'id' => 'ContributionRecur',
'label' => ts('Recurring Contributions'),
'grouping' => NULL,
'table_name' => 'civicrm_contribution_recur',
'is_multiple' => FALSE,
'allow_is_multiple' => FALSE,
],
[
'id' => 'Group',
'label' => ts('Groups'),
'grouping' => NULL,
'table_name' => 'civicrm_group',
'is_multiple' => FALSE,
'allow_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,
'allow_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,
'allow_is_multiple' => TRUE,
],
[
'id' => 'Participant',
'label' => ts('Participants'),
'grouping' => NULL,
'table_name' => 'civicrm_participant',
'is_multiple' => FALSE,
'allow_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,
'allow_is_multiple' => FALSE,
],
[
'id' => 'Address',
'label' => ts('Addresses'),
'grouping' => NULL,
'table_name' => 'civicrm_address',
'is_multiple' => FALSE,
'allow_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,
'allow_is_multiple' => FALSE,
],
[
'id' => 'Contact',
'label' => ts('Contacts'),
'grouping' => NULL,
'table_name' => 'civicrm_contact',
'is_multiple' => TRUE,
'allow_is_multiple' => TRUE,
],
];
// `CustomGroup.extends` stores contact type as if it were an entity.
Expand All @@ -2243,7 +2243,7 @@ public static function getCustomGroupExtendsOptions() {
'label' => $contactInfo['label'],
'grouping' => 'contact_sub_type',
'table_name' => 'civicrm_contact',
'is_multiple' => TRUE,
'allow_is_multiple' => TRUE,
'icon' => $contactInfo['icon'],
];
}
Expand All @@ -2255,7 +2255,7 @@ public static function getCustomGroupExtendsOptions() {
'label' => $ogValue['label'],
'grouping' => $ogValue['grouping'] ?? NULL,
'table_name' => $ogValue['name'],
'is_multiple' => !empty($ogValue['filter']),
'allow_is_multiple' => !empty($ogValue['filter']),
];
}
foreach ($options as &$option) {
Expand Down
2 changes: 1 addition & 1 deletion CRM/Custom/Form/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public function buildQuickForm() {
// 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'));
$this->assign('allowMultiple', array_column($extendsOptions, 'allow_is_multiple', 'id'));
// Used by warnDataLoss
$this->assign('defaultSubtypes', $this->_values['extends_entity_column_value'] ?? []);
// Used to initially hide selects with no options
Expand Down
2 changes: 1 addition & 1 deletion ang/afform/afsearchTabNote.aff.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div af-fieldset="">
<crm-search-display-table search-name="Contact_Summary_Notes" display-name="Contact_Summary_Notes_Tab" filters="{entity_id: options.contact_id, entity_table: 'civicrm_contact'}"></crm-search-display-table>
<crm-search-display-table search-name="Contact_Summary_Notes" display-name="Contact_Summary_Notes_Tab" filters="{entity_id: options.entity_id, entity_table: 'civicrm_contact'}"></crm-search-display-table>
</div>
4 changes: 2 additions & 2 deletions ang/afform/afsearchTabRel.aff.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<div af-fieldset="">
<crm-search-display-table search-name="Contact_Summary_Relationships" display-name="Contact_Summary_Relationships_Active" filters="{near_contact_id: options.contact_id, is_current: true}"></crm-search-display-table>
<crm-search-display-table search-name="Contact_Summary_Relationships" display-name="Contact_Summary_Relationships_Active" filters="{near_contact_id: options.entity_id, is_current: true}"></crm-search-display-table>
<h3>{{:: ts('Inactive Relationships') }}</h3>
<div class="help">{{:: ts('These relationships are Disabled OR have a past End Date.') }}</div>
<crm-search-display-table search-name="Contact_Summary_Relationships" display-name="Contact_Summary_Relationships_Inactive" filters="{near_contact_id: options.contact_id, is_current: false}"></crm-search-display-table>
<crm-search-display-table search-name="Contact_Summary_Relationships" display-name="Contact_Summary_Relationships_Inactive" filters="{near_contact_id: options.entity_id, is_current: false}"></crm-search-display-table>
<div class="help">
<strong>{{:: ts('Permissioned Relationships:') }}</strong>
<i class="crm-i fa-eye"></i> {{:: ts('This contact can be viewed by the other.') }}
Expand Down
65 changes: 59 additions & 6 deletions ext/afform/core/Civi/Api4/Action/CustomGroup/GetAfforms.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class GetAfforms extends \Civi\Api4\Generic\BasicBatchAction {
'afblockCustom',
'afformUpdateCustom',
'afformCreateCustom',
'afformViewCustom',
'afsearchTabCustom',
];

Expand Down Expand Up @@ -74,6 +75,7 @@ protected function doTask($item) {
break;

case 'form':
$forms[] = $this->generateViewForm($item);
$forms[] = $this->generateUpdateForm($item);
if ($item['is_multiple']) {
$forms[] = $this->generateCreateForm($item);
Expand All @@ -83,11 +85,9 @@ protected function doTask($item) {
case 'search':
// TODO:
// 1. tabs with grid display
// 2. tabs for other entities (e.g. Event)
if (
$item['is_multiple']
&& ($item['style'] === 'Tab with table')
&& CoreUtil::isContact($item['extends'])
) {
$forms[] = $this->generateTabForm($item);
}
Expand Down Expand Up @@ -125,6 +125,56 @@ private function generateFieldBlock($item): array {
return $afform;
}

private function generateViewForm($item): array {
$afform = [
'name' => 'afformViewCustom_' . $item['name'],
'type' => 'form',
'title' => E::ts('View %1', [1 => $item['title']]),
'description' => '',
'is_public' => FALSE,
// NOTE: we will use RBAC for entities to ensure
// this form does not allow folks who shouldn't
// to edit contacts
'permission' => ['access CiviCRM'],
'server_route' => 'civicrm/af/custom/' . $item['name'] . '/view',
'icon' => $item['icon'],
];
if ($this->getLayout) {

// form entity depends on whether this is a multirecord custom group
$formEntity = $item['is_multiple'] ?
[
'type' => 'Custom_' . $item['name'],
'name' => 'Record',
'label' => $item['extends'] . ' ' . $item['title'],
'parent_field' => 'entity_id',
'parent_field_defn' => [
'input_type' => 'Hidden',
'label' => FALSE,
],
] :
[
'type' => $item['extends'],
'name' => $item['extends'] . '1',
'label' => $item['extends'],
'parent_field' => 'id',
'parent_field_defn' => [
'input_type' => 'Hidden',
'label' => FALSE,
],
];

$afform['layout'] = \CRM_Core_Smarty::singleton()->fetchWith(
'afform/customGroups/afformView.tpl',
[
'formEntity' => $formEntity,
'group' => $item,
]
);
}
return $afform;
}

private function generateUpdateForm($item): array {
$afform = [
'name' => 'afformUpdateCustom_' . $item['name'],
Expand Down Expand Up @@ -165,7 +215,7 @@ private function generateUpdateForm($item): array {
];

$afform['layout'] = \CRM_Core_Smarty::singleton()->fetchWith(
'afform/customGroups/afform.tpl',
'afform/customGroups/afformEdit.tpl',
[
'formEntity' => $formEntity,
'formActions' => [
Expand Down Expand Up @@ -206,7 +256,7 @@ private function generateCreateForm($item): array {
],
];
$afform['layout'] = \CRM_Core_Smarty::singleton()->fetchWith(
'afform/customGroups/afform.tpl',
'afform/customGroups/afformEdit.tpl',
[
'formEntity' => $formEntity,
'formActions' => [
Expand All @@ -222,10 +272,11 @@ private function generateCreateForm($item): array {
}

private function generateTabForm($item): array {
$extendsLabel = CoreUtil::getInfoItem($item['extends'], 'title');
$afform = [
// name required to replace the existing tab
'name' => 'afsearchTabCustom_' . $item['name'],
'description' => E::ts('Contact summary tab display for %1', [1 => $item['title']]),
'description' => E::ts('%1 tab display for %2', [1 => $extendsLabel, 2 => $item['title']]),
'type' => 'search',
'is_public' => FALSE,
// Q: should this be more permissive if user has access
Expand All @@ -243,7 +294,9 @@ private function generateTabForm($item): array {
$afform['summary_contact_type'] = [$item['extends']];
}
else {
// TODO implement tabs for other tabsets
// tabs for other entities are placed without any
// additional afform meta
// @see civicrm_admin_ui_civicrm_tabset
}
if ($this->getLayout) {
// TODO: the template should be a table or grid depending
Expand Down
12 changes: 10 additions & 2 deletions ext/afform/core/afform.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,20 @@ function afform_civicrm_tabset($tabsetName, &$tabs, $context) {
'icon' => 'crm-i ' . ($afform['icon'] ?: 'fa-list-alt'),
'is_active' => TRUE,
'contact_type' => _afform_get_contact_types($summaryContactType) ?: NULL,
'template' => 'afform/contactSummary/AfformTab.tpl',
'template' => 'afform/InlineAfform.tpl',
'module' => $afform['module_name'],
'directive' => $afform['directive_name'],
];
// If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module.
// If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module
// and assign contact id to required smarty variable
if (empty($context['caller'])) {
// note we assign the contact id to entity_id as preferred key
// but also contact_id to maintain backwards compatibility with older
// afforms
CRM_Core_Smarty::singleton()->assign('afformOptions', [
'entity_id' => $context['contact_id'],
'contact_id' => $context['contact_id'],
]);
Civi::service('angularjs.loader')->addModules($afform['module_name']);
}
}
Expand Down
5 changes: 5 additions & 0 deletions ext/afform/core/templates/afform/InlineAfform.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<crm-angular-js modules="{$afform.module}">
<form id="bootstrap-theme">
<{$afform.directive} options='{$afformOptions|@json_encode}'></{$afform.directive}>
</form>
</crm-angular-js>
5 changes: 0 additions & 5 deletions ext/afform/core/templates/afform/contactSummary/AfformTab.tpl

This file was deleted.

2 changes: 1 addition & 1 deletion ext/afform/core/templates/afform/customGroups/afblock.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

{foreach from=$group.field_names item=field_name}
{* for multiple record fields there is no need to prepend
the group name because it is provided as the join_entity above *}
the group name because it will be the form entity itself *}
<af-field name="{if !$group.is_multiple}{$group.name}.{/if}{$field_name}" />
{/foreach}

Expand Down
22 changes: 22 additions & 0 deletions ext/afform/core/templates/afform/customGroups/afformView.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<af-form ctrl="afform">
<af-entity
type="{$formEntity.type}"
name="{$formEntity.name}"
label="{$formEntity.label}"
actions='{ldelim}create: false, update: true{rdelim}'
security="RBAC"
url-autofill="1"
/>

<fieldset af-fieldset="{$formEntity.name}" class="af-container">
<af-field
name="{$formEntity.parent_field}"
defn='{$formEntity.parent_field_defn|@json_encode}'
/>
{foreach from=$group.field_names item=field_name}
{* for multiple record fields there is no need to prepend
the group name because it will be the form entity itself *}
<af-field name="{if !$group.is_multiple}{$group.name}.{/if}{$field_name}" defn="{ldelim}input_type: 'DisplayOnly'{rdelim}" />
{/foreach}
</fieldset>
</af-form>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
<crm-search-display-{$display_type}
search-name="{$saved_search}"
display-name="{$search_display}"
filters="{ldelim}entity_id: options.contact_id{rdelim}"
filters="{ldelim}entity_id: options.entity_id{rdelim}"
/>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,11 @@ protected function getButtonColumn($group) {
}

public static function getAllManaged() {
// for now we only fetch for Groups that have a Tab with table on the contact summary
// for now we only fetch for Groups that have a Tab
$all = \Civi\Api4\CustomGroup::getSearchKit(FALSE)
->addWhere('is_active', '=', TRUE)
->addWhere('is_multiple', '=', TRUE)
->addWhere('style', 'IN', ['Tab', 'Tab with table'])
->addWhere('extends', 'IN', ['Contact', 'Individual', 'Household', 'Organization'])
->execute()
->column('managed');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function addCustomGroupLinks(\Civi\Core\Event\GenericHookEvent $event) {
$groupName = substr($name, 7);
$event->entities[$name]['paths']['add'] = "civicrm/af/custom/{$groupName}/create#?entity_id=[entity_id]";
$event->entities[$name]['paths']['update'] = "civicrm/af/custom/{$groupName}/update#?Record=[id]";
$event->entities[$name]['paths']['view'] = "civicrm/af/custom/{$groupName}/view#?Record=[id]";
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions ext/civicrm_admin_ui/ang/afformTabMember.aff.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<div af-fieldset="">
<crm-search-display-table search-name="Contact_Summary_Memberships" display-name="Contact_Summary_Memberships_Active" filters="{contact_id: options.contact_id, 'status_id.is_current_member': true}"></crm-search-display-table>
<crm-search-display-table search-name="Contact_Summary_Memberships" display-name="Contact_Summary_Memberships_Active" filters="{contact_id: options.entity_id, 'status_id.is_current_member': true}"></crm-search-display-table>
<h3>{{:: ts('Pending and Inactive Memberships') }}</h3>
<crm-search-display-table search-name="Contact_Summary_Memberships" display-name="Contact_Summary_Memberships_Inactive" filters="{contact_id: options.contact_id, 'status_id.is_current_member': false}"></crm-search-display-table>
<crm-search-display-table search-name="Contact_Summary_Memberships" display-name="Contact_Summary_Memberships_Inactive" filters="{contact_id: options.entity_id, 'status_id.is_current_member': false}"></crm-search-display-table>
</div>
<div class="af-markup">
<hr />
</div>
<div af-fieldset="" af-title="Membership Types">
<crm-search-display-table search-name="Contact_Summary_Membership_Type" display-name="Contact_Summary_Membership_Type" filters="{member_of_contact_id: options.contact_id, 'is_active': true}"></crm-search-display-table>
<crm-search-display-table search-name="Contact_Summary_Membership_Type" display-name="Contact_Summary_Membership_Type" filters="{member_of_contact_id: options.entity_id, 'is_active': true}"></crm-search-display-table>
</div>
2 changes: 1 addition & 1 deletion ext/civicrm_admin_ui/ang/afsearchTabActivity.aff.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
<af-field name="status_id" defn="{input_attrs: {multiple: true, placeholder: ts('Status')}, label: false}" />
<af-field name="activity_date_time" defn="{input_type: 'Select', search_range: true, input_attrs: {placeholder: ts('Date')}, label: false}" />
</div>
<crm-search-display-table search-name="Contact_Summary_Activities" display-name="Contact_Summary_Activities_Tab" filters="{'Activity_ActivityContact_Contact_01.id': options.contact_id}"></crm-search-display-table>
<crm-search-display-table search-name="Contact_Summary_Activities" display-name="Contact_Summary_Activities_Tab" filters="{'Activity_ActivityContact_Contact_01.id': options.entity_id}"></crm-search-display-table>
</div>
Loading

0 comments on commit 10a132b

Please sign in to comment.