Afform - Fix chain-select of contry and address to work on SearchKit forms
authorColeman Watts <coleman@civicrm.org>
Wed, 6 Oct 2021 15:49:46 +0000 (11:49 -0400)
committerColeman Watts <coleman@civicrm.org>
Wed, 6 Oct 2021 15:49:46 +0000 (11:49 -0400)
Multiple fixes to the chainSelect mechanics
- Provides visual feedback when reloading options
- Works with defalut values from url args
- Works with multi-valued fields
- Works with searchKit joins

ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js
ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js
ext/afform/core/Civi/Afform/AfformMetadataInjector.php
ext/afform/core/ang/af/afField.component.js

index ed1a98f73ad0716eaac7dec855ddce9aea39d0f4..14937f6540dd921c133cf5675e6ed249df05dc60 100644 (file)
@@ -86,7 +86,7 @@
             "#tag": "af-field",
             name: (prefix ? prefix + '.' : '') + field.name
           };
-          if (field.input_type === 'Select') {
+          if (field.input_type === 'Select' || field.input_type === 'ChainSelect') {
             tag.defn = {input_attrs: {multiple: true}};
           } else if (field.input_type === 'Date') {
             tag.defn = {input_type: 'Select', search_range: true};
index 2cc5e04a7d843e8c62a48f7f663902e544b51e36..f740d3d3f6b9ec903ee02581064b1c515322aeec 100644 (file)
@@ -76,7 +76,7 @@
       this.canBeMultiple = function() {
         return this.isSearch() &&
           !_.includes(['Date', 'Timestamp'], ctrl.getDefn().data_type) &&
-          _.includes(['Select', 'EntityRef'], $scope.getProp('input_type'));
+          _.includes(['Select', 'EntityRef', 'ChainSelect'], $scope.getProp('input_type'));
       };
 
       this.getRangeElements = function(type) {
index fca28b8694e64061d16fb2fb11237abffc72ec2c..a8e596e720915c0aff178527404e20088071fffd 100644 (file)
@@ -104,7 +104,7 @@ class AfformMetadataInjector {
       $isSearchRange = !empty($fieldDefn['search_range']) && \CRM_Utils_JS::decode($fieldDefn['search_range']);
 
       // Default placeholder for select inputs
-      if ($inputType === 'Select') {
+      if ($inputType === 'Select' || $inputType === 'ChainSelect') {
         $fieldInfo['input_attrs']['placeholder'] = E::ts('Select');
       }
       elseif ($inputType === 'EntityRef') {
@@ -163,7 +163,7 @@ class AfformMetadataInjector {
     $params = [
       'action' => $action,
       'where' => [['name', 'IN', $namesToMatch]],
-      'select' => ['name', 'label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options', 'fk_entity'],
+      'select' => ['name', 'label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options', 'entity', '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,
index 60d41c0cdf8f476975fc63df3df1791f89f92251..34d425c378327a0d740e877e2195e3ecb592678e 100644 (file)
     controller: function($scope, $element, crmApi4, $timeout, $location) {
       var ts = $scope.ts = CRM.ts('org.civicrm.afform'),
         ctrl = this,
+        // Prefix used for SearchKit explicit joins
+        namePrefix = '',
         boolOptions = [{id: true, label: ts('Yes')}, {id: false, label: ts('No')}],
+        // Used to store chain select options loaded on-the-fly
+        chainSelectOptions = null,
         // Only used for is_primary radio button
         noOptions = [{id: true, label: ''}];
 
@@ -29,6 +33,9 @@
 
         $element.addClass('af-field-type-' + _.kebabCase(ctrl.defn.input_type));
 
+        if (this.defn.name !== this.fieldName) {
+          namePrefix = this.fieldName.substr(0, this.fieldName.length - this.defn.name.length);
+        }
 
         if (ctrl.defn.search_range) {
           // Initialize value as object unless using relative date select
 
         // ChainSelect - watch control field & reload options as needed
         if (ctrl.defn.input_type === 'ChainSelect') {
-          $scope.$watch('dataProvider.getFieldData()[defn.input_attrs.control_field]', function(val) {
+          var controlField = namePrefix + ctrl.defn.input_attrs.control_field;
+          $scope.$watch('dataProvider.getFieldData()["' + controlField + '"]', function(val) {
+            // After switching option list, remove invalid options
+            function validateValue() {
+              var options = $scope.getOptions(),
+                value = $scope.dataProvider.getFieldData()[ctrl.fieldName];
+              if (_.isArray(value)) {
+                _.remove(value, function(item) {
+                  return !_.find(options, function(option) {return option.id == item;});
+                });
+              } else if (value && !_.find(options, function(option) {return option.id == value;})) {
+                $scope.dataProvider.getFieldData()[ctrl.fieldName] = '';
+              }
+            }
             if (val) {
+              $('input[crm-ui-select]', $element).addClass('loading').prop('disabled', true);
               var params = {
-                where: [['name', '=', ctrl.fieldName]],
+                where: [['name', '=', ctrl.defn.name]],
                 select: ['options'],
                 loadOptions: ['id', 'label'],
                 values: {}
               };
               params.values[ctrl.defn.input_attrs.control_field] = val;
-              crmApi4($scope.dataProvider.getEntityType(), 'getFields', params, 0)
+              crmApi4(ctrl.defn.entity, 'getFields', params, 0)
                 .then(function(data) {
-                  ctrl.defn.options = data.options;
+                  $('input[crm-ui-select]', $element).removeClass('loading').prop('disabled', false);
+                  chainSelectOptions = data.options;
+                  validateValue();
                 });
+            } else {
+              chainSelectOptions = null;
+              validateValue();
             }
-          });
+          }, true);
         }
 
         // Wait for parent controllers to initialize
       };
 
       $scope.getOptions = function () {
-        return ctrl.defn.options || (ctrl.fieldName === 'is_primary' && ctrl.defn.input_type === 'Radio' ? noOptions : boolOptions);
+        return chainSelectOptions || ctrl.defn.options || (ctrl.fieldName === 'is_primary' && ctrl.defn.input_type === 'Radio' ? noOptions : boolOptions);
       };
 
       $scope.select2Options = function() {