From cb5eef6b8195e60851bb827d0569a695ed9972b9 Mon Sep 17 00:00:00 2001 From: colemanw <coleman@civicrm.org> Date: Tue, 26 Sep 2023 19:56:35 -0400 Subject: [PATCH] Afform - Quick add links for Autocomplete fields --- CRM/Core/Form.php | 3 + CRM/Core/Resources.php | 37 ++++++++++ ang/afform/afformQuickAddIndividual.aff.html | 18 +++++ ang/afform/afformQuickAddIndividual.aff.php | 14 ++++ ang/crmUi.js | 2 + .../afGuiEditor/elements/afGuiField-menu.html | 5 ++ .../elements/afGuiField.component.js | 13 ++++ ext/afform/core/ang/af/fields/EntityRef.html | 1 + js/Common.js | 73 ++++++++++++++++--- templates/CRM/common/l10n.js.tpl | 1 + 10 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 ang/afform/afformQuickAddIndividual.aff.html create mode 100644 ang/afform/afformQuickAddIndividual.aff.php diff --git a/CRM/Core/Form.php b/CRM/Core/Form.php index 0cffa991c2f5..3a04c3bb23f5 100644 --- a/CRM/Core/Form.php +++ b/CRM/Core/Form.php @@ -2324,6 +2324,9 @@ public function addAutocomplete(string $name, string $label = '', array $props = $props['data-select-params'] = json_encode($props['select']); $props['data-api-params'] = json_encode($props['api']); $props['data-api-entity'] = $props['entity']; + if (!empty($props['select']['quickAdd'])) { + Civi::service('angularjs.loader')->addModules(['af']); + } CRM_Utils_Array::remove($props, 'select', 'api', 'entity'); return $this->add('text', $name, $label, $props, $required); } diff --git a/CRM/Core/Resources.php b/CRM/Core/Resources.php index f1610b35f63c..9b2e1e1c2d9a 100644 --- a/CRM/Core/Resources.php +++ b/CRM/Core/Resources.php @@ -432,10 +432,47 @@ public static function renderL10nJs(GenericHookEvent $e) { 'contactSearch' => json_encode(!empty($params['includeEmailInName']) ? ts('Search by name/email or id...') : ts('Search by name or id...')), 'otherSearch' => json_encode(ts('Enter search term or id...')), 'entityRef' => self::getEntityRefMetadata(), + 'quickAdd' => self::getQuickAddForms($e->params['cid']), ]; $e->content = CRM_Core_Smarty::singleton()->fetchWith('CRM/common/l10n.js.tpl', $params); } + /** + * Gets links to "Quick Add" forms, for use in Autocomplete widgets + * + * @param int $cid + * @return array + */ + private static function getQuickAddForms(int $cid): array { + $forms = []; + try { + $contactTypes = CRM_Contact_BAO_ContactType::getAllContactTypes(); + $routes = \Civi\Api4\Route::get(FALSE) + ->addSelect('path', 'title', 'access_arguments') + ->addWhere('path', 'LIKE', 'civicrm/quick-add/%') + ->execute(); + foreach ($routes as $route) { + // Ensure user has permission to use the form + if (!empty($route['access_arguments'][0]) && !CRM_Core_Permission::check($route['access_arguments'][0], $cid)) { + continue; + } + // Ensure API entity exists + [, , $entityType] = array_pad(explode('/', $route['path']), 3, '*'); + if (\Civi\Api4\Utils\CoreUtil::entityExists($entityType)) { + $forms[] = [ + 'entity' => $entityType, + 'path' => $route['path'], + 'title' => $route['title'], + 'icon' => \Civi\Api4\Utils\CoreUtil::getInfoItem($entityType, 'icon'), + ]; + } + } + } + catch (CRM_Core_Exception $e) { + } + return $forms; + } + /** * @return bool * is this page request an ajax snippet? diff --git a/ang/afform/afformQuickAddIndividual.aff.html b/ang/afform/afformQuickAddIndividual.aff.html new file mode 100644 index 000000000000..93e8e7d84e52 --- /dev/null +++ b/ang/afform/afformQuickAddIndividual.aff.html @@ -0,0 +1,18 @@ +<af-form ctrl="afform"> + <af-entity data="{contact_type: 'Individual'}" type="Individual" name="Individual1" actions="{create: true, update: false}" security="RBAC" /> + <fieldset af-fieldset="Individual1" class="af-container"> + <div class="af-container"> + <div class="af-container af-layout-inline"> + <af-field name="first_name" /> + <af-field name="middle_name" /> + <af-field name="last_name" /> + </div> + <div af-join="Email" data="{is_primary: true}"> + <div class="af-container af-layout-inline"> + <af-field name="email" /> + </div> + </div> + </div> + </fieldset> + <button class="af-button btn btn-primary" crm-icon="fa-check" ng-click="afform.submit()">Submit</button> +</af-form> diff --git a/ang/afform/afformQuickAddIndividual.aff.php b/ang/afform/afformQuickAddIndividual.aff.php new file mode 100644 index 000000000000..4b7ca783f08b --- /dev/null +++ b/ang/afform/afformQuickAddIndividual.aff.php @@ -0,0 +1,14 @@ +<?php + +return [ + 'type' => 'form', + 'title' => ts('New Individual'), + 'icon' => 'fa-list-alt', + 'server_route' => 'civicrm/quick-add/Individual', + 'permission' => [ + 'add contacts' + ], + 'permission_operator' => 'AND', + 'submit_enabled' => TRUE, + 'create_submission' => FALSE, +]; diff --git a/ang/crmUi.js b/ang/crmUi.js index 718bf22b0a71..811fd8101ba0 100644 --- a/ang/crmUi.js +++ b/ang/crmUi.js @@ -729,6 +729,7 @@ crmAutocompleteParams: '<', multi: '<', autoOpen: '<', + quickAdd: '<', staticOptions: '<' }, link: function(scope, element, attr, ctrl) { @@ -791,6 +792,7 @@ // Only auto-open if there are no static options minimumInputLength: ctrl.autoOpen && _.isEmpty(ctrl.staticOptions) ? 0 : 1, static: ctrl.staticOptions || [], + quickAdd: ctrl.quickAdd, }); }); }; diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html index 9c49cbcccdc7..38600b0901ca 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField-menu.html @@ -38,6 +38,11 @@ <input crm-ui-select="{data: $ctrl.editor.securityModes}" ng-model="getSet('security')" ng-model-options="{getterSetter: true}" class="form-control"> </div> </li> +<li ng-if="$ctrl.fieldDefn.input_type === 'EntityRef'" title="{{:: ts('Allow a new entity to be created via quick-add popup') }}"> + <div href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown"> + <input crm-ui-select="{data: $ctrl.quickAddLinks, multiple: true, placeholder: ts('Quick Add')}" ng-model="getSet('input_attrs.quickAdd')" ng-model-options="{getterSetter: true}" class="form-control"> + </div> +</li> <li ng-if="$ctrl.fieldDefn.input_type === 'EntityRef'"> <a href ng-click="toggleAttr('input_attrs.autoOpen'); $event.stopPropagation(); $event.target.blur();" title="{{:: ts('Show autocomplete results without typing') }}"> <i class="crm-i fa-{{ getProp('input_attrs.autoOpen') ? 'check-' : '' }}square-o"></i> diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js index 1b9f03495dbe..9e94963cf5aa 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js @@ -47,6 +47,19 @@ inputTypes.push(type); } }); + // Quick-add links for autocompletes + this.quickAddLinks = []; + let allowedEntity = (ctrl.getFkEntity() || {}).entity; + let allowedEntities = (allowedEntity === 'Contact') ? ['Individual', 'Household', 'Organization'] : [allowedEntity]; + (CRM.config.quickAdd || []).forEach((link) => { + if (allowedEntities.includes(link.entity)) { + this.quickAddLinks.push({ + id: link.path, + icon: link.icon, + text: link.title, + }); + } + }); this.searchOperators = CRM.afAdmin.search_operators; // If field has limited operators, set appropriately if (ctrl.fieldDefn.operators && ctrl.fieldDefn.operators.length) { diff --git a/ext/afform/core/ang/af/fields/EntityRef.html b/ext/afform/core/ang/af/fields/EntityRef.html index 38447c8eaf55..cd415943b978 100644 --- a/ext/afform/core/ang/af/fields/EntityRef.html +++ b/ext/afform/core/ang/af/fields/EntityRef.html @@ -8,5 +8,6 @@ crm-autocomplete-params="{formName: 'afform:' + $ctrl.afFieldset.getFormName(), fieldName: $ctrl.afFieldset.getName() + ':' + $ctrl.fieldName}" multi="$ctrl.defn.input_attrs.multiple" auto-open="$ctrl.defn.input_attrs.autoOpen" + quick-add="$ctrl.defn.input_attrs.quickAdd" placeholder="{{:: $ctrl.defn.input_attrs.placeholder }}" ng-change="$ctrl.onSelectEntity()" > diff --git a/js/Common.js b/js/Common.js index f1bb53494466..1227255f0842 100644 --- a/js/Common.js +++ b/js/Common.js @@ -538,7 +538,23 @@ if (!CRM.vars) CRM.vars = {}; }); } - function getStaticOptionMarkup(staticItems) { + function renderQuickAddMarkup(quickAddLinks) { + if (!quickAddLinks || !quickAddLinks.length) { + return ''; + } + let markup = '<div class="crm-entityref-links crm-entityref-quick-add">'; + CRM.config.quickAdd.forEach((link) => { + if (quickAddLinks.includes(link.path)) { + markup += ' <a class="crm-hover-button" href="' + _.escape(CRM.url(link.path)) + '">' + + '<i class="crm-i ' + _.escape(link.icon) + '" aria-hidden="true"></i> ' + + _.escape(link.title) + '</a>'; + } + }); + markup += '</div>'; + return markup; + } + + function renderStaticOptionMarkup(staticItems) { if (!staticItems.length) { return ''; } @@ -559,9 +575,11 @@ if (!CRM.vars) CRM.vars = {}; } select2Options = select2Options || {}; return $(this).each(function() { - var $el = $(this).off('.crmEntity'), - staticItems = getStaticOptions(select2Options.static), - multiple = !!select2Options.multiple; + const $el = $(this).off('.crmEntity'); + let staticItems = getStaticOptions(select2Options.static), + quickAddLinks = select2Options.quickAdd, + multiple = !!select2Options.multiple, + key = apiParams.key || 'id'; $el.crmSelect2(_.extend({ ajax: { @@ -604,18 +622,25 @@ if (!CRM.vars) CRM.vars = {}; } }, formatInputTooShort: function() { - var txt = _.escape($.fn.select2.defaults.formatInputTooShort.call(this)); - txt += getStaticOptionMarkup(staticItems); - return txt; + let html = _.escape($.fn.select2.defaults.formatInputTooShort.call(this)); + html += renderStaticOptionMarkup(staticItems); + html += renderQuickAddMarkup(quickAddLinks); + return html; + }, + formatNoMatches: function() { + let html = _.escape($.fn.select2.defaults.formatNoMatches); + html += renderQuickAddMarkup(quickAddLinks); + return html; } }, select2Options)); - $el.on('select2-open.crmEntity', function() { + $el.on('select2-open.crmEntity', () => { var $el = $(this); $('#select2-drop') .off('.crmEntity') - .on('click.crmEntity', '.crm-entityref-links-static a', function(e) { - var id = $(this).attr('href').substr(1), + // Add static item to selection when clicking static links + .on('click.crmEntity', '.crm-entityref-links-static a', () => { + let id = $(this).attr('href').substring(1), item = _.findWhere(staticItems, {id: id}); $el.select2('close'); if (multiple) { @@ -628,6 +653,34 @@ if (!CRM.vars) CRM.vars = {}; $el.select2('data', item, true); } return false; + }) + // Pop-up Afform when clicking quick-add links + .on('click.crmEntity', '.crm-entityref-quick-add a', () => { + let url = $(this).attr('href'); + $el.select2('close'); + CRM.loadForm(url).on('crmFormSuccess', (e, data) => { + // Quick-add Afform has been submitted, parse submission data for id of created entity + const response = data.submissionResponse && data.submissionResponse[0]; + let createdId; + if (typeof response === 'object') { + // Loop through entities created by the afform (there should be only one) + Object.keys(response).forEach((entity) => { + if (Array.isArray(response[entity]) && response[entity][0] && response[entity][0][key]) { + createdId = response[entity][0][key]; + } + }); + } + // Update field value with new id and the widget will automatically fetch the label + if (createdId) { + if (multiple && $el.val()) { + // Select2 v3 uses a string instead of array for multiple values + $el.val($el.val() + ',' + createdId).change(); + } else { + $el.val('' + createdId).change(); + } + } + }); + return false; }); }); }); diff --git a/templates/CRM/common/l10n.js.tpl b/templates/CRM/common/l10n.js.tpl index 427ad77d5f26..5e8f34e45f6a 100644 --- a/templates/CRM/common/l10n.js.tpl +++ b/templates/CRM/common/l10n.js.tpl @@ -24,6 +24,7 @@ CRM.config.ajaxPopupsEnabled = {$ajaxPopupsEnabled|@json_encode}; CRM.config.allowAlertAutodismissal = {$allowAlertAutodismissal|@json_encode}; CRM.config.resourceCacheCode = {$resourceCacheCode|@json_encode}; + CRM.config.quickAdd = {$quickAdd|@json_encode}; // Merge entityRef settings CRM.config.entityRef = $.extend({ldelim}{rdelim}, {$entityRef|@json_encode}, CRM.config.entityRef || {ldelim}{rdelim});