APIv4 action to supply data to the new autocomplete widget.
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Generic;
+
+use Civi\Api4\Utils\CoreUtil;
+
+/**
+ * Retrieve $ENTITIES for an autocomplete form field.
+ */
+class AutocompleteAction extends AbstractAction {
+ use Traits\SavedSearchInspectorTrait;
+
+ /**
+ * Autocomplete search input for search mode
+ *
+ * @var string
+ */
+ protected $input = '';
+
+ /**
+ * Array of ids for render mode
+ *
+ * @var array
+ */
+ protected $ids;
+
+ /**
+ * @var int
+ */
+ protected $page = 1;
+
+ /**
+ * Name of SavedSearch to use for filtering.
+ * @var string
+ */
+ protected $savedSearch;
+
+ /**
+ * @var string
+ */
+ protected $formName;
+
+ /**
+ * @var string
+ */
+ protected $fieldName;
+
+ /**
+ * Fetch results.
+ *
+ * @param \Civi\Api4\Generic\Result $result
+ */
+ public function _run(Result $result) {
+ $entityName = $this->getEntityName();
+ $fields = CoreUtil::getApiClass($entityName)::get()->entityFields();
+ $idField = CoreUtil::getIdFieldName($entityName);
+ $labelField = CoreUtil::getInfoItem($entityName, 'label_field');
+ $map = [
+ 'id' => $idField,
+ 'text' => $labelField,
+ ];
+ // FIXME: Use metadata
+ if (isset($fields['description'])) {
+ $map['description'] = 'description';
+ }
+ if (isset($fields['color'])) {
+ $map['color'] = 'color';
+ }
+ if (isset($fields['icon'])) {
+ $map['icon'] = 'icon';
+ }
+
+ if (!$this->savedSearch) {
+ $this->savedSearch = ['api_entity' => $entityName];
+ }
+ $this->loadSavedSearch();
+ // Render mode: fetch by id
+ if ($this->ids) {
+ $this->_apiParams['where'][] = [$idField, 'IN', $this->ids];
+ $resultsPerPage = NULL;
+ }
+ // Search mode: fetch a page of results based on input
+ else {
+ $resultsPerPage = \Civi::settings()->get('search_autocomplete_count') ?: 10;
+ // Adding one extra result allows us to see if there are any more
+ $this->_apiParams['limit'] = $resultsPerPage + 1;
+ $this->_apiParams['offset'] = ($this->page - 1) * $resultsPerPage;
+ if (strlen($this->input)) {
+ $prefix = \Civi::settings()->get('includeWildCardInName') ? '%' : '';
+ $this->_apiParams['where'][] = [$labelField, 'LIKE', $prefix . $this->input . '%'];
+ }
+ }
+ if (empty($this->_apiParams['having'])) {
+ $this->_apiParams['select'] = array_values($map);
+ }
+ else {
+ $this->_apiParams['select'] = array_merge($this->_apiParams['select'], array_values($map));
+ }
+ $this->_apiParams['checkPermissions'] = $this->getCheckPermissions();
+ $apiResult = civicrm_api4($entityName, 'get', $this->_apiParams);
+ $rawResults = array_slice((array) $apiResult, 0, $resultsPerPage);
+ foreach ($rawResults as $row) {
+ $mapped = [];
+ foreach ($map as $key => $fieldName) {
+ $mapped[$key] = $row[$fieldName];
+ }
+ $result[] = $mapped;
+ }
+ $result->setCountMatched($apiResult->countFetched());
+ }
+
+}
->setCheckPermissions($checkPermissions);
}
+ /**
+ * @param bool $checkPermissions
+ * @return AutocompleteAction
+ */
+ public static function autocomplete($checkPermissions = TRUE) {
+ return (new AutocompleteAction(static::getEntityName(), __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
/**
* @inheritDoc
*/
->setCheckPermissions($checkPermissions);
}
+ /**
+ * @param bool $checkPermissions
+ * @return AutocompleteAction
+ */
+ public static function autocomplete($checkPermissions = TRUE) {
+ return (new AutocompleteAction(static::getEntityName(), __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
}
<?php
-namespace Civi\Api4\Action\SearchDisplay;
+namespace Civi\Api4\Generic\Traits;
use Civi\API\Request;
use Civi\Api4\Query\SqlExpression;
return !in_array($idField, $apiParams['groupBy']);
}
+ /**
+ * @param array $fieldNames
+ * If multiple field names are given they will be combined in an OR clause
+ * @param mixed $value
+ */
+ protected function applyFilter(array $fieldNames, $value) {
+ // Global setting determines if % wildcard should be added to both sides (default) or only the end of a search string
+ $prefixWithWildcard = \Civi::settings()->get('includeWildCardInName');
+
+ // Based on the first field, decide which clause to add this condition to
+ $fieldName = $fieldNames[0];
+ $field = $this->getField($fieldName);
+ // If field is not found it must be an aggregated column & belongs in the HAVING clause.
+ if (!$field) {
+ $this->_apiParams += ['having' => []];
+ $clause =& $this->_apiParams['having'];
+ }
+ // If field belongs to an EXCLUDE join, it should be added as a join condition
+ else {
+ $prefix = strpos($fieldName, '.') ? explode('.', $fieldName)[0] : NULL;
+ foreach ($this->_apiParams['join'] ?? [] as $idx => $join) {
+ if (($join[1] ?? 'LEFT') === 'EXCLUDE' && (explode(' AS ', $join[0])[1] ?? '') === $prefix) {
+ $clause =& $this->_apiParams['join'][$idx];
+ }
+ }
+ }
+ // Default: add filter to WHERE clause
+ if (!isset($clause)) {
+ $clause =& $this->_apiParams['where'];
+ }
+
+ $filterClauses = [];
+
+ foreach ($fieldNames as $fieldName) {
+ $field = $this->getField($fieldName);
+ $dataType = $field['data_type'] ?? NULL;
+ // Array is either associative `OP => VAL` or sequential `IN (...)`
+ if (is_array($value)) {
+ $value = array_filter($value, [$this, 'hasValue']);
+ // If array does not contain operators as keys, assume array of values
+ if (array_diff_key($value, array_flip(CoreUtil::getOperators()))) {
+ // Use IN for regular fields
+ if (empty($field['serialize'])) {
+ $filterClauses[] = [$fieldName, 'IN', $value];
+ }
+ // Use an OR group of CONTAINS for array fields
+ else {
+ $orGroup = [];
+ foreach ($value as $val) {
+ $orGroup[] = [$fieldName, 'CONTAINS', $val];
+ }
+ $filterClauses[] = ['OR', $orGroup];
+ }
+ }
+ // Operator => Value array
+ else {
+ $andGroup = [];
+ foreach ($value as $operator => $val) {
+ $andGroup[] = [$fieldName, $operator, $val];
+ }
+ $filterClauses[] = ['AND', $andGroup];
+ }
+ }
+ elseif (!empty($field['serialize'])) {
+ $filterClauses[] = [$fieldName, 'CONTAINS', $value];
+ }
+ elseif (!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) {
+ $filterClauses[] = [$fieldName, '=', $value];
+ }
+ elseif ($prefixWithWildcard) {
+ $filterClauses[] = [$fieldName, 'CONTAINS', $value];
+ }
+ else {
+ $filterClauses[] = [$fieldName, 'LIKE', $value . '%'];
+ }
+ }
+ // Single field
+ if (count($filterClauses) === 1) {
+ $clause[] = $filterClauses[0];
+ }
+ else {
+ $clause[] = ['OR', $filterClauses];
+ }
+ }
+
+ /**
+ * Checks if a filter contains a non-empty value
+ *
+ * "Empty" search values are [], '', and NULL.
+ * Also recursively checks arrays to ensure they contain at least one non-empty value.
+ *
+ * @param $value
+ * @return bool
+ */
+ protected function hasValue($value) {
+ return $value !== '' && $value !== NULL && (!is_array($value) || array_filter($value, [$this, 'hasValue']));
+ }
+
}
namespace Civi\Api4\Action\SearchDisplay;
use Civi\API\Exception\UnauthorizedException;
-use Civi\Api4\Generic\Traits\ArrayQueryActionTrait;
use Civi\Api4\Query\SqlField;
use Civi\Api4\SearchDisplay;
use Civi\Api4\Utils\CoreUtil;
*/
abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
- use SavedSearchInspectorTrait;
- use ArrayQueryActionTrait;
+ use \Civi\Api4\Generic\Traits\SavedSearchInspectorTrait;
+ use \Civi\Api4\Generic\Traits\ArrayQueryActionTrait;
/**
* Either the name of the display or an array containing the display definition (for preview mode)
return $result;
}
- /**
- * @param array $fieldNames
- * If multiple field names are given they will be combined in an OR clause
- * @param mixed $value
- */
- private function applyFilter(array $fieldNames, $value) {
- // Global setting determines if % wildcard should be added to both sides (default) or only the end of a search string
- $prefixWithWildcard = \Civi::settings()->get('includeWildCardInName');
-
- // Based on the first field, decide which clause to add this condition to
- $fieldName = $fieldNames[0];
- $field = $this->getField($fieldName);
- // If field is not found it must be an aggregated column & belongs in the HAVING clause.
- if (!$field) {
- $this->_apiParams += ['having' => []];
- $clause =& $this->_apiParams['having'];
- }
- // If field belongs to an EXCLUDE join, it should be added as a join condition
- else {
- $prefix = strpos($fieldName, '.') ? explode('.', $fieldName)[0] : NULL;
- foreach ($this->_apiParams['join'] ?? [] as $idx => $join) {
- if (($join[1] ?? 'LEFT') === 'EXCLUDE' && (explode(' AS ', $join[0])[1] ?? '') === $prefix) {
- $clause =& $this->_apiParams['join'][$idx];
- }
- }
- }
- // Default: add filter to WHERE clause
- if (!isset($clause)) {
- $clause =& $this->_apiParams['where'];
- }
-
- $filterClauses = [];
-
- foreach ($fieldNames as $fieldName) {
- $field = $this->getField($fieldName);
- $dataType = $field['data_type'] ?? NULL;
- // Array is either associative `OP => VAL` or sequential `IN (...)`
- if (is_array($value)) {
- $value = array_filter($value, [$this, 'hasValue']);
- // If array does not contain operators as keys, assume array of values
- if (array_diff_key($value, array_flip(CoreUtil::getOperators()))) {
- // Use IN for regular fields
- if (empty($field['serialize'])) {
- $filterClauses[] = [$fieldName, 'IN', $value];
- }
- // Use an OR group of CONTAINS for array fields
- else {
- $orGroup = [];
- foreach ($value as $val) {
- $orGroup[] = [$fieldName, 'CONTAINS', $val];
- }
- $filterClauses[] = ['OR', $orGroup];
- }
- }
- // Operator => Value array
- else {
- $andGroup = [];
- foreach ($value as $operator => $val) {
- $andGroup[] = [$fieldName, $operator, $val];
- }
- $filterClauses[] = ['AND', $andGroup];
- }
- }
- elseif (!empty($field['serialize'])) {
- $filterClauses[] = [$fieldName, 'CONTAINS', $value];
- }
- elseif (!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) {
- $filterClauses[] = [$fieldName, '=', $value];
- }
- elseif ($prefixWithWildcard) {
- $filterClauses[] = [$fieldName, 'CONTAINS', $value];
- }
- else {
- $filterClauses[] = [$fieldName, 'LIKE', $value . '%'];
- }
- }
- // Single field
- if (count($filterClauses) === 1) {
- $clause[] = $filterClauses[0];
- }
- else {
- $clause[] = ['OR', $filterClauses];
- }
- }
-
/**
* Transforms the SORT param (which is expected to be an array of arrays)
* to the ORDER BY clause (which is an associative array of [field => DIR]
return $result ?: $alias;
}
- /**
- * Checks if a filter contains a non-empty value
- *
- * "Empty" search values are [], '', and NULL.
- * Also recursively checks arrays to ensure they contain at least one non-empty value.
- *
- * @param $value
- * @return bool
- */
- private function hasValue($value) {
- return $value !== '' && $value !== NULL && (!is_array($value) || array_filter($value, [$this, 'hasValue']));
- }
-
/**
* Returns a list of afform fields used as search filters
*
namespace Civi\Api4\Action\SearchDisplay;
+use Civi\Api4\Generic\Traits\SavedSearchInspectorTrait;
use Civi\Api4\SavedSearch;
use Civi\Api4\Utils\FormattingUtil;
use Civi\Search\Display;