Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Afform Gui - Add support for entityRef fields #20216

Merged
merged 1 commit into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -127,6 +127,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() {
$entityInfo += $apiInfo;
$data['entities'][$entityName] = $entityInfo;
}
// 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,6 +1,12 @@
<?php
return [
'defaults' => "{'url-autofill': '1'}",
'defaults' => "{
data: {
source_contact_id: 'user_contact_id',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colemanw does source_contact_id show up in the UI? just wondering if this is changeable and what would user_contact_id reference if its an anon submission?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is the default and can be changed, but I think it's a sensible default. It makes adding a new activity look like this:
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(also see Comments in PR description about this)

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}}" >