AfformAdmin - use api-callback for loading metadata
authorColeman Watts <coleman@civicrm.org>
Mon, 25 Jan 2021 15:51:55 +0000 (10:51 -0500)
committerColeman Watts <coleman@civicrm.org>
Sat, 30 Jan 2021 01:41:05 +0000 (20:41 -0500)
This switches from pre-loading all field data, to a more incremental approach of only loading the data needed by the form being edited,
then loading more metadata if additional entities or blocks are added to the form.

The Afform.loadAdminData api action is added to facilitate loading metadata for different types of afforms,
and incrementally loading metadata for new entities & blocks as they are added to an afform being edited.

18 files changed:
CRM/Utils/Array.php
ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php [moved from ext/afform/admin/CRM/AfformAdmin/Utils.php with 51% similarity]
ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php [new file with mode: 0644]
ext/afform/admin/afformEntities/Activity.php
ext/afform/admin/afformEntities/Household.php
ext/afform/admin/afformEntities/Individual.php
ext/afform/admin/afformEntities/Organization.php
ext/afform/admin/ang/afAdmin.ang.php
ext/afform/admin/ang/afAdmin.js
ext/afform/admin/ang/afAdmin/afAdminGui.controller.js
ext/afform/admin/ang/afGuiEditor.ang.php
ext/afform/admin/ang/afGuiEditor.js
ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html
ext/afform/admin/ang/afGuiEditor/afGuiEntity.html
ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js
ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html
ext/afform/admin/info.xml

index 5ecdb554d58b29b404cecba3f84c0c40ff8caf90..19df58eda87ec86d8c5f36101856047b967c821a 100644 (file)
@@ -80,6 +80,41 @@ class CRM_Utils_Array {
     return NULL;
   }
 
+  /**
+   * Recursively searches through a given array for all matches
+   *
+   * @param $collection
+   * @param $predicate
+   * @return array
+   */
+  public static function findAll($collection, $predicate) {
+    $results = [];
+    $search = function($collection) use (&$search, &$results, $predicate) {
+      if (is_array($collection)) {
+        if (is_callable($predicate)) {
+          if ($predicate($collection)) {
+            $results[] = $collection;
+          }
+        }
+        elseif (is_array($predicate)) {
+          if (count(array_intersect_assoc($collection, $predicate)) === count($predicate)) {
+            $results[] = $collection;
+          }
+        }
+        else {
+          if (array_key_exists($predicate, $collection)) {
+            $results[] = $collection;
+          }
+        }
+        foreach ($collection as $item) {
+          $search($item);
+        }
+      }
+    };
+    $search($collection);
+    return $results;
+  }
+
   /**
    * Wraps and slightly changes the behavior of PHP's array_search().
    *
similarity index 51%
rename from ext/afform/admin/CRM/AfformAdmin/Utils.php
rename to ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
index f9fc4d89753f91787d82fa9d21eec21bf22fb1ac..c0474106d6375b8d714bfd8ad3ec82127cbf66b0 100644 (file)
@@ -1,7 +1,10 @@
 <?php
+
+namespace Civi\AfformAdmin;
+
 use CRM_AfformAdmin_ExtensionUtil as E;
 
-class CRM_AfformAdmin_Utils {
+class AfformAdminMeta {
 
   /**
    * @return array
@@ -18,13 +21,51 @@ class CRM_AfformAdmin_Utils {
   }
 
   /**
-   * Loads metadata for the gui editor.
-   *
-   * FIXME: This is a prototype and should get broken out into separate callbacks with hooks, events, etc.
+   * @param $entityName
+   * @return array|void
+   */
+  public static function getAfformEntity($entityName) {
+    // Optimization: look here before scanning every other extension
+    global $civicrm_root;
+    $fileName = \CRM_Utils_File::addTrailingSlash($civicrm_root) . "ext/afform/admin/afformEntities/$entityName.php";
+    if (is_file($fileName)) {
+      return include $fileName;
+    }
+    foreach (\CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles() as $ext) {
+      $fileName = \CRM_Utils_File::addTrailingSlash(dirname($ext['filePath'])) . "afformEntities/$entityName.php";
+      if (is_file($fileName)) {
+        return include $fileName;
+      }
+    }
+  }
+
+  /**
+   * @param $entityName
    * @return array
    */
