3 namespace Civi\Api4\Action\SearchDisplay
;
5 use Civi\API\Exception\UnauthorizedException
;
6 use Civi\Api4\Generic\Traits\ArrayQueryActionTrait
;
7 use Civi\Api4\Query\SqlField
;
8 use Civi\Api4\SearchDisplay
;
9 use Civi\Api4\Utils\CoreUtil
;
12 * Base class for running a search.
14 * @method $this setDisplay(array|string $display)
15 * @method array|string|null getDisplay()
16 * @method $this setSort(array $sort)
17 * @method array getSort()
18 * @method $this setFilters(array $filters)
19 * @method array getFilters()
20 * @method $this setSeed(string $seed)
21 * @method string getSeed()
22 * @method $this setAfform(string $afform)
23 * @method string getAfform()
24 * @package Civi\Api4\Action\SearchDisplay
26 abstract class AbstractRunAction
extends \Civi\Api4\Generic\AbstractAction
{
28 use SavedSearchInspectorTrait
;
31 * Either the name of the display or an array containing the display definition (for preview mode)
33 * Leave NULL to use the autogenerated default.
35 * @var string|array|null
40 * Array of fields to use for ordering the results
46 * Search conditions that will be automatically added to the WHERE or HAVING clauses
49 protected $filters = [];
52 * Integer used as a seed when ordering by RAND().
53 * This keeps the order stable enough to use a pager with random sorting.
60 * Name of Afform, if this display is embedded (used for permissioning)
71 * @param \Civi\Api4\Generic\Result $result
72 * @throws UnauthorizedException
73 * @throws \API_Exception
75 public function _run(\Civi\Api4\Generic\Result
$result) {
76 // Only administrators can use this in unsecured "preview mode"
77 if ((is_array($this->savedSearch
) ||
is_array($this->display
)) && $this->checkPermissions
&& !\CRM_Core_Permission
::check('administer CiviCRM data')) {
78 throw new UnauthorizedException('Access denied');
80 $this->loadSavedSearch();
81 if (is_string($this->display
)) {
82 $this->display
= SearchDisplay
::get(FALSE)
83 ->setSelect(['*', 'type:name'])
84 ->addWhere('name', '=', $this->display
)
85 ->addWhere('saved_search_id', '=', $this->savedSearch
['id'])
86 ->execute()->single();
88 elseif (is_null($this->display
)) {
89 $this->display
= SearchDisplay
::getDefault(FALSE)
90 ->addSelect('*', 'type:name')
91 ->setSavedSearch($this->savedSearch
)
94 // Displays with acl_bypass must be embedded on an afform which the user has access to
96 $this->checkPermissions
&& !empty($this->display
['acl_bypass']) &&
97 !\CRM_Core_Permission
::check('all CiviCRM permissions and ACLs') && !$this->loadAfform()
99 throw new UnauthorizedException('Access denied');
102 $this->_apiParams
['checkPermissions'] = empty($this->display
['acl_bypass']);
103 $this->display
['settings']['columns'] = $this->display
['settings']['columns'] ??
[];
105 $this->processResult($result);
108 abstract protected function processResult(\Civi\Api4\Generic\Result
$result);
111 * Transforms each row into an array of raw data and an array of formatted columns
113 * @param \Civi\Api4\Generic\Result $result
114 * @return array{data: array, columns: array}[]
116 protected function formatResult(\Civi\Api4\Generic\Result
$result): array {
118 $keyName = CoreUtil
::getIdFieldName($this->savedSearch
['api_entity']);
119 foreach ($result as $index => $record) {
120 $data = $columns = [];
121 foreach ($this->getSelectClause() as $key => $item) {
122 $data[$key] = $this->getValue($key, $record, $index);
124 foreach ($this->display
['settings']['columns'] as $column) {
125 $columns[] = $this->formatColumn($column, $data);
127 $style = $this->getCssStyles($this->display
['settings']['cssRules'] ??
[], $data);
130 'columns' => $columns,
131 'cssClass' => implode(' ', $style),
133 if (isset($data[$keyName])) {
134 $row['key'] = $data[$keyName];
144 * @param int $rowIndex
147 private function getValue($key, $data, $rowIndex) {
148 // Get value from api result unless this is a pseudo-field which gets a calculated value
150 case 'result_row_num':
151 return $rowIndex +
1 +
($this->_apiParams
['offset'] ??
0);
153 case 'user_contact_id':
154 return \CRM_Core_Session
::getLoggedInContactID();
157 if (!empty($data[$key])) {
158 $item = $this->getSelectExpression($key);
159 if ($item['expr'] instanceof SqlField
&& $item['fields'][0]['fk_entity'] === 'File') {
160 return $this->generateFileUrl($data[$key]);
163 return $data[$key] ??
NULL;
168 * Convert file id to a readable url
172 * @throws \CRM_Core_Exception
174 private function generateFileUrl($fileID) {
175 $entityId = \CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_EntityFile',
180 $fileHash = \CRM_Core_BAO_File
::generateFileHash($entityId, $fileID);
181 return $this->getUrl('civicrm/file', [
192 * @return array{val: mixed, links: array, edit: array, label: string, title: string, image: array, cssClass: string}
194 private function formatColumn($column, $data) {
195 $column +
= ['rewrite' => NULL, 'label' => NULL];
197 switch ($column['type']) {
199 $rawValue = $data[$column['key']] ??
NULL;
200 if (!$this->hasValue($rawValue) && isset($column['empty_value'])) {
201 $out['val'] = $this->replaceTokens($column['empty_value'], $data, 'view');
203 elseif ($column['rewrite']) {
204 $out['val'] = $this->replaceTokens($column['rewrite'], $data, 'view');
207 $out['val'] = $this->formatViewValue($column['key'], $rawValue);
209 if ($this->hasValue($column['label']) && (!empty($column['forceLabel']) ||
$this->hasValue($out['val']))) {
210 $out['label'] = $this->replaceTokens($column['label'], $data, 'view');
212 if (!empty($column['link'])) {
213 $links = $this->formatFieldLinks($column, $data, $out['val']);
215 $out['links'] = $links;
218 elseif (!empty($column['editable']) && !$column['rewrite']) {
219 $edit = $this->formatEditableColumn($column, $data);
221 $out['edit'] = $edit;
227 $out['img'] = $this->formatImage($column, $data);
229 $out['val'] = $this->replaceTokens($column['image']['alt'] ??
NULL, $data, 'view');
231 if ($this->hasValue($column['label']) && (!empty($column['forceLabel']) ||
$out['img'])) {
232 $out['label'] = $this->replaceTokens($column['label'], $data, 'view');
234 if (!empty($column['link'])) {
235 $links = $this->formatFieldLinks($column, $data, '');
237 $out['links'] = $links;
245 $out = $this->formatLinksColumn($column, $data);
249 if (isset($column['title']) && strlen($column['title'])) {
250 $out['title'] = $this->replaceTokens($column['title'], $data, 'view');
252 $cssClass = $this->getCssStyles($column['cssRules'] ??
[], $data);
253 if (!empty($column['alignment'])) {
254 $cssClass[] = $column['alignment'];
257 $out['cssClass'] = implode(' ', $cssClass);
263 * Evaluates conditional style rules
265 * Rules are in the format ['css class', 'field_name', 'OPERATOR', 'value']
267 * @param array[] $styleRules
271 protected function getCssStyles(array $styleRules, array $data) {
273 foreach ($styleRules as $clause) {
274 $cssClass = $clause[0] ??
'';
276 $condition = $this->getCssRuleCondition($clause);
277 if (is_null($condition[0]) ||
(ArrayQueryActionTrait
::filterCompare($data, $condition))) {
278 $classes[] = $cssClass;
286 * Returns the condition of a cssRules
288 * @param array $clause
291 protected function getCssRuleCondition($clause) {
292 $fieldKey = $clause[1] ??
NULL;
293 // For fields used in group by, add aggregation and change operator from = to CONTAINS
294 // FIXME: This assumes the operator is always set to '=', which so far is all the admin UI supports.
295 // That's only a safe assumption as long as the admin UI doesn't have an operator selector.
296 // @see ang/crmSearchAdmin/displays/common/searchAdminCssRules.html
297 if ($fieldKey && $this->canAggregate($fieldKey)) {
298 $clause[2] = 'CONTAINS';
299 $fieldKey = 'GROUP_CONCAT_' . str_replace(['.', ':'], '_', $clause[1]);
301 return [$fieldKey, $clause[2] ??
'IS NOT EMPTY', $clause[3] ??
NULL];
305 * Return fields needed for the select clause by a set of css rules
307 * @param array $cssRules
310 protected function getCssRulesSelect($cssRules) {
312 foreach ($cssRules as $clause) {
313 $fieldKey = $clause[1] ??
NULL;
315 // For fields used in group by, add aggregation
316 $select[] = $this->canAggregate($fieldKey) ?
"GROUP_CONCAT($fieldKey) AS GROUP_CONCAT_" . str_replace(['.', ':'], '_', $fieldKey) : $fieldKey;
323 * Format a field value as links
327 * @return array{text: string, url: string, target: string}[]
329 private function formatFieldLinks($column, $data, $value): array {
331 foreach ((array) $value as $index => $val) {
332 $path = $this->getLinkPath($column['link'], $data, $index);
333 $path = $this->replaceTokens($path, $data, 'url', $index);
337 'url' => $this->getUrl($path),
339 if (!empty($column['link']['target'])) {
340 $link['target'] = $column['link']['target'];
349 * Format links for a menu/buttons/links column
350 * @param array $column
352 * @return array{text: string, url: string, target: string, style: string, icon: string}[]
354 private function formatLinksColumn($column, $data): array {
355 $out = ['links' => []];
356 if (isset($column['text'])) {
357 $out['text'] = $this->replaceTokens($column['text'], $data, 'view');
359 foreach ($column['links'] as $item) {
360 $path = $this->replaceTokens($this->getLinkPath($item, $data), $data, 'url');
363 'text' => $this->replaceTokens($item['text'] ??
'', $data, 'view'),
364 'url' => $this->getUrl($path),
366 foreach (['target', 'style', 'icon'] as $prop) {
367 if (!empty($item[$prop])) {
368 $link[$prop] = $item[$prop];
371 $out['links'][] = $link;
381 * @return string|null
383 private function getLinkPath($link, $data = NULL, $index = 0) {
384 $path = $link['path'] ??
NULL;
385 if (!$path && !empty($link['entity']) && !empty($link['action'])) {
386 $entity = $link['entity'];
387 $idField = $idKey = CoreUtil
::getIdFieldName($entity);
388 // Hack to support links to relationships
389 if ($entity === 'Relationship') {
390 $entity = 'RelationshipCache';
391 $idKey = 'relationship_id';
393 $path = CoreUtil
::getInfoItem($entity, 'paths')[$link['action']] ??
NULL;
395 if ($path && !empty($link['join'])) {
396 $prefix = $link['join'] . '.';
398 // This is a bit clunky, the function_join_field gets un-munged later by $this->getJoinFromAlias()
399 if ($this->canAggregate($prefix . $idKey)) {
400 $prefix = 'GROUP_CONCAT_' . str_replace('.', '_', $prefix);
403 $path = str_replace('[', '[' . $prefix, $path);
405 // Check access for edit/update links
406 // (presumably if a record is shown in SearchKit the user already has view access, and the check is expensive)
407 if ($path && isset($data) && $link['action'] !== 'view') {
408 $id = $data[$prefix . $idKey] ??
NULL;
409 $id = is_array($id) ?
$id[$index] ??
NULL : $id;
411 $access = civicrm_api4($link['entity'], 'checkAccess', [
412 'action' => $link['action'],
427 * @param string $path
428 * @param array $query
431 private function getUrl(string $path, $query = NULL) {
432 if ($path[0] === '/' ||
strpos($path, 'http://') ||
strpos($path, 'https://')) {
435 // Use absolute urls when downloading spreadsheet
436 $absolute = $this->getActionName() === 'download';
437 return \CRM_Utils_System
::url($path, $query, $absolute, NULL, FALSE);
443 * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, fk_entity: string, value_key: string, record: array, value: mixed}|null
445 private function formatEditableColumn($column, $data) {
446 $editable = $this->getEditableInfo($column['key']);
447 if (!empty($data[$editable['id_path']])) {
448 $access = civicrm_api4($editable['entity'], 'checkAccess', [
449 'action' => 'update',
451 $editable['id_key'] => $data[$editable['id_path']],
457 $editable['record'] = [
458 $editable['id_key'] => $data[$editable['id_path']],
460 $editable['value'] = $data[$editable['value_path']];
461 \CRM_Utils_Array
::remove($editable, 'id_key', 'id_path', 'value_path');
469 * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, fk_entity: string, value_key: string, value_path: string, id_key: string, id_path: string}|null
471 private function getEditableInfo($key) {
472 [$key] = explode(':', $key);
473 $field = $this->getField($key);
474 // If field is an implicit join, use the original fk field
475 if (!empty($field['implicit_join'])) {
476 return $this->getEditableInfo(substr($key, 0, -1 - strlen($field['name'])));
479 $idKey = CoreUtil
::getIdFieldName($field['entity']);
480 $idPath = ($field['explicit_join'] ?
$field['explicit_join'] . '.' : '') . $idKey;
481 // Hack to support editing relationships
482 if ($field['entity'] === 'RelationshipCache') {
483 $field['entity'] = 'Relationship';
484 $idPath = ($field['explicit_join'] ?
$field['explicit_join'] . '.' : '') . 'relationship_id';
487 'entity' => $field['entity'],
488 'input_type' => $field['input_type'],
489 'data_type' => $field['data_type'],
490 'options' => !empty($field['options']),
491 'serialize' => !empty($field['serialize']),
492 'fk_entity' => $field['fk_entity'],
493 'value_key' => $field['name'],
494 'value_path' => $key,
496 'id_path' => $idPath,
505 * @return array{url: string, width: int, height: int}|NULL
507 private function formatImage($column, $data) {
508 $tokenExpr = $column['rewrite'] ?
: '[' . $column['key'] . ']';
509 $url = $this->replaceTokens($tokenExpr, $data, 'url');
510 if (!$url && !empty($column['empty_value'])) {
511 $url = $this->replaceTokens($column['empty_value'], $data, 'url');
518 'height' => $column['image']['height'] ??
NULL,
519 'width' => $column['image']['width'] ??
NULL,
524 * @param string $tokenExpr
526 * @param string $format view|raw|url
530 private function replaceTokens($tokenExpr, $data, $format, $index = 0) {
532 foreach ($this->getTokens($tokenExpr) as $token) {
533 $val = $data[$token] ??
NULL;
534 if (isset($val) && $format === 'view') {
535 $val = $this->formatViewValue($token, $val);
537 $replacement = is_array($val) ?
$val[$index] ??
'' : $val;
538 // A missing token value in a url invalidates it
539 if ($format === 'url' && (!isset($replacement) ||
$replacement === '')) {
542 $tokenExpr = str_replace('[' . $token . ']', $replacement, $tokenExpr);
549 * Format raw field value according to data type
551 * @param mixed $rawValue
552 * @return array|string
554 protected function formatViewValue($key, $rawValue) {
555 if (is_array($rawValue)) {
556 return array_map(function($val) use ($key) {
557 return $this->formatViewValue($key, $val);
561 $dataType = $this->getSelectExpression($key)['dataType'] ??
NULL;
563 $formatted = $rawValue;
567 if (is_bool($rawValue)) {
568 $formatted = $rawValue ?
ts('Yes') : ts('No');
573 $formatted = \CRM_Utils_Money
::format($rawValue);
578 $formatted = \CRM_Utils_Date
::customFormat($rawValue);
585 * Applies supplied filters to the where clause
587 protected function applyFilters() {
588 // Allow all filters that are included in SELECT clause or are fields on the Afform.
589 $allowedFilters = array_merge($this->getSelectAliases(), $this->getAfformFilters());
591 // Ignore empty strings
592 $filters = array_filter($this->filters
, [$this, 'hasValue']);
597 foreach ($filters as $key => $value) {
598 $fieldNames = explode(',', $key);
599 if (in_array($key, $allowedFilters, TRUE) ||
!array_diff($fieldNames, $allowedFilters)) {
600 $this->applyFilter($fieldNames, $value);
606 * Returns an array of field names or aliases + allowed suffixes from the SELECT clause
609 protected function getSelectAliases() {
611 $selectAliases = array_map(function($select) {
612 return array_slice(explode(' AS ', $select), -1)[0];
613 }, $this->savedSearch
['api_params']['select']);
614 foreach ($selectAliases as $alias) {
615 [$alias] = explode(':', $alias);
617 foreach (['name', 'label', 'abbr'] as $allowedSuffix) {
618 $result[] = $alias . ':' . $allowedSuffix;
625 * @param array $fieldNames
626 * If multiple field names are given they will be combined in an OR clause
627 * @param mixed $value
629 private function applyFilter(array $fieldNames, $value) {
630 // Global setting determines if % wildcard should be added to both sides (default) or only the end of a search string
631 $prefixWithWildcard = \Civi
::settings()->get('includeWildCardInName');
633 // Based on the first field, decide which clause to add this condition to
634 $fieldName = $fieldNames[0];
635 $field = $this->getField($fieldName);
636 // If field is not found it must be an aggregated column & belongs in the HAVING clause.
638 $clause =& $this->_apiParams
['having'];
640 // If field belongs to an EXCLUDE join, it should be added as a join condition
642 $prefix = strpos($fieldName, '.') ?
explode('.', $fieldName)[0] : NULL;
643 foreach ($this->_apiParams
['join'] as $idx => $join) {
644 if (($join[1] ??
'LEFT') === 'EXCLUDE' && (explode(' AS ', $join[0])[1] ??
'') === $prefix) {
645 $clause =& $this->_apiParams
['join'][$idx];
649 // Default: add filter to WHERE clause
650 if (!isset($clause)) {
651 $clause =& $this->_apiParams
['where'];
656 foreach ($fieldNames as $fieldName) {
657 $field = $this->getField($fieldName);
658 $dataType = $field['data_type'] ??
NULL;
659 // Array is either associative `OP => VAL` or sequential `IN (...)`
660 if (is_array($value)) {
661 $value = array_filter($value, [$this, 'hasValue']);
662 // If array does not contain operators as keys, assume array of values
663 if (array_diff_key($value, array_flip(CoreUtil
::getOperators()))) {
664 // Use IN for regular fields
665 if (empty($field['serialize'])) {
666 $filterClauses[] = [$fieldName, 'IN', $value];
668 // Use an OR group of CONTAINS for array fields
671 foreach ($value as $val) {
672 $orGroup[] = [$fieldName, 'CONTAINS', $val];
674 $filterClauses[] = ['OR', $orGroup];
677 // Operator => Value array
680 foreach ($value as $operator => $val) {
681 $andGroup[] = [$fieldName, $operator, $val];
683 $filterClauses[] = ['AND', $andGroup];
686 elseif (!empty($field['serialize'])) {
687 $filterClauses[] = [$fieldName, 'CONTAINS', $value];
689 elseif (!empty($field['options']) ||
in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) {
690 $filterClauses[] = [$fieldName, '=', $value];
692 elseif ($prefixWithWildcard) {
693 $filterClauses[] = [$fieldName, 'CONTAINS', $value];
696 $filterClauses[] = [$fieldName, 'LIKE', $value . '%'];
700 if (count($filterClauses) === 1) {
701 $clause[] = $filterClauses[0];
704 $clause[] = ['OR', $filterClauses];
709 * Transforms the SORT param (which is expected to be an array of arrays)
710 * to the ORDER BY clause (which is an associative array of [field => DIR]
714 protected function getOrderByFromSort() {
715 // Drag-sortable tables have a forced order
716 if (!empty($this->display
['settings']['draggable'])) {
717 return [$this->display
['settings']['draggable'] => 'ASC'];
720 $defaultSort = $this->display
['settings']['sort'] ??
[];
721 $currentSort = $this->sort
;
723 // Verify requested sort corresponds to sortable columns
724 foreach ($this->sort
as $item) {
725 $column = array_column($this->display
['settings']['columns'], NULL, 'key')[$item[0]] ??
NULL;
726 if (!$column ||
(isset($column['sortable']) && !$column['sortable'])) {
732 foreach ($currentSort ?
: $defaultSort as $item) {
733 // Apply seed to random sorting
734 if ($item[0] === 'RAND()' && isset($this->seed
)) {
735 $item[0] = 'RAND(' . $this->seed
. ')';
737 $orderBy[$item[0]] = $item[1];
743 * Adds additional fields to the select clause required to render the display
745 * @param array $apiParams
747 protected function augmentSelectClause(&$apiParams): void
{
748 $existing = array_map(function($item) {
749 return explode(' AS ', $item)[1] ??
$item;
750 }, $apiParams['select']);
752 // Add primary key field if actions are enabled
753 if (!empty($this->display
['settings']['actions']) ||
!empty($this->display
['settings']['draggable'])) {
754 $additions = CoreUtil
::getInfoItem($this->savedSearch
['api_entity'], 'primary_key');
756 // Add draggable column (typically "weight")
757 if (!empty($this->display
['settings']['draggable'])) {
758 $additions[] = $this->display
['settings']['draggable'];
760 // Add style conditions for the display
761 foreach ($this->getCssRulesSelect($this->display
['settings']['cssRules'] ??
[]) as $addition) {
762 $additions[] = $addition;
764 $possibleTokens = '';
765 foreach ($this->display
['settings']['columns'] as $column) {
766 // Collect display values in which a token is allowed
767 $possibleTokens .= ($column['rewrite'] ??
'');
768 $possibleTokens .= ($column['title'] ??
'');
769 $possibleTokens .= ($column['empty_value'] ??
'');
770 if (!empty($column['link'])) {
771 $possibleTokens .= $this->getLinkPath($column['link']) ??
'';
773 foreach ($column['links'] ??
[] as $link) {
774 $possibleTokens .= $link['text'] ??
'';
775 $possibleTokens .= $this->getLinkPath($link) ??
'';
778 // Select id & value for in-place editing
779 if (!empty($column['editable'])) {
780 $editable = $this->getEditableInfo($column['key']);
782 $additions[] = $editable['value_path'];
783 $additions[] = $editable['id_path'];
786 // Add style conditions for the column
787 foreach ($this->getCssRulesSelect($column['cssRules'] ??
[]) as $addition) {
788 $additions[] = $addition;
791 // Add fields referenced via token
792 $tokens = $this->getTokens($possibleTokens);
793 // Only add fields not already in SELECT clause
794 $additions = array_diff(array_merge($additions, $tokens), $existing);
795 // Tokens for aggregated columns start with 'GROUP_CONCAT_'
796 foreach ($additions as $index => $alias) {
797 if (strpos($alias, 'GROUP_CONCAT_') === 0) {
798 $additions[$index] = 'GROUP_CONCAT(' . $this->getJoinFromAlias(explode('_', $alias, 3)[2]) . ') AS ' . $alias;
801 $this->_selectClause
= NULL;
802 $apiParams['select'] = array_unique(array_merge($apiParams['select'], $additions));
808 private function getTokens($str) {
810 preg_match_all('/\\[([^]]+)\\]/', $str, $tokens);
811 return array_unique($tokens[1]);
815 * Given an alias like Contact_Email_01_location_type_id
816 * this will return Contact_Email_01.location_type_id
817 * @param string $alias
820 protected function getJoinFromAlias(string $alias) {
822 foreach ($this->_apiParams
['join'] as $join) {
823 $joinName = explode(' AS ', $join[0])[1];
824 if (strpos($alias, $joinName) === 0) {
825 $parsed = $joinName . '.' . substr($alias, strlen($joinName) +
1);
826 // Ensure we are using the longest match
827 if (strlen($parsed) > strlen($result)) {
832 return $result ?
: $alias;
836 * Checks if a filter contains a non-empty value
838 * "Empty" search values are [], '', and NULL.
839 * Also recursively checks arrays to ensure they contain at least one non-empty value.
844 private function hasValue($value) {
845 return $value !== '' && $value !== NULL && (!is_array($value) ||
array_filter($value, [$this, 'hasValue']));
849 * Returns a list of filter fields and directive filters
851 * Automatically applies directive filters
855 private function getAfformFilters() {
856 $afform = $this->loadAfform();
860 // Get afform field filters
861 $filterKeys = array_column(\CRM_Utils_Array
::findAll(
862 $afform['layout'] ??
[],
863 ['#tag' => 'af-field']
865 // Get filters passed into search display directive from Afform markup
866 $filterAttr = $afform['searchDisplay']['filters'] ??
NULL;
867 if ($filterAttr && is_string($filterAttr) && $filterAttr[0] === '{') {
868 foreach (\CRM_Utils_JS
::decode($filterAttr) as $filterKey => $filterVal) {
869 // Automatically apply filters from the markup if they have a value
870 if ($filterVal !== NULL) {
871 unset($this->filters
[$filterKey]);
872 if ($this->hasValue($filterVal)) {
873 $this->applyFilter(explode(',', $filterKey), $filterVal);
876 // If it's a javascript variable it will have come back from decode() as NULL;
877 // whitelist it to allow it to be passed to this api from javascript.
879 $filterKeys[] = $filterKey;
887 * Return afform with name specified in api call.
889 * Verifies the searchDisplay is embedded in the afform and the user has permission to view it.
891 * @return array|false|null
893 private function loadAfform() {
894 // Only attempt to load afform once.
895 if ($this->afform
&& !isset($this->_afform
)) {
896 $this->_afform
= FALSE;
897 // Permission checks are enabled in this api call to ensure the user has permission to view the form
898 $afform = \Civi\Api4\Afform
::get()
899 ->addWhere('name', '=', $this->afform
)
900 ->setLayoutFormat('shallow')
901 ->execute()->first();
902 // Validate that the afform contains this search display
903 $afform['searchDisplay'] = \CRM_Utils_Array
::findAll(
904 $afform['layout'] ??
[],
905 ['#tag' => "{$this->display['type:name']}", 'display-name' => $this->display
['name']]
907 if ($afform['searchDisplay']) {
908 $this->_afform
= $afform;
911 return $this->_afform
;
915 * Extra calculated fields provided by SearchKit
918 public static function getPseudoFields(): array {
921 'name' => 'result_row_num',
922 'fieldName' => 'result_row_num',
923 'title' => ts('Row Number'),
924 'label' => ts('Row Number'),
925 'description' => ts('Index of each row, starting from 1 on the first page'),
927 'data_type' => 'Integer',
931 'name' => 'user_contact_id',
932 'fieldName' => 'result_row_num',
933 'title' => ts('Current User ID'),
934 'label' => ts('Current User ID'),
935 'description' => ts('Contact ID of the current user if logged in'),
937 'data_type' => 'Integer',