From 344e8290127f78eb32c4b66322bbca24bffb7e4a Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Fri, 27 Dec 2019 14:55:49 -0500 Subject: [PATCH] Refactor af-block directive into af-repeat and af-join This allows mix-n-match between blocks and repeatable things, allowing entity fieldsets to be repeated, as well as arbitrary non-repeatable blocks. --- ext/afform/core/CRM/Afform/ArrayHtml.php | 2 +- ext/afform/core/Civi/Afform/FormDataModel.php | 67 ++-- .../Api4/Action/Afform/AbstractProcessor.php | 29 +- .../core/Civi/Api4/Action/Afform/Get.php | 4 +- .../core/Civi/Api4/Action/Afform/Prefill.php | 17 +- .../core/Civi/Api4/Action/Afform/Submit.php | 83 ++-- ext/afform/core/Civi/Api4/Afform.php | 6 +- ext/afform/core/afform.php | 8 +- ext/afform/core/ang/af.ang.php | 5 +- ext/afform/core/ang/af/Block.js | 63 --- ext/afform/core/ang/af/Field.js | 11 +- ext/afform/core/ang/af/Fieldset.js | 27 +- ext/afform/core/ang/af/Form.js | 2 +- ext/afform/core/ang/af/Join.js | 47 +++ ext/afform/core/ang/af/Repeat.js | 70 ++++ ext/afform/core/ang/af/afBlock.html | 5 - ext/afform/core/ang/af/afRepeat.html | 5 + ext/afform/core/ang/af/fields/CheckBox.html | 4 +- ext/afform/core/ang/af/fields/Date.html | 2 +- ext/afform/core/ang/af/fields/Number.html | 2 +- ext/afform/core/ang/af/fields/Radio.html | 2 +- .../core/ang/af/fields/RichTextEditor.html | 2 +- ext/afform/core/ang/af/fields/Select.html | 2 +- ext/afform/core/ang/af/fields/Text.html | 2 +- ext/afform/core/ang/af/fields/TextArea.html | 2 +- ext/afform/core/ang/afCore.css | 4 +- .../core/ang/blockEmailDefault.aff.json | 6 +- ext/afform/core/ang/blockIMDefault.aff.json | 6 +- .../core/ang/blockNameIndividual.aff.html | 5 + .../core/ang/blockNameIndividual.aff.json | 4 + .../core/ang/blockPhoneDefault.aff.json | 6 +- .../core/ang/blockWebsiteDefault.aff.json | 6 +- .../phpunit/Civi/Afform/FormDataModelTest.php | 8 +- ext/afform/gui/afform_gui.php | 8 +- ext/afform/gui/ang/afGuiEditor.js | 370 ++++++++++-------- ext/afform/gui/ang/afGuiEditor/canvas.html | 2 +- .../gui/ang/afGuiEditor/container-menu.html | 10 +- ext/afform/gui/ang/afGuiEditor/container.html | 12 +- ext/afform/gui/ang/afGuiEditor/entity.html | 4 +- .../tests/phpunit/api/v4/AfformUsageTest.php | 8 +- 40 files changed, 544 insertions(+), 384 deletions(-) delete mode 100644 ext/afform/core/ang/af/Block.js create mode 100644 ext/afform/core/ang/af/Join.js create mode 100644 ext/afform/core/ang/af/Repeat.js delete mode 100644 ext/afform/core/ang/af/afBlock.html create mode 100644 ext/afform/core/ang/af/afRepeat.html create mode 100644 ext/afform/core/ang/blockNameIndividual.aff.html create mode 100644 ext/afform/core/ang/blockNameIndividual.aff.json diff --git a/ext/afform/core/CRM/Afform/ArrayHtml.php b/ext/afform/core/CRM/Afform/ArrayHtml.php index 8034429068..84a4ae15d4 100644 --- a/ext/afform/core/CRM/Afform/ArrayHtml.php +++ b/ext/afform/core/CRM/Afform/ArrayHtml.php @@ -379,7 +379,7 @@ class CRM_Afform_ArrayHtml { * @return bool */ public function isNodeEditable(array $item) { - if ($item['#tag'] === 'af-field' || $item['#tag'] === 'af-form' || isset($item['af-fieldset']) || isset($item['af-block'])) { + if ($item['#tag'] === 'af-field' || $item['#tag'] === 'af-form' || isset($item['af-fieldset']) || isset($item['af-join'])) { return TRUE; } $editableClasses = ['af-container', 'af-text', 'af-button']; diff --git a/ext/afform/core/Civi/Afform/FormDataModel.php b/ext/afform/core/Civi/Afform/FormDataModel.php index 3a9a811c89..b256584aa0 100644 --- a/ext/afform/core/Civi/Afform/FormDataModel.php +++ b/ext/afform/core/Civi/Afform/FormDataModel.php @@ -2,12 +2,13 @@ namespace Civi\Afform; +use Civi\Api4\Afform; + /** * Class FormDataModel * @package Civi\Afform * - * The FormDataModel examines a form and determines the list of entities/fields - * which are used by the form. + * Examines a form and determines the entities, fields & joins in use. */ class FormDataModel { @@ -18,50 +19,58 @@ class FormDataModel { protected $entities; /** - * Gets entity metadata and all blocks & fields from the form - * - * @param array $layout - * The root element of the layout, in shallow/deep format. - * @return static - * Parsed summary of the entities used in a given form. + * @var array */ - public static function create($layout) { + protected $blocks = []; + + public function __construct($layout) { $root = AHQ::makeRoot($layout); - $entities = array_column(AHQ::getTags($root, 'af-entity'), NULL, 'name'); - foreach (array_keys($entities) as $entity) { - $entities[$entity]['fields'] = $entities[$entity]['blocks'] = []; + $this->entities = array_column(AHQ::getTags($root, 'af-entity'), NULL, 'name'); + foreach (Afform::get()->setCheckPermissions(FALSE)->addSelect('name')->execute() as $block) { + $this->blocks[_afform_angular_module_name($block['name'], 'dash')] = $block; } - self::parseFields($layout, $entities); - - $self = new static(); - $self->entities = $entities; - return $self; + foreach (array_keys($this->entities) as $entity) { + $this->entities[$entity]['fields'] = $this->entities[$entity]['joins'] = []; + } + $this->parseFields($layout); } /** * @param array $nodes - * @param array $entities - * A list of entities, keyed by name. - * This will be updated to populate 'fields' and 'blocks'. - * Ex: $entities['spouse']['type'] = 'Contact'; * @param string $entity + * @param string $join */ - protected static function parseFields($nodes, &$entities, $entity = NULL) { + protected function parseFields($nodes, $entity = NULL, $join = NULL) { foreach ($nodes as $node) { if (!is_array($node) || !isset($node['#tag'])) { - //nothing + continue; } - elseif (!empty($node['af-fieldset'])) { - self::parseFields($node['#children'], $entities, $node['af-fieldset']); + elseif (!empty($node['af-fieldset']) && !empty($node['#children'])) { + $this->parseFields($node['#children'], $node['af-fieldset'], $join); } elseif ($entity && $node['#tag'] === 'af-field') { - $entities[$entity]['fields'][$node['name']] = AHQ::getProps($node); + if ($join) { + $this->entities[$entity]['joins'][$join]['fields'][$node['name']] = AHQ::getProps($node); + } + else { + $this->entities[$entity]['fields'][$node['name']] = AHQ::getProps($node); + } } - elseif ($entity && !empty($node['af-block'])) { - $entities[$entity]['blocks'][$node['af-block']] = AHQ::getProps($node); + elseif ($entity && !empty($node['af-join'])) { + $this->entities[$entity]['joins'][$node['af-join']] = AHQ::getProps($node); + $this->parseFields($node['#children'] ?? [], $entity, $node['af-join']); } elseif (!empty($node['#children'])) { - self::parseFields($node['#children'], $entities, $entity); + $this->parseFields($node['#children'], $entity, $join); + } + // Recurse into embedded blocks + if (isset($this->blocks[$node['#tag']])) { + if (!isset($this->blocks[$node['#tag']]['layout'])) { + $this->blocks[$node['#tag']] = Afform::get()->setCheckPermissions(FALSE)->setSelect(['name', 'layout'])->addWhere('name', '=', $this->blocks[$node['#tag']]['name'])->execute()->first(); + } + if (!empty($this->blocks[$node['#tag']]['layout'])) { + $this->parseFields($this->blocks[$node['#tag']]['layout'], $entity, $join); + } } } } diff --git a/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php b/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php index abd8805806..416bde2d90 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php @@ -45,7 +45,7 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction { } } - $this->_formDataModel = FormDataModel::create($this->_afform['layout']); + $this->_formDataModel = new FormDataModel($this->_afform['layout']); $this->validateArgs(); $result->exchangeArray($this->processForm()); } @@ -69,4 +69,31 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction { */ abstract protected function processForm(); + /** + * @param $mainEntityName + * @param $joinEntityName + * @param $mainEntityId + * @return array + * @throws \API_Exception + */ + protected static function getJoinWhereClause($mainEntityName, $joinEntityName, $mainEntityId) { + $joinMeta = \Civi::$statics[__CLASS__][__FUNCTION__][$joinEntityName] ?? NULL; + $params = []; + if (!$joinMeta) { + $joinMeta = civicrm_api4($joinEntityName, 'getFields', ['checkPermissions' => FALSE, 'action' => 'create', 'select' => ['name']])->column('name'); + \Civi::$statics[__CLASS__][__FUNCTION__][$joinEntityName] = $joinMeta; + } + if (in_array('entity_id', $joinMeta)) { + $params[] = ['entity_id', '=', $mainEntityId]; + if (in_array('entity_table', $joinMeta)) { + $params[] = ['entity_table', '=', 'civicrm_' . _civicrm_api_get_entity_name_from_camel($mainEntityName)]; + } + } + else { + $mainEntityField = _civicrm_api_get_entity_name_from_camel($mainEntityName) . '_id'; + $params[] = [$mainEntityField, '=', $mainEntityId]; + } + return $params; + } + } diff --git a/ext/afform/core/Civi/Api4/Action/Afform/Get.php b/ext/afform/core/Civi/Api4/Action/Afform/Get.php index 65b56ba5f1..0f2dda46bd 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/Get.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/Get.php @@ -96,9 +96,9 @@ class Get extends \Civi\Api4\Generic\BasicGetAction { 'description' => '', 'is_public' => FALSE, 'permission' => 'access CiviCRM', - 'block' => 'Custom_' . $custom['name'], + 'join' => 'Custom_' . $custom['name'], 'extends' => 'Contact', - 'repeatable' => TRUE, + 'repeat' => TRUE, 'has_base' => TRUE, ]; if ($getLayout) { diff --git a/ext/afform/core/Civi/Api4/Action/Afform/Prefill.php b/ext/afform/core/Civi/Api4/Action/Afform/Prefill.php index 5332066dca..dd9ba99807 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/Prefill.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/Prefill.php @@ -49,17 +49,17 @@ class Prefill extends AbstractProcessor { 'select' => array_keys($entity['fields']), 'checkPermissions' => $checkPermissions, ]); - $data = $result->first(); - if ($data) { - $data['blocks'] = []; - foreach ($entity['blocks'] ?? [] as $blockEntity => $block) { - $data['blocks'][$blockEntity] = (array) civicrm_api4($blockEntity, 'get', [ - 'where' => [['contact_id', '=', $data['id']]], - 'limit' => $block['max'] ?? 0, + foreach ($result as $item) { + $data = ['fields' => $item]; + foreach ($entity['joins'] ?? [] as $joinEntity => $join) { + $data['joins'][$joinEntity] = (array) civicrm_api4($joinEntity, 'get', [ + 'where' => $this->getJoinWhereClause($entity['type'], $joinEntity, $item['id']), + 'limit' => !empty($join['af-repeat']) ? $join['max'] ?? 0 : 1, + 'select' => array_keys($join['fields']), 'checkPermissions' => $checkPermissions, ]); } - $this->_data[$entity['name']] = $data; + $this->_data[$entity['name']][] = $data; } } @@ -68,6 +68,7 @@ class Prefill extends AbstractProcessor { * * @param $entity * @param $mode + * @throws \API_Exception */ private function autoFillEntity($entity, $mode) { $id = NULL; diff --git a/ext/afform/core/Civi/Api4/Action/Afform/Submit.php b/ext/afform/core/Civi/Api4/Action/Afform/Submit.php index 2f563cfe1c..54a36f49d1 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/Submit.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/Submit.php @@ -3,7 +3,6 @@ namespace Civi\Api4\Action\Afform; use Civi\Afform\Event\AfformSubmitEvent; -use Civi\API\Exception\NotImplementedException; /** * Class Submit @@ -23,8 +22,15 @@ class Submit extends AbstractProcessor { protected function processForm() { $entityValues = []; foreach ($this->_formDataModel->getEntities() as $entityName => $entity) { - // Predetermined values override submitted values - $entityValues[$entity['type']][$entityName] = ($entity['af-values'] ?? []) + ($this->values[$entityName] ?? []); + foreach ($this->values[$entityName] ?? [] as $values) { + $entityValues[$entity['type']][$entityName][] = $values + ['fields' => []]; + // Predetermined values override submitted values + if (!empty($entity['af-values'])) { + foreach ($entityValues[$entity['type']][$entityName] as $index => $vals) { + $entityValues[$entity['type']][$entityName][$index]['fields'] = $entity['af-values'] + $vals['fields']; + } + } + } } $event = new AfformSubmitEvent($this->_formDataModel->getEntities(), $entityValues); @@ -44,29 +50,11 @@ class Submit extends AbstractProcessor { * @throws \API_Exception * @see afform_civicrm_config */ - public function processContacts(AfformSubmitEvent $event) { - foreach ($event->entityValues['Contact'] ?? [] as $entityName => $contact) { - $blocks = $contact['blocks'] ?? []; - unset($contact['blocks']); - $saved = civicrm_api4('Contact', 'save', ['records' => [$contact]])->first(); - foreach ($blocks as $entity => $block) { - $values = self::filterEmptyBlocks($entity, $block); - // FIXME: Replace/delete should only be done to known contacts - if ($values) { - civicrm_api4($entity, 'replace', [ - 'where' => [['contact_id', '=', $saved['id']]], - 'records' => $values, - ]); - } - else { - try { - civicrm_api4($entity, 'delete', [ - 'where' => [['contact_id', '=', $saved['id']]], - ]); - } catch (\API_Exception $e) { - // No records to delete - } - } + public static function processContacts(AfformSubmitEvent $event) { + foreach ($event->entityValues['Contact'] ?? [] as $entityName => $contacts) { + foreach ($contacts as $contact) { + $saved = civicrm_api4('Contact', 'save', ['records' => [$contact['fields']]])->first(); + self::saveJoins('Contact', $saved['id'], $contact['joins'] ?? []); } } unset($event->entityValues['Contact']); @@ -78,23 +66,50 @@ class Submit extends AbstractProcessor { * @see afform_civicrm_config */ public static function processGenericEntity(AfformSubmitEvent $event) { - foreach ($event->entityValues as $entityType => $records) { - civicrm_api4($entityType, 'save', [ - 'records' => $records, - ]); + foreach ($event->entityValues as $entityType => $entities) { + // Each record is an array of one or more items (can be > 1 if af-repeat is used) + foreach ($entities as $entityName => $records) { + foreach ($records as $record) { + $saved = civicrm_api4($entityType, 'save', ['records' => [$record['fields']]])->first(); + self::saveJoins($entityType, $saved['id'], $record['joins'] ?? []); + } + } unset($event->entityValues[$entityType]); } } + protected static function saveJoins($mainEntityName, $entityId, $joins) { + foreach ($joins as $joinEntityName => $join) { + $values = self::filterEmptyJoins($joinEntityName, $join); + // FIXME: Replace/delete should only be done to known contacts + if ($values) { + civicrm_api4($joinEntityName, 'replace', [ + 'where' => self::getJoinWhereClause($mainEntityName, $joinEntityName, $entityId), + 'records' => $values, + ]); + } + else { + try { + civicrm_api4($joinEntityName, 'delete', [ + 'where' => self::getJoinWhereClause($mainEntityName, $joinEntityName, $entityId), + ]); + } + catch (\API_Exception $e) { + // No records to delete + } + } + } + } + /** - * Filter out blocks that have been left blank on the form + * Filter out joins that have been left blank on the form * * @param $entity - * @param $block + * @param $join * @return array */ - private static function filterEmptyBlocks($entity, $block) { - return array_filter($block, function($item) use($entity) { + private static function filterEmptyJoins($entity, $join) { + return array_filter($join, function($item) use($entity) { switch ($entity) { case 'Email': return !empty($item['email']); diff --git a/ext/afform/core/Civi/Api4/Afform.php b/ext/afform/core/Civi/Api4/Afform.php index 7dd1522f06..f289477b8d 100644 --- a/ext/afform/core/Civi/Api4/Afform.php +++ b/ext/afform/core/Civi/Api4/Afform.php @@ -97,7 +97,7 @@ class Afform extends AbstractEntity { 'name' => 'block', ], [ - 'name' => 'extends', + 'name' => 'join', ], [ 'name' => 'title', @@ -111,8 +111,8 @@ class Afform extends AbstractEntity { 'data_type' => 'Boolean', ], [ - 'name' => 'repeatable', - 'data_type' => 'Boolean', + 'name' => 'repeat', + 'data_type' => 'Mixed', ], [ 'name' => 'server_route', diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php index 3a4f8ae871..8502faeb12 100644 --- a/ext/afform/core/afform.php +++ b/ext/afform/core/afform.php @@ -322,12 +322,12 @@ function afform_civicrm_alterAngular($angular) { ->alterHtml(';\\.aff\\.html$;', function($doc, $path) { try { $module = \Civi::service('angular')->getModule(basename($path, '.aff.html')); - $meta = \Civi\Api4\Afform::get()->addWhere('name', '=', $module['_afform'])->addSelect('block')->setCheckPermissions(FALSE)->execute()->first(); + $meta = \Civi\Api4\Afform::get()->addWhere('name', '=', $module['_afform'])->setSelect(['join', 'block'])->setCheckPermissions(FALSE)->execute()->first(); } catch (Exception $e) { } - $blockEntity = $meta['block'] ?? NULL; + $blockEntity = $meta['join'] ?? $meta['block'] ?? NULL; if (!$blockEntity) { $entities = _afform_getMetadata($doc); } @@ -335,12 +335,12 @@ function afform_civicrm_alterAngular($angular) { foreach (pq('af-field', $doc) as $afField) { /** @var DOMElement $afField */ $entityName = pq($afField)->parents('[af-fieldset]')->attr('af-fieldset'); - $blockName = pq($afField)->parents('[af-block]')->attr('af-block'); + $joinName = pq($afField)->parents('[af-join]')->attr('af-join'); if (!$blockEntity && !preg_match(';^[a-zA-Z0-9\_\-\. ]+$;', $entityName)) { throw new \CRM_Core_Exception("Cannot process $path: malformed entity name ($entityName)"); } $entityType = $blockEntity ?? $entities[$entityName]['type']; - _af_fill_field_metadata($blockName ? $blockName : $entityType, $afField); + _af_fill_field_metadata($joinName ? $joinName : $entityType, $afField); } }); $angular->add($fieldMetadata); diff --git a/ext/afform/core/ang/af.ang.php b/ext/afform/core/ang/af.ang.php index 1949657794..d927eb8a34 100644 --- a/ext/afform/core/ang/af.ang.php +++ b/ext/afform/core/ang/af.ang.php @@ -18,8 +18,9 @@ return [ 'af-entity' => 'E', 'af-fieldset' => 'A', 'af-form' => 'E', - 'af-block' => 'A', - 'af-block-item' => 'A', + 'af-join' => 'A', + 'af-repeat' => 'A', + 'af-repeat-item' => 'A', 'af-field' => 'E', ], ]; diff --git a/ext/afform/core/ang/af/Block.js b/ext/afform/core/ang/af/Block.js deleted file mode 100644 index 68e9a7b4a5..0000000000 --- a/ext/afform/core/ang/af/Block.js +++ /dev/null @@ -1,63 +0,0 @@ -(function(angular, $, _) { - // Example usage:
- angular.module('af') - .directive('afBlock', function() { - return { - restrict: 'A', - require: ['^^afFieldset'], - scope: { - blockName: '@afBlock', - min: '=', - max: '=', - addLabel: '@', - addIcon: '@' - }, - transclude: true, - templateUrl: '~/af/afBlock.html', - link: function($scope, $el, $attr, ctrls) { - var ts = $scope.ts = CRM.ts('afform'); - $scope.afFieldset = ctrls[0]; - }, - controller: function($scope) { - this.getItems = $scope.getItems = function() { - var data = $scope.afFieldset.getData(); - data.blocks = data.blocks || {}; - var block = (data.blocks[$scope.blockName] = data.blocks[$scope.blockName] || []); - while ($scope.min && block.length < $scope.min) { - block.push({}); - } - return block; - }; - - $scope.addItem = function() { - $scope.getItems().push({}); - }; - - $scope.removeItem = function(index) { - $scope.getItems().splice(index, 1); - }; - - $scope.canAdd = function() { - return !$scope.max || $scope.getItems().length < $scope.max; - }; - - $scope.canRemove = function() { - return !$scope.min || $scope.getItems().length > $scope.min; - }; - } - }; - }) - .directive('afBlockItem', function() { - return { - restrict: 'A', - scope: { - item: '=afBlockItem' - }, - controller: function($scope) { - this.getData = function() { - return $scope.item; - }; - } - }; - }); -})(angular, CRM.$, CRM._); diff --git a/ext/afform/core/ang/af/Field.js b/ext/afform/core/ang/af/Field.js index e55b13c8a4..5bf7c9b8db 100644 --- a/ext/afform/core/ang/af/Field.js +++ b/ext/afform/core/ang/af/Field.js @@ -1,9 +1,10 @@ (function(angular, $, _) { + var id = 0; // Example usage:
angular.module('af').directive('afField', function() { return { restrict: 'E', - require: ['^^afFieldset', '?^afBlockItem'], + require: ['^^afForm', '^^afFieldset', '?^^afJoin', '?^^afRepeatItem'], templateUrl: '~/af/afField.html', scope: { fieldName: '@name', @@ -11,10 +12,10 @@ }, link: function($scope, $el, $attr, ctrls) { var ts = $scope.ts = CRM.ts('afform'), - afFieldset = ctrls[0], - blockItem = ctrls[1]; - $scope.fieldId = afFieldset.getDefn().modelName + '-' + $scope.fieldName; - $scope.getData = blockItem ? blockItem.getData : afFieldset.getData; + closestController = $($el).closest('[af-fieldset],[af-join],[af-repeat-item]'), + afForm = ctrls[0]; + $scope.dataProvider = closestController.is('[af-repeat-item]') ? ctrls[3] : ctrls[2] || ctrls[1]; + $scope.fieldId = afForm.getFormMeta().name + '-' + $scope.fieldName + '-' + id++; $el.addClass('af-field-type-' + _.kebabCase($scope.defn.input_type)); diff --git a/ext/afform/core/ang/af/Fieldset.js b/ext/afform/core/ang/af/Fieldset.js index 69dde11b15..a62483cc85 100644 --- a/ext/afform/core/ang/af/Fieldset.js +++ b/ext/afform/core/ang/af/Fieldset.js @@ -3,23 +3,30 @@ angular.module('af').directive('afFieldset', function() { return { restrict: 'A', - require: '^afForm', - scope: { + require: ['afFieldset', '^afForm'], + bindToController: { modelName: '@afFieldset' }, - link: function($scope, $el, $attr, afFormCtrl) { - $scope.afFormCtrl = afFormCtrl; + link: function($scope, $el, $attr, ctrls) { + var self = ctrls[0]; + self.afFormCtrl = ctrls[1]; }, controller: function($scope){ - this.getDefn = function getDefn() { - return $scope.afFormCtrl.getEntity($scope.modelName); - // return $scope.modelDefn; + this.getDefn = function() { + return this.afFormCtrl.getEntity(this.modelName); }; - this.getData = function getData() { - return $scope.afFormCtrl.getData($scope.modelName); + this.getData = function() { + return this.afFormCtrl.getData(this.modelName); }; this.getName = function() { - return $scope.modelName; + return this.modelName; + }; + this.getFieldData = function() { + var data = this.getData(); + if (!data.length) { + data.push({fields: {}}); + } + return data[0].fields; }; } }; diff --git a/ext/afform/core/ang/af/Form.js b/ext/afform/core/ang/af/Form.js index fcf699df03..06f3b9d183 100644 --- a/ext/afform/core/ang/af/Form.js +++ b/ext/afform/core/ang/af/Form.js @@ -20,7 +20,7 @@ this.registerEntity = function registerEntity(entity) { schema[entity.modelName] = entity; - data[entity.modelName] = entity.data || {}; + data[entity.modelName] = []; }; this.getEntity = function getEntity(name) { return schema[name]; diff --git a/ext/afform/core/ang/af/Join.js b/ext/afform/core/ang/af/Join.js new file mode 100644 index 0000000000..35d6b350ed --- /dev/null +++ b/ext/afform/core/ang/af/Join.js @@ -0,0 +1,47 @@ +(function(angular, $, _) { + // Example usage:
+ angular.module('af') + .directive('afJoin', function() { + return { + restrict: 'A', + require: ['afJoin', '^^afFieldset', '?^^afRepeatItem'], + bindToController: { + entity: '@afJoin', + }, + link: function($scope, $el, $attr, ctrls) { + var self = ctrls[0]; + self.afFieldset = ctrls[1]; + self.repeatItem = ctrls[2]; + }, + controller: function($scope) { + var self = this; + this.getData = function() { + var data, fieldsetData; + if (self.repeatItem) { + data = self.repeatItem.item; + } else { + fieldsetData = self.afFieldset.getData(); + if (!fieldsetData.length) { + fieldsetData.push({fields: {}, joins: {}}); + } + data = fieldsetData[0]; + } + if (!data.joins) { + data.joins = {}; + } + if (!data.joins[self.entity]) { + data.joins[self.entity] = []; + } + return data.joins[self.entity]; + }; + this.getFieldData = function() { + var data = this.getData(); + if (!data.length) { + data.push({}); + } + return data[0]; + }; + } + }; + }); +})(angular, CRM.$, CRM._); diff --git a/ext/afform/core/ang/af/Repeat.js b/ext/afform/core/ang/af/Repeat.js new file mode 100644 index 0000000000..52337c9276 --- /dev/null +++ b/ext/afform/core/ang/af/Repeat.js @@ -0,0 +1,70 @@ +(function(angular, $, _) { + // Example usage:
+ angular.module('af') + .directive('afRepeat', function() { + return { + restrict: 'A', + require: ['?afFieldset', '?afJoin'], + transclude: true, + scope: { + min: '=', + max: '=', + addLabel: '@afRepeat', + addIcon: '@' + }, + templateUrl: '~/af/afRepeat.html', + link: function($scope, $el, $attr, ctrls) { + $scope.afFieldset = ctrls[0]; + $scope.afJoin = ctrls[1]; + }, + controller: function($scope) { + this.getItems = $scope.getItems = function() { + var data = $scope.afJoin ? $scope.afJoin.getData() : $scope.afFieldset.getData(); + while ($scope.min && data.length < $scope.min) { + data.push(getRepeatType() === 'join' ? {} : {fields: {}, joins: {}}); + } + return data; + }; + + function getRepeatType() { + return $scope.afJoin ? 'join' : 'fieldset'; + } + this.getRepeatType = getRepeatType; + + $scope.addItem = function() { + $scope.getItems().push(getRepeatType() === 'join' ? {} : {fields: {}}); + }; + + $scope.removeItem = function(index) { + $scope.getItems().splice(index, 1); + }; + + $scope.canAdd = function() { + return !$scope.max || $scope.getItems().length < $scope.max; + }; + + $scope.canRemove = function() { + return !$scope.min || $scope.getItems().length > $scope.min; + }; + } + }; + }) + .directive('afRepeatItem', function() { + return { + restrict: 'A', + require: ['afRepeatItem', '^^afRepeat'], + bindToController: { + item: '=afRepeatItem' + }, + link: function($scope, $el, $attr, ctrls) { + var self = ctrls[0]; + self.afRepeat = ctrls[1]; + }, + controller: function($scope) { + this.getFieldData = function() { + return this.afRepeat.getRepeatType() === 'join' ? this.item : this.item.fields; + }; + } + }; + }); +})(angular, CRM.$, CRM._); diff --git a/ext/afform/core/ang/af/afBlock.html b/ext/afform/core/ang/af/afBlock.html deleted file mode 100644 index 463f38348e..0000000000 --- a/ext/afform/core/ang/af/afBlock.html +++ /dev/null @@ -1,5 +0,0 @@ -
- - -
- diff --git a/ext/afform/core/ang/af/afRepeat.html b/ext/afform/core/ang/af/afRepeat.html new file mode 100644 index 0000000000..98346d3fe9 --- /dev/null +++ b/ext/afform/core/ang/af/afRepeat.html @@ -0,0 +1,5 @@ +
+ + +
+ diff --git a/ext/afform/core/ang/af/fields/CheckBox.html b/ext/afform/core/ang/af/fields/CheckBox.html index fbdab6b0b2..be20b9efe5 100644 --- a/ext/afform/core/ang/af/fields/CheckBox.html +++ b/ext/afform/core/ang/af/fields/CheckBox.html @@ -1,7 +1,7 @@
  • - +
- + diff --git a/ext/afform/core/ang/af/fields/Date.html b/ext/afform/core/ang/af/fields/Date.html index 8e46274415..5e7d7c21d7 100644 --- a/ext/afform/core/ang/af/fields/Date.html +++ b/ext/afform/core/ang/af/fields/Date.html @@ -1 +1 @@ - + diff --git a/ext/afform/core/ang/af/fields/Number.html b/ext/afform/core/ang/af/fields/Number.html index 2028c402c5..111549c786 100644 --- a/ext/afform/core/ang/af/fields/Number.html +++ b/ext/afform/core/ang/af/fields/Number.html @@ -1 +1 @@ - + diff --git a/ext/afform/core/ang/af/fields/Radio.html b/ext/afform/core/ang/af/fields/Radio.html index b3e9917b60..f4ee1fae42 100644 --- a/ext/afform/core/ang/af/fields/Radio.html +++ b/ext/afform/core/ang/af/fields/Radio.html @@ -1,4 +1,4 @@ diff --git a/ext/afform/core/ang/af/fields/RichTextEditor.html b/ext/afform/core/ang/af/fields/RichTextEditor.html index cedb722104..e2dbc6e9d1 100644 --- a/ext/afform/core/ang/af/fields/RichTextEditor.html +++ b/ext/afform/core/ang/af/fields/RichTextEditor.html @@ -1 +1 @@ - + diff --git a/ext/afform/core/ang/af/fields/Select.html b/ext/afform/core/ang/af/fields/Select.html index a89e8464ec..a3cb1e2abd 100644 --- a/ext/afform/core/ang/af/fields/Select.html +++ b/ext/afform/core/ang/af/fields/Select.html @@ -1 +1 @@ - + diff --git a/ext/afform/core/ang/af/fields/Text.html b/ext/afform/core/ang/af/fields/Text.html index 619196a073..008b3441c6 100644 --- a/ext/afform/core/ang/af/fields/Text.html +++ b/ext/afform/core/ang/af/fields/Text.html @@ -1 +1 @@ - + diff --git a/ext/afform/core/ang/af/fields/TextArea.html b/ext/afform/core/ang/af/fields/TextArea.html index f19d4c9771..66a2c5a375 100644 --- a/ext/afform/core/ang/af/fields/TextArea.html +++ b/ext/afform/core/ang/af/fields/TextArea.html @@ -1 +1 @@ - + diff --git a/ext/afform/core/ang/afCore.css b/ext/afform/core/ang/afCore.css index 209c8420ab..59070b0df5 100644 --- a/ext/afform/core/ang/afCore.css +++ b/ext/afform/core/ang/afCore.css @@ -18,10 +18,10 @@ a.af-api4-action-idle { vertical-align: bottom; } -[af-block-item] { +[af-repeat-item] { position: relative; } -#bootstrap-theme [af-block-item] .af-block-remove-btn { +#bootstrap-theme [af-repeat-item] .af-repeat-remove-btn { min-width: 30px; position: absolute; top: 0; diff --git a/ext/afform/core/ang/blockEmailDefault.aff.json b/ext/afform/core/ang/blockEmailDefault.aff.json index 12d6107473..107756e1c1 100644 --- a/ext/afform/core/ang/blockEmailDefault.aff.json +++ b/ext/afform/core/ang/blockEmailDefault.aff.json @@ -1,6 +1,6 @@ { "title": "Email Block (default)", - "block": "Email", - "extends": "Contact", - "repeatable": true + "block": "Contact", + "join": "Email", + "repeat": true } diff --git a/ext/afform/core/ang/blockIMDefault.aff.json b/ext/afform/core/ang/blockIMDefault.aff.json index 776f64f285..261df8a830 100644 --- a/ext/afform/core/ang/blockIMDefault.aff.json +++ b/ext/afform/core/ang/blockIMDefault.aff.json @@ -1,6 +1,6 @@ { "title": "IM Block (default)", - "block": "IM", - "extends": "Contact", - "repeatable": true + "block": "Contact", + "join": "IM", + "repeat": true } diff --git a/ext/afform/core/ang/blockNameIndividual.aff.html b/ext/afform/core/ang/blockNameIndividual.aff.html new file mode 100644 index 0000000000..1142cd9d33 --- /dev/null +++ b/ext/afform/core/ang/blockNameIndividual.aff.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/ext/afform/core/ang/blockNameIndividual.aff.json b/ext/afform/core/ang/blockNameIndividual.aff.json new file mode 100644 index 0000000000..9a328cac39 --- /dev/null +++ b/ext/afform/core/ang/blockNameIndividual.aff.json @@ -0,0 +1,4 @@ +{ + "title": "Individual Name (default)", + "block": "Contact" +} diff --git a/ext/afform/core/ang/blockPhoneDefault.aff.json b/ext/afform/core/ang/blockPhoneDefault.aff.json index 00fb70916c..9419dbdb17 100644 --- a/ext/afform/core/ang/blockPhoneDefault.aff.json +++ b/ext/afform/core/ang/blockPhoneDefault.aff.json @@ -1,6 +1,6 @@ { "title": "Phone Block (default)", - "block": "Phone", - "extends": "Contact", - "repeatable": true + "block": "Contact", + "join": "Phone", + "repeat": true } diff --git a/ext/afform/core/ang/blockWebsiteDefault.aff.json b/ext/afform/core/ang/blockWebsiteDefault.aff.json index c6530dcf4e..71b1e68a78 100644 --- a/ext/afform/core/ang/blockWebsiteDefault.aff.json +++ b/ext/afform/core/ang/blockWebsiteDefault.aff.json @@ -1,6 +1,6 @@ { "title": "Website Block (default)", - "block": "Website", - "extends": "Contact", - "repeatable": true + "block": "Contact", + "join": "Website", + "repeat": true } diff --git a/ext/afform/core/tests/phpunit/Civi/Afform/FormDataModelTest.php b/ext/afform/core/tests/phpunit/Civi/Afform/FormDataModelTest.php index 76aafd70b3..43a4a55744 100644 --- a/ext/afform/core/tests/phpunit/Civi/Afform/FormDataModelTest.php +++ b/ext/afform/core/tests/phpunit/Civi/Afform/FormDataModelTest.php @@ -48,7 +48,7 @@ class FormDataModelTest extends \PHPUnit\Framework\TestCase implements HeadlessI 'propA' => ['name' => 'propA'], 'propB' => ['name' => 'propB', 'defn' => ['title' => 'Whiz']], ], - 'blocks' => [], + 'joins' => [], ], ], ]; @@ -60,13 +60,13 @@ class FormDataModelTest extends \PHPUnit\Framework\TestCase implements HeadlessI 'type' => 'Foo', 'name' => 'foobar', 'fields' => [], - 'blocks' => [], + 'joins' => [], ], 'whiz_bang' => [ 'type' => 'Whiz', 'name' => 'whiz_bang', 'fields' => [], - 'blocks' => [], + 'joins' => [], ], ], ]; @@ -81,7 +81,7 @@ class FormDataModelTest extends \PHPUnit\Framework\TestCase implements HeadlessI */ public function testGetEntities($html, $expectEntities) { $parser = new \CRM_Afform_ArrayHtml(); - $fdm = FormDataModel::create($parser->convertHtmlToArray($html)); + $fdm = new FormDataModel($parser->convertHtmlToArray($html)); $this->assertEquals($expectEntities, $fdm->getEntities()); } diff --git a/ext/afform/gui/afform_gui.php b/ext/afform/gui/afform_gui.php index ad7028e743..db98953764 100644 --- a/ext/afform/gui/afform_gui.php +++ b/ext/afform/gui/afform_gui.php @@ -186,12 +186,14 @@ function afform_gui_civicrm_buildAsset($asset, $params, &$mimeType, &$content) { $blockData = \Civi\Api4\Afform::get() ->setCheckPermissions(FALSE) ->addWhere('block', 'IS NOT NULL') - ->setSelect(['name', 'title', 'block', 'extends', 'layout', 'repeatable']) + ->setSelect(['name', 'title', 'block', 'join', 'layout', 'repeat']) ->setFormatWhitespace(TRUE) ->setLayoutFormat('shallow') ->execute(); foreach ($blockData as $block) { - $entityWhitelist[] = $block['block']; + if (!empty($block['join']) && !in_array($block['join'], $entityWhitelist)) { + $entityWhitelist[] = $block['join']; + } $data['blocks'][_afform_angular_module_name($block['name'], 'dash')] = $block; } @@ -246,7 +248,7 @@ function afform_gui_civicrm_buildAsset($asset, $params, &$mimeType, &$content) { 'where' => [['input_type', 'IS NOT NULL']], ]; - // Get fields for main entities + block entities + // Get fields for main entities + joined entities foreach (array_unique($entityWhitelist) as $entityName) { $data['fields'][$entityName] = (array) civicrm_api4($entityName, 'getFields', $getFieldParams, 'name'); diff --git a/ext/afform/gui/ang/afGuiEditor.js b/ext/afform/gui/ang/afGuiEditor.js index 1b5340b11d..2297fcbafa 100644 --- a/ext/afform/gui/ang/afGuiEditor.js +++ b/ext/afform/gui/ang/afGuiEditor.js @@ -240,6 +240,7 @@ $scope.controls = {}; $scope.fieldList = []; $scope.blockList = []; + $scope.blockTitles = []; $scope.elementList = []; $scope.elementTitles = []; @@ -275,17 +276,23 @@ function buildBlockList(search) { $scope.blockList.length = 0; + $scope.blockTitles.length = 0; _.each($scope.editor.meta.blocks, function(block, directive) { - if (!search || _.contains(block.name.toLowerCase(), search) || _.contains(block.title.toLowerCase(), search)) { - $scope.blockList.push({ - "#tag": "div", - "af-block": block.block, - "add-label": ts('Add'), - min: '1', - "#children": [ - {"#tag": directive} - ] - }); + if (!search || _.contains(directive, search) || _.contains(block.name.toLowerCase(), search) || _.contains(block.title.toLowerCase(), search)) { + var item = {"#tag": block.join ? "div" : directive}; + if (block.join) { + item['af-join'] = block.join; + item['#children'] = [{"#tag": directive}]; + } + if (block.repeat) { + item['af-repeat'] = ts('Add'); + item.min = '1'; + if (typeof block.repeat === 'number') { + item.max = '' + block.repeat; + } + } + $scope.blockList.push(item); + $scope.blockTitles.push(block.title); } }); } @@ -323,11 +330,18 @@ return check($scope.editor.scope.layout['#children'], {'#tag': 'af-field', name: fieldName}); }; - $scope.blockInUse = function(blockType) { - return check($scope.editor.scope.layout['#children'], {'af-block': blockType}); + $scope.blockInUse = function(block) { + if (block['af-join']) { + return check($scope.editor.scope.layout['#children'], {'af-join': block['af-join']}); + } + var fieldsInBlock = _.pluck(findRecursive($scope.editor.meta.blocks[block['#tag']].layout, {'#tag': 'af-field'}), 'name'); + return check($scope.editor.scope.layout['#children'], function(item) { + return item['#tag'] === 'af-field' && _.includes(fieldsInBlock, item.name); + }); }; - // Recursively check for a matching object in a multi-level collection + // Check for a matching item for this entity + // Recursively checks the form layout, including block directives function check(group, criteria, found) { if (!found) { found = {}; @@ -341,9 +355,14 @@ return false; } if (_.isPlainObject(item)) { + // Recurse through everything but skip fieldsets for other entities if ((!item['af-fieldset'] || (item['af-fieldset'] === $scope.entity.name)) && item['#children']) { check(item['#children'], criteria, found); } + // Recurse into block directives + else if (item['#tag'] && item['#tag'] in $scope.editor.meta.blocks) { + check($scope.editor.meta.blocks[item['#tag']].layout, criteria, found); + } } }); return found.match; @@ -370,37 +389,14 @@ templateUrl: '~/afGuiEditor/container.html', scope: { node: '=afGuiContainer', - block: '=', + join: '=', entityName: '=' }, require: ['^^afGuiEditor', '?^^afGuiContainer'], link: function($scope, element, attrs, ctrls) { + var ts = $scope.ts = CRM.ts(); $scope.editor = ctrls[0]; $scope.parentContainer = ctrls[1]; - }, - controller: function($scope) { - var container = $scope.container = this; - var ts = $scope.ts = CRM.ts(); - this.node = $scope.node; - - this.getNodeType = function(node) { - if (!node) { - return null; - } - if (node['#tag'] === 'af-field') { - return 'field'; - } - if (node['af-fieldset']) { - return 'fieldset'; - } - if (node['af-block']) { - return 'block'; - } - var classes = splitClass(node['class']), - types = ['af-container', 'af-text', 'af-button', 'af-markup'], - type = _.intersection(types, classes); - return type.length ? type[0].replace('af-', '') : null; - }; $scope.addElement = function(type, props) { var classes = type.split('.'); @@ -421,25 +417,6 @@ } }; - this.removeElement = function(element) { - removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey}); - }; - - this.getEntityName = function() { - return $scope.entityName.split('-block-')[0]; - }; - - // Returns the primary entity type for this container e.g. "Contact" - this.getMainEntityType = function() { - return $scope.editor && $scope.editor.getEntity(container.getEntityName()).type; - }; - - // Returns the entity type for fields within this conainer (block entity type if this is a block, else the primary entity type) - this.getFieldEntityType = function() { - var blockType = $scope.entityName.split('-block-'); - return blockType[1] || ($scope.editor && $scope.editor.getEntity(blockType[0]).type); - }; - $scope.isSelectedFieldset = function(entityName) { return entityName === $scope.editor.getselectedEntityName(); }; @@ -482,145 +459,202 @@ }; // Block settings - var selectedBlock = $scope.selectedBlock = { - name: null, - layout: null, - override: false, - options: [], - }; + var block = {}; + $scope.block = null; $scope.getSetChildren = function(val) { - var collection = selectedBlock.layout || ($scope.node && $scope.node['#children']); + var collection = block.layout || ($scope.node && $scope.node['#children']); return arguments.length ? (collection = val) : collection; }; - function getBlockOptions() { - var blockOptions = []; - _.each($scope.editor.meta.blocks, function(block, id) { - if (block.block === $scope.container.getFieldEntityType()) { - blockOptions.push({ - id: id, - text: block.title + (selectedBlock.name === id && selectedBlock.override ? ' ' + ts('(overridden)') : ''), - }); - } - }); - return blockOptions; - } - - if ($scope.block) { + $scope.isRepeatable = function() { + return $scope.node['af-fieldset'] || (block.directive && $scope.editor.meta.blocks[block.directive].repeat) || $scope.join; + }; - $scope.isRepeatable = function() { - return "fixme"; - }; + $scope.toggleRepeat = function() { + if ('af-repeat' in $scope.node) { + delete $scope.node.max; + delete $scope.node.min; + delete $scope.node['af-repeat']; + delete $scope.node['add-icon']; + } else { + $scope.node.min = '1'; + $scope.node['af-repeat'] = ts('Add'); + } + }; - $scope.toggleRepeat = function() { - if ($scope.node.min === '1' && $scope.node.max === '1') { - delete $scope.node.max; + $scope.getSetMin = function(val) { + if (arguments.length) { + if ($scope.node.max && val > parseInt($scope.node.max, 10)) { + $scope.node.max = '' + val; + } + if (!val) { delete $scope.node.min; - $scope.node['add-label'] = ts('Add'); - } else { - $scope.node.min = $scope.node.max = '1'; - delete $scope.node['add-label']; } - }; - - $scope.getSetMin = function(val) { - if (arguments.length) { - if ($scope.node.max && val >= parseInt($scope.node.max, 10)) { - $scope.node.max = '' + (1 + val); - } - if (!val) { - delete $scope.node.min; - } - else { - $scope.node.min = '' + val; - } + else { + $scope.node.min = '' + val; } - return $scope.node.min ? parseInt($scope.node.min, 10) : null; - }; + } + return $scope.node.min ? parseInt($scope.node.min, 10) : null; + }; - $scope.getSetMax = function(val) { - if (arguments.length) { - if (!$scope.node.max && $scope.node.min === '1' && val === 1) { - val++; - } - if ($scope.node.min && val && val <= parseInt($scope.node.min, 10)) { - $scope.node.min = '' + (val - 1); - } - if (typeof val !== 'number') { - delete $scope.node.max; - } - else { - $scope.node.max = '' + val; - } + $scope.getSetMax = function(val) { + if (arguments.length) { + if ($scope.node.min && val && val < parseInt($scope.node.min, 10)) { + $scope.node.min = '' + val; } - return $scope.node.max ? parseInt($scope.node.max, 10) : null; - }; + if (typeof val !== 'number') { + delete $scope.node.max; + } + else { + $scope.node.max = '' + val; + } + } + return $scope.node.max ? parseInt($scope.node.max, 10) : null; + }; + + $scope.pickAddIcon = function() { + openIconPicker($scope.node, 'add-icon'); + }; + + function getBlockNode() { + return !$scope.join ? $scope.node : ($scope.node['#children'] && $scope.node['#children'].length === 1 ? $scope.node['#children'][0] : null); + } + + function setBlockDirective(directive) { + if ($scope.join) { + $scope.node['#children'] = [{'#tag': directive}]; + } else { + delete $scope.node['#children']; + delete $scope.node['class']; + $scope.node['#tag'] = directive; + } + } + + function overrideBlockContents(layout) { + $scope.node['#children'] = layout || []; + if (!$scope.join) { + $scope.node['#tag'] = 'div'; + $scope.node['class'] = 'af-container'; + } + block.override = true; + block.layout = null; + } + + $scope.layouts = { + 'af-layout-rows': ts('Contents display as rows'), + 'af-layout-cols': ts('Contents are evenly-spaced columns'), + 'af-layout-inline': ts('Contents are arranged inline') + }; - $scope.pickAddIcon = function() { - openIconPicker($scope.node, 'add-icon'); + $scope.getLayout = function() { + if (!$scope.node) { + return ''; + } + return _.intersection(splitClass($scope.node['class']), _.keys($scope.layouts))[0] || 'af-layout-rows'; + }; + + $scope.setLayout = function(val) { + var classes = ['af-container']; + if (val !== 'af-layout-rows') { + classes.push(val); + } + modifyClasses($scope.node, _.keys($scope.layouts), classes); + }; + + if (($scope.node['#tag'] in $scope.editor.meta.blocks) || $scope.join) { + + block = $scope.block = { + directive: null, + layout: null, + override: false, + options: [], }; - $scope.$watch('selectedBlock.name', function (name, oldVal) { - if (name && name !== oldVal) { - selectedBlock.layout = _.cloneDeep($scope.editor.meta.blocks[name].layout); - $scope.node['#children'] = [{'#tag': name}]; - selectedBlock.override = false; - selectedBlock.options = getBlockOptions(); + _.each($scope.editor.meta.blocks, function(blockInfo, directive) { + if (directive === $scope.node['#tag'] || blockInfo.join === $scope.container.getFieldEntityType()) { + block.options.push({ + id: directive, + text: blockInfo.title + }); } - else if (!name && oldVal) { - if (selectedBlock.layout) { - $scope.node['#children'] = selectedBlock.layout; - } - selectedBlock.override = false; - selectedBlock.layout = null; - selectedBlock.options = getBlockOptions(); + }); + + $scope.$watch('block.directive', function (directive, oldVal) { + if (directive && directive !== oldVal) { + block.layout = _.cloneDeep($scope.editor.meta.blocks[directive].layout); + setBlockDirective(directive); + block.override = false; + } + else if (!directive && oldVal) { + overrideBlockContents(block.layout); + block.override = false; } }); - $scope.$watch('selectedBlock.layout', function (layout, oldVal) { - if (selectedBlock.name && !selectedBlock.override && layout && layout !== oldVal && !angular.equals(layout, $scope.editor.meta.blocks[selectedBlock.name].layout)) { - $scope.node['#children'] = selectedBlock.layout; - selectedBlock.override = true; - selectedBlock.layout = null; - selectedBlock.options = getBlockOptions(); + $scope.$watch('block.layout', function (layout, oldVal) { + if (block.directive && !block.override && layout && layout !== oldVal && !angular.equals(layout, $scope.editor.meta.blocks[block.directive].layout)) { + overrideBlockContents(block.layout); } }, true); $scope.$watch("node['#children']", function (children) { - if (!selectedBlock.name && children && children.length === 1 && children[0]['#tag'] in $scope.editor.meta.blocks) { - selectedBlock.name = children[0]['#tag']; - selectedBlock.override = false; - } else if (selectedBlock.name && selectedBlock.override && !selectedBlock.layout && children && angular.equals(children, $scope.editor.meta.blocks[selectedBlock.name].layout)) { - selectedBlock.layout = _.cloneDeep($scope.editor.meta.blocks[selectedBlock.name].layout); - $scope.node['#children'] = [{'#tag': selectedBlock.name}]; - selectedBlock.override = false; + if (getBlockNode() && getBlockNode()['#tag'] in $scope.editor.meta.blocks) { + block.directive = getBlockNode()['#tag']; + block.override = false; + } else if (block.directive && block.override && !block.layout && children && angular.equals(children, $scope.editor.meta.blocks[block.directive].layout)) { + block.layout = _.cloneDeep($scope.editor.meta.blocks[block.directive].layout); + $scope.node['#children'] = [{'#tag': block.directive}]; + block.override = false; } - selectedBlock.options = getBlockOptions(); }, true); } - else { - $scope.layouts = { - 'af-layout-rows': ts('Contents display as rows'), - 'af-layout-cols': ts('Contents are evenly-spaced columns'), - 'af-layout-inline': ts('Contents are arranged inline') - }; + }, + controller: function($scope) { + var container = $scope.container = this; + this.node = $scope.node; - $scope.getLayout = function() { - if (!$scope.node) { - return ''; - } - return _.intersection(splitClass($scope.node['class']), _.keys($scope.layouts))[0] || 'af-layout-rows'; - }; + this.getNodeType = function(node) { + if (!node) { + return null; + } + if (node['#tag'] === 'af-field') { + return 'field'; + } + if (node['af-fieldset']) { + return 'fieldset'; + } + if (node['af-join']) { + return 'join'; + } + if (node['#tag'] && node['#tag'] in $scope.editor.meta.blocks) { + return 'container'; + } + var classes = splitClass(node['class']), + types = ['af-container', 'af-text', 'af-button', 'af-markup'], + type = _.intersection(types, classes); + return type.length ? type[0].replace('af-', '') : null; + }; + + this.removeElement = function(element) { + removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey}); + }; + + this.getEntityName = function() { + return $scope.entityName.split('-join-')[0]; + }; + + // Returns the primary entity type for this container e.g. "Contact" + this.getMainEntityType = function() { + return $scope.editor && $scope.editor.getEntity(container.getEntityName()).type; + }; + + // Returns the entity type for fields within this conainer (join entity type if this is a join, else the primary entity type) + this.getFieldEntityType = function() { + var joinType = $scope.entityName.split('-join-'); + return joinType[1] || ($scope.editor && $scope.editor.getEntity(joinType[0]).type); + }; - $scope.setLayout = function(val) { - var classes = ['af-container']; - if (val !== 'af-layout-rows') { - classes.push(val); - } - modifyClasses($scope.node, _.keys($scope.layouts), classes); - }; - } } }; }); diff --git a/ext/afform/gui/ang/afGuiEditor/canvas.html b/ext/afform/gui/ang/afGuiEditor/canvas.html index 85b611631c..5dbad937dc 100644 --- a/ext/afform/gui/ang/afGuiEditor/canvas.html +++ b/ext/afform/gui/ang/afGuiEditor/canvas.html @@ -13,6 +13,6 @@
-
+
diff --git a/ext/afform/gui/ang/afGuiEditor/container-menu.html b/ext/afform/gui/ang/afGuiEditor/container-menu.html index 2d0b54a381..6c9b62176c 100644 --- a/ext/afform/gui/ang/afGuiEditor/container-menu.html +++ b/ext/afform/gui/ang/afGuiEditor/container-menu.html @@ -3,7 +3,7 @@
  • {{ ts('Add rich content') }}
  • {{ ts('Add button') }}
  • -
  • +
  • {{ ts('Element:') }}
  • -
  • +
  • - + - - + -
  • diff --git a/ext/afform/gui/ang/afGuiEditor/container.html b/ext/afform/gui/ang/afGuiEditor/container.html index b77dcf17d7..d620e89232 100644 --- a/ext/afform/gui/ang/afGuiEditor/container.html +++ b/ext/afform/gui/ang/afGuiEditor/container.html @@ -1,11 +1,11 @@
    {{ editor.getEntity(entityName).label }} - {{ ts('%1 block:', {1: block}) }} + {{ join ? ts(join) + ':' : ts('Block:') }} {{ tags[node['#tag']].toLowerCase() }} - - +
    diff --git a/ext/afform/gui/ang/afGuiEditor/entity.html b/ext/afform/gui/ang/afGuiEditor/entity.html index 874fa119a4..dcc760aa0c 100644 --- a/ext/afform/gui/ang/afGuiEditor/entity.html +++ b/ext/afform/gui/ang/afGuiEditor/entity.html @@ -23,8 +23,8 @@
    -
    - {{ editor.meta.blocks[block['#children'][0]['#tag']].title }} +
    + {{ blockTitles[$index] }}
    diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformUsageTest.php index 14823d2b42..b79d9d532f 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/AfformUsageTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/AfformUsageTest.php @@ -53,12 +53,12 @@ EOHTML; ->setArgs([]) ->execute() ->indexBy('name'); - $this->assertEquals('Logged In', $prefill['me']['values']['first_name']); - $this->assertRegExp('/^User/', $prefill['me']['values']['last_name']); + $this->assertEquals('Logged In', $prefill['me']['values'][0]['fields']['first_name']); + $this->assertRegExp('/^User/', $prefill['me']['values'][0]['fields']['last_name']); $me = $prefill['me']['values']; - $me['first_name'] = 'Firsty'; - $me['last_name'] = 'Lasty'; + $me[0]['fields']['first_name'] = 'Firsty'; + $me[0]['fields']['last_name'] = 'Lasty'; Civi\Api4\Afform::submit() ->setName($this->formName) -- 2.25.1