$params = [
'action' => $action,
'where' => [['name', 'IN', $namesToMatch]],
- 'select' => ['name', 'label', 'input_type', 'input_attrs', 'help_pre', 'help_post', 'options', 'entity', 'fk_entity'],
+ 'select' => ['name', '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,
$params['values'] = ['contact_type' => $entityName];
$entityName = 'Contact';
}
- $fields = civicrm_api4($entityName, 'getFields', $params);
- $field = $originalField = $fields->first();
+ foreach (civicrm_api4($entityName, 'getFields', $params) as $field) {
+ // In the highly unlikely event of 2 fields returned, prefer the exact match
+ if ($field['name'] === $fieldName) {
+ break;
+ }
+ }
// If this is an implicit join, get new field from fk entity
if ($field['name'] !== $fieldName && $field['fk_entity']) {
$params['where'] = [['name', '=', substr($fieldName, 1 + strrpos($fieldName, '.'))]];
+ $originalField = $field;
$field = civicrm_api4($field['fk_entity'], 'getFields', $params)->first();
if ($field) {
$field['label'] = $originalField['label'] . ' ' . $field['label'];
*/
protected $blocks = [];
+ /**
+ * @var array
+ */
+ protected $searchDisplays = [];
+
/**
* @var array
* Ex: $secureApi4s['spouse'] = function($entity, $action, $params){...};
* @param array $nodes
* @param string $entity
* @param string $join
+ * @param string $searchDisplay
*/
- protected function parseFields($nodes, $entity = NULL, $join = NULL) {
+ protected function parseFields($nodes, $entity = NULL, $join = NULL, $searchDisplay = NULL) {
foreach ($nodes as $node) {
if (!is_array($node) || !isset($node['#tag'])) {
continue;
}
- elseif (!empty($node['af-fieldset']) && !empty($node['#children'])) {
- $this->parseFields($node['#children'], $node['af-fieldset'], $join);
+ elseif (isset($node['af-fieldset']) && !empty($node['#children'])) {
+ $searchDisplay = $node['af-fieldset'] ? NULL : $this->findSearchDisplay($node);
+ $this->parseFields($node['#children'], $node['af-fieldset'], $join, $searchDisplay);
+ }
+ elseif ($searchDisplay && $node['#tag'] === 'af-field') {
+ $this->searchDisplays[$searchDisplay]['fields'][$node['name']] = AHQ::getProps($node);
}
elseif ($entity && $node['#tag'] === 'af-field') {
if ($join) {
$this->parseFields($node['#children'] ?? [], $entity, $node['af-join']);
}
elseif (!empty($node['#children'])) {
- $this->parseFields($node['#children'], $entity, $join);
+ $this->parseFields($node['#children'], $entity, $join, $searchDisplay);
}
// Recurse into embedded blocks
if (isset($this->blocks[$node['#tag']])) {
$this->blocks[$node['#tag']] = Afform::get()->setCheckPermissions(FALSE)->setSelect(['name', 'layout'])->addWhere('name', '=', $this->blocks[$node['#tag']]['name'])->execute()->first();
}
if (!empty($this->blocks[$node['#tag']]['layout'])) {
- $this->parseFields($this->blocks[$node['#tag']]['layout'], $entity, $join);
+ $this->parseFields($this->blocks[$node['#tag']]['layout'], $entity, $join, $searchDisplay);
}
}
}
}
+ /**
+ * Finds a search display within a fieldset
+ *
+ * @param array $node
+ */
+ public function findSearchDisplay($node) {
+ foreach (\Civi\Search\Display::getDisplayTypes(['name']) as $displayType) {
+ foreach (AHQ::getTags($node, $displayType['name']) as $display) {
+ $this->searchDisplays[$display['display-name']]['searchName'] = $display['search-name'];
+ return $display['display-name'];
+ }
+ }
+ }
+
/**
* @return array[]
* Ex: $entities['spouse']['type'] = 'Contact';
return $this->entities[$entityName] ?? NULL;
}
+ /**
+ * @return array
+ */
+ public function getSearchDisplay($displayName) {
+ return $this->searchDisplays[$displayName] ?? NULL;
+ }
+
}
--- /dev/null
+<?php
+
+namespace Civi\Api4\Action\Afform;
+
+use Civi\Api4\SavedSearch;
+use Civi\Api4\Utils\FormattingUtil;
+
+/**
+ * Loads option values for a form field
+ *
+ * @method $this setFieldName(string $fieldName)
+ * @method $this setModelName(string $modelName)
+ * @method $this setJoinEntity(string $joinEntity)
+ * @method $this setValues(array $values)
+ * @method string getFieldName()
+ * @method string getModelName()
+ * @method string getJoinEntity()
+ * @method array getValues()
+ * @package Civi\Api4\Action\Afform
+ */
+class GetOptions extends AbstractProcessor {
+
+ /**
+ * @var string
+ * @required
+ */
+ protected $modelName;
+
+ /**
+ * @var string
+ * @required
+ */
+ protected $fieldName;
+
+ /**
+ * @var string
+ */
+ protected $joinEntity;
+
+ /**
+ * @var array
+ */
+ protected $values;
+
+ /**
+ * @return array
+ * @throws \API_Exception
+ */
+ protected function processForm() {
+ $formEntity = $this->_formDataModel->getEntity($this->modelName);
+ $searchDisplay = $this->_formDataModel->getSearchDisplay($this->modelName);
+ $fieldName = $this->fieldName;
+
+ // For data-entry forms
+ if ($formEntity) {
+ $entity = $this->joinEntity ?: $formEntity['type'];
+ if ($this->joinEntity && !isset($formEntity['joins'][$this->joinEntity]['fields'][$this->fieldName])) {
+ throw new \API_Exception('Cannot get options for field not present on form');
+ }
+ elseif (!$this->joinEntity && !isset($formEntity['fields'][$this->fieldName])) {
+ throw new \API_Exception('Cannot get options for field not present on form');
+ }
+ }
+ // For search forms, get entity from savedSearch api params
+ elseif ($searchDisplay) {
+ if (!isset($searchDisplay['fields'][$this->fieldName])) {
+ throw new \API_Exception('Cannot get options for field not present on form');
+ }
+ $savedSearch = SavedSearch::get(FALSE)
+ ->addWhere('name', '=', $searchDisplay['searchName'])
+ ->addSelect('api_entity', 'api_params')
+ ->execute()->single();
+ // If field is not prefixed with a join, it's from the main entity
+ $entity = $savedSearch['api__entity'];
+ // Check to see if field belongs to a join
+ foreach ($savedSearch['api_params']['join'] ?? [] as $join) {
+ [$joinEntity, $joinAlias] = array_pad(explode(' AS ', $join[0]), 2, '');
+ if (strpos($fieldName, $joinAlias . '.') === 0) {
+ $entity = $joinEntity;
+ $fieldName = substr($fieldName, strlen($joinAlias) + 1);
+ }
+ }
+ }
+
+ return civicrm_api4($entity, 'getFields', [
+ 'checkPermissions' => FALSE,
+ 'where' => [['name', '=', $fieldName]],
+ 'select' => ['options'],
+ 'loadOptions' => ['id', 'label'],
+ 'values' => FormattingUtil::filterByPrefix($this->values, $this->fieldName, $fieldName),
+ ], 0)['options'] ?: [];
+ }
+
+ protected function loadEntities() {
+ // Do nothing; this action doesn't need entity data
+ }
+
+}
->setCheckPermissions($checkPermissions);
}
+ /**
+ * @param bool $checkPermissions
+ * @return Action\Afform\GetOptions
+ */
+ public static function getOptions($checkPermissions = TRUE) {
+ return (new Action\Afform\GetOptions('Afform', __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
/**
* @param bool $checkPermissions
* @return Generic\BasicBatchAction
"default" => ["administer CiviCRM"],
// These all check form-level permissions
'get' => [],
+ 'getOptions' => [],
'prefill' => [],
'submit' => [],
'submitFile' => [],
*/
function afform_civicrm_alterApiRoutePermissions(&$permissions, $entity, $action) {
if ($entity == 'Afform') {
- if ($action == 'prefill' || $action == 'submit' || $action == 'submitFile') {
+ // These actions should be accessible to anonymous users; permissions are checked internally
+ $allowedActions = ['prefill', 'submit', 'submitFile', 'getOptions'];
+ if (in_array($action, $allowedActions, TRUE)) {
$permissions = CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION;
}
}
$scope.dataProvider.getFieldData()[ctrl.fieldName] = '';
}
}
- if (val) {
+ if (val && (typeof val === 'number' || val.length)) {
$('input[crm-ui-select]', $element).addClass('loading').prop('disabled', true);
var params = {
- where: [['name', '=', ctrl.defn.name]],
- select: ['options'],
- loadOptions: ['id', 'label'],
- values: {}
+ name: ctrl.afFieldset.getFormName(),
+ modelName: ctrl.afFieldset.getName(),
+ fieldName: ctrl.fieldName,
+ joinEntity: ctrl.afJoin ? ctrl.afJoin.entity : null,
+ values: $scope.dataProvider.getFieldData()
};
- params.values[ctrl.defn.input_attrs.control_field] = val;
- crmApi4(ctrl.defn.entity, 'getFields', params, 0)
+ crmApi4('Afform', 'getOptions', params)
.then(function(data) {
- $('input[crm-ui-select]', $element).removeClass('loading').prop('disabled', false);
- chainSelectOptions = data.options;
+ $('input[crm-ui-select]', $element).removeClass('loading').prop('disabled', !data.length);
+ chainSelectOptions = data;
validateValue();
});
} else {
return data[0].fields;
};
this.getFormName = function() {
- return ctrl.afFormCtrl ? ctrl.afFormCtrl.getMeta().name : $scope.meta.name;
+ return ctrl.afFormCtrl ? ctrl.afFormCtrl.getFormMeta().name : $scope.meta.name;
};
}
};
<fieldset af-fieldset="me">
<af-field name="first_name" />
<af-field name="last_name" />
+ <div af-join="Address" min="1" af-repeat="Add">
+ <afblock-contact-address></afblock-contact-address>
+ </div>
</fieldset>
</af-form>
EOHTML;
$this->assertEquals('Lasty', $contact['last_name']);
}
+ public function testChainSelect(): void {
+ $this->useValues([
+ 'layout' => self::$layouts['aboutMe'],
+ 'permission' => CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION,
+ ]);
+
+ // Get states for USA
+ $result = Civi\Api4\Afform::getOptions()
+ ->setName($this->formName)
+ ->setModelName('me')
+ ->setFieldName('state_province_id')
+ ->setJoinEntity('Address')
+ ->setValues(['country_id' => CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Country', 'United States', 'id', 'name')])
+ ->execute();
+ $this->assertEquals('Alabama', $result[0]['label']);
+
+ // Get states for UK
+ $result = Civi\Api4\Afform::getOptions()
+ ->setName($this->formName)
+ ->setModelName('me')
+ ->setFieldName('state_province_id')
+ ->setJoinEntity('Address')
+ ->setValues(['country_id' => CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Country', 'United Kingdom', 'id', 'name')])
+ ->execute();
+ $this->assertEquals('Aberdeen City', $result[0]['label']);
+ }
+
public function testCheckEntityReferenceFieldsReplacement(): void {
$this->useValues([
'layout' => self::$layouts['registerSite'],