Afform - Enable embedded forms on contact summary screen as blocks and tabs
authorColeman Watts <coleman@civicrm.org>
Wed, 24 Mar 2021 03:09:35 +0000 (23:09 -0400)
committerColeman Watts <coleman@civicrm.org>
Tue, 6 Apr 2021 23:21:04 +0000 (19:21 -0400)
ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
ext/afform/admin/ang/afGuiEditor.js
ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
ext/afform/admin/ang/afGuiEditor/config-form.html
ext/afform/core/Civi/Api4/Afform.php
ext/afform/core/afform.php
ext/afform/core/templates/afform/contactSummary/AfformBlock.tpl [new file with mode: 0644]
ext/afform/core/templates/afform/contactSummary/AfformTab.tpl [new file with mode: 0644]
ext/search/Civi/Api4/Action/SearchDisplay/Run.php

index 2c572eb9b973ee1c7e9d906dc6aeb977ce74d510..ba860905b40ae7366f8a70fb99899a45b4c9cdab 100644 (file)
@@ -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'])) {
index 3b78fe067d5de34bcee8b7283562b0d9c352652e..a545b6ba29eabf8818d25554e5de52b6a2f185eb 100644 (file)
@@ -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')});
                 }
               }
             });
index d1913cbf0b6ef01238cd6fd389c195cb0e6fcac3..112ef9bf0171a3d328aad07dc2f4ee90763273e3 100644 (file)
 
         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
         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;
index 7c86404c1236cdeca124ed629cb77f2e1f931116..40d9c33bf1ca515cd4ac1a1b1d17786f0332b412 100644 (file)
       </label>
       <p class="help-block">{{:: ts('Allow CiviCRM users to add the form to their home dashboard.') }}</p>
     </div>
+
+    <div class="form-group">
+      <div class="form-inline">
+        <label>
+          <input type="checkbox" ng-checked="afform.contact_summary" ng-click="editor.toggleContactSummary()">
+          {{:: ts('Add to contact summary page') }}
+        </label>
+        <select class="form-control" ng-model="afform.contact_summary" ng-if="afform.contact_summary">
+          <option value="block">{{:: ts('As Block') }}</option>
+          <option value="tab">{{:: ts('As Tab') }}</option>
+        </select>
+      </div>
+      <p class="help-block">{{:: ts('Placement can be configured using the Contact Layout Editor.') }}</p>
+    </div>
+    <div class="form-group" ng-if="afform.contact_summary && editor.searchDisplay && editor.searchFilters.length > 1">
+      <div class="form-inline">
+        <label for="af_config_form_search_filters">
+          {{:: ts('Filter on:') }}
+        </label>
+        <select class="form-control" id="af_config_form_search_filters" ng-model="editor.searchDisplay.filters">
+          <option ng-repeat="option in editor.searchFilters" value="{{ option.key }}">{{ option.label }}</option>
+        </select>
+      </div>
+      <p class="help-block">{{:: ts('Choose which contact from the search should match the contact being viewed.') }}</p>
+    </div>
   </fieldset>
 
 </ng-form>
index 31a3c387d46337d53850302e0d7715924b8036af..dfcd80ef4b9f53be0317ddbc6fb7111054b85143 100644 (file)
@@ -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',
index 2e70d5de67deddc7e606df30452d48abd65b4c1e..8fd080ea99eafcad4114bad8a2b8b63479bd72fb 100644 (file)
@@ -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' => '<div class="crm-summary-block">' . $content . '</div>',
+        '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 (file)
index 0000000..7940af4
--- /dev/null
@@ -0,0 +1,5 @@
+<crm-angular-js modules="{$block.module}">
+  <div id="bootstrap-theme">
+    <{$block.directive} options="{ldelim}contact_id: {$contactId}{rdelim}"></{$block.directive}>
+  </div>
+</crm-angular-js>
diff --git a/ext/afform/core/templates/afform/contactSummary/AfformTab.tpl b/ext/afform/core/templates/afform/contactSummary/AfformTab.tpl
new file mode 100644 (file)
index 0000000..1cf8432
--- /dev/null
@@ -0,0 +1,5 @@
+<crm-angular-js modules="{$tabValue.module}">
+  <div id="bootstrap-theme">
+    <{$tabValue.directive} options="{ldelim}contact_id: {$contactId}{rdelim}"></{$tabValue.directive}>
+  </div>
+</crm-angular-js>
index 5a8d5504ef81977445c2f161a4294527bfbee219..8316e1d88130b1dabb05178f3104319f03900107 100644 (file)
@@ -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;
       }
     }