-  public static function getGuiSettings() {
-    $getFieldParams = [
+  public static function getApiEntity($entityName) {
+    if (in_array($entityName, ['Individual', 'Household', 'Organization'])) {
+      $contactTypes = \CRM_Contact_BAO_ContactType::basicTypeInfo();
+      return [
+        'entity' => 'Contact',
+        'label' => $contactTypes[$entityName]['label'],
+      ];
+    }
+    $info = ("\Civi\Api4\\{$entityName}")::getInfo();
+    return [
+      'entity' => $entityName,
+      'label' => $info['title'],
+      'icon' => $info['icon'],
+    ];
+  }
+
+  /**
+   * @param $entityName
+   * @param array $params
+   * @return array
+   */
+  public static function getFields($entityName, $params = []) {
+    $params += [
       'checkPermissions' => FALSE,
       'includeCustom' => TRUE,
       'loadOptions' => ['id', 'label'],
@@ -32,64 +73,65 @@ class CRM_AfformAdmin_Utils {
       'select' => ['name', 'label', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize', 'data_type'],
       'where' => [['input_type', 'IS NOT NULL']],
     ];
+    if (in_array($entityName, ['Individual', 'Household', 'Organization'])) {
+      $params['values']['contact_type'] = $entityName;
+      $entityName = 'Contact';
+    }
+    if ($entityName === 'Address') {
+      // The stateProvince option list is waaay too long unless country limits are set
+      if (!\Civi::settings()->get('provinceLimit')) {
+        // If no province limit, restrict it to the default country, or if there's no default, pick one to avoid breaking the UI
+        $params['values']['country_id'] = \Civi::settings()->get('defaultContactCountry') ?: 1228;
+      }
+      $params['values']['state_province_id'] = \Civi::settings()->get('defaultContactStateProvince');
+    }
+    return (array) civicrm_api4($entityName, 'getFields', $params, 'name');
+  }
 
+  /**
+   * Loads metadata for the gui editor.
+   *
+   * @return array
+   */
+  public static function getGuiSettings() {
     $data = [
       'entities' => [
-        'Contact' => [
-          'entity' => 'Contact',
-          'label' => E::ts('Contact'),
-          'fields' => (array) civicrm_api4('Contact', 'getFields', $getFieldParams, 'name'),
-        ],
+        'Contact' => self::getApiEntity('Contact'),
       ],
-      'blocks' => [],
     ];
 
-    $contactTypes = CRM_Contact_BAO_ContactType::basicTypeInfo();
+    $contactTypes = \CRM_Contact_BAO_ContactType::basicTypeInfo();
 
     // Scan all extensions for entities & input types
-    foreach (CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles() as $ext) {
-      $dir = CRM_Utils_File::addTrailingSlash(dirname($ext['filePath']));
+    foreach (\CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles() as $ext) {
+      $dir = \CRM_Utils_File::addTrailingSlash(dirname($ext['filePath']));
       if (is_dir($dir)) {
         // Scan for entities
         foreach (glob($dir . 'afformEntities/*.php') as $file) {
           $entity = include $file;
-          // Skip disabled contact types
-          if (!empty($entity['contact_type']) && !isset($contactTypes[$entity['contact_type']])) {
-            continue;
-          }
+          $afformEntity = basename($file, '.php');
+          // Contact pseudo-entities (Individual, Organization, Household) get special treatment,
+          // notably their fields are pre-loaded since they are both commonly-used and nonstandard
           if (!empty($entity['contact_type'])) {
+            // Skip disabled contact types
+            if (!isset($contactTypes[$entity['contact_type']])) {
+              continue;
+            }
             $entity['label'] = $contactTypes[$entity['contact_type']]['label'];
           }
-          // For Contact pseudo-entities (Individual, Organization, Household)
-          $values = array_intersect_key($entity, ['contact_type' => NULL]);
-          $afformEntity = $entity['contact_type'] ?? $entity['entity'];
-          $entity['fields'] = (array) civicrm_api4($entity['entity'], 'getFields', $getFieldParams + ['values' => $values], 'name');
+          elseif (empty($entity['label']) || empty($entity['icon'])) {
+            $entity += self::getApiEntity($entity['entity']);
+          }
           $data['entities'][$afformEntity] = $entity;
         }
         // Scan for input types
         foreach (glob($dir . 'ang/afGuiEditor/inputType/*.html') as $file) {
-          $matches = [];
-          preg_match('/([-a-z_A-Z0-9]*).html/', $file, $matches);
-          $data['inputType'][$matches[1]] = $matches[1];
+          $name = basename($file, '.html');
+          $data['inputType'][$name] = $name;
         }
       }
     }
 
-    // Load fields from afform blocks with joins
-    $blockData = \Civi\Api4\Afform::get()
-      ->setCheckPermissions(FALSE)
-      ->addWhere('join', 'IS NOT NULL')
-      ->setSelect(['join'])
-      ->execute();
-    foreach ($blockData as $block) {
-      if (!isset($data['entities'][$block['join']]['fields'])) {
-        $data['entities'][$block['join']]['entity'] = $block['join'];
-        // Normally you shouldn't pass variables to ts() but very common strings like "Email" should already exist
-        $data['entities'][$block['join']]['label'] = E::ts($block['join']);
-        $data['entities'][$block['join']]['fields'] = (array) civicrm_api4($block['join'], 'getFields', $getFieldParams, 'name');
-      }
-    }
-
     // Todo: add method for extensions to define other elements
     $data['elements'] = [
       'container' => [
@@ -160,7 +202,7 @@ class CRM_AfformAdmin_Utils {
     ];
 
     $data['permissions'] = [];
-    foreach (CRM_Core_Permission::basicPermissions(TRUE, TRUE) as $name => $perm) {
+    foreach (\CRM_Core_Permission::basicPermissions(TRUE, TRUE) as $name => $perm) {
       $data['permissions'][] = [
         'id' => $name,
         'text' => $perm[0],
diff --git a/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php b/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php
new file mode 100644 (file)
index 0000000..350ea82
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+
+namespace Civi\Api4\Action\Afform;
+
+use Civi\AfformAdmin\AfformAdminMeta;
+use Civi\Api4\Afform;
+
+/**
+ * This action is used by the Afform Admin extension to load metadata for the Admin GUI.
+ *
+ * @package Civi\Api4\Action\Afform
+ */
+class LoadAdminData extends \Civi\Api4\Generic\AbstractAction {
+
+  /**
+   * Any properties already known about the afform
+   * @var array
+   */
+  protected $definition;
+
+  /**
+   * Entity type when creating a new form
+   * @var string
+   */
+  protected $entity;
+
+  public function _run(\Civi\Api4\Generic\Result $result) {
+    $info = ['fields' => [], 'blocks' => []];
+    $entities = [];
+    $newForm = empty($this->definition['name']);
+
+    if (!$newForm) {
+      // Load existing afform if name provided
+      $info['definition'] = $this->loadForm($this->definition['name']);
+    }
+    else {
+      // Create new blank afform
+      switch ($this->definition['type']) {
+        case 'form':
+          $info['definition'] = $this->definition + [
+            'title' => '',
+            'permission' => 'access CiviCRM',
+            'layout' => [
+              [
+                '#tag' => 'af-form',
+                'ctrl' => 'afform',
+                '#children' => [],
+              ],
+            ],
+          ];
+          break;
+
+        case 'block':
+          $info['definition'] = $this->definition + [
+            'title' => '',
+            'layout' => [],
+          ];
+          break;
+      }
+    }
+
+    $getFieldsMode = 'create';
+
+    // Generate list of possibly embedded afform tags to search for
+    $allAfforms = \Civi::service('afform_scanner')->findFilePaths();
+    foreach ($allAfforms as $name => $path) {
+      $allAfforms[$name] = _afform_angular_module_name($name, 'dash');
+    }
+    // Find all entities by recursing into embedded afforms
+    $scanBlocks = function($layout) use (&$scanBlocks, &$info, &$entities, $allAfforms) {
+      // Find declared af-entity tags
+      foreach (\CRM_Utils_Array::findAll($layout, ['#tag' => 'af-entity']) as $afEntity) {
+        // Convert "Contact" to "Individual", "Organization" or "Household"
+        if ($afEntity['type'] === 'Contact' && !empty($afEntity['data'])) {
+          $data = \CRM_Utils_JS::decode($afEntity['data']);
+          $entities[] = $data['contact_type'] ?? $afEntity['type'];
+        }
+        else {
+          $entities[] = $afEntity['type'];
+        }
+      }
+      $joins = array_column(\CRM_Utils_Array::findAll($layout, 'af-join'), 'af-join');
+      $entities = array_unique(array_merge($entities, $joins));
+      $blockTags = array_unique(array_column(\CRM_Utils_Array::findAll($layout, function($el) use ($allAfforms) {
+        return in_array($el['#tag'], $allAfforms);
+      }), '#tag'));
+      foreach ($blockTags as $blockTag) {
+        if (!isset($info['blocks'][$blockTag])) {
+          // Load full contents of block used on the form, then recurse into it
+          $embeddedForm = Afform::get($this->checkPermissions)
+            ->setFormatWhitespace(TRUE)
+            ->setLayoutFormat('shallow')
+            ->addWhere('directive_name', '=', $blockTag)
+            ->execute()->first();
+          if ($embeddedForm['type'] === 'block') {
+            $info['blocks'][$blockTag] = $embeddedForm;
+          }
+          if (!empty($embeddedForm['join'])) {
+            $entities = array_unique(array_merge($entities, [$embeddedForm['join']]));
+          }
+          $scanBlocks($embeddedForm['layout']);
+        }
+      }
+    };
+
+    if ($info['definition']['type'] === 'form') {
+      if ($newForm) {
+        $entities[] = $this->entity;
+        $defaultEntity = AfformAdminMeta::getAfformEntity($this->entity);
+        if (!empty($defaultEntity['boilerplate'])) {
+          $scanBlocks($defaultEntity['boilerplate']);
+        }
+      }
+      else {
+        $scanBlocks($info['definition']['layout']);
+      }
+
+      if (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
+        $entities[] = 'Contact';
+      }
+
+      // The full contents of blocks used on the form have been loaded. Get basic info about others relevant to these entities.
+      $blockInfo = Afform::get($this->checkPermissions)
+        ->addSelect('name', 'title', 'block', 'join', 'directive_name')
+        ->addWhere('type', '=', 'block')
+        ->addWhere('block', 'IN', $entities)
+        ->addWhere('directive_name', 'NOT IN', array_keys($info['blocks']))
+        ->execute();
+      $info['blocks'] = array_merge(array_values($info['blocks']), (array) $blockInfo);
+    }
+
+    if ($info['definition']['type'] === 'block') {
+      $entities[] = $info['definition']['join'] ?? $info['definition']['block'];
+    }
+
+    // Optimization - since contact fields are a combination of these three,
+    // we'll combine them client-side rather than sending them via ajax.
+    if (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
+      $entities = array_diff($entities, ['Contact']);
+    }
+
+    foreach ($entities as $entity) {
+      $info['entities'][$entity] = AfformAdminMeta::getApiEntity($entity);
+      $info['fields'][$entity] = AfformAdminMeta::getFields($entity, ['action' => $getFieldsMode]);
+    }
+
+    $result[] = $info;
+  }
+
+  private function loadForm($name) {
+    return Afform::get($this->checkPermissions)
+      ->setFormatWhitespace(TRUE)
+      ->setLayoutFormat('shallow')
+      ->addWhere('name', '=', $name)
+      ->execute()->first();
+  }
+
+  public function fields() {
+    return [
+      [
+        'name' => 'definition',
+        'data_type' => 'Array',
+      ],
+      [
+        'name' => 'blocks',
+        'data_type' => 'Array',
+      ],
+      [
+        'name' => 'fields',
+        'data_type' => 'Array',
+      ],
+    ];
+  }
+
+  /**
+   * @return array
+   */
+  public function getDefinition():array {
+    return $this->definition;
+  }
+
+  /**
+   * @param array $definition
+   */
+  public function setDefinition(array $definition) {
+    $this->definition = $definition;
+    return $this;
+  }
+
+}
index 32c3b8f02b8b6c55858ae1200854f20cba7cb531..ae8a651eba6bcf1c5ccd864447f25ad48a86af14 100644 (file)
@@ -1,7 +1,6 @@
 <?php
 return [
   'entity' => 'Activity',
-  'label' => ts('Activity'),
   'defaults' => "{'url-autofill': '1'}",
   'boilerplate' => [
     ['#tag' => 'af-field', 'name' => 'subject'],
index abe3253c5f5d13c6f5eb13d712129e052b065188..00173428d7c18e12840fe13a796f1726ef2bb04c 100644 (file)
@@ -9,6 +9,7 @@ return [
     },
     'url-autofill': '1'
   }",
+  'icon' => 'fa-home',
   'boilerplate' => [
     ['#tag' => 'afblock-name-household'],
   ],
index 3d651fc35cb2392be0c5167a561f98deb44b3579..b6c972754bf41c11c357215d4526824ff18d5326 100644 (file)
@@ -9,6 +9,7 @@ return [
     },
     'url-autofill': '1'
   }",
+  'icon' => 'fa-user',
   'boilerplate' => [
     ['#tag' => 'afblock-name-individual'],
   ],
index 99f0ebbb0f463c9c51a519af5181c02207b3e3b4..aba69f9f44d5fddfbb27bed75439edda44dee13a 100644 (file)
@@ -9,6 +9,7 @@ return [
     },
     'url-autofill': '1'
   }",
+  'icon' => 'fa-building',
   'boilerplate' => [
     ['#tag' => 'afblock-name-organization'],
   ],
index 818a6f4ca0522c94258482959ced1b35158a8e4d..1944d717b05184dbb20da40714a4861078a3b3b1 100644 (file)
@@ -9,7 +9,7 @@ return [
   'css' => [],
   'partials' => ['ang/afAdmin'],
   'requires' => ['api4', 'afGuiEditor', 'crmRouteBinder'],
-  'settingsFactory' => ['CRM_AfformAdmin_Utils', 'getAdminSettings'],
+  'settingsFactory' => ['Civi\AfformAdmin\AfformAdminMeta', 'getAdminSettings'],
   'basePages' => ['civicrm/admin/afform'],
   'bundles' => ['bootstrap3'],
 ];
index 1284b00bb7dc13a70a22285946b2dea6b77b46c9..4d8a85670060d3d925ba78fa8464fa65cb21fe91 100644 (file)
           }
         }
       });
-      $routeProvider.when('/create/:type', {
+      $routeProvider.when('/create/:type/:entity', {
         controller: 'afAdminGui',
-        template: '<af-gui-editor type="$ctrl.type"></af-gui-editor>',
+        template: '<af-gui-editor mode="create" data="$ctrl.data" entity="$ctrl.entity"></af-gui-editor>',
+        resolve: {
+          // Load data for gui editor
+          data: function($route, crmApi4) {
+            return crmApi4('Afform', 'loadAdminData', {
+              definition: {type: $route.current.params.type},
+              entity: $route.current.params.entity
+            }, 0);
+          }
+        }
       });
       $routeProvider.when('/edit/:name', {
         controller: 'afAdminGui',
-        template: '<af-gui-editor name="$ctrl.name"></af-gui-editor>',
+        template: '<af-gui-editor mode="edit" data="$ctrl.data"></af-gui-editor>',
+        resolve: {
+          // Load data for gui editor
+          data: function($route, crmApi4) {
+            return crmApi4('Afform', 'loadAdminData', {
+              definition: {name: $route.current.params.name}
+            }, 0);
+          }
+        }
       });
     });
 
index a7713e31b87628a8c0da2982759390d3d67a31d2..457af5926b92aecee99bf11f7dbeb2ae7996c499 100644 (file)
@@ -1,15 +1,11 @@
 (function(angular, $, _) {
   "use strict";
 
-  angular.module('afAdmin').controller('afAdminGui', function($scope, $routeParams) {
-    var ts = $scope.ts = CRM.ts(),
-      ctrl = $scope.$ctrl = this;
-
-    // Edit mode
-    this.name = $routeParams.name;
-    // Create mode
-    this.type = $routeParams.type;
-
+  angular.module('afAdmin').controller('afAdminGui', function($scope, $route, data) {
+    $scope.$ctrl = this;
+    this.entity = $route.current.params.entity;
+    // Pass through result from api Afform.loadAdminData
+    this.data = data;
   });
 
 })(angular, CRM.$, CRM._);
index 4cca7330668125b4106e5e69c265efe8d8782443..c792ee0d6485debe8f72522b6b95b8bd8ba8ef74 100644 (file)
@@ -9,7 +9,7 @@ return [
   'css' => ['ang/afGuiEditor.css'],
   'partials' => ['ang/afGuiEditor'],
   'requires' => ['crmUi', 'crmUtil', 'dialogService', 'api4', 'crmMonaco', 'ui.sortable'],
-  'settingsFactory' => ['CRM_AfformAdmin_Utils', 'getGuiSettings'],
+  'settingsFactory' => ['Civi\AfformAdmin\AfformAdminMeta', 'getGuiSettings'],
   'basePages' => [],
   'exports' => [
     'af-gui-editor' => 'E',
index 2acf4f7322a8a136968270e4dc8202235ac4db39..5b1253fc920f8e61bc2155171f65ac1257cf67ec 100644 (file)
@@ -1,5 +1,6 @@
 (function(angular, $, _) {
   "use strict";
+
   angular.module('afGuiEditor', CRM.angRequires('afGuiEditor'))
 
     .service('afGui', function(crmApi4, $parse, $q) {
       }
 
       return {
-        // Initialize/refresh data about the current afform + available blocks
-        initialize: function(afName) {
-          var promise = crmApi4('Afform', 'get', {
-            layoutFormat: 'shallow',
-            formatWhitespace: true,
-            where: [afName ? ["OR", [["name", "=", afName], ["block", "IS NOT NULL"]]] : ["block", "IS NOT NULL"]]
+        // Called when loading a new afform for editing - clears out stale metadata
+        resetMeta: function() {
+          _.each(CRM.afGuiEditor.entities, function(entity) {
+            delete entity.fields;
           });
-          promise.then(function(afforms) {
-            CRM.afGuiEditor.blocks = {};
-            _.each(afforms, function(form) {
-              evaluate(form.layout);
-              if (form.block) {
-                CRM.afGuiEditor.blocks[form.directive_name] = form;
+          CRM.afGuiEditor.blocks = {};
+        },
+
+        // Takes the results from api.Afform.loadAdminData and processes the metadata
+        // Note this runs once when loading a new afform for editing (just after this.resetMeta is called)
+        // and it also runs when adding new entities or joins to the form.
+        addMeta: function(data) {
+          evaluate(data.definition.layout);
+          if (data.definition.type === 'block') {
+            CRM.afGuiEditor.blocks[data.definition.directive_name] = data.definition;
+          }
+          // Add new or updated blocks
+          _.each(data.blocks, function(block) {
+            // Avoid overwriting complete block record with an incomplete one
+            if (!CRM.afGuiEditor.blocks[block.directive_name] || block.layout) {
+              if (block.layout) {
+                evaluate(block.layout);
               }
-            });
+              CRM.afGuiEditor.blocks[block.directive_name] = block;
+            }
           });
-          return promise;
+          _.each(data.entities, function(entity, entityName) {
+            if (!CRM.afGuiEditor.entities[entityName]) {
+              CRM.afGuiEditor.entities[entityName] = entity;
+            }
+          });
+          _.each(data.fields, function(fields, entityName) {
+            if (CRM.afGuiEditor.entities[entityName]) {
+              CRM.afGuiEditor.entities[entityName].fields = fields;
+            }
+          });
+          // Optimization - since contact fields are a combination of these three,
+          // the server doesn't send contact fields if sending contact-type fields
+          if ('Individual' in data.fields || 'Household' in data.fields || 'Organization' in data.fields) {
+            CRM.afGuiEditor.entities.Contact.fields = _.assign({},
+              (CRM.afGuiEditor.entities.Individual || {}).fields,
+              (CRM.afGuiEditor.entities.Household || {}).fields,
+              (CRM.afGuiEditor.entities.Organization || {}).fields
+            );
+          }
         },
 
         meta: CRM.afGuiEditor,
index d2a1f4889a2ce31ac5b5f193886a09551f214f8a..68fa65a9c6f91bade3d317e88fb53963f598f8ff 100644 (file)
@@ -5,8 +5,9 @@
   angular.module('afGuiEditor').component('afGuiEditor', {
     templateUrl: '~/afGuiEditor/afGuiEditor.html',
     bindings: {
-      type: '<',
-      name: '<'
+      data: '<',
+      entity: '<',
+      mode: '@'
     },
     controllerAs: 'editor',
     controller: function($scope, crmApi4, afGui, $parse, $timeout, $location) {
       $scope.selectedEntityName = null;
       this.meta = afGui.meta;
       var editor = this;
-      var newForm = {
-        title: '',
-        permission: 'access CiviCRM',
-        type: 'form',
-        layout: [{
-          '#tag': 'af-form',
-          ctrl: 'afform',
-          '#children': []
-        }]
-      };
 
       this.$onInit = function() {
-        // Fetch the current form plus all blocks
-        afGui.initialize(editor.name)
-          .then(initializeForm);
+        // Load the current form plus blocks & fields
+        afGui.resetMeta();
+        afGui.addMeta(this.data);
+        initializeForm();
       };
 
       // Initialize the current form
-      function initializeForm(afforms) {
-        $scope.afform = _.findWhere(afforms, {name: editor.name});
+      function initializeForm() {
+        $scope.afform = editor.data.definition;
         if (!$scope.afform) {
-          $scope.afform = _.cloneDeep(newForm);
-          if (editor.name) {
-            alert('Error: unknown form "' + editor.name + '"');
-          }
+          alert('Error: unknown form');
         }
         $scope.canvasTab = 'layout';
         $scope.layoutHtml = '';
         editor.layout = afGui.findRecursive($scope.afform.layout, {'#tag': 'af-form'})[0];
         $scope.entities = afGui.findRecursive(editor.layout['#children'], {'#tag': 'af-entity'}, 'name');
 
-        if (!editor.name) {
-          editor.addEntity('Individual');
+        if (editor.mode === 'create') {
+          editor.addEntity(editor.entity);
           editor.layout['#children'].push(afGui.meta.elements.submit.element);
         }
 
         // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model
-        $scope.changesSaved = !editor.name ? false : 1;
+        $scope.changesSaved = editor.mode === 'edit' ? 1 : false;
         $scope.$watch('afform', function () {
           $scope.changesSaved = $scope.changesSaved === 1;
         }, true);
@@ -70,7 +59,7 @@
           });
       };
 
-      this.addEntity = function(type) {
+      this.addEntity = function(type, selectTab) {
         var meta = afGui.meta.entities[type],
           num = 1;
         // Give this new entity a unique name
           '#tag': 'af-entity',
           type: meta.entity,
           name: type + num,
-          label: meta.label + ' ' + num
+          label: meta.label + ' ' + num,
+          loading: true,
         });
-        // Add this af-entity tag after the last existing one
-        var pos = 1 + _.findLastIndex(editor.layout['#children'], {'#tag': 'af-entity'});
-        editor.layout['#children'].splice(pos, 0, $scope.entities[type + num]);
-        // Create a new af-fieldset container for the entity
-        var fieldset = _.cloneDeep(afGui.meta.elements.fieldset.element);
-        fieldset['af-fieldset'] = type + num;
-        fieldset['#children'][0]['#children'][0]['#text'] = meta.label + ' ' + num;
-        // Add boilerplate contents
-        _.each(meta.boilerplate, function(tag) {
-          fieldset['#children'].push(tag);
-        });
-        // Attempt to place the new af-fieldset after the last one on the form
-        pos = 1 + _.findLastIndex(editor.layout['#children'], 'af-fieldset');
-        if (pos) {
-          editor.layout['#children'].splice(pos, 0, fieldset);
+
+        function addToCanvas() {
+          // Add this af-entity tag after the last existing one
+          var pos = 1 + _.findLastIndex(editor.layout['#children'], {'#tag': 'af-entity'});
+          editor.layout['#children'].splice(pos, 0, $scope.entities[type + num]);
+          // Create a new af-fieldset container for the entity
+          var fieldset = _.cloneDeep(afGui.meta.elements.fieldset.element);
+          fieldset['af-fieldset'] = type + num;
+          fieldset['#children'][0]['#children'][0]['#text'] = meta.label + ' ' + num;
+          // Add boilerplate contents
+          _.each(meta.boilerplate, function (tag) {
+            fieldset['#children'].push(tag);
+          });
+          // Attempt to place the new af-fieldset after the last one on the form
+          pos = 1 + _.findLastIndex(editor.layout['#children'], 'af-fieldset');
+          if (pos) {
+            editor.layout['#children'].splice(pos, 0, fieldset);
+          } else {
+            editor.layout['#children'].push(fieldset);
+          }
+          delete $scope.entities[type + num].loading;
+          if (selectTab) {
+            editor.selectEntity(type + num);
+          }
+        }
+
+        if (meta.fields) {
+          addToCanvas();
         } else {
-          editor.layout['#children'].push(fieldset);
+          crmApi4('Afform', 'loadAdminData', {
+            definition: {type: 'form'},
+            entity: type
+          }, 0).then(function(data) {
+            afGui.addMeta(data);
+            addToCanvas();
+          });
         }
-        return type + num;
       };
 
       this.removeEntity = function(entityName) {
         }
       };
 
-      $scope.addEntity = function(entityType) {
-        var entityName = editor.addEntity(entityType);
-        editor.selectEntity(entityName);
-      };
-
       $scope.save = function() {
         $scope.saving = $scope.changesSaved = true;
         crmApi4('Afform', 'save', {formatWhitespace: true, records: [JSON.parse(angular.toJson($scope.afform))]})
           .then(function (data) {
             $scope.saving = false;
             $scope.afform.name = data[0].name;
-            $location.url('/edit/' + data[0].name);
+            if (editor.mode !== 'edit') {
+              $location.url('/edit/' + data[0].name);
+            }
           });
       };
 
index 060123c104bf1b048850e63e761d69954fcd5db6..9307e904c7582658ab3beb6bdfb845fdd9bb557f 100644 (file)
@@ -7,7 +7,8 @@
       </li>
       <li role="presentation" ng-repeat="entity in entities" ng-class="{active: selectedEntityName === entity.name}">
         <a href ng-click="editor.selectEntity(entity.name)">
-          <span af-gui-editable ng-model="entity.label">{{ entity.label }}</span>
+          <span ng-if="!entity.loading" af-gui-editable ng-model="entity.label">{{ entity.label }}</span>
+          <i ng-if="entity.loading" class="crm-i fa-spin fa-spinner"></i>
         </a>
       </li>
       <li role="presentation" class="dropdown">
         </a>
         <ul class="dropdown-menu">
           <li ng-repeat="(entityName, entity) in editor.meta.entities" ng-if="entity.defaults">
-            <a href ng-click="addEntity(entityName)">{{ entity.label }}</a>
+            <a href ng-click="editor.addEntity(entityName, true)">
+              <i class="crm-i {{:: entity.icon }}"></i>
+              {{:: entity.label }}
+            </a>
           </li>
         </ul>
       </li>
index f7c8e823573a44c98b78b04583dd7e2b59158067..f55fc71a8f56a04103757748d26249ea5bb0487d 100644 (file)
@@ -1,4 +1,4 @@
-<div class="af-gui-columns crm-flex-box">
+<div class="af-gui-columns crm-flex-box" ng-if="!$ctrl.entity.loading">
   <fieldset class="af-gui-entity-values">
     <legend>{{:: ts('Values:') }}</legend>
     <div class="form-inline" ng-if="getMeta().fields[fieldName]" ng-repeat="(fieldName, value) in $ctrl.entity.data">
@@ -54,7 +54,7 @@
   <i class="crm-i fa-trash"></i>
 </a>
 
-<fieldset>
+<fieldset ng-if="!$ctrl.entity.loading">
   <legend>{{:: ts('Options') }}</legend>
   <div ng-include="'~/afGuiEditor/entityConfig/' + $ctrl.entity.type + '.html'"></div>
 </fieldset>
index 7cf73a63f1f4d1b2a522f43caba4354eb521143c..0953d61916313b0c51bf484545713b145847e511 100644 (file)
 
       this.$onInit = function() {
         if ((ctrl.node['#tag'] in afGui.meta.blocks) || ctrl.join) {
+          var blockNode = getBlockNode(),
+            blockTag = blockNode ? blockNode['#tag'] : null;
+          if (blockTag && (blockTag in afGui.meta.blocks) && !afGui.meta.blocks[blockTag].layout) {
+            ctrl.loading = true;
+            crmApi4('Afform', 'loadAdminData', {
+              definition: {name: afGui.meta.blocks[blockTag].name}
+            }, 0).then(function(data) {
+              afGui.addMeta(data);
+              initializeBlockContainer();
+              ctrl.loading = false;
+            });
+          }
           initializeBlockContainer();
         }
       };
index 5fab5d725840cd7b59adee12799396b1136ccf64..a1ae54be3e2aa1ff4697aa770af251afb599bd87 100644 (file)
@@ -1,5 +1,5 @@
 <div class="af-gui-bar" ng-if="$ctrl.node['#tag'] !== 'af-form'" ng-click="selectEntity()" >
-  <div class="form-inline" af-gui-menu>
+  <div ng-if="!$ctrl.loading" class="form-inline" af-gui-menu>
     <span ng-if="$ctrl.getNodeType($ctrl.node) == 'fieldset'">{{ $ctrl.editor.getEntity($ctrl.entityName).label }}</span>
     <span ng-if="block">{{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }}</span>
     <span ng-if="!block">{{ tags[$ctrl.node['#tag']].toLowerCase() }}</span>
@@ -13,8 +13,9 @@
     </button>
     <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiContainer-menu.html'"></ul>
   </div>
+  <div ng-if="$ctrl.loading"><i class="crm-i fa-spin fa-spinner"></i></div>
 </div>
-<div ui-sortable="{handle: '.af-gui-bar', connectWith: '[ui-sortable]', cancel: 'input,textarea,button,select,option,a,.dropdown-menu', placeholder: 'af-gui-dropzone', containment: '#afGuiEditor-canvas-body'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="getSetChildren" ng-model-options="{getterSetter: true}" class="af-gui-layout {{ getLayout() }}">
+<div ng-if="!$ctrl.loading" ui-sortable="{handle: '.af-gui-bar', connectWith: '[ui-sortable]', cancel: 'input,textarea,button,select,option,a,.dropdown-menu', placeholder: 'af-gui-dropzone', containment: '#afGuiEditor-canvas-body'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="getSetChildren" ng-model-options="{getterSetter: true}" class="af-gui-layout {{ getLayout() }}">
   <div ng-repeat="item in getSetChildren()" >
     <div ng-switch="$ctrl.getNodeType(item)">
       <af-gui-container ng-switch-when="fieldset" node="item" delete-this="$ctrl.removeElement(item)" style="{{ item.style }}" class="af-gui-container af-gui-fieldset af-gui-container-type-{{ item['#tag'] }}" ng-class="{'af-entity-selected': isSelectedFieldset(item['af-fieldset'])}" entity-name="item['af-fieldset']" data-entity="{{ item['af-fieldset'] }}" ></af-gui-container>
index 17a4f7101acf7537145d415c875e9d27a39fbbf4..892731c3634061e41f1617f92176021ca7f5cea9 100644 (file)
@@ -27,4 +27,7 @@
   <civix>
     <namespace>CRM/AfformAdmin</namespace>
   </civix>
+  <classloader>
+    <psr4 prefix="Civi\" path="Civi" />
+  </classloader>
 </extension>