Skip to content

Commit

Permalink
Afform Gui - Add support for entityRef fields
Browse files Browse the repository at this point in the history
Adds a widget for EntityRef and allows it to be changed to Number (for entering ID)
or Select (for choosing another entity on a form).
  • Loading branch information
colemanw committed May 4, 2021
1 parent da3e08e commit c24ed93
Show file tree
Hide file tree
Showing 15 changed files with 128 additions and 32 deletions.
10 changes: 5 additions & 5 deletions Civi/Api4/Generic/BasicGetFieldsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,13 @@ public function fields() {
'name' => 'input_type',
'data_type' => 'String',
'options' => [
'ChainSelect' => ts('ChainSelect'),
'CheckBox' => ts('CheckBox'),
'Date' => ts('Date'),
'EntityRef' => ts('EntityRef'),
'ChainSelect' => ts('Chain-Select'),
'CheckBox' => ts('Checkboxes'),
'Date' => ts('Date Picker'),
'EntityRef' => ts('Autocomplete Entity'),
'File' => ts('File'),
'Number' => ts('Number'),
'Radio' => ts('Radio'),
'Radio' => ts('Radio Buttons'),
'Select' => ts('Select'),
'Text' => ts('Text'),
],
Expand Down
8 changes: 8 additions & 0 deletions Civi/Api4/Service/Spec/Provider/ActivitySpecProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,29 @@ public function modifySpec(RequestSpec $spec) {

$field = new FieldSpec('source_contact_id', 'Activity', 'Integer');
$field->setTitle(ts('Source Contact'));
$field->setLabel(ts('Added by'));
$field->setDescription(ts('Contact who created this activity.'));
$field->setRequired($action === 'create');
$field->setFkEntity('Contact');
$field->setInputType('EntityRef');
$spec->addFieldSpec($field);

$field = new FieldSpec('target_contact_id', 'Activity', 'Array');
$field->setTitle(ts('Target Contacts'));
$field->setLabel(ts('With Contact(s)'));
$field->setDescription(ts('Contact(s) involved in this activity.'));
$field->setFkEntity('Contact');
$field->setInputType('EntityRef');
$field->setInputAttrs(['multiple' => TRUE]);
$spec->addFieldSpec($field);

$field = new FieldSpec('assignee_contact_id', 'Activity', 'Array');
$field->setTitle(ts('Assignee Contacts'));
$field->setLabel(ts('Assigned to'));
$field->setDescription(ts('Contact(s) assigned to this activity.'));
$field->setFkEntity('Contact');
$field->setInputType('EntityRef');
$field->setInputAttrs(['multiple' => TRUE]);
$spec->addFieldSpec($field);
}

Expand Down
2 changes: 1 addition & 1 deletion Civi/Api4/Service/Spec/SpecFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public static function setInputTypeAndAttrs(FieldSpec &$fieldSpec, $data, $dataT
'Link' => 'Url',
];
$inputType = $map[$inputType] ?? $inputType;
if ($inputType == 'Select' && !empty($data['serialize'])) {
if (in_array($inputType, ['Select', 'EntityRef'], TRUE) && !empty($data['serialize'])) {
$inputAttrs['multiple'] = TRUE;
}
if ($inputType == 'Date' && !empty($inputAttrs['formatType'])) {
Expand Down
15 changes: 13 additions & 2 deletions ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ public static function getGuiSettings() {

$contactTypes = \CRM_Contact_BAO_ContactType::basicTypeInfo();

// Call getFields on getFields to get input type labels
$inputTypeLabels = \Civi\Api4\Contact::getFields()
->setLoadOptions(TRUE)
->setAction('getFields')
->addWhere('name', '=', 'input_type')
->execute()
->column('options')[0];

// Scan all extensions for entities & input types
foreach (\CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles() as $ext) {
$dir = \CRM_Utils_File::addTrailingSlash(dirname($ext['filePath']));
Expand All @@ -143,10 +151,13 @@ public static function getGuiSettings() {
}
$data['entities'][$afformEntity] = $entity;
}
// Scan for input types
// Scan for input types, use label from getFields if available
foreach (glob($dir . 'ang/afGuiEditor/inputType/*.html') as $file) {
$name = basename($file, '.html');
$data['inputType'][$name] = $name;
$data['inputType'][] = [
'name' => $name,
'label' => $inputTypeLabels[$name] ?? E::ts($name),
];
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion ext/afform/admin/afformEntities/Activity.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<?php
return [
'entity' => 'Activity',
'defaults' => "{'url-autofill': '1'}",
'defaults' => "{
data: {
source_contact_id: 'user_contact_id',
activity_type_id: ''
},
'url-autofill': '1'
}",
'boilerplate' => [
['#tag' => 'af-field', 'name' => 'subject'],
],
Expand Down
6 changes: 3 additions & 3 deletions ext/afform/admin/ang/afGuiEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,9 @@
padding-left: 15px;
}

#afGuiEditor .af-gui-button > .btn.disabled {
cursor: default !important;
color: white !important;
#afGuiEditor #afGuiEditor-canvas-body .btn[disabled] {
cursor: default;
color: white;
}
#afGuiEditor .af-gui-button > .btn.disabled .crm-editable-enabled:hover:not(:focus) {
border-color: #f4f4f4 !important;
Expand Down
4 changes: 4 additions & 0 deletions ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@
return $scope.afform;
};

this.getEntities = function(filter) {
return filter ? _.filter($scope.entities, filter) : _.toArray($scope.entities);
};

this.toggleContactSummary = function() {
if ($scope.afform.contact_summary) {
$scope.afform.contact_summary = false;
Expand Down
25 changes: 17 additions & 8 deletions ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
"use strict";

// Cribbed from the Api4 Explorer
angular.module('afGuiEditor').directive('afGuiFieldValue', function() {
angular.module('afGuiEditor').directive('afGuiFieldValue', function(afGui) {
return {
scope: {
field: '=afGuiFieldValue'
},
require: 'ngModel',
require: {
ngModel: 'ngModel',
editor: '^^afGuiEditor'
},
link: function (scope, element, attrs, ctrl) {
var ts = scope.ts = CRM.ts('org.civicrm.afform_admin'),
multi;
Expand All @@ -25,7 +28,8 @@
}

function makeWidget(field) {
var $el = $(element),
var options,
$el = $(element),
inputType = field.input_type,
dataType = field.data_type;
multi = field.serialize || dataType === 'Array';
Expand All @@ -34,9 +38,14 @@
}
else if (field.fk_entity || field.options || dataType === 'Boolean') {
if (field.fk_entity) {
$el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}});
// Static options for choosing current user or other entities on the form
options = field.fk_entity === 'Contact' ? ['user_contact_id'] : [];
_.each(ctrl.editor.getEntities({type: field.fk_entity}), function(entity) {
options.push({id: entity.name, label: entity.label, icon: afGui.meta.entities[entity.type].icon});
});
$el.crmEntityRef({entity: field.fk_entity, select: {multiple: multi}, static: options});
} else if (field.options) {
var options = _.transform(field.options, function(options, val) {
options = _.transform(field.options, function(options, val) {
options.push({id: val.id, text: val.label});
}, []);
$el.select2({data: options, multiple: multi});
Expand Down Expand Up @@ -72,13 +81,13 @@
};

// Copied from ng-list
ctrl.$parsers.push(parseList);
ctrl.$formatters.push(function(value) {
ctrl.ngModel.$parsers.push(parseList);
ctrl.ngModel.$formatters.push(function(value) {
return _.isArray(value) ? value.join(', ') : value;
});

// Copied from ng-list
ctrl.$isEmpty = function(value) {
ctrl.ngModel.$isEmpty = function(value) {
return !value || !value.length;
};

Expand Down
2 changes: 1 addition & 1 deletion ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</div>
</div>
</div>
<button type="button" class="btn {{ getSetStyle() }} disabled">
<button type="button" class="btn {{ getSetStyle() }}" disabled>
<span class="crm-editable-enabled" ng-click="pickIcon()" >
<i class="crm-i {{ $ctrl.node['crm-icon'] }}"></i>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div href ng-click="$event.stopPropagation()" class="af-gui-field-select-in-dropdown">
<label>{{:: ts('Type:') }}</label>
<select class="form-control" ng-model="getSet('input_type')" ng-model-options="{getterSetter: true}" title="{{:: ts('Field type') }}">
<option ng-repeat="(type, label) in meta.inputType" value="{{ type }}" ng-if="inputTypeCanBe(type)">{{ label }}</option>
<option ng-repeat="type in $ctrl.inputTypes" value="{{ type.name }}">{{ type.label }}</option>
</select>
</div>
</li>
Expand Down
56 changes: 48 additions & 8 deletions ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
{id: '1', label: ts('Yes')},
{id: '0', label: ts('No')}
],
entityRefOptions = [],
singleElement = [''],
// When search-by-range is enabled the second element gets a suffix for some properties like "placeholder2"
rangeElements = ['', '2'],
Expand All @@ -29,7 +30,29 @@
relativeDatesWithoutPickRange = relativeDatesWithPickRange.slice(1);

this.$onInit = function() {
$scope.meta = afGui.meta;
ctrl.inputTypes = _.transform(_.cloneDeep(afGui.meta.inputType), function(inputTypes, type) {
if (inputTypeCanBe(type.name)) {
// Change labels for EntityRef fields
if (ctrl.getDefn().input_type === 'EntityRef') {
var entity = ctrl.getFkEntity();
if (entity && type.name === 'EntityRef') {
type.label = ts('Autocomplete %1', {1: entity.label});
}
if (entity && type.name === 'Number') {
type.label = ts('%1 ID', {1: entity.label});
}
if (entity && type.name === 'Select') {
type.label = ts('Select Form %1', {1: entity.label});
}
}
inputTypes.push(type);
}
});
};

this.getFkEntity = function() {
var fkEntity = ctrl.getDefn().fk_entity;
return ctrl.editor.meta.entities[fkEntity];
};

this.isSearch = function() {
Expand All @@ -52,7 +75,7 @@
this.canBeMultiple = function() {
return this.isSearch() &&
!_.includes(['Date', 'Timestamp'], ctrl.getDefn().data_type) &&
$scope.getProp('input_type') === 'Select';
_.includes(['Select', 'EntityRef'], $scope.getProp('input_type'));
};

this.getRangeElements = function(type) {
Expand Down Expand Up @@ -92,6 +115,17 @@
if (_.includes(['Date', 'Timestamp'], $scope.getProp('data_type'))) {
return $scope.getProp('search_range') ? relativeDatesWithPickRange : relativeDatesWithoutPickRange;
}
if (ctrl.getDefn().input_type === 'EntityRef') {
// Build a list of all entities in this form that can be referenced by this field.
var newOptions = _.map(ctrl.editor.getEntities({type: ctrl.getDefn().fk_entity}), function(entity) {
return {id: entity.name, label: entity.label};
}, []);
// Store it in a stable variable for the sake of ng-repeat
if (!angular.equals(newOptions, entityRefOptions)) {
entityRefOptions = newOptions;
}
return entityRefOptions;
}
return ctrl.getDefn().options || ($scope.getProp('input_type') === 'CheckBox' ? null : yesNo);
};

Expand All @@ -104,15 +138,18 @@
$('#afGuiEditor').addClass('af-gui-editing-content');
};

$scope.inputTypeCanBe = function(type) {
function inputTypeCanBe(type) {
var defn = ctrl.getDefn();
if (defn.input_type === type) {
return true;
}
switch (type) {
case 'CheckBox':
case 'Radio':
return defn.options || defn.data_type === 'Boolean';

case 'Select':
return defn.options || defn.data_type === 'Boolean' || (defn.input_type === 'Date' && ctrl.isSearch());
return defn.options || defn.data_type === 'Boolean' || defn.input_type === 'EntityRef' || (defn.input_type === 'Date' && ctrl.isSearch());

case 'Date':
return defn.input_type === 'Date';
Expand All @@ -121,13 +158,16 @@
case 'RichTextEditor':
return (defn.data_type === 'Text' || defn.data_type === 'String');

case 'ChainSelect':
return defn.input_type === 'ChainSelect';
case 'Text':
return !(defn.options || defn.input_type === 'Date' || defn.input_type === 'EntityRef' || defn.data_type === 'Boolean');

case 'Number':
return !(defn.options || defn.data_type === 'Boolean');

default:
return true;
return false;
}
};
}

// Returns a value from either the local field defn or the base defn
$scope.getProp = function(propName) {
Expand Down
8 changes: 8 additions & 0 deletions ext/afform/admin/ang/afGuiEditor/inputType/EntityRef.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="form-inline">
<div class="input-group">
<input autocomplete="off" type="text" class="form-control" placeholder="{{:: ts('Select %1', {1: $ctrl.getFkEntity().label}) }}" title="{{:: ts('Click to add placeholder text') }}" ng-model="getSet('input_attrs.placeholder')" ng-model-options="{getterSetter: true}">
<div class="input-group-btn">
<button type="button" class="btn btn-default" disabled><i class="crm-i fa-search"></i></button>
</div>
</div>
</div>
11 changes: 10 additions & 1 deletion ext/afform/core/Civi/Afform/AfformMetadataInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private static function fillFieldMetadata($entityName, $action, \DOMElement $afF
$params = [
'action' => $action,
'where' => [['name', '=', $fieldName]],
'select' => ['label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options'],
'select' => ['label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options', 'fk_entity'],
'loadOptions' => ['id', 'label'],
// If the admin included this field on the form, then it's OK to get metadata about the field regardless of user permissions.
'checkPermissions' => FALSE,
Expand Down Expand Up @@ -124,6 +124,15 @@ private static function fillFieldMetadata($entityName, $action, \DOMElement $afF
if ($inputType === 'Select') {
$fieldInfo['input_attrs']['placeholder'] = E::ts('Select');
}
elseif ($inputType === 'EntityRef') {
$info = civicrm_api4('Entity', 'get', [
'where' => [['name', '=', $fieldInfo['fk_entity']]],
'checkPermissions' => FALSE,
'select' => ['title', 'title_plural'],
], 0);
$label = empty($fieldInfo['input_attrs']['multiple']) ? $info['title'] : $info['title_plural'];
$fieldInfo['input_attrs']['placeholder'] = E::ts('Select %1', [1 => $label]);
}

if ($fieldInfo['input_type'] === 'Date') {
// This flag gets used by the afField controller
Expand Down
2 changes: 1 addition & 1 deletion ext/afform/core/ang/af/afField.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
};
};

// Getter/Setter function for fields of type select.
// Getter/Setter function for fields of type select or entityRef.
$scope.getSetSelect = function(val) {
var currentVal = $scope.dataProvider.getFieldData()[ctrl.fieldName];
// Setter
Expand Down
1 change: 1 addition & 0 deletions ext/afform/core/ang/af/fields/EntityRef.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input class="form-control" id="{{:: fieldId }}" ng-model="getSetSelect" ng-model-options="{getterSetter: true}" crm-entityref="{entity: $ctrl.defn.fk_entity, select: {multiple: !!$ctrl.defn.input_attrs.multiple, placeholder: $ctrl.defn.input_attrs.placeholder}}" >

0 comments on commit c24ed93

Please sign in to comment.