From 6810eade643b70a263ec9322cbfa675d3ee0de24 Mon Sep 17 00:00:00 2001 From: benjamin Date: Fri, 15 Nov 2024 11:39:25 +0000 Subject: [PATCH] move custom group block generation to CustomGroup api action; add dynamic forms for editing and adding custom group data --- .../core/Civi/Api4/Action/Afform/Get.php | 60 ---- .../Api4/Action/CustomGroup/GetAfforms.php | 295 ++++++++++++++++++ ext/afform/core/afform.php | 2 +- .../templates/afform/customGroups/afblock.tpl | 13 +- .../templates/afform/customGroups/afform.tpl | 19 ++ 5 files changed, 321 insertions(+), 68 deletions(-) create mode 100644 ext/afform/core/Civi/Api4/Action/CustomGroup/GetAfforms.php create mode 100644 ext/afform/core/templates/afform/customGroups/afform.tpl diff --git a/ext/afform/core/Civi/Api4/Action/Afform/Get.php b/ext/afform/core/Civi/Api4/Action/Afform/Get.php index 14e498cbc0ba..396291b1c536 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/Get.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/Get.php @@ -137,66 +137,6 @@ protected function checkPermission($afform) { return \CRM_Core_Permission::check($afform['permission']); } - /** - * Generates afform blocks from custom field sets. - * - * @param \Civi\Core\Event\GenericHookEvent $event - * @throws \CRM_Core_Exception - */ - public static function getCustomGroupBlocks($event) { - // Early return if blocks are not requested - if ($event->getTypes && !in_array('block', $event->getTypes, TRUE)) { - return; - } - - $getNames = $event->getNames; - $getLayout = $event->getLayout; - $groupNames = []; - $afforms =& $event->afforms; - foreach ($getNames['name'] ?? [] as $name) { - if (str_starts_with($name, 'afblockCustom_') && strlen($name) > 13) { - $groupNames[] = substr($name, 14); - } - } - // Early return if this api call is fetching afforms by name and those names are not custom-related - if ((!empty($getNames['name']) && !$groupNames) - || (!empty($getNames['module_name']) && !str_contains(implode(' ', $getNames['module_name']), 'afblockCustom')) - || (!empty($getNames['directive_name']) && !str_contains(implode(' ', $getNames['directive_name']), 'afblock-custom')) - ) { - return; - } - $filters = [ - 'is_active' => TRUE, - ]; - if ($groupNames) { - $filters['name'] = $groupNames; - } - $customGroups = \CRM_Core_BAO_CustomGroup::getAll($filters); - foreach ($customGroups as $custom) { - $name = 'afblockCustom_' . $custom['name']; - $item = [ - 'name' => $name, - 'type' => 'block', - 'requires' => [], - 'title' => E::ts('%1 block', [1 => $custom['title']]), - 'description' => '', - 'is_public' => FALSE, - 'permission' => ['access CiviCRM'], - 'entity_type' => $custom['extends'], - ]; - if ($custom['is_multiple']) { - $item['join_entity'] = 'Custom_' . $custom['name']; - } - if ($getLayout) { - $item['layout'] = \CRM_Core_Smarty::singleton()->fetchWith( - 'afform/customGroups/afblock.tpl', - ['custom' => $custom] - ); - } - $afforms[$name] = $item; - } - } - /** * Find search display tags in afform markup * diff --git a/ext/afform/core/Civi/Api4/Action/CustomGroup/GetAfforms.php b/ext/afform/core/Civi/Api4/Action/CustomGroup/GetAfforms.php new file mode 100644 index 000000000000..58e69f616f73 --- /dev/null +++ b/ext/afform/core/Civi/Api4/Action/CustomGroup/GetAfforms.php @@ -0,0 +1,295 @@ +addSelect('name') + ->addWhere('custom_group_id', '=', $item['id']) + ->addWhere('is_active', '=', TRUE) + ->execute() + ->column('name'); + + // we also needs this for + + foreach ($this->formTypes as $type) { + switch ($type) { + case 'block': + $forms[] = $this->generateFieldBlock($item); + break; + + case 'form': + $forms[] = $this->generateUpdateForm($item); + if ($item['is_multiple']) { + $forms[] = $this->generateCreateForm($item); + } + break; + + // TODO: implement search kit listings for multi value + // case 'search': + // if ($item['is_multiple']) { + // $forms[] = $this->generateSearchForm($item); + // } + // break; + } + } + + return [ + 'id' => $item['id'], + 'forms' => $forms, + ]; + } + + private function generateFieldBlock($item): array { + $afform = [ + 'name' => 'afblockCustom_' . $item['name'], + 'type' => 'block', + 'requires' => [], + 'title' => E::ts('%1 block', [1 => $item['title']]), + 'description' => '', + 'is_public' => FALSE, + 'permission' => ['access CiviCRM'], + 'entity_type' => $item['extends'], + ]; + if ($item['is_multiple']) { + $afform['join_entity'] = 'Custom_' . $item['name']; + } + if ($this->getLayout) { + $afform['layout'] = \CRM_Core_Smarty::singleton()->fetchWith( + 'afform/customGroups/afblock.tpl', + ['group' => $item] + ); + } + return $afform; + } + + private function generateUpdateForm($item): array { + $afform = [ + 'name' => 'afformUpdateCustom_' . $item['name'], + 'type' => 'form', + 'title' => E::ts('Update %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'] . '/update', + ]; + 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/afform.tpl', + [ + 'formEntity' => $formEntity, + 'formActions' => [ + 'create' => FALSE, + 'update' => TRUE, + ], + 'urlAutofill' => TRUE, + 'blockDirective' => _afform_angular_module_name('afblockCustom_' . $item['name'], 'dash'), + ] + ); + } + return $afform; + } + + private function generateCreateForm($item): array { + $afform = [ + 'name' => 'afformCreateCustom_' . $item['name'], + 'type' => 'form', + 'title' => E::ts('Add %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'] . '/create', + ]; + if ($this->getLayout) { + $formEntity = [ + 'type' => 'Custom_' . $item['name'], + 'name' => 'Record', + 'label' => $item['extends'] . ' ' . $item['title'], + 'parent_field' => 'entity_id', + 'parent_field_defn' => [ + 'input_type' => 'Hidden', + 'label' => FALSE, + ], + ]; + $afform['layout'] = \CRM_Core_Smarty::singleton()->fetchWith( + 'afform/customGroups/afform.tpl', + [ + 'formEntity' => $formEntity, + 'formActions' => [ + 'create' => TRUE, + 'update' => FALSE, + ], + 'urlAutofill' => FALSE, + 'blockDirective' => _afform_angular_module_name('afblockCustom_' . $item['name'], 'dash'), + ] + ); + } + return $afform; + } + + public static function getCustomGroupAfforms($event) { + $formGenerate = \Civi\Api4\CustomGroup::getAfforms(FALSE) + ->addWhere('is_active', '=', TRUE); + + // only generate layout if this is required by the Afform.get + $formGenerate->setGetLayout($event->getLayout); + + // if the Afform.get is limited to specific form types + // we can limit to those + if ($event->getTypes) { + $formGenerate->setFormTypes($event->getTypes); + } + + // if the Afform.get is limited to specific form names + // we can limit our action to specific custom groups + if ($event->getNames) { + $groupNames = self::parseGroupNamesFromAfformGetNames($event->getNames); + if (!$groupNames) { + // Afform.get is limited to specific form names, none of which + // correspond to a custom group form, so we can return early + return; + } + + $formGenerate->addWhere('name', 'IN', $groupNames); + } + + $formsByGroup = $formGenerate->execute()->column('forms'); + + // add generated forms back to the hook event + // indexing by the form name + $forms = array_merge(...$formsByGroup); + + foreach ($forms as $form) { + $event->afforms[$form['name']] = $form; + } + } + + protected static function parseGroupNamesFromAfformGetNames(array $getNames): array { + // preserves previous logic when module_name or directive_name + // are specified + // + // I suspect we cannot more accurately parse kebab case names as special chars + // from group name may have been lost? + if ( + (!empty($getNames['module_name']) && !str_contains(implode(' ', $getNames['module_name']), 'afblockCustom')) + || (!empty($getNames['directive_name']) && !str_contains(implode(' ', $getNames['directive_name']), 'afblock-custom')) + ) { + return []; + } + + $formNames = $getNames['name'] ?? []; + + $groupNames = array_map(fn ($name) => self::parseGroupNameFromFormName($name), $formNames); + + // filter nulls where form name didnt correspond to a group name + return array_filter($groupNames); + } + + /** + * For afform base name, get the custom group name it corresponds to + * Or null if none + * + * @return ?string + */ + protected static function parseGroupNameFromFormName(string $name): ?string { + $prefixLength = strpos($name, '_'); + + if (!$prefixLength) { + // no prefix + return NULL; + } + $prefix = substr($name, 0, $prefixLength); + + if (!in_array($prefix, self::FORM_TYPES)) { + // prefix doesn't match our custom group + // form prefixes + return NULL; + } + + // TODO the prefix tells us which types of forms we + // want - so we could further optimise by passing + // that along to the generate action + + // we have a match - return everything after the '_' + return substr($name, $prefixLength + 1); + } + +} diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php index c50f5bd518f5..e82b0fddae64 100644 --- a/ext/afform/core/afform.php +++ b/ext/afform/core/afform.php @@ -46,7 +46,7 @@ function afform_civicrm_config(&$config) { $dispatcher->addListener('hook_civicrm_angularModules', '_afform_hook_civicrm_angularModules', -1000); $dispatcher->addListener('hook_civicrm_alterAngular', ['\Civi\Afform\AfformMetadataInjector', 'preprocess']); $dispatcher->addListener('hook_civicrm_check', ['\Civi\Afform\StatusChecks', 'hook_civicrm_check']); - $dispatcher->addListener('civi.afform.get', ['\Civi\Api4\Action\Afform\Get', 'getCustomGroupBlocks']); + $dispatcher->addListener('civi.afform.get', ['\Civi\Api4\Action\CustomGroup\GetAfforms', 'getCustomGroupAfforms']); } /** diff --git a/ext/afform/core/templates/afform/customGroups/afblock.tpl b/ext/afform/core/templates/afform/customGroups/afblock.tpl index 51bcc88b4641..785631766ad1 100644 --- a/ext/afform/core/templates/afform/customGroups/afblock.tpl +++ b/ext/afform/core/templates/afform/customGroups/afblock.tpl @@ -1,14 +1,13 @@ -{if $custom.help_pre} -
{$custom.help_pre}
+{if $group.help_pre} +
{$group.help_pre}
{/if} -{foreach from=$custom.fields item=field} +{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 *} - + {/foreach} -{if $custom.help_post} -
{$custom.help_post}
+{if $group.help_post} +
{$group.help_post}
{/if} - diff --git a/ext/afform/core/templates/afform/customGroups/afform.tpl b/ext/afform/core/templates/afform/customGroups/afform.tpl new file mode 100644 index 000000000000..5d6fa7dac0c3 --- /dev/null +++ b/ext/afform/core/templates/afform/customGroups/afform.tpl @@ -0,0 +1,19 @@ + + + +
+ + <{$blockDirective} /> +
+ +