Afform - provide easy way to add navigation menu from the form
authorColeman Watts <coleman@civicrm.org>
Sun, 17 Jul 2022 15:56:31 +0000 (11:56 -0400)
committerColeman Watts <coleman@civicrm.org>
Mon, 18 Jul 2022 16:25:14 +0000 (12:25 -0400)
ext/afform/admin/ang/afGuiEditor.css
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/Civi/Api4/Utils/AfformSaveTrait.php
ext/afform/core/afform.php

index 585a397845953e61974d11bcbaa1ec5c6066747e..445fab469b9dd19bdf31c7a664bba1fe673b1a06 100644 (file)
   margin-bottom: 10px;
 }
 
-#afGuiEditor-palette-config .form-inline label {
-  min-width: 110px;
-}
-
 #afGuiEditor-palette-config .af-gui-entity-palette [type=search] {
   width: 120px;
   padding: 3px 3px 3px 5px;
index e99e02c58ce8be88d4718e71de7deaf0f01bf741..12f4d927de4cc5d09ca4ea805d1fd858fecf9ae0 100644 (file)
         $scope.entities = {};
         setEditorLayout();
 
+        if (editor.afform.navigation) {
+          loadNavigationMenu();
+        }
+
         if (editor.getFormType() === 'form') {
           editor.allowEntityConfig = true;
           $scope.entities = _.mapValues(afGui.findRecursive(editor.layout['#children'], {'#tag': 'af-entity'}, 'name'), backfillEntityDefaults);
         }
       };
 
