From: Coleman Watts Date: Wed, 27 Jul 2022 22:56:44 +0000 (-0400) Subject: APIv4 - Add autocomplete action X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=8ba6997ee334fd1bcbc438b50780b8c743f861da;p=civicrm-core.git APIv4 - Add autocomplete action APIv4 action to supply data to the new autocomplete widget. --- diff --git a/Civi/Api4/Generic/AutocompleteAction.php b/Civi/Api4/Generic/AutocompleteAction.php new file mode 100644 index 0000000000..339245f184 --- /dev/null +++ b/Civi/Api4/Generic/AutocompleteAction.php @@ -0,0 +1,122 @@ +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()); + } + +} diff --git a/Civi/Api4/Generic/BasicEntity.php b/Civi/Api4/Generic/BasicEntity.php index 066ef04e7a..87ae77c443 100644 --- a/Civi/Api4/Generic/BasicEntity.php +++ b/Civi/Api4/Generic/BasicEntity.php @@ -136,6 +136,15 @@ abstract class BasicEntity extends AbstractEntity { ->setCheckPermissions($checkPermissions); } + /** + * @param bool $checkPermissions + * @return AutocompleteAction + */ + public static function autocomplete($checkPermissions = TRUE) { + return (new AutocompleteAction(static::getEntityName(), __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + /** * @inheritDoc */ diff --git a/Civi/Api4/Generic/DAOEntity.php b/Civi/Api4/Generic/DAOEntity.php index 3bec3919bb..ea7473e87a 100644 --- a/Civi/Api4/Generic/DAOEntity.php +++ b/Civi/Api4/Generic/DAOEntity.php @@ -90,4 +90,13 @@ abstract class DAOEntity extends AbstractEntity { ->setCheckPermissions($checkPermissions); } + /** + * @param bool $checkPermissions + * @return AutocompleteAction + */ + public static function autocomplete($checkPermissions = TRUE) { + return (new AutocompleteAction(static::getEntityName(), __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + } diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/SavedSearchInspectorTrait.php b/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php similarity index 60% rename from ext/search_kit/Civi/Api4/Action/SearchDisplay/SavedSearchInspectorTrait.php rename to Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php index 1f935a4619..aeb9648d5e 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/SavedSearchInspectorTrait.php +++ b/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php @@ -1,6 +1,6 @@ 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'])); + } + } diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index 5df9ef6d1e..69526ce04f 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -3,7 +3,6 @@ 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; @@ -26,8 +25,8 @@ use Civi\Api4\Utils\FormattingUtil; */ 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) @@ -828,91 +827,6 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { 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] @@ -1047,19 +961,6 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { 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 * diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/GetDefault.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/GetDefault.php index 56dc8967b9..2f387cfce7 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/GetDefault.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/GetDefault.php @@ -2,6 +2,7 @@ namespace Civi\Api4\Action\SearchDisplay; +use Civi\Api4\Generic\Traits\SavedSearchInspectorTrait; use Civi\Api4\SavedSearch; use Civi\Api4\Utils\FormattingUtil; use Civi\Search\Display;