This allows the user to set a join block (email, phone, address, etc) as either repeatable
OR with a set value for is_primary or location_type.
Fixes dev/core#2703
return [
'type' => 'join',
'repeat_max' => NULL,
+ 'unique_fields' => ['is_primary', 'location_type_id'],
];
return [
'type' => 'join',
'repeat_max' => NULL,
+ 'unique_fields' => ['is_primary', 'location_type_id'],
];
return [
'type' => 'join',
'repeat_max' => NULL,
+ 'unique_fields' => ['is_primary', 'location_type_id'],
];
return [
'type' => 'join',
'repeat_max' => NULL,
+ 'unique_fields' => ['is_primary', 'location_type_id'],
];
return [
'type' => 'join',
'repeat_max' => NULL,
+ 'unique_fields' => ['website_type_id'],
];
opacity: 1;
transition: opacity 0s;
}
-#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > button > span,
+/* Fix button colors when bar is highlighted */
+#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > .btn-group > .btn-group > button > span,
#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > span {
color: white;
}
-#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > button:hover > span,
-#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .open > button > span,
-#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > button:focus > span {
+#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > .btn-group > .btn-group > button:hover > span,
+#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > .btn-group > .btn-group > button[aria-expanded=true] > span,
+#afGuiEditor #afGuiEditor-canvas .af-entity-selected > .af-gui-bar > .form-inline > .btn-group > .btn-group > button:focus > span {
color: #0071bd;
}
--- /dev/null
+// https://civicrm.org/licensing
+(function(angular, $, _) {
+ "use strict";
+
+ // Menu item to control the border property of a node
+ angular.module('afGuiEditor').component('afGuiContainerMultiToggle', {
+ templateUrl: '~/afGuiEditor/afGuiContainerMultiToggle.html',
+ bindings: {
+ entity: '<'
+ },
+ require: {
+ container: '^^afGuiContainer'
+ },
+ controller: function($scope, afGui) {
+ var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin'),
+ ctrl = this;
+ this.menuItems = [];
+ this.uniqueFields = {};
+
+ this.$onInit = function() {
+ this.menuItems.push({
+ key: 'repeat',
+ label: ts('Multiple')
+ });
+ _.each(afGui.getEntity(this.entity).unique_fields, function(fieldName) {
+ var field = ctrl.uniqueFields[fieldName] = afGui.getField(ctrl.entity, fieldName);
+ ctrl.menuItems.push({});
+ if (field.options) {
+ _.each(field.options, function(option) {
+ ctrl.menuItems.push({
+ field: fieldName,
+ key: option.id,
+ label: option.label
+ });
+ });
+ } else {
+ ctrl.menuItems.push({
+ field: fieldName,
+ key: true,
+ label: field.label
+ });
+ }
+ });
+ };
+
+ this.isMulti = function() {
+ return 'af-repeat' in ctrl.container.node;
+ };
+
+ this.isSelected = function(item) {
+ if (!item.field && item.key === 'repeat') {
+ return ctrl.isMulti();
+ }
+ if (ctrl.container.node.data) {
+ var field = ctrl.uniqueFields[item.field];
+ if (field.options) {
+ return ctrl.container.node.data[item.field] === item.key;
+ }
+ return ctrl.container.node.data[item.field];
+ }
+ return false;
+ };
+
+ this.selectOption = function(item) {
+ if (!item.field && item.key === 'repeat') {
+ return ctrl.container.toggleRepeat();
+ }
+ if (ctrl.isMulti()) {
+ ctrl.container.toggleRepeat();
+ }
+ var field = ctrl.uniqueFields[item.field];
+ ctrl.container.node.data = ctrl.container.node.data || {};
+ if (field.options) {
+ if (ctrl.container.node.data[item.field] === item.key) {
+ delete ctrl.container.node.data[item.field];
+ } else {
+ ctrl.container.node.data = {};
+ ctrl.container.node.data[item.field] = item.key;
+ ctrl.container.removeField(item.field);
+ }
+ } else if (ctrl.container.node.data[item.field]) {
+ delete ctrl.container.node.data[item.field];
+ } else {
+ ctrl.container.node.data = {};
+ ctrl.container.node.data[item.field] = true;
+ ctrl.container.removeField(item.field);
+ }
+ if (_.isEmpty(ctrl.container.node.data)) {
+ delete ctrl.container.node.data;
+ }
+ };
+
+ this.getButtonText = function() {
+ if (ctrl.isMulti()) {
+ return ts('Multiple');
+ }
+ var output = ts('Single');
+ _.each(ctrl.container.node.data, function(val, fieldName) {
+ if (val && (fieldName in ctrl.uniqueFields)) {
+ var field = ctrl.uniqueFields[fieldName];
+ if (field.options) {
+ output = _.result(_.findWhere(field.options, {id: val}), 'label');
+ } else {
+ output = field.label;
+ }
+ return false;
+ }
+ });
+ return output;
+ };
+
+ }
+ });
+
+})(angular, CRM.$, CRM._);
--- /dev/null
+<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" title="{{:: ts('Toggle multi or single block') }}">
+ <span>
+ <i class="crm-i fa-{{ $ctrl.isMulti() ? 'repeat' : 'dot-circle-o' }}"></i>
+ {{ $ctrl.getButtonText() }}
+ </span>
+</button>
+<ul class="dropdown-menu dropdown-menu-right">
+ <li ng-repeat="item in $ctrl.menuItems" class="{{ item.key ? '' : 'divider' }}">
+ <a href ng-if="item.key" ng-click="$ctrl.selectOption(item)">
+ <i class="crm-i fa-{{ $ctrl.isSelected(item) ? 'check-' : '' }}circle-o"></i>
+ {{:: item.label }}
+ </a>
+ </li>
+</ul>
</li>
<li ng-if="isRepeatable()" ng-click="$event.stopPropagation()">
<div class="af-gui-field-select-in-dropdown form-inline">
- <label ng-click="toggleRepeat()">
+ <label ng-click="$ctrl.toggleRepeat()">
<i class="crm-i fa-{{ $ctrl.node['af-repeat'] || $ctrl.node['af-repeat'] === '' ? 'check-' : '' }}square-o"></i>
{{:: ts('Repeat') }}
</label>
return ctrl.node['af-fieldset'] || (block.directive && afGui.meta.blocks[block.directive].repeat) || ctrl.join;
};
- $scope.toggleRepeat = function() {
+ this.toggleRepeat = function() {
if ('af-repeat' in ctrl.node) {
delete ctrl.node.max;
delete ctrl.node.min;
} else {
ctrl.node.min = '1';
ctrl.node['af-repeat'] = ts('Add');
+ delete ctrl.node.data;
}
};
// or from afformEntity php file for core entities.
$scope.getRepeatMax = function() {
if (ctrl.join) {
- return afGui.getEntity(ctrl.join).repeat_max || '';
+ return ctrl.getJoinEntity().repeat_max || '';
}
return '';
};
afGui.removeRecursive($scope.getSetChildren(), {$$hashKey: element.$$hashKey});
};
+ this.removeField = function(fieldName) {
+ afGui.removeRecursive($scope.getSetChildren(), {'#tag': 'af-field', name: fieldName});
+ };
+
this.getEntityName = function() {
return ctrl.entityName ? ctrl.entityName.split('-join-')[0] : null;
};
+ this.getJoinEntity = function() {
+ if (!ctrl.join) {
+ return null;
+ }
+ return afGui.getEntity(ctrl.join);
+ };
+
// Returns the primary entity type for this container e.g. "Contact"
this.getMainEntityType = function() {
return ctrl.editor && ctrl.editor.getEntity(ctrl.getEntityName()).type;
<div class="af-gui-bar" ng-if="$ctrl.node['#tag']" ng-click="selectEntity()" >
- <div ng-if="!$ctrl.loading" class="form-inline" af-gui-menu>
+ <div ng-if="!$ctrl.loading" class="form-inline">
<span ng-if="$ctrl.getNodeType($ctrl.node) == 'fieldset'">{{ $ctrl.editor.getEntity($ctrl.entityName).label }}</span>
<span ng-if="block">{{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }}</span>
<span ng-if="!block">{{ tags[$ctrl.node['#tag']] }}</span>
<option ng-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" ng-if="block && !block.layout" ng-click="saveBlock()">{{:: ts('Save...') }}</button>
- <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-element-button pull-right" data-toggle="dropdown" title="{{:: ts('Configure') }}">
- <span><i class="crm-i fa-gear"></i></span>
- </button>
- <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiContainer-menu.html'"></ul>
+ <div class="btn-group pull-right">
+ <af-gui-container-multi-toggle ng-if="!ctrl.loading && ($ctrl.join || $ctrl.node['af-repeat'])" entity="$ctrl.getFieldEntityType()" class="btn-group"></af-gui-container-multi-toggle>
+ <div class="btn-group" af-gui-menu>
+ <button type="button" class="btn btn-default btn-xs dropdown-toggle af-gui-add-element-button" data-toggle="dropdown" title="{{:: ts('Configure') }}">
+ <span><i class="crm-i fa-gear"></i></span>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right" ng-if="menu.open" ng-include="'~/afGuiEditor/elements/afGuiContainer-menu.html'"></ul>
+ </div>
+ </div>
</div>
<div ng-if="$ctrl.loading"><i class="crm-i fa-spin fa-spinner"></i></div>
</div>
'*' => [
'*' => 'text',
'af-fieldset' => 'text',
+ 'data' => 'js',
],
'af-entity' => [
'#selfClose' => TRUE,
'name' => 'text',
'type' => 'text',
- 'data' => 'js',
'security' => 'text',
'actions' => 'js',
],
foreach ($values['joins'] as $joinEntity => &$joinValues) {
// Enforce the limit set by join[max]
$joinValues = array_slice($joinValues, 0, $entity['joins'][$joinEntity]['max'] ?? NULL);
- // Only accept values from join fields on the form
foreach ($joinValues as $index => $vals) {
+ // Only accept values from join fields on the form
$joinValues[$index] = array_intersect_key($vals, $entity['joins'][$joinEntity]['fields'] ?? []);
+ // Merge in pre-set data
+ $joinValues[$index] = array_merge($joinValues[$index], $entity['joins'][$joinEntity]['data'] ?? []);
}
}
$entityValues[$entityName][] = $values;
->setLayoutFormat('shallow')
->setFormatWhitespace(TRUE)
->execute()->single();
- $this->assertEquals(2, $block['repeat']);
$this->assertEquals('afblock-custom-my-things', $block['directive_name']);
$this->assertEquals('my_text', $block['layout'][0]['name']);
$this->assertEquals('my_friend', $block['layout'][1]['name']);