Afform - Unify 'is_dashlet', 'is_token' & 'contact_summary' as 'placement'
authorcolemanw <coleman@civicrm.org>
Sun, 8 Oct 2023 08:14:29 +0000 (04:14 -0400)
committercolemanw <coleman@civicrm.org>
Wed, 11 Oct 2023 15:27:05 +0000 (11:27 -0400)
Gathering these items into an option group makes future placement
options easy to add, and easy for other extensions to contribute to.

22 files changed:
ang/afform/afsearchTabNote.aff.php
ang/afform/afsearchTabRel.aff.php
ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
ext/afform/admin/ang/afAdmin.js
ext/afform/admin/ang/afAdmin/afAdminList.controller.js
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/CRM/Afform/AfformScanner.php
ext/afform/core/Civi/Afform/Tokens.php
ext/afform/core/Civi/Afform/Utils.php
ext/afform/core/Civi/Api4/Action/Afform/Get.php
ext/afform/core/Civi/Api4/Action/Afform/Revert.php
ext/afform/core/Civi/Api4/Afform.php
ext/afform/core/Civi/Api4/Utils/AfformSaveTrait.php
ext/afform/core/afform.php
ext/afform/core/api/v3/Afform.php
ext/afform/core/managed/AfformPlacement.mgd.php [new file with mode: 0644]
ext/afform/core/tests/phpunit/Civi/Afform/AfformContactSummaryTest.php
ext/afform/core/tests/phpunit/Civi/Afform/AfformMetadataTest.php
ext/afform/mock/tests/phpunit/api/v4/AfformTest.php
ext/civigrant/ang/afsearchTabGrant.aff.php

index c22bf3a04904fc6b57b6e96f6d8cbdf576957c34..c2a99ba08fd580915262e165cdd760d4446abca3 100644 (file)
@@ -4,7 +4,7 @@ return [
   'type' => 'search',
   'title' => ts('Notes'),
   'description' => '',
-  'contact_summary' => 'tab',
+  'placement' => ['contact_summary_tab'],
   'summary_weight' => 100,
   'icon' => 'fa-sticky-note-o',
   'summary_contact_type' => NULL,
index 3eb3cb03aec38f7b166d6c32436df6d538b3e658..43af4f00df36175cd3b6eb4b70fa24ce1a201e3d 100644 (file)
@@ -6,7 +6,7 @@ return [
   'permission' => [
     'access CiviCRM',
   ],
-  'contact_summary' => 'tab',
+  'placement' => ['contact_summary_tab'],
   'icon' => 'fa-handshake-o',
   'summary_weight' => 80,
   'permission_operator' => 'AND',
index cf283b74a7f33a0dd368a1a8de58c0c6bc6cd144..e1516a21e644b887b5c4481227274fd14ca3e27f 100644 (file)
@@ -13,6 +13,12 @@ class AfformAdminMeta {
    * @return array
    */
   public static function getAdminSettings() {
+    $afformPlacement = \CRM_Utils_Array::formatForSelect2((array) \Civi\Api4\OptionValue::get(FALSE)
+      ->addSelect('value', 'label', 'icon', 'description')
+      ->addWhere('is_active', '=', TRUE)
+      ->addWhere('option_group_id:name', '=', 'afform_placement')
+      ->addOrderBy('weight')
+      ->execute(), 'label', 'value');
     $afformTypes = (array) \Civi\Api4\OptionValue::get(FALSE)
       ->addSelect('name', 'label', 'icon')
       ->addWhere('is_active', '=', TRUE)
@@ -31,6 +37,7 @@ class AfformAdminMeta {
     }
     return [
       'afform_type' => $afformTypes,
+      'afform_placement' => $afformPlacement,
       'search_operators' => \Civi\Afform\Utils::getSearchOperators(),
     ];
   }
index 8d45473732edc232d2c3ca791a90eceea8bdf811..af06f68e98cb3cb21c20ba09faa5054bad6cb854 100644 (file)
@@ -11,7 +11,7 @@
           // Load data for lists
           afforms: function(crmApi4) {
             return crmApi4('Afform', 'get', {
-              select: ['name', 'title', 'type', 'server_route', 'is_public', 'submission_count', 'submission_date', 'submit_limit', 'submit_enabled', 'submit_currently_open', 'has_local', 'has_base', 'base_module', 'base_module:label', 'is_dashlet', 'contact_summary:label']
+              select: ['name', 'title', 'type', 'server_route', 'is_public', 'submission_count', 'submission_date', 'submit_limit', 'submit_enabled', 'submit_currently_open', 'has_local', 'has_base', 'base_module', 'base_module:label', 'placement:label']
             });
           }
         }
index 70c9536f936e1cff667f01378dc3393565269ceb..621947cf52344d648b14cd2cc6a7b21c459a1ebf 100644 (file)
 
     this.afforms = _.transform(afforms, function(afforms, afform) {
       afform.type = afform.type || 'system';
-      // Aggregate a couple fields for the "Placement" column
-      afform.placement = [];
-      if (afform.is_dashlet) {
-        afform.placement.push(ts('Dashboard'));
-      }
-      if (afform['contact_summary:label']) {
-        afform.placement.push(afform['contact_summary:label']);
-      }
+      afform.placement = afform['placement:label'];
       if (afform.submission_date) {
         afform.submission_date = CRM.utils.formatDate(afform.submission_date);
       }
index 120798c721265e1e09ddbccc7248f10d66d787b2..6b52f6228d27c233bb49b09039f61065aab4ec34 100644 (file)
           });
         },
 
-        meta: CRM.afGuiEditor,
+        meta: _.extend(CRM.afGuiEditor, CRM.afAdmin),
 
         getEntity: function(entityName) {
           return CRM.afGuiEditor.entities[entityName];
index 07eb9fabeb69d5468ce2a2d72ef8df7478ea3587..d2279c950f66b06f1183f0d50de5973b3044abfe 100644 (file)
           delete editor.afform.name;
           delete editor.afform.server_route;
           delete editor.afform.navigation;
-          editor.afform.is_dashlet = false;
           editor.afform.title += ' ' + ts('(copy)');
         }
         editor.afform.icon = editor.afform.icon || 'fa-list-alt';
+        editor.afform.placement = editor.afform.placement || [];
         $scope.canvasTab = 'layout';
         $scope.layoutHtml = '';
         $scope.entities = {};
         return filter ? _.filter($scope.entities, filter) : _.toArray($scope.entities);
       };
 
