3 namespace Civi\Api4\Action\SearchDisplay
;
5 use Civi\Api4\SavedSearch
;
6 use Civi\Api4\Utils\FormattingUtil
;
7 use Civi\Search\Display
;
8 use CRM_Search_ExtensionUtil
as E
;
9 use Civi\Api4\Query\SqlEquation
;
10 use Civi\Api4\Query\SqlExpression
;
11 use Civi\Api4\Query\SqlField
;
12 use Civi\Api4\Query\SqlFunction
;
13 use Civi\Api4\Query\SqlFunctionGROUP_CONCAT
;
14 use Civi\Api4\Utils\CoreUtil
;
15 use Civi\API\Exception\UnauthorizedException
;
18 * Return the default results table for a saved search.
20 * @package Civi\Api4\Action\SearchDisplay
22 class GetDefault
extends \Civi\Api4\Generic\AbstractAction
{
24 use SavedSearchInspectorTrait
;
25 use \Civi\Api4\Generic\Traits\ArrayQueryActionTrait
;
26 use \Civi\Api4\Generic\Traits\SelectParamTrait
;
29 * Either the name of the savedSearch or an array containing the savedSearch definition (for preview mode)
30 * @var string|array|null
32 protected $savedSearch;
40 * @param \Civi\Api4\Generic\Result $result
41 * @throws UnauthorizedException
42 * @throws \API_Exception
44 public function _run(\Civi\Api4\Generic\Result
$result) {
45 // Only administrators can use this in unsecured "preview mode"
46 if (is_array($this->savedSearch
) && $this->checkPermissions
&& !\CRM_Core_Permission
::check('administer CiviCRM data')) {
47 throw new UnauthorizedException('Access denied');
49 $this->loadSavedSearch();
50 $this->expandSelectClauseWildcards();
51 // Use label from saved search
52 $label = $this->savedSearch
['label'] ??
'';
53 // Fall back on entity title as label
54 if (!strlen($label) && !empty($this->savedSearch
['api_entity'])) {
55 $label = CoreUtil
::getInfoItem($this->savedSearch
['api_entity'], 'title_plural');
60 'saved_search_id' => $this->savedSearch
['id'] ??
NULL,
63 'acl_bypass' => FALSE,
66 'limit' => \Civi
::settings()->get('default_pager_size'),
67 'classes' => ['table', 'table-striped'],
70 'expose_limit' => TRUE,
76 // Supply default sort if no orderBy given in api params
77 if (!empty($this->savedSearch
['api_entity']) && empty($this->savedSearch
['api_params']['orderBy'])) {
78 $defaultSort = CoreUtil
::getInfoItem($this->savedSearch
['api_entity'], 'order_by');
80 $display['settings']['sort'][] = [$defaultSort, 'ASC'];
83 foreach ($this->getSelectClause() as $key => $clause) {
84 $display['settings']['columns'][] = $this->configureColumn($clause, $key);
86 $display['settings']['columns'][] = [
91 'style' => 'secondary-outline',
92 'alignment' => 'text-right',
93 'links' => $this->getLinksMenu(),
95 $fields = $this->entityFields();
96 // Allow implicit-join-style selection of saved search fields
97 if ($this->savedSearch
) {
98 $display +
= \CRM_Utils_Array
::prefixKeys($this->savedSearch
, 'saved_search_id.');
99 $fields +
= \CRM_Utils_Array
::prefixKeys(SavedSearch
::get()->entityFields(), 'saved_search_id.');
101 // Fill pseudoconstant keys with raw values for replacement
102 foreach ($this->select
as $fieldExpr) {
103 [$fieldName, $suffix] = array_pad(explode(':', $fieldExpr), 2, NULL);
104 if ($suffix && array_key_exists($fieldName, $display)) {
105 $display[$fieldExpr] = $display[$fieldName];
108 $results = [$display];
109 // Replace pseudoconstants
110 FormattingUtil
::formatOutputValues($results, $fields);
111 $result->exchangeArray($this->selectArray($results));
115 * @param array{fields: array, expr: SqlExpression, dataType: string} $clause
119 private function configureColumn($clause, $key) {
123 'sortable' => !empty($clause['fields']),
124 'label' => $this->getColumnLabel($clause['expr']),
126 $this->getColumnLink($col, $clause);
131 * @param \Civi\Api4\Query\SqlExpression $expr
134 private function getColumnLabel(SqlExpression
$expr) {
135 if ($expr instanceof SqlFunction
) {
137 foreach ($expr->getArgs() as $arg) {
138 foreach ($arg['expr'] ??
[] as $ex) {
139 $args[] = $this->getColumnLabel($ex);
142 return '(' . $expr->getTitle() . ')' . ($args ?
' ' . implode(',', array_filter($args)) : '');
144 if ($expr instanceof SqlEquation
) {
146 foreach ($expr->getArgs() as $arg) {
147 $args[] = $this->getColumnLabel($arg['expr']);
149 return '(' . implode(',', array_filter($args)) . ')';
151 elseif ($expr instanceof SqlField
) {
152 $field = $this->getField($expr->getExpr());
154 if (!empty($field['explicit_join'])) {
155 $label = $this->getJoinLabel($field['explicit_join']) . ': ';
157 if (!empty($field['implicit_join'])) {
158 $field = $this->getField(substr($expr->getAlias(), 0, -1 - strlen($field['name'])));
160 return $label . $field['label'];
168 * @param string $joinAlias
171 private function getJoinLabel($joinAlias) {
172 if (!isset($this->_joinMap
)) {
173 $this->_joinMap
= [];
174 $joinCount = [$this->savedSearch
['api_entity'] => 1];
175 foreach ($this->savedSearch
['api_params']['join'] ??
[] as $join) {
176 [$entityName, $alias] = explode(' AS ', $join[0]);
178 if (!empty($joinCount[$entityName])) {
179 $num = ' ' . (++
$joinCount[$entityName]);
182 $joinCount[$entityName] = 1;
184 $label = CoreUtil
::getInfoItem($entityName, 'title');
185 $this->_joinMap
[$alias] = $label . $num;
188 return $this->_joinMap
[$joinAlias];
193 * @param array{fields: array, expr: SqlExpression, dataType: string} $clause
195 private function getColumnLink(&$col, $clause) {
196 if ($clause['expr'] instanceof SqlField ||
$clause['expr'] instanceof SqlFunctionGROUP_CONCAT
) {
197 $field = $clause['fields'][0] ??
NULL;
199 CoreUtil
::getInfoItem($field['entity'], 'label_field') === $field['name'] &&
200 !empty(CoreUtil
::getInfoItem($field['entity'], 'paths')['view'])
203 'entity' => $field['entity'],
204 'join' => implode('.', array_filter([$field['explicit_join'], $field['implicit_join']])),
207 // Hack to support links to relationships
208 if ($col['link']['entity'] === 'RelationshipCache') {
209 $col['link']['entity'] = 'Relationship';
211 $col['title'] = E
::ts('View %1', [1 => CoreUtil
::getInfoItem($field['entity'], 'title')]);
219 private function getLinksMenu() {
221 $mainEntity = $this->savedSearch
['api_entity'] ??
NULL;
222 if ($mainEntity && !$this->canAggregate(CoreUtil
::getIdFieldName($mainEntity))) {
223 foreach (CoreUtil
::getInfoItem($mainEntity, 'paths') as $action => $path) {
224 $link = $this->formatMenuLink($mainEntity, $action);
230 $keys = ['entity' => TRUE, 'bridge' => TRUE];
231 foreach ($this->getJoins() as $join) {
232 if (!$this->canAggregate($join['alias'] . '.' . CoreUtil
::getIdFieldName($join['entity']))) {
233 foreach (array_filter(array_intersect_key($join, $keys)) as $joinEntity) {
234 foreach (CoreUtil
::getInfoItem($joinEntity, 'paths') as $action => $path) {
235 $link = $this->formatMenuLink($joinEntity, $action, $join['alias']);
247 * @param string $entity
248 * @param string $action
249 * @param string $joinAlias
252 private function formatMenuLink(string $entity, string $action, string $joinAlias = NULL) {
253 if ($joinAlias && $entity === $this->getJoin($joinAlias)['entity']) {
254 $entityLabel = $this->getJoinLabel($joinAlias);
259 $link = Display
::getEntityLinks($entity, $entityLabel)[$action] ??
NULL;
260 return $link ?
$link +
['join' => $joinAlias] : NULL;