From 490565d08a13dbd2fd2a7887f29ff9c5fe2f187c Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Mon, 25 Jan 2021 10:51:55 -0500 Subject: [PATCH] AfformAdmin - use api-callback for loading metadata 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. --- CRM/Utils/Array.php | 35 ++++ .../AfformAdmin/AfformAdminMeta.php} | 126 ++++++++---- .../Civi/Api4/Action/Afform/LoadAdminData.php | 190 ++++++++++++++++++ ext/afform/admin/afformEntities/Activity.php | 1 - ext/afform/admin/afformEntities/Household.php | 1 + .../admin/afformEntities/Individual.php | 1 + .../admin/afformEntities/Organization.php | 1 + ext/afform/admin/ang/afAdmin.ang.php | 2 +- ext/afform/admin/ang/afAdmin.js | 23 ++- .../ang/afAdmin/afAdminGui.controller.js | 14 +- ext/afform/admin/ang/afGuiEditor.ang.php | 2 +- ext/afform/admin/ang/afGuiEditor.js | 57 ++++-- .../ang/afGuiEditor/afGuiEditor.component.js | 103 +++++----- .../ang/afGuiEditor/afGuiEditorPalette.html | 8 +- .../admin/ang/afGuiEditor/afGuiEntity.html | 4 +- .../elements/afGuiContainer.component.js | 12 ++ .../afGuiEditor/elements/afGuiContainer.html | 5 +- ext/afform/admin/info.xml | 3 + 18 files changed, 462 insertions(+), 126 deletions(-) rename ext/afform/admin/{CRM/AfformAdmin/Utils.php => Civi/AfformAdmin/AfformAdminMeta.php} (51%) create mode 100644 ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php diff --git a/CRM/Utils/Array.php b/CRM/Utils/Array.php index 5ecdb554d5..19df58eda8 100644 --- a/CRM/Utils/Array.php +++ b/CRM/Utils/Array.php @@ -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(). * diff --git a/ext/afform/admin/CRM/AfformAdmin/Utils.php b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php similarity index 51% rename from ext/afform/admin/CRM/AfformAdmin/Utils.php rename to ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php index f9fc4d8975..c0474106d6 100644 --- a/ext/afform/admin/CRM/AfformAdmin/Utils.php +++ b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php @@ -1,7 +1,10 @@ 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 index 0000000000..350ea8252a --- /dev/null +++ b/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php @@ -0,0 +1,190 @@ + [], '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; + } + +} diff --git a/ext/afform/admin/afformEntities/Activity.php b/ext/afform/admin/afformEntities/Activity.php index 32c3b8f02b..ae8a651eba 100644 --- a/ext/afform/admin/afformEntities/Activity.php +++ b/ext/afform/admin/afformEntities/Activity.php @@ -1,7 +1,6 @@ 'Activity', - 'label' => ts('Activity'), 'defaults' => "{'url-autofill': '1'}", 'boilerplate' => [ ['#tag' => 'af-field', 'name' => 'subject'], diff --git a/ext/afform/admin/afformEntities/Household.php b/ext/afform/admin/afformEntities/Household.php index abe3253c5f..00173428d7 100644 --- a/ext/afform/admin/afformEntities/Household.php +++ b/ext/afform/admin/afformEntities/Household.php @@ -9,6 +9,7 @@ return [ }, 'url-autofill': '1' }", + 'icon' => 'fa-home', 'boilerplate' => [ ['#tag' => 'afblock-name-household'], ], diff --git a/ext/afform/admin/afformEntities/Individual.php b/ext/afform/admin/afformEntities/Individual.php index 3d651fc35c..b6c972754b 100644 --- a/ext/afform/admin/afformEntities/Individual.php +++ b/ext/afform/admin/afformEntities/Individual.php @@ -9,6 +9,7 @@ return [ }, 'url-autofill': '1' }", + 'icon' => 'fa-user', 'boilerplate' => [ ['#tag' => 'afblock-name-individual'], ], diff --git a/ext/afform/admin/afformEntities/Organization.php b/ext/afform/admin/afformEntities/Organization.php index 99f0ebbb0f..aba69f9f44 100644 --- a/ext/afform/admin/afformEntities/Organization.php +++ b/ext/afform/admin/afformEntities/Organization.php @@ -9,6 +9,7 @@ return [ }, 'url-autofill': '1' }", + 'icon' => 'fa-building', 'boilerplate' => [ ['#tag' => 'afblock-name-organization'], ], diff --git a/ext/afform/admin/ang/afAdmin.ang.php b/ext/afform/admin/ang/afAdmin.ang.php index 818a6f4ca0..1944d717b0 100644 --- a/ext/afform/admin/ang/afAdmin.ang.php +++ b/ext/afform/admin/ang/afAdmin.ang.php @@ -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'], ]; diff --git a/ext/afform/admin/ang/afAdmin.js b/ext/afform/admin/ang/afAdmin.js index 1284b00bb7..4d8a856700 100644 --- a/ext/afform/admin/ang/afAdmin.js +++ b/ext/afform/admin/ang/afAdmin.js @@ -17,13 +17,30 @@ } } }); - $routeProvider.when('/create/:type', { + $routeProvider.when('/create/:type/:entity', { controller: 'afAdminGui', - template: '', + template: '', + 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: '', + template: '', + resolve: { + // Load data for gui editor + data: function($route, crmApi4) { + return crmApi4('Afform', 'loadAdminData', { + definition: {name: $route.current.params.name} + }, 0); + } + } }); }); diff --git a/ext/afform/admin/ang/afAdmin/afAdminGui.controller.js b/ext/afform/admin/ang/afAdmin/afAdminGui.controller.js index a7713e31b8..457af5926b 100644 --- a/ext/afform/admin/ang/afAdmin/afAdminGui.controller.js +++ b/ext/afform/admin/ang/afAdmin/afAdminGui.controller.js @@ -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._); diff --git a/ext/afform/admin/ang/afGuiEditor.ang.php b/ext/afform/admin/ang/afGuiEditor.ang.php index 4cca733066..c792ee0d64 100644 --- a/ext/afform/admin/ang/afGuiEditor.ang.php +++ b/ext/afform/admin/ang/afGuiEditor.ang.php @@ -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', diff --git a/ext/afform/admin/ang/afGuiEditor.js b/ext/afform/admin/ang/afGuiEditor.js index 2acf4f7322..5b1253fc92 100644 --- a/ext/afform/admin/ang/afGuiEditor.js +++ b/ext/afform/admin/ang/afGuiEditor.js @@ -1,5 +1,6 @@ (function(angular, $, _) { "use strict"; + angular.module('afGuiEditor', CRM.angRequires('afGuiEditor')) .service('afGui', function(crmApi4, $parse, $q) { @@ -65,23 +66,51 @@ } 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, diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js index d2a1f4889a..68fa65a9c6 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js @@ -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) { @@ -16,44 +17,32 @@ $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 @@ -81,27 +70,46 @@ '#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) { @@ -145,18 +153,15 @@ } }; - $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); + } }); }; diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html b/ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html index 060123c104..9307e904c7 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html @@ -7,7 +7,8 @@
  • - {{ entity.label }} + {{ entity.label }} +
  • diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEntity.html b/ext/afform/admin/ang/afGuiEditor/afGuiEntity.html index f7c8e82357..f55fc71a8f 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEntity.html +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEntity.html @@ -1,4 +1,4 @@ -
    +
    {{:: ts('Values:') }}
    @@ -54,7 +54,7 @@ -
    +
    {{:: ts('Options') }}
    diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js index 7cf73a63f1..0953d61916 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js @@ -17,6 +17,18 @@ 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(); } }; diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html index 5fab5d7258..a1ae54be3e 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html @@ -1,5 +1,5 @@
    -
    +
    {{ $ctrl.editor.getEntity($ctrl.entityName).label }} {{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }} {{ tags[$ctrl.node['#tag']].toLowerCase() }} @@ -13,8 +13,9 @@
    +
    -
    +
    diff --git a/ext/afform/admin/info.xml b/ext/afform/admin/info.xml index 17a4f7101a..892731c363 100644 --- a/ext/afform/admin/info.xml +++ b/ext/afform/admin/info.xml @@ -27,4 +27,7 @@ CRM/AfformAdmin + + + -- 2.25.1