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.
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().
*
<?php
+
+namespace Civi\AfformAdmin;
+
use CRM_AfformAdmin_ExtensionUtil as E;
-class CRM_AfformAdmin_Utils {
+class AfformAdminMeta {
/**
* @return array
}
/**
- * 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'],
'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' => [
];
$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],
--- /dev/null
+<?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;
+ }
+
+}
<?php
return [
'entity' => 'Activity',
- 'label' => ts('Activity'),
'defaults' => "{'url-autofill': '1'}",
'boilerplate' => [
['#tag' => 'af-field', 'name' => 'subject'],
},
'url-autofill': '1'
}",
+ 'icon' => 'fa-home',
'boilerplate' => [
['#tag' => 'afblock-name-household'],
],
},
'url-autofill': '1'
}",
+ 'icon' => 'fa-user',
'boilerplate' => [
['#tag' => 'afblock-name-individual'],
],
},
'url-autofill': '1'
}",
+ 'icon' => 'fa-building',
'boilerplate' => [
['#tag' => 'afblock-name-organization'],
],
'css' => [],
'partials' => ['ang/afAdmin'],
'requires' => ['api4', 'afGuiEditor', 'crmRouteBinder'],
- 'settingsFactory' => ['CRM_AfformAdmin_Utils', 'getAdminSettings'],
+ 'settingsFactory' => ['Civi\AfformAdmin\AfformAdminMeta', 'getAdminSettings'],
'basePages' => ['civicrm/admin/afform'],
'bundles' => ['bootstrap3'],
];
}
}
});
- $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);
+ }
+ }
});
});
(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._);
'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',
(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,
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);
});
};
- 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);
+ }
});
};
</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>
-<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">
<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>
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();
}
};
<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>
</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>
<civix>
<namespace>CRM/AfformAdmin</namespace>
</civix>
+ <classloader>
+ <psr4 prefix="Civi\" path="Civi" />
+ </classloader>
</extension>