* @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'];
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 {
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);
+ }
}
}
}
}
}
- $this->_formDataModel = FormDataModel::create($this->_afform['layout']);
+ $this->_formDataModel = new FormDataModel($this->_afform['layout']);
$this->validateArgs();
$result->exchangeArray($this->processForm());
}
*/
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;
+ }
+
}
'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) {
'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;
}
}
*
* @param $entity
* @param $mode
+ * @throws \API_Exception
*/
private function autoFillEntity($entity, $mode) {
$id = NULL;
namespace Civi\Api4\Action\Afform;
use Civi\Afform\Event\AfformSubmitEvent;
-use Civi\API\Exception\NotImplementedException;
/**
* Class Submit
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);
* @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']);
* @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']);
'name' => 'block',
],
[
- 'name' => 'extends',
+ 'name' => 'join',
],
[
'name' => 'title',
'data_type' => 'Boolean',
],
[
- 'name' => 'repeatable',
- 'data_type' => 'Boolean',
+ 'name' => 'repeat',
+ 'data_type' => 'Mixed',
],
[
'name' => 'server_route',
->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);
}
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);
'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',
],
];
+++ /dev/null
-(function(angular, $, _) {
- // Example usage: <div af-block="Email" min="1" max="3" add-label="Add email" ><div block-email-default /></div>
- 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._);
(function(angular, $, _) {
+ var id = 0;
// Example usage: <div af-fieldset="myModel"><af-field name="do_not_email" /></div>
angular.module('af').directive('afField', function() {
return {
restrict: 'E',
- require: ['^^afFieldset', '?^afBlockItem'],
+ require: ['^^afForm', '^^afFieldset', '?^^afJoin', '?^^afRepeatItem'],
templateUrl: '~/af/afField.html',
scope: {
fieldName: '@name',
},
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));
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;
};
}
};
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];
--- /dev/null
+(function(angular, $, _) {
+ // Example usage: <div af-join="Email" min="1" max="3" add-label="Add email" ><div join-email-default /></div>
+ 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._);
--- /dev/null
+(function(angular, $, _) {
+ // Example usage: <div af-repeat="Email" min="1" max="3" add-label="Add email" ><div repeat-email-default /></div>
+ 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._);
+++ /dev/null
-<div af-block-item="item" ng-repeat="item in getItems()">
- <ng-transclude />
- <button crm-icon="fa-ban" class="btn btn-xs af-block-remove-btn" ng-if="canRemove()" ng-click="removeItem($index)"></button>
-</div>
-<button crm-icon="{{ addIcon || 'fa-plus' }}" class="btn btn-sm af-block-add-btn" ng-if="canAdd()" ng-click="addItem()">{{ addLabel }}</button>
--- /dev/null
+<div af-repeat-item="item" ng-repeat="item in getItems()">
+ <ng-transclude />
+ <button crm-icon="fa-ban" class="btn btn-xs af-repeat-remove-btn" ng-if="canRemove()" ng-click="removeItem($index)"></button>
+</div>
+<button crm-icon="{{ addIcon || 'fa-plus' }}" class="btn btn-sm af-repeat-add-btn" ng-if="canAdd()" ng-click="addItem()">{{ addLabel }}</button>
<ul class="crm-checkbox-list" id="{{ fieldId }}" ng-if="defn.options">
<li ng-repeat="opt in defn.options" >
- <input type="checkbox" checklist-model="getData()[name]" id="{{ fieldId + opt.key }}" checklist-value="opt.key" />
+ <input type="checkbox" checklist-model="dataProvider.getFieldData()[fieldName]" id="{{ fieldId + opt.key }}" checklist-value="opt.key" />
<label for="{{ fieldId + opt.key }}">{{ opt.label }}</label>
</li>
</ul>
-<input type="checkbox" ng-if="!defn.options" id="{{ fieldId }}" ng-model="getData()[fieldName]" ng-true-value="'1'" ng-false-value="'0'" />
+<input type="checkbox" ng-if="!defn.options" id="{{ fieldId }}" ng-model="dataProvider.getFieldData()[fieldName]" ng-true-value="'1'" ng-false-value="'0'" />
-<input crm-ui-datepicker="defn.input_attrs" id="{{ fieldId }}" ng-model="getData()[fieldName]" />
+<input crm-ui-datepicker="defn.input_attrs" id="{{ fieldId }}" ng-model="dataProvider.getFieldData()[fieldName]" />
-<input class="crm-form-text" type="number" id="{{ fieldId }}" ng-model="getData()[fieldName]" placeholder="{{ defn.input_attrs.placeholder }}" />
+<input class="crm-form-text" type="number" id="{{ fieldId }}" ng-model="dataProvider.getFieldData()[fieldName]" placeholder="{{ defn.input_attrs.placeholder }}" />
<label ng-repeat="opt in getOptions()" >
- <input class="crm-form-radio" type="radio" ng-model="getData()[fieldName]" value="{{ opt.key }}" />
+ <input class="crm-form-radio" type="radio" ng-model="dataProvider.getFieldData()[fieldName]" value="{{ opt.key }}" />
{{ opt.label }}
</label>
-<textarea crm-ui-richtext id="{{ fieldId }}" ng-model="getData()[fieldName]" ></textarea>
+<textarea crm-ui-richtext id="{{ fieldId }}" ng-model="dataProvider.getFieldData()[fieldName]" ></textarea>
-<input crm-ui-select="{data: select2Options, multiple: defn.input_attrs.multiple, placeholder: defn.input_attrs.placeholder}" id="{{ fieldId }}" ng-model="getData()[fieldName]" />
+<input crm-ui-select="{data: select2Options, multiple: defn.input_attrs.multiple, placeholder: defn.input_attrs.placeholder}" id="{{ fieldId }}" ng-model="dataProvider.getFieldData()[fieldName]" />
-<input class="crm-form-text" type="text" id="{{ fieldId }}" ng-model="getData()[fieldName]" placeholder="{{ defn.input_attrs.placeholder }}" />
+<input class="crm-form-text" type="text" id="{{ fieldId }}" ng-model="dataProvider.getFieldData()[fieldName]" placeholder="{{ defn.input_attrs.placeholder }}" />
-<textarea class="crm-form-textarea" id="{{ fieldId }}" ng-model="getData()[fieldName]" ></textarea>
+<textarea class="crm-form-textarea" id="{{ fieldId }}" ng-model="dataProvider.getFieldData()[fieldName]" ></textarea>
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;
{
"title": "Email Block (default)",
- "block": "Email",
- "extends": "Contact",
- "repeatable": true
+ "block": "Contact",
+ "join": "Email",
+ "repeat": true
}
{
"title": "IM Block (default)",
- "block": "IM",
- "extends": "Contact",
- "repeatable": true
+ "block": "Contact",
+ "join": "IM",
+ "repeat": true
}
--- /dev/null
+<div class="af-container af-layout-inline">
+ <af-field name="first_name" />
+ <af-field name="middle_name" />
+ <af-field name="last_name" />
+</div>
--- /dev/null
+{
+ "title": "Individual Name (default)",
+ "block": "Contact"
+}
{
"title": "Phone Block (default)",
- "block": "Phone",
- "extends": "Contact",
- "repeatable": true
+ "block": "Contact",
+ "join": "Phone",
+ "repeat": true
}
{
"title": "Website Block (default)",
- "block": "Website",
- "extends": "Contact",
- "repeatable": true
+ "block": "Contact",
+ "join": "Website",
+ "repeat": true
}
'propA' => ['name' => 'propA'],
'propB' => ['name' => 'propB', 'defn' => ['title' => 'Whiz']],
],
- 'blocks' => [],
+ 'joins' => [],
],
],
];
'type' => 'Foo',
'name' => 'foobar',
'fields' => [],
- 'blocks' => [],
+ 'joins' => [],
],
'whiz_bang' => [
'type' => 'Whiz',
'name' => 'whiz_bang',
'fields' => [],
- 'blocks' => [],
+ 'joins' => [],
],
],
];
*/
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());
}
$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;
}
'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');
$scope.controls = {};
$scope.fieldList = [];
$scope.blockList = [];
+ $scope.blockTitles = [];
$scope.elementList = [];
$scope.elementTitles = [];
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);
}
});
}
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 = {};
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;
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('.');
}
};
- 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();
};
};
// 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);
- };
- }
}
};
});
</form>
</div>
<div class="panel-body">
- <div af-gui-container="layout" entity-name="" />
+ <div ng-if="layout" af-gui-container="layout" entity-name="" />
</div>
</div>
<li><a href ng-click="addElement('div.af-markup', {'#markup': false})">{{ ts('Add rich content') }}</a></li>
<li><a href ng-click="addElement('button.af-button.btn-primary', {'crm-icon': 'fa-check', 'ng-click': 'afform.submit()'})">{{ ts('Add button') }}</a></li>
<li role="separator" class="divider"></li>
-<li>
+<li ng-if="tags[node['#tag']]">
<div class="af-gui-field-select-in-dropdown form-inline" ng-click="$event.stopPropagation()">
{{ ts('Element:') }}
<select class="form-control" ng-model="node['#tag']" title="{{ ts('Container type') }}">
</select>
</div>
</li>
-<li ng-if="block && isRepeatable()" ng-click="$event.stopPropagation()">
+<li ng-if="isRepeatable()" ng-click="$event.stopPropagation()">
<div class="af-gui-field-select-in-dropdown form-inline">
<label ng-click="toggleRepeat()">
- <i class="crm-i fa-{{ node.min === '1' && node.max === '1' ? '' : 'check-' }}square-o"></i>
+ <i class="crm-i fa-{{ node['af-repeat'] || node['af-repeat'] === '' ? 'check-' : '' }}square-o"></i>
{{ ts('Repeat') }}
</label>
- <span ng-style="{visibility: node.min === '1' && node.max === '1' ? 'hidden' : 'visible'}">
+ <span ng-style="{visibility: node['af-repeat'] || node['af-repeat'] === '' ? 'visible' : 'hidden'}">
<input type="number" class="form-control" ng-model="getSetMin" ng-model-options="{getterSetter: true}" placeholder="{{ ts('min') }}" min="0" step="1" />
- - <input type="number" class="form-control" ng-model="getSetMax" ng-model-options="{getterSetter: true}" placeholder="{{ ts('max') }}" min="1" step="1" />
+ - <input type="number" class="form-control" ng-model="getSetMax" ng-model-options="{getterSetter: true}" placeholder="{{ ts('max') }}" min="2" step="1" />
</span>
</div>
</li>
<div class="af-gui-bar" ng-if="node['#tag'] !== 'af-form'" ng-click="selectEntity()" >
<div class="form-inline" af-gui-menu>
<span ng-if="container.getNodeType(node) == 'fieldset'">{{ editor.getEntity(entityName).label }}</span>
- <span ng-if="block">{{ ts('%1 block:', {1: block}) }}</span>
+ <span ng-if="block">{{ join ? ts(join) + ':' : ts('Block:') }}</span>
<span ng-if="!block">{{ tags[node['#tag']].toLowerCase() }}</span>
- <select ng-if="block" ng-model="selectedBlock.name">
+ <select ng-if="block" ng-model="block.directive">
<option value="">{{ ts('Custom') }}</option>
- <option value="{{ option.id }}" ng-repeat="option in selectedBlock.options track by option.id">{{ option.text }}</option>
+ <option value="{{ option.id }}" ng-repeat="option in block.options track by option.id">{{ option.text }}</option>
</select>
<button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-element-button pull-right" data-toggle="dropdown" title="{{ ts('Add Element') }}">
<span><i class="crm-i fa-plus"></i></span>
<div ng-switch="container.getNodeType(item)">
<div ng-switch-when="fieldset" af-gui-container="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'] }}" />
<div ng-switch-when="container" af-gui-container="item" style="{{ item.style }}" class="af-gui-container af-gui-container-type-{{ item['#tag'] }}" entity-name="entityName" data-entity="{{ entityName }}" />
- <div ng-switch-when="block" af-gui-container="item" style="{{ item.style }}" class="af-gui-container af-gui-block" block="item['af-block']" entity-name="entityName + '-block-' + item['af-block']" data-entity="{{ entityName + '-block-' + item['af-block'] }}" />
+ <div ng-switch-when="join" af-gui-container="item" style="{{ item.style }}" class="af-gui-container" join="item['af-join']" entity-name="entityName + '-join-' + item['af-join']" data-entity="{{ entityName + '-join-' + item['af-join'] }}" />
<div ng-switch-when="field" af-gui-field="item" />
<div ng-switch-when="text" af-gui-text="item" class="af-gui-element af-gui-text" />
<div ng-switch-when="markup" af-gui-markup="item" class="af-gui-markup" />
</div>
</div>
</div>
-<div ng-if="block && !(node.min === '1' && node.max === '1')" class="af-gui-button">
+<div ng-if="node['af-repeat'] || node['af-repeat'] === ''" class="af-gui-button">
<button class="btn btn-xs btn-primary disabled">
<span class="crm-editable-enabled" ng-click="pickAddIcon()" >
<i class="crm-i {{ node['add-icon'] || 'fa-plus' }}"></i>
</span>
- <span af-gui-editable ng-model="node['add-label']">{{ node['add-label'] }}</span>
+ <span af-gui-editable ng-model="node['af-repeat']">{{ node['af-repeat'] }}</span>
</button>
</div>
<div ng-if="blockList.length">
<label>{{ ts('Blocks') }}</label>
<div ui-sortable="{update: buildPaletteLists, items: '> div:not(.disabled)', connectWith: '[data-entity=' + entity.name + '] > [ui-sortable]'}" ng-model="blockList">
- <div ng-repeat="block in blockList" ng-class="{disabled: blockInUse(block['af-block'])}">
- {{ editor.meta.blocks[block['#children'][0]['#tag']].title }}
+ <div ng-repeat="block in blockList" ng-class="{disabled: blockInUse(block)}">
+ {{ blockTitles[$index] }}
</div>
</div>
</div>
->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)