From 783a2874ddf2462a69fe7122df399135a8a3ddaa Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 4 May 2021 16:58:51 -0400 Subject: [PATCH] Afform Gui - Add support for entityRef fields 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). --- Civi/Api4/Generic/BasicGetFieldsAction.php | 10 ++-- .../Spec/Provider/ActivitySpecProvider.php | 8 +++ Civi/Api4/Service/Spec/SpecFormatter.php | 2 +- .../Civi/AfformAdmin/AfformAdminMeta.php | 15 ++++- ext/afform/admin/afformEntities/Activity.php | 8 ++- ext/afform/admin/ang/afGuiEditor.css | 6 +- .../ang/afGuiEditor/afGuiEditor.component.js | 4 ++ .../afGuiEditor/afGuiFieldValue.directive.js | 25 ++++++--- .../ang/afGuiEditor/elements/afGuiButton.html | 2 +- .../afGuiEditor/elements/afGuiField-menu.html | 2 +- .../elements/afGuiField.component.js | 56 ++++++++++++++++--- .../ang/afGuiEditor/inputType/EntityRef.html | 8 +++ .../Civi/Afform/AfformMetadataInjector.php | 11 +++- ext/afform/core/ang/af/afField.component.js | 2 +- ext/afform/core/ang/af/fields/EntityRef.html | 1 + 15 files changed, 128 insertions(+), 32 deletions(-) create mode 100644 ext/afform/admin/ang/afGuiEditor/inputType/EntityRef.html create mode 100644 ext/afform/core/ang/af/fields/EntityRef.html diff --git a/Civi/Api4/Generic/BasicGetFieldsAction.php b/Civi/Api4/Generic/BasicGetFieldsAction.php index c554e5c1d9a6..6aee1082f212 100644 --- a/Civi/Api4/Generic/BasicGetFieldsAction.php +++ b/Civi/Api4/Generic/BasicGetFieldsAction.php @@ -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'), ], diff --git a/Civi/Api4/Service/Spec/Provider/ActivitySpecProvider.php b/Civi/Api4/Service/Spec/Provider/ActivitySpecProvider.php index 95504953b135..8c5b824678ec 100644 --- a/Civi/Api4/Service/Spec/Provider/ActivitySpecProvider.php +++ b/Civi/Api4/Service/Spec/Provider/ActivitySpecProvider.php @@ -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); } diff --git a/Civi/Api4/Service/Spec/SpecFormatter.php b/Civi/Api4/Service/Spec/SpecFormatter.php index 341ab2bbd16c..d7d474b5854b 100644 --- a/Civi/Api4/Service/Spec/SpecFormatter.php +++ b/Civi/Api4/Service/Spec/SpecFormatter.php @@ -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'])) { diff --git a/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php index 9146d03667bf..dbbb5044a14e 100644 --- a/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php +++ b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php @@ -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'])); @@ -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), + ]; } } } diff --git a/ext/afform/admin/afformEntities/Activity.php b/ext/afform/admin/afformEntities/Activity.php index a18f17f3b73f..200ddeb7e8e6 100644 --- a/ext/afform/admin/afformEntities/Activity.php +++ b/ext/afform/admin/afformEntities/Activity.php @@ -1,6 +1,12 @@ "{'url-autofill': '1'}", + 'defaults' => "{ + data: { + source_contact_id: 'user_contact_id', + activity_type_id: '' + }, + 'url-autofill': '1' + }", 'boilerplate' => [ ['#tag' => 'af-field', 'name' => 'subject'], ], diff --git a/ext/afform/admin/ang/afGuiEditor.css b/ext/afform/admin/ang/afGuiEditor.css index 3667005a1223..8537bc578b8a 100644 --- a/ext/afform/admin/ang/afGuiEditor.css +++ b/ext/afform/admin/ang/afGuiEditor.css @@ -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; diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js index acbf3e23db97..8adebf6a865b 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js @@ -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; diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js b/ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js index 57dab3b1191a..9ca8f4c98be5 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiFieldValue.directive.js @@ -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; @@ -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'; @@ -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}); @@ -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; }; diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html index b2234776825a..91b70098597d 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiButton.html @@ -8,7 +8,7 @@ - + + + diff --git a/ext/afform/core/Civi/Afform/AfformMetadataInjector.php b/ext/afform/core/Civi/Afform/AfformMetadataInjector.php index 10b965484dc5..3033386fe7e1 100644 --- a/ext/afform/core/Civi/Afform/AfformMetadataInjector.php +++ b/ext/afform/core/Civi/Afform/AfformMetadataInjector.php @@ -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, @@ -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 diff --git a/ext/afform/core/ang/af/afField.component.js b/ext/afform/core/ang/af/afField.component.js index 8a18811f21e7..3453ac7682b4 100644 --- a/ext/afform/core/ang/af/afField.component.js +++ b/ext/afform/core/ang/af/afField.component.js @@ -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 diff --git a/ext/afform/core/ang/af/fields/EntityRef.html b/ext/afform/core/ang/af/fields/EntityRef.html new file mode 100644 index 000000000000..2874725fbf25 --- /dev/null +++ b/ext/afform/core/ang/af/fields/EntityRef.html @@ -0,0 +1 @@ +