-      this.toggleContactSummary = function() {
-        if (editor.afform.contact_summary) {
-          editor.afform.contact_summary = null;
+      this.isContactSummary = function() {
+        return editor.afform.placement.includes('contact_summary_block') || editor.afform.placement.includes('contact_summary_tab');
+      };
+
+      this.onChangePlacement = function() {
+        if (!editor.isContactSummary()) {
           _.each(editor.searchDisplays, function(searchDisplay) {
             delete searchDisplay.element.filters;
           });
         } else {
-          editor.afform.contact_summary = 'block';
           _.each(editor.searchDisplays, function(searchDisplay) {
             var filterOptions = getSearchFilterOptions(searchDisplay.settings);
             if (filterOptions.length) {
index d18e23cfdca1c51f18ec4229cb74c66c21d06621..b439b4c81621c6e6af7f12eaef4b775804ef5336 100644 (file)
@@ -43,7 +43,7 @@
       <label for="af_config_form_server_route">
         {{:: ts('Page Route') }}
       </label>
-      <input ng-model="editor.afform.server_route" name="server_route" class="form-control" id="af_config_form_server_route" pattern="^civicrm\/[-0-9a-zA-Z\/_]+$" onfocus="this.value = this.value || 'civicrm/'" onblur="if (this.value === 'civicrm/') this.value = ''" title="{{:: ts('Path must begin with &quot;civicrm/&quot;') }}" ng-model-options="editor.debounceMode">
+      <input ng-model="editor.afform.server_route" name="server_route" class="form-control" id="af_config_form_server_route" pattern="^civicrm\/[-0-9a-zA-Z\/_]+$" onfocus="this.value = this.value || 'civicrm/'" onblur="if (this.value === 'civicrm/') this.value = ''" title="{{:: ts('Path must begin with &quot;civicrm/&quot;') }}" placeholder="{{:: ts('None') }}" ng-model-options="editor.debounceMode">
       <p class="help-block">{{:: ts('Expose the form as a standalone webpage. (Example: "civicrm/my-form")') }}</p>
     </div>
 
       </label>
     </div>
 
-    <div class="form-group" ng-if="!!editor.afform.server_route">
-      <label>
-        <input type="checkbox" ng-model="editor.afform.is_token">
-        {{:: ts('Provide Email Token') }}
-      </label>
-      <p class="help-block">{{:: ts('Allows CiviMail authors to easily link to this page') }}</p>
-    </div>
-
     <div class="form-group">
       <div class="form-inline">
         <label ng-class="{disabled: !editor.afform.server_route}">
     </div>
 
     <div class="form-group">
+      <label for="afform_placement">
+        {{:: ts('Expose To') }}
+      </label>
+      <input ng-list crm-ui-select="{multiple: true, data: editor.meta.afform_placement, placeholder: ts('None')}" class="form-control" id="afform_placement" ng-model="editor.afform.placement" ng-change="editor.onChangePlacement()">
+      <p class="help-block">{{:: ts('Additional contexts in which the form can be embedded') }}</p>
+    </div>
+
+    <div class="form-group" ng-if="editor.afform.placement.includes('contact_summary_block') || editor.afform.placement.includes('contact_summary_tab')">
       <div class="form-inline">
-        <label>
-          <input type="checkbox" ng-checked="editor.afform.contact_summary" ng-click="editor.toggleContactSummary()">
-          {{:: ts('Add to Contact Summary Page') }}
-        </label>
-        <select class="form-control" ng-model="editor.afform.contact_summary" ng-if="editor.afform.contact_summary">
-          <option value="block">{{:: ts('As Block') }}</option>
-          <option value="tab">{{:: ts('As Tab') }}</option>
-        </select>
+        <label for="afform_summary_contact_type">{{:: ts('For') }}</label>
+        <input class="form-control" crm-autocomplete="'ContactType'" id="afform_summary_contact_type" ng-model="editor.afform.summary_contact_type" auto-open="true" multi="true" crm-autocomplete-params="{key: 'name'}" placeholder="{{:: ts('Any contact type') }}">
       </div>
-      <div class="form-inline" ng-if="editor.afform.contact_summary">
+      <div class="form-inline">
         <label for="afform_summary_weight">{{:: ts('Position') }}</label>
         <input class="form-control" type="number" id="afform_summary_weight" ng-model="editor.afform.summary_weight" placeholder="{{:: ts('Auto') }}">
-        <label for="afform_summary_contact_type">{{:: ts('For') }}</label>
-        <input class="form-control" crm-autocomplete="'ContactType'" id="afform_summary_contact_type" ng-model="editor.afform.summary_contact_type" auto-open="true" multi="true" crm-autocomplete-params="{key: 'name'}" placeholder="{{:: ts('Any contact type') }}">
       </div>
-      <p class="help-block" ng-show="editor.afform.contact_summary">
-        {{:: ts('Placement can be configured using the Contact Layout Editor.') }}
-      </p>
-    </div>
-
-    <div class="form-group">
-      <label>
-        <input type="checkbox" ng-model="editor.afform.is_dashlet">
-        {{:: ts('Add to Dashboard') }}
-      </label>
-      <p class="help-block">{{:: ts('Allow CiviCRM users to add the form to their home dashboard.') }}</p>
     </div>
 
   </fieldset>
index 766b66c1b393b9b55580e8292349a2d4cd31b1fd..5a27b5e71dcde2933bd36f3e6446242c236748a2 100644 (file)
@@ -150,9 +150,7 @@ class CRM_Afform_AfformScanner {
       'requires' => [],
       'title' => '',
       'description' => '',
-      'is_dashlet' => FALSE,
       'is_public' => FALSE,
-      'is_token' => FALSE,
       'permission' => ['access CiviCRM'],
       'type' => 'system',
     ];
index aa50defa49f2980fd3c9f5c8ee48978683fe869e..8e4e120c33505e98309323d038a5cf8a86b5b49f 100644 (file)
@@ -176,8 +176,8 @@ class Tokens extends AutoService implements EventSubscriberInterface {
    */
   public static function getTokenForms() {
     if (!isset(\Civi::$statics[__CLASS__]['tokenForms'])) {
-      $tokenForms = (array) \Civi\Api4\Afform::get(0)
-        ->addWhere('is_token', '=', TRUE)
+      $tokenForms = (array) \Civi\Api4\Afform::get(FALSE)
+        ->addWhere('placement', 'CONTAINS', 'msg_token')
         ->addSelect('name', 'title', 'server_route', 'is_public')
         ->execute()
         ->indexBy('name');
index 613787f0c6ee271ac3b20eb25e5c1cab16b55381..27a0a8ca8ceec6646308b6eec48fbb7aabcaa8bd 100644 (file)
@@ -78,4 +78,24 @@ class Utils {
     ];
   }
 
+  public static function shouldReconcileManaged(array $updatedAfform, array $originalAfform = []): bool {
+    $isChanged = function($field) use ($updatedAfform, $originalAfform) {
+      return ($updatedAfform[$field] ?? NULL) !== ($originalAfform[$field] ?? NULL);
+    };
+
+    return $isChanged('placement') ||
+      $isChanged('navigation') ||
+      (!empty($updatedAfform['placement']) && $isChanged('title')) ||
+      (!empty($updatedAfform['navigation']) && ($isChanged('title') || $isChanged('permission') || $isChanged('icon') || $isChanged('server_route')));
+  }
+
+  public static function shouldClearMenuCache(array $updatedAfform, array $originalAfform = []): bool {
+    $isChanged = function($field) use ($updatedAfform, $originalAfform) {
+      return ($updatedAfform[$field] ?? NULL) !== ($originalAfform[$field] ?? NULL);
+    };
+
+    return $isChanged('server_route') ||
+      (!empty($updatedAfform['server_route']) && $isChanged('title'));
+  }
+
 }
index 6ee84870734b4a534f1ebde417cca13907d0d304..9559b619aeceb52d50acdd0377efadbf63d0e72d 100644 (file)
@@ -91,6 +91,9 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
       if ($getSearchDisplays) {
         $afforms[$name]['search_displays'] = $this->getSearchDisplays($afforms[$name]['layout']);
       }
+      if (!isset($afforms[$name]['placement']) && $this->_isFieldSelected('placement')) {
+        self::convertLegacyPlacement($afforms[$name]);
+      }
     }
 
     if ($getLayout && $this->layoutFormat !== 'html') {
@@ -177,9 +180,7 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
         'requires' => [],
         'title' => E::ts('%1 block', [1 => $custom['title']]),
         'description' => '',
-        'is_dashlet' => FALSE,
         'is_public' => FALSE,
-        'is_token' => FALSE,
         'permission' => ['access CiviCRM'],
         'join_entity' => 'Custom_' . $custom['name'],
         'entity_type' => $custom['extends'],
@@ -216,4 +217,18 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
     return $searchDisplays;
   }
 
+  private static function convertLegacyPlacement(array &$afform): void {
+    $afform['placement'] = [];
+    if (!empty($afform['is_dashlet'])) {
+      $afform['placement'][] = 'dashboard_dashlet';
+    }
+    if (!empty($afform['is_token'])) {
+      $afform['placement'][] = 'msg_token';
+    }
+    if (!empty($afform['contact_summary'])) {
+      $afform['placement'][] = 'contact_summary_' . $afform['contact_summary'];
+    }
+    unset($afform['is_dashlet'], $afform['is_token'], $afform['contact_summary']);
+  }
+
 }
index a30c2d76b140bd1567fbc0c161356e7ec380a30b..d51e39a3b1d6c12badc8461eb7e2ec16ae679c81 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Civi\Api4\Action\Afform;
 
+use Civi\Afform\Utils;
 use Civi\Api4\Generic\Result;
 use CRM_Afform_ExtensionUtil as E;
 
@@ -65,15 +66,12 @@ class Revert extends \Civi\Api4\Generic\BasicBatchAction {
     $original = (array) $scanner->getMeta($item['name']);
 
     // If the dashlet setting changed, managed entities must be reconciled
-    if (
-      (empty($item['is_dashlet']) !== empty($original['is_dashlet'])) ||
-      ($item['is_dashlet'] && ($item['title'] ?? '') !== ($original['title'] ?? ''))
-    ) {
+    if (Utils::shouldReconcileManaged($item, $original)) {
       $this->flushManaged = TRUE;
     }
 
     // If the server_route changed, reset menu cache
-    if (($item['server_route'] ?? '') !== ($original['server_route'] ?? '')) {
+    if (Utils::shouldClearMenuCache($item, $original)) {
       $this->flushMenu = TRUE;
     }
 
@@ -86,7 +84,7 @@ class Revert extends \Civi\Api4\Generic\BasicBatchAction {
    * @return string[]
    */
   protected function getSelect() {
-    return ['name', 'title', 'is_dashlet', 'server_route'];
+    return ['name', 'title', 'placement', 'server_route'];
   }
 
 }
index 83c481a5bfe5710cc879ff1041fcd0008b526df2..dfd1f13d0a794873c26ca9f3f0c2132a61a8f01c 100644 (file)
@@ -166,28 +166,10 @@ class Afform extends Generic\AbstractEntity {
           'title' => E::ts('Description'),
         ],
         [
-          'name' => 'is_dashlet',
-          'title' => E::ts('Dashboard Dashlet'),
-          'data_type' => 'Boolean',
-        ],
-        [
-          'name' => 'is_public',
-          'title' => E::ts('Is Public'),
-          'data_type' => 'Boolean',
-        ],
-        [
-          'name' => 'is_token',
-          'title' => E::ts('Generate Tokens'),
-          'data_type' => 'Boolean',
-        ],
-        [
-          'name' => 'contact_summary',
-          'title' => E::ts('Contact Summary'),
-          'data_type' => 'String',
-          'options' => [
-            'block' => E::ts('Contact Summary Block'),
-            'tab' => E::ts('Contact Summary Tab'),
-          ],
+          'name' => 'placement',
+          'title' => E::ts('Placement'),
+          'pseudoconstant' => ['optionGroupName' => 'afform_placement'],
+          'data_type' => 'Array',
         ],
         [
           'name' => 'summary_contact_type',
@@ -209,6 +191,11 @@ class Afform extends Generic\AbstractEntity {
           'name' => 'server_route',
           'title' => E::ts('Page Route'),
         ],
+        [
+          'name' => 'is_public',
+          'title' => E::ts('Is Public'),
+          'data_type' => 'Boolean',
+        ],
         [
           'name' => 'permission',
           'title' => E::ts('Permission'),
index 699f3b6498208f09ff63fca4a17819f939444b1a..aae55b43183f719ea0699cfeb70df9d1d2985811 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Civi\Api4\Utils;
 
+use Civi\Afform\Utils;
 use CRM_Afform_ExtensionUtil as E;
 
 /**
@@ -67,24 +68,12 @@ trait AfformSaveTrait {
     // We may have changed list of files covered by the cache.
     _afform_clear();
 
-    $isChanged = function($field) use ($item, $orig) {
-      return ($item[$field] ?? NULL) !== ($orig[$field] ?? NULL);
-    };
-
     // If the dashlet or navigation setting changed, managed entities must be reconciled
-    // TODO: If this list of conditions gets any longer, then
-    // maybe we should unconditionally reconcile and accept the small performance drag.
-    if (
-      $isChanged('is_dashlet') ||
-      $isChanged('navigation') ||
-      (!empty($meta['is_dashlet']) && $isChanged('title')) ||
-      (!empty($meta['navigation']) && ($isChanged('title') || $isChanged('permission') || $isChanged('icon') || $isChanged('server_route')))
-    ) {
+    if (Utils::shouldReconcileManaged($item, $orig ?? [])) {
       \CRM_Core_ManagedEntities::singleton()->reconcile(E::LONG_NAME);
     }
 
-    // Right now, permission-checks are completely on-demand.
-    if ($isChanged('server_route') /* || $isChanged('permission') */) {
+    if (Utils::shouldClearMenuCache($item, $orig ?? [])) {
       \CRM_Core_Menu::store();
     }
 
index 674f44be723cf9c7f560c3f4e8a46c1941d82ecf..bd812e89a54cbee03b865a378a08c42d6d285753 100644 (file)
@@ -105,7 +105,11 @@ function afform_civicrm_managed(&$entities, $modules) {
     if (empty($afform['name'])) {
       continue;
     }
+    // Backward-compat with legacy `is_dashlet`
     if (!empty($afform['is_dashlet'])) {
+      $afform['placement'][] = 'dashboard_dashlet';
+    }
+    if (in_array('dashboard_dashlet', $afform['placement'] ?? [], TRUE)) {
       $entities[] = [
         'module' => E::LONG_NAME,
         'name' => 'afform_dashlet_' . $afform['name'],
@@ -141,7 +145,7 @@ function afform_civicrm_managed(&$entities, $modules) {
             'weight' => $afform['navigation']['weight'] ?? 0,
             'url' => $afform['server_route'],
             'is_active' => 1,
-            'icon' => 'crm-i ' . $afform['icon'],
+            'icon' => !empty($afform['icon']) ? 'crm-i ' . $afform['icon'] : '',
             'domain_id' => $domain['id'],
           ],
           'match' => ['domain_id', 'name'],
@@ -174,7 +178,7 @@ function afform_civicrm_tabset($tabsetName, &$tabs, $context) {
   $contactTypes = array_merge((array) ($context['contact_type'] ?? []), $context['contact_sub_type'] ?? []);
   $afforms = Civi\Api4\Afform::get(FALSE)
     ->addSelect('name', 'title', 'icon', 'module_name', 'directive_name', 'summary_contact_type', 'summary_weight')
-    ->addWhere('contact_summary', '=', 'tab')
+    ->addWhere('placement', 'CONTAINS', 'contact_summary_tab')
     ->addOrderBy('title')
     ->execute();
   $weight = 111;
@@ -213,7 +217,7 @@ function afform_civicrm_pageRun(&$page) {
   }
   $afforms = Civi\Api4\Afform::get(FALSE)
     ->addSelect('name', 'title', 'icon', 'module_name', 'directive_name', 'summary_contact_type')
-    ->addWhere('contact_summary', '=', 'block')
+    ->addWhere('placement', 'CONTAINS', 'contact_summary_block')
     ->addOrderBy('summary_weight')
     ->addOrderBy('title')
     ->execute();
@@ -257,7 +261,7 @@ function afform_civicrm_pageRun(&$page) {
 function afform_civicrm_contactSummaryBlocks(&$blocks) {
   $afforms = \Civi\Api4\Afform::get(FALSE)
     ->setSelect(['name', 'title', 'directive_name', 'module_name', 'type', 'type:icon', 'type:label', 'summary_contact_type'])
-    ->addWhere('contact_summary', '=', 'block')
+    ->addWhere('placement', 'CONTAINS', 'contact_summary_block')
     ->addOrderBy('title')
     ->execute();
   foreach ($afforms as $index => $afform) {
index fda6038cb506502cd02ccb7ea772f1b0e427dbe0..6c2863cc4531bbf2c5f82e461d0195790358f3bd 100644 (file)
@@ -65,9 +65,8 @@ function _civicrm_api3_afform_get_spec(&$fields) {
     'title' => 'Type',
     'type' => CRM_Utils_Type::T_STRING,
   ];
-  $fields['is_dashlet'] = [
-    'title' => 'Dashlet',
-    'type' => CRM_Utils_Type::T_BOOLEAN,
+  $fields['placement'] = [
+    'title' => 'Placement',
   ];
   $fields['is_public'] = [
     'title' => 'Public',
diff --git a/ext/afform/core/managed/AfformPlacement.mgd.php b/ext/afform/core/managed/AfformPlacement.mgd.php
new file mode 100644 (file)
index 0000000..cd95994
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+
+use CRM_Afform_ExtensionUtil as E;
+
+// Option group for Afform.placement field
+return [
+  [
+    'name' => 'AfformPlacement',
+    'entity' => 'OptionGroup',
+    'update' => 'always',
+    'cleanup' => 'always',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'afform_placement',
+        'title' => E::ts('Afform Placement'),
+        'is_reserved' => TRUE,
+        'is_active' => TRUE,
+        'option_value_fields' => [
+          'name',
+          'label',
+          'icon',
+          'description',
+        ],
+      ],
+      'match' => ['name'],
+    ],
+  ],
+  [
+    'name' => 'AfformPlacement:dashboard_dashlet',
+    'entity' => 'OptionValue',
+    'cleanup' => 'always',
+    'update' => 'always',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'option_group_id.name' => 'afform_placement',
+        'name' => 'dashboard_dashlet',
+        'value' => 'dashboard_dashlet',
+        'label' => E::ts('Dashboard Dashlet'),
+        'is_reserved' => TRUE,
+        'is_active' => TRUE,
+        'icon' => 'fa-tachometer',
+        'description' => E::ts('Allow CiviCRM users to add the form to their home dashboard.'),
+      ],
+      'match' => ['option_group_id', 'name'],
+    ],
+  ],
+  [
+    'name' => 'AfformPlacement:contact_summary_tab',
+    'entity' => 'OptionValue',
+    'cleanup' => 'always',
+    'update' => 'always',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'option_group_id.name' => 'afform_placement',
+        'name' => 'contact_summary_tab',
+        'value' => 'contact_summary_tab',
+        'label' => E::ts('Contact Summary Tab'),
+        'is_reserved' => TRUE,
+        'is_active' => TRUE,
+        'icon' => 'fa-address-card-o',
+        'description' => E::ts('Add tab to contact summary page.'),
+      ],
+      'match' => ['option_group_id', 'name'],
+    ],
+  ],
+  [
+    'name' => 'AfformPlacement:contact_summary_block',
+    'entity' => 'OptionValue',
+    'cleanup' => 'always',
+    'update' => 'always',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'option_group_id.name' => 'afform_placement',
+        'name' => 'contact_summary_block',
+        'value' => 'contact_summary_block',
+        'label' => E::ts('Contact Summary Block'),
+        'is_reserved' => TRUE,
+        'is_active' => TRUE,
+        'icon' => 'fa-columns',
+        'description' => E::ts('Add block to main contact summary tab.'),
+      ],
+      'match' => ['option_group_id', 'name'],
+    ],
+  ],
+  [
+    'name' => 'AfformPlacement:msg_token',
+    'entity' => 'OptionValue',
+    'cleanup' => 'always',
+    'update' => 'always',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'option_group_id.name' => 'afform_placement',
+        'name' => 'msg_token',
+        'value' => 'msg_token',
+        'label' => E::ts('Message Tokens'),
+        'is_reserved' => TRUE,
+        'is_active' => TRUE,
+        'icon' => 'fa-code',
+        'description' => E::ts('Allows CiviMail authors to easily link to this page'),
+      ],
+      'match' => ['option_group_id', 'name'],
+    ],
+  ],
+];
index 0397eba8ac2a275d0dfdfc0e89d50a4e642abca1..3fe58995f4233e57912a6d8d9b9504871df6ae96 100644 (file)
@@ -43,25 +43,25 @@ class AfformContactSummaryTest extends TestCase implements HeadlessInterface {
     Afform::create()
       ->addValue('name', $this->formNames[0])
       ->addValue('title', 'Test B')
-      ->addValue('contact_summary', 'tab')
+      ->addValue('placement', ['contact_summary_tab'])
       ->addValue('summary_contact_type', ['Organization'])
       ->execute();
     Afform::create()
       ->addValue('name', $this->formNames[1])
       ->addValue('title', 'Test C')
-      ->addValue('contact_summary', 'tab')
+      ->addValue('placement', ['contact_summary_tab'])
       ->addValue('summary_contact_type', ['FooBar'])
       ->addValue('icon', 'smiley-face')
       ->execute();
     Afform::create()
       ->addValue('name', $this->formNames[2])
       ->addValue('title', 'Test A')
-      ->addValue('contact_summary', 'tab')
+      ->addValue('placement', ['contact_summary_tab'])
       ->execute();
     Afform::create()
       ->addValue('name', $this->formNames[3])
       ->addValue('title', 'Test D')
-      ->addValue('contact_summary', 'tab')
+      ->addValue('placement', ['contact_summary_tab'])
       ->addValue('summary_contact_type', ['Individual'])
       ->addValue('summary_weight', 99)
       ->execute();
@@ -116,14 +116,14 @@ class AfformContactSummaryTest extends TestCase implements HeadlessInterface {
       ->addValue('name', $this->formNames[0])
       ->addValue('title', 'Test B')
       ->addValue('type', 'search')
-      ->addValue('contact_summary', 'block')
+      ->addValue('placement', ['contact_summary_block'])
       ->addValue('summary_contact_type', ['Individual', 'Household'])
       ->execute();
     Afform::create()
       ->addValue('name', $this->formNames[1])
       ->addValue('title', 'Test C')
       ->addValue('type', 'form')
-      ->addValue('contact_summary', 'block')
+      ->addValue('placement', ['contact_summary_block'])
       ->addValue('summary_contact_type', ['Farm'])
       ->addValue('icon', 'smiley-face')
       ->execute();
@@ -131,13 +131,13 @@ class AfformContactSummaryTest extends TestCase implements HeadlessInterface {
       ->addValue('name', $this->formNames[2])
       ->addValue('type', 'form')
       ->addValue('title', 'Test A')
-      ->addValue('contact_summary', 'block')
+      ->addValue('placement', ['contact_summary_block'])
       ->execute();
     Afform::create()
       ->addValue('name', $this->formNames[3])
       ->addValue('type', 'form')
       ->addValue('title', 'A Weight Test')
-      ->addValue('contact_summary', 'block')
+      ->addValue('placement', ['contact_summary_block'])
       ->addValue('summary_weight', 99)
       ->execute();
 
index 607ee3851a0e6b931b8fa5dc71718c7a5fa37ed0..ed2b0e251ac1cbfe2eddb606d1f266f5348cd57c 100644 (file)
@@ -21,7 +21,7 @@ class AfformMetadataTest extends \PHPUnit\Framework\TestCase implements Headless
     $this->assertEquals(['name', 'label', 'icon', 'description'], $fields['type']['suffixes']);
 
     $this->assertTrue($fields['base_module']['options']);
-    $this->assertTrue($fields['contact_summary']['options']);
+    $this->assertTrue($fields['placement']['options']);
   }
 
   public function testGetEntityFields():void {
index 6b381ed1fabe51a22308fa04518daf41fed0298e..c087e8ba739b49ccb73080438617d77cf8c6f56d 100644 (file)
@@ -31,8 +31,8 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
 
   public function getBasicDirectives() {
     return [
-      ['mockPage', ['title' => '', 'description' => '', 'server_route' => 'civicrm/mock-page', 'permission' => ['access Foobar'], 'is_dashlet' => TRUE, 'submit_enabled' => TRUE]],
-      ['mockBareFile', ['title' => '', 'description' => '', 'permission' => ['access CiviCRM'], 'is_dashlet' => FALSE, 'submit_enabled' => TRUE]],
+      ['mockPage', ['title' => '', 'description' => '', 'server_route' => 'civicrm/mock-page', 'permission' => ['access Foobar'], 'placement' => ['dashboard_dashlet'], 'submit_enabled' => TRUE]],
+      ['mockBareFile', ['title' => '', 'description' => '', 'permission' => ['access CiviCRM'], 'placement' => [], 'submit_enabled' => TRUE]],
       ['mockFoo', ['title' => '', 'description' => '', 'permission' => ['access CiviCRM']], 'submit_enabled' => TRUE],
       ['mock-weird-name', ['title' => 'Weird Name', 'description' => '', 'permission' => ['access CiviCRM']], 'submit_enabled' => TRUE],
     ];
@@ -55,7 +55,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
       $dashlet = Dashboard::get(FALSE)
         ->addWhere('name', '=', $formName)
         ->execute();
-      if (!empty($afform['is_dashlet'])) {
+      if (in_array('dashboard_dashlet', $afform['placement'] ?? [], TRUE)) {
         $this->assertCount(1, $dashlet);
       }
       else {
@@ -73,7 +73,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
     $this->assertEquals($get($originalMetadata, 'title'), $get($result[0], 'title'), $message);
     $this->assertEquals($get($originalMetadata, 'description'), $get($result[0], 'description'), $message);
     $this->assertEquals($get($originalMetadata, 'server_route'), $get($result[0], 'server_route'), $message);
-    $this->assertEquals($get($originalMetadata, 'is_dashlet'), $get($result[0], 'is_dashlet'), $message);
+    $this->assertEquals($get($originalMetadata, 'placement') ?? [], $get($result[0], 'placement'), $message);
     $this->assertEquals($get($originalMetadata, 'permission'), $get($result[0], 'permission'), $message);
     $this->assertTrue(is_array($result[0]['layout']), $message);
     $this->assertEquals(TRUE, $get($result[0], 'has_base'), $message);
@@ -86,7 +86,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
       ->addWhere('name', '=', $formName)
       ->addValue('description', 'The temporary description')
       ->addValue('permission', ['access foo', 'access bar'])
-      ->addValue('is_dashlet', empty($originalMetadata['is_dashlet']))
+      ->addValue('placement', empty($originalMetadata['placement']) ? ['dashboard_dashlet'] : [])
       ->execute();
     $this->assertEquals($formName, $result[0]['name'], $message);
     $this->assertEquals('The temporary description', $result[0]['description'], $message);
@@ -98,7 +98,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
     $this->assertEquals($formName, $result[0]['name'], $message);
     $this->assertEquals($get($originalMetadata, 'title'), $get($result[0], 'title'), $message);
     $this->assertEquals('The temporary description', $get($result[0], 'description'), $message);
-    $this->assertEquals(empty($originalMetadata['is_dashlet']), $get($result[0], 'is_dashlet'), $message);
+    $this->assertNotEquals($get($originalMetadata, 'placement'), $get($result[0], 'placement'), $message);
     $this->assertEquals($get($originalMetadata, 'server_route'), $get($result[0], 'server_route'), $message);
     $this->assertEquals(['access foo', 'access bar'], $get($result[0], 'permission'), $message);
     $this->assertTrue(is_array($result[0]['layout']), $message);
@@ -117,7 +117,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
     $this->assertEquals($get($originalMetadata, 'description'), $get($result[0], 'description'), $message);
     $this->assertEquals($get($originalMetadata, 'server_route'), $get($result[0], 'server_route'), $message);
     $this->assertEquals($get($originalMetadata, 'permission'), $get($result[0], 'permission'), $message);
-    $this->assertEquals($get($originalMetadata, 'is_dashlet'), $get($result[0], 'is_dashlet'), $message);
+    $this->assertEquals($get($originalMetadata, 'placement') ?? [], $get($result[0], 'placement'), $message);
     $this->assertTrue(is_array($result[0]['layout']), $message);
     $this->assertEquals(TRUE, $get($result[0], 'has_base'), $message);
     $this->assertEquals(FALSE, $get($result[0], 'has_local'), $message);
index af3297ac2d1e9a6b206b4f4810c91acd1eb63156..ae187701c0d0291c34df5000c0a9758df6892cbb 100644 (file)
@@ -4,7 +4,7 @@ use CRM_Grant_ExtensionUtil as E;
 return [
   'type' => 'search',
   'title' => E::ts('Grants'),
-  'contact_summary' => 'tab',
+  'placement' => ['contact_summary_tab'],
   'summary_weight' => 60,
   'icon' => 'fa-money',
   'server_route' => '',