From c63f20d31a854b85b302d808fbdc1705374d4f00 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 23 Mar 2021 23:09:35 -0400 Subject: [PATCH] Afform - Enable embedded forms on contact summary screen as blocks and tabs --- .../Civi/AfformAdmin/AfformAdminMeta.php | 2 +- ext/afform/admin/ang/afGuiEditor.js | 10 ++- .../ang/afGuiEditor/afGuiEditor.component.js | 53 +++++++++++ .../admin/ang/afGuiEditor/config-form.html | 25 ++++++ ext/afform/core/Civi/Api4/Afform.php | 6 +- ext/afform/core/afform.php | 90 +++++++++++++++++-- .../afform/contactSummary/AfformBlock.tpl | 5 ++ .../afform/contactSummary/AfformTab.tpl | 5 ++ .../Civi/Api4/Action/SearchDisplay/Run.php | 20 ++++- 9 files changed, 200 insertions(+), 16 deletions(-) create mode 100644 ext/afform/core/templates/afform/contactSummary/AfformBlock.tpl create mode 100644 ext/afform/core/templates/afform/contactSummary/AfformTab.tpl diff --git a/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php index 2c572eb9b9..ba860905b4 100644 --- a/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php +++ b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php @@ -84,7 +84,7 @@ class AfformAdminMeta { 'includeCustom' => TRUE, 'loadOptions' => ['id', 'label'], 'action' => 'create', - 'select' => ['name', 'label', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize', 'data_type'], + 'select' => ['name', 'label', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize', 'data_type', 'fk_entity'], 'where' => [['input_type', 'IS NOT NULL']], ]; if (in_array($entityName, ['Individual', 'Household', 'Organization'])) { diff --git a/ext/afform/admin/ang/afGuiEditor.js b/ext/afform/admin/ang/afGuiEditor.js index 3b78fe067d..a545b6ba29 100644 --- a/ext/afform/admin/ang/afGuiEditor.js +++ b/ext/afform/admin/ang/afGuiEditor.js @@ -6,15 +6,17 @@ .service('afGui', function(crmApi4, $parse, $q) { // Parse strings of javascript that php couldn't interpret + // TODO: Figure out which attributes actually need to be evaluated, as a whitelist would be less error-prone than a blacklist + var doNotEval = ['filters']; function evaluate(collection) { _.each(collection, function(item) { if (_.isPlainObject(item)) { evaluate(item['#children']); - _.each(item, function(node, idx) { - if (_.isString(node)) { - var str = _.trim(node); + _.each(item, function(prop, key) { + if (_.isString(prop) && !_.includes(doNotEval, key)) { + var str = _.trim(prop); if (str[0] === '{' || str[0] === '[' || str.slice(0, 3) === 'ts(') { - item[idx] = $parse(str)({ts: CRM.ts('afform')}); + item[key] = $parse(str)({ts: CRM.ts('afform')}); } } }); diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js index d1913cbf0b..112ef9bf01 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js @@ -75,6 +75,10 @@ else if (editor.getFormType() === 'search') { editor.layout['#children'] = afGui.findRecursive($scope.afform.layout, {'af-fieldset': ''})[0]['#children']; + editor.searchDisplay = afGui.findRecursive(editor.layout['#children'], function(item) { + return item['#tag'] && item['#tag'].indexOf('crm-search-display-') === 0; + })[0]; + editor.searchFilters = getSearchFilterOptions(); } // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model @@ -175,6 +179,55 @@ return $scope.afform; }; + this.toggleContactSummary = function() { + if ($scope.afform.contact_summary) { + $scope.afform.contact_summary = false; + if ($scope.afform.type === 'search') { + delete editor.searchDisplay.filters; + } + } else { + $scope.afform.contact_summary = 'block'; + if ($scope.afform.type === 'search') { + editor.searchDisplay.filters = editor.searchFilters[0].key; + } + } + }; + + function getSearchFilterOptions() { + var searchDisplay = editor.meta.searchDisplays[editor.searchDisplay['search-name'] + '.' + editor.searchDisplay['display-name']], + entityCount = {}, + options = []; + + addFields(searchDisplay['saved_search.api_entity'], ''); + + _.each(searchDisplay['saved_search.api_params'].join, function(join) { + var joinInfo = join[0].split(' AS '); + addFields(joinInfo[0], joinInfo[1] + '.'); + }); + + function addFields(entityName, prefix) { + var entity = afGui.getEntity(entityName); + entityCount[entity.entity] = (entityCount[entity.entity] || 0) + 1; + var count = (entityCount[entity.entity] > 1 ? ' ' + entityCount[entity.entity] : ''); + if (entityName === 'Contact') { + options.push({ + key: "{'" + prefix + "id': options.contact_id}", + label: entity.label + count + }); + } else { + _.each(entity.fields, function(field) { + if (field.fk_entity === 'Contact') { + options.push({ + key: "{'" + prefix + field.name + "': options.contact_id}", + label: entity.label + count + ' ' + field.label + }); + } + }); + } + } + return options; + } + // Validates that a drag-n-drop action is allowed this.onDrop = function(event, ui) { var sort = ui.item.sortable; diff --git a/ext/afform/admin/ang/afGuiEditor/config-form.html b/ext/afform/admin/ang/afGuiEditor/config-form.html index 7c86404c12..40d9c33bf1 100644 --- a/ext/afform/admin/ang/afGuiEditor/config-form.html +++ b/ext/afform/admin/ang/afGuiEditor/config-form.html @@ -58,6 +58,31 @@

{{:: ts('Allow CiviCRM users to add the form to their home dashboard.') }}

+ +
+
+ + +
+

{{:: ts('Placement can be configured using the Contact Layout Editor.') }}

+
+
+
+ + +
+

{{:: ts('Choose which contact from the search should match the contact being viewed.') }}

+
diff --git a/ext/afform/core/Civi/Api4/Afform.php b/ext/afform/core/Civi/Api4/Afform.php index 31a3c387d4..dfcd80ef4b 100644 --- a/ext/afform/core/Civi/Api4/Afform.php +++ b/ext/afform/core/Civi/Api4/Afform.php @@ -157,6 +157,10 @@ class Afform extends Generic\AbstractEntity { 'name' => 'is_token', 'data_type' => 'Boolean', ], + [ + 'name' => 'contact_summary', + 'data_type' => 'String', + ], [ 'name' => 'repeat', 'data_type' => 'Mixed', @@ -172,7 +176,7 @@ class Afform extends Generic\AbstractEntity { 'data_type' => 'Array', ], ]; - + // Calculated fields returned by get action if ($self->getAction() === 'get') { $fields[] = [ 'name' => 'module_name', diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php index 2e70d5de67..8fd080ea99 100644 --- a/ext/afform/core/afform.php +++ b/ext/afform/core/afform.php @@ -174,16 +174,94 @@ function afform_civicrm_managed(&$entities) { } /** - * Implements hook_civicrm_caseTypes(). + * Implements hook_civicrm_tabset(). * - * Generate a list of case-types. + * Adds afforms as contact summary tabs. + */ +function afform_civicrm_tabset($tabsetName, &$tabs, $context) { + if ($tabsetName !== 'civicrm/contact/view') { + return; + } + $scanner = \Civi::service('afform_scanner'); + $weight = 111; + foreach ($scanner->getMetas() as $afform) { + if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'tab') { + $module = _afform_angular_module_name($afform['name']); + $tabs[] = [ + 'id' => $afform['name'], + 'title' => $afform['title'], + 'weight' => $weight++, + 'icon' => 'crm-i fa-list-alt', + 'is_active' => TRUE, + 'template' => 'afform/contactSummary/AfformTab.tpl', + 'module' => $module, + 'directive' => _afform_angular_module_name($afform['name'], 'dash'), + ]; + // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module. + if (empty($context['caller'])) { + Civi::service('angularjs.loader')->addModules($module); + } + } + } +} + +/** + * Implements hook_civicrm_pageRun(). * - * Note: This hook only runs in CiviCRM 4.4+. + * Adds afforms as contact summary blocks. + */ +function afform_civicrm_pageRun(&$page) { + if (get_class($page) !== 'CRM_Contact_Page_View_Summary') { + return; + } + $scanner = \Civi::service('afform_scanner'); + $cid = $page->get('cid'); + $side = 'left'; + foreach ($scanner->getMetas() as $afform) { + if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'block') { + $module = _afform_angular_module_name($afform['name']); + $block = [ + 'module' => $module, + 'directive' => _afform_angular_module_name($afform['name'], 'dash'), + ]; + $content = CRM_Core_Smarty::singleton()->fetchWith('afform/contactSummary/AfformBlock.tpl', ['contactId' => $cid, 'block' => $block]); + CRM_Core_Region::instance("contact-basic-info-$side")->add([ + 'markup' => '
' . $content . '
', + 'weight' => 1, + ]); + Civi::service('angularjs.loader')->addModules($module); + $side = $side === 'left' ? 'right' : 'left'; + } + } +} + +/** + * Implements hook_civicrm_contactSummaryBlocks(). * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes + * @link https://github.com/civicrm/org.civicrm.contactlayout */ -function afform_civicrm_caseTypes(&$caseTypes) { - _afform_civix_civicrm_caseTypes($caseTypes); +function afform_civicrm_contactSummaryBlocks(&$blocks) { + $scanner = \Civi::service('afform_scanner'); + foreach ($scanner->getMetas() as $afform) { + if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'block') { + // Provide our own group for this block to visually distinguish it on the contact summary editor palette. + $blocks += [ + 'afform' => [ + 'title' => ts('Form Builder'), + 'icon' => 'fa-list-alt', + 'blocks' => [], + ], + ]; + $blocks['afform']['blocks'][$afform['name']] = [ + 'title' => $afform['title'], + 'tpl_file' => 'afform/contactSummary/AfformBlock.tpl', + 'module' => _afform_angular_module_name($afform['name']), + 'directive' => _afform_angular_module_name($afform['name'], 'dash'), + 'sample' => [], + 'edit' => 'civicrm/admin/afform#/edit/' . $afform['name'], + ]; + } + } } /** diff --git a/ext/afform/core/templates/afform/contactSummary/AfformBlock.tpl b/ext/afform/core/templates/afform/contactSummary/AfformBlock.tpl new file mode 100644 index 0000000000..7940af4612 --- /dev/null +++ b/ext/afform/core/templates/afform/contactSummary/AfformBlock.tpl @@ -0,0 +1,5 @@ + +
+ <{$block.directive} options="{ldelim}contact_id: {$contactId}{rdelim}"> +
+
diff --git a/ext/afform/core/templates/afform/contactSummary/AfformTab.tpl b/ext/afform/core/templates/afform/contactSummary/AfformTab.tpl new file mode 100644 index 0000000000..1cf843254d --- /dev/null +++ b/ext/afform/core/templates/afform/contactSummary/AfformTab.tpl @@ -0,0 +1,5 @@ + +
+ <{$tabValue.directive} options="{ldelim}contact_id: {$contactId}{rdelim}"> +
+
diff --git a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php index 5a8d5504ef..8316e1d881 100644 --- a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php @@ -80,6 +80,7 @@ class Run extends \Civi\Api4\Generic\AbstractAction { } if (is_string($this->display) && !empty($this->savedSearch['id'])) { $this->display = SearchDisplay::get(FALSE) + ->setSelect(['*', 'type:name']) ->addWhere('name', '=', $this->display) ->addWhere('saved_search_id', '=', $this->savedSearch['id']) ->execute()->first(); @@ -317,10 +318,20 @@ class Run extends \Civi\Api4\Generic\AbstractAction { */ private function getAfformFilters() { $afform = $this->loadAfform(); - return array_column(\CRM_Utils_Array::findAll( + if (!$afform) { + return []; + } + // Get afform field filters + $filters = array_column(\CRM_Utils_Array::findAll( $afform['layout'] ?? [], ['#tag' => 'af-field'] ), 'name'); + // Get filters passed into search display directive + $filterAttr = $afform['searchDisplay']['filters'] ?? NULL; + if ($filterAttr && is_string($filterAttr) && $filterAttr[0] === '{') { + $filters = array_unique(array_merge($filters, array_keys(\CRM_Utils_JS::getRawProps($filterAttr)))); + } + return $filters; } /** @@ -340,10 +351,11 @@ class Run extends \Civi\Api4\Generic\AbstractAction { ->setLayoutFormat('shallow') ->execute()->first(); // Validate that the afform contains this search display - if (\CRM_Utils_Array::findAll( + $afform['searchDisplay'] = \CRM_Utils_Array::findAll( $afform['layout'] ?? [], - ['#tag' => "crm-search-display-{$this->display['type']}", 'display-name' => $this->display['name']]) - ) { + ['#tag' => "{$this->display['type:name']}", 'display-name' => $this->display['name']] + )[0] ?? NULL; + if ($afform['searchDisplay']) { $this->_afform = $afform; } } -- 2.25.1