+      this.toggleNavigation = function() {
+        if (editor.afform.navigation) {
+          editor.afform.navigation = null;
+        } else {
+          loadNavigationMenu();
+          editor.afform.navigation = {
+            parent: null,
+            label: editor.afform.title,
+            weight: 0
+          };
+        }
+      };
+
+      function loadNavigationMenu() {
+        if ('navigationMenu' in editor) {
+          return;
+        }
+        editor.navigationMenu = null;
+        var conditions = [
+          ['domain_id', '=', 'current_domain'],
+          ['name', '!=', 'Home']
+        ];
+        if (editor.afform.name) {
+          conditions.push(['name', '!=', editor.afform.name]);
+        }
+        crmApi4('Navigation', 'get', {
+          select: ['name', 'label', 'parent_id', 'icon'],
+          where: conditions,
+          orderBy: {weight: 'ASC'}
+        }).then(function(items) {
+          editor.navigationMenu = buildTree(items, null);
+        });
+      }
+
+      function buildTree(items, parentId) {
+        return _.transform(items, function(navigationMenu, item) {
+          if (parentId === item.parent_id) {
+            var children = buildTree(items, item.id),
+              menuItem = {
+                id: item.name,
+                text: item.label,
+                icon: item.icon
+              };
+            if (children.length) {
+              menuItem.children = children;
+            }
+            navigationMenu.push(menuItem);
+          }
+        }, []);
+      }
+
       // Collects all search displays currently on the form
       function getSearchDisplaysOnForm() {
         var searchFieldsets = afGui.findRecursive(editor.afform.layout, {'af-fieldset': ''});
index ca85a02cb13460507b20c273f2309922c6eeeccf..c316e9ec4e47193a80959abf432bb477f9891b21 100644 (file)
@@ -32,7 +32,7 @@
 
     <div class="form-group" ng-class="{'has-error': !!config_form.server_route.$error.pattern}">
       <label for="af_config_form_server_route">
-        {{:: ts('Page') }}
+        {{:: 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">
       <p class="help-block">{{:: ts('Expose the form as a standalone webpage. (Example: "civicrm/my-form")') }}</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 class="form-inline">
+        <label ng-class="{disabled: !editor.afform.server_route}">
+          <input type="checkbox" ng-checked="editor.afform.server_route && editor.afform.navigation" ng-disabled="!editor.afform.server_route" ng-click="editor.toggleNavigation()">
+          {{:: ts('Add to Navigation Menu') }}
+        </label>
+        <div class="form-group" ng-if="editor.afform.navigation">
+          <input class="form-control" ng-model="editor.afform.navigation.label" ng-model-options="editor.debounceMode" placeholder="{{:: ts('Title') }}" required>
+          <span ng-if="!editor.navigationMenu">
+            <input class="form-control loading" disabled crm-ui-select="{placeholder: ts('Loading menu items'), data: []}">
+          </span>
+          <span ng-if="editor.navigationMenu">
+            <input class="form-control" ng-model="editor.afform.navigation.parent"
+                   crm-ui-select="{allowClear: true, placeholder: ts('Top Level'), data: editor.navigationMenu || []}">
+          </span>
+          <label for="afform-admin-navigation-weight">{{:: ts('Order') }}</label>
+          <input class="form-control" id="afform-admin-navigation-weight" type="number" placeholder="{{:: ts('Order') }}" min="0" step="1" ng-model="editor.afform.navigation.weight" required>
+        </div>
+      </div>
+      <p class="help-block disabled" ng-if="!editor.afform.server_route">{{:: ts('Requires a page route') }}</p>
+    </div>
+
+    <div class="form-group" ng-show="!!editor.afform.navigation || editor.afform.contact_summary === 'tab'">
+      <div class="form-inline">
+        <label for="afform_icon">{{:: ts('Icon') }}</label>
+        <input required id="afform_icon" ng-model="editor.afform.icon" crm-ui-icon-picker class="form-control">
+      </div>
     </div>
 
     <div class="form-group">
           <option value="block">{{:: ts('As Block') }}</option>
           <option value="tab">{{:: ts('As Tab') }}</option>
         </select>
-        <div class="form-group" ng-show="editor.afform.contact_summary === 'tab'">
-          <input required ng-model="editor.afform.icon" crm-ui-icon-picker class="form-control">
-        </div>
       </div>
-      <p class="help-block">{{:: ts('Placement can be configured using the Contact Layout Editor.') }}</p>
+      <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>
 
   <!--  Submit actions are only applicable to form types with a submit button (exclude blocks and search forms) -->
index 15d2aea8d8f2a06943dfd033b4ef1ea4bf328a31..2a21cf756cb00912f433063abd75740366ca1b1c 100644 (file)
@@ -182,6 +182,11 @@ class Afform extends Generic\AbstractEntity {
           'name' => 'create_submission',
           'data_type' => 'Boolean',
         ],
+        [
+          'name' => 'navigation',
+          'data_type' => 'Array',
+          'description' => 'Insert into navigation menu {parent: string, label: string, weight: int}',
+        ],
         [
           'name' => 'layout',
           'data_type' => 'Array',
index 0d109351af5793c78e3def04dd89ea1c3c822d32..466705a00d34422c27581c0dd1e66aa0e027f8f4 100644 (file)
@@ -67,10 +67,14 @@ trait AfformSaveTrait {
       return ($item[$field] ?? NULL) !== ($orig[$field] ?? NULL);
     };
 
-    // If the dashlet setting changed, managed entities must be reconciled
+    // 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') ||
-      (!empty($meta['is_dashlet']) && $isChanged('title'))
+      $isChanged('navigation') ||
+      (!empty($meta['is_dashlet']) && $isChanged('title')) ||
+      (!empty($meta['navigation']) && ($isChanged('title') || $isChanged('permission') || $isChanged('icon') || $isChanged('server_route')))
     ) {
       \CRM_Core_ManagedEntities::singleton()->reconcile(E::LONG_NAME);
     }
index be15c8f32277c0abc031cd07976b944267fab3de..b1d294b44f0929e832bc4c0f3733e92cda8f2b61 100644 (file)
@@ -13,7 +13,7 @@ function _afform_fields_filter($params) {
   $result = [];
   $fields = \Civi\Api4\Afform::getfields(FALSE)->setAction('create')->execute()->indexBy('name');
   foreach ($fields as $fieldName => $field) {
-    if (isset($params[$fieldName])) {
+    if (array_key_exists($fieldName, $params)) {
       $result[$fieldName] = $params[$fieldName];
 
       if ($field['data_type'] === 'Boolean' && !is_bool($params[$fieldName])) {
@@ -140,32 +140,66 @@ function afform_civicrm_managed(&$entities, $modules) {
     // This AfformScanner instance only lives during this method call, and it feeds off the regular cache.
     $scanner = new CRM_Afform_AfformScanner();
   }
+  $domains = NULL;
 
   foreach ($scanner->getMetas() as $afform) {
-    if (empty($afform['is_dashlet']) || empty($afform['name'])) {
+    if (empty($afform['name'])) {
       continue;
     }
-    $entities[] = [
-      'module' => E::LONG_NAME,
-      'name' => 'afform_dashlet_' . $afform['name'],
-      'entity' => 'Dashboard',
-      'update' => 'always',
-      // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused
-      'cleanup' => 'always',
-      'params' => [
-        'version' => 4,
-        'values' => [
-          // Q: Should we loop through all domains?
-          'domain_id' => 'current_domain',
-          'is_active' => TRUE,
-          'name' => $afform['name'],
-          'label' => $afform['title'] ?? E::ts('(Untitled)'),
-          'directive' => _afform_angular_module_name($afform['name'], 'dash'),
-          'permission' => "@afform:" . $afform['name'],
-          'url' => NULL,
+    if (!empty($afform['is_dashlet'])) {
+      $entities[] = [
+        'module' => E::LONG_NAME,
+        'name' => 'afform_dashlet_' . $afform['name'],
+        'entity' => 'Dashboard',
+        'update' => 'always',
+        // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused
+        'cleanup' => 'always',
+        'params' => [
+          'version' => 4,
+          'values' => [
+            // Q: Should we loop through all domains?
+            'domain_id' => 'current_domain',
+            'is_active' => TRUE,
+            'name' => $afform['name'],
+            'label' => $afform['title'] ?? E::ts('(Untitled)'),
+            'directive' => _afform_angular_module_name($afform['name'], 'dash'),
+            'permission' => "@afform:" . $afform['name'],
+            'url' => NULL,
+          ],
         ],
-      ],
-    ];
+      ];
+    }
+    if (!empty($afform['navigation']) && !empty($afform['server_route'])) {
+      $domains = $domains ?: \Civi\Api4\Domain::get(FALSE)->addSelect('id')->execute();
+      foreach ($domains as $domain) {
+        $params = [
+          'version' => 4,
+          'values' => [
+            'name' => $afform['name'],
+            'label' => $afform['navigation']['label'] ?: $afform['title'],
+            'permission' => (array) $afform['permission'],
+            'permission_operator' => 'OR',
+            'weight' => $afform['navigation']['weight'] ?? 0,
+            'url' => $afform['server_route'],
+            'is_active' => 1,
+            'icon' => 'crm-i ' . $afform['icon'],
+            'domain_id' => $domain['id'],
+          ],
+          'match' => ['domain_id', 'name'],
+        ];
+        if (!empty($afform['navigation']['parent'])) {
+          $params['values']['parent_id.name'] = $afform['navigation']['parent'];
+        }
+        $entities[] = [
+          'module' => E::LONG_NAME,
+          'name' => 'navigation_' . $afform['name'] . '_' . $domain['id'],
+          'cleanup' => 'always',
+          'update' => 'unmodified',
+          'entity' => 'Navigation',
+          'params' => $params,
+        ];
+      }
+    }
   }
 }