SearchKit - Expose default display to the UI
[civicrm-core.git] / ext / search_kit / Civi / Api4 / Action / SearchDisplay / GetDefault.php
CommitLineData
ea04af0c
CW
1<?php
2
3namespace Civi\Api4\Action\SearchDisplay;
4
5use Civi\Search\Display;
6use CRM_Search_ExtensionUtil as E;
7use Civi\Api4\Query\SqlEquation;
8use Civi\Api4\Query\SqlExpression;
9use Civi\Api4\Query\SqlField;
10use Civi\Api4\Query\SqlFunction;
11use Civi\Api4\Query\SqlFunctionGROUP_CONCAT;
12use Civi\Api4\Utils\CoreUtil;
13use Civi\API\Exception\UnauthorizedException;
14
15/**
16 * Return the default results table for a saved search.
17 *
18 * @package Civi\Api4\Action\SearchDisplay
19 */
20class GetDefault extends \Civi\Api4\Generic\AbstractAction {
21
22 use SavedSearchInspectorTrait;
5c952e51
CW
23 use \Civi\Api4\Generic\Traits\ArrayQueryActionTrait;
24 use \Civi\Api4\Generic\Traits\SelectParamTrait;
ea04af0c
CW
25
26 /**
27 * Either the name of the savedSearch or an array containing the savedSearch definition (for preview mode)
28 * @var string|array|null
29 */
30 protected $savedSearch;
31
32 /**
33 * @var array
34 */
35 private $_joinMap;
36
37 /**
38 * @param \Civi\Api4\Generic\Result $result
39 * @throws UnauthorizedException
40 * @throws \API_Exception
41 */
42 public function _run(\Civi\Api4\Generic\Result $result) {
43 // Only administrators can use this in unsecured "preview mode"
44 if (is_array($this->savedSearch) && $this->checkPermissions && !\CRM_Core_Permission::check('administer CiviCRM data')) {
45 throw new UnauthorizedException('Access denied');
46 }
47 $this->loadSavedSearch();
5c952e51 48 $this->expandSelectClauseWildcards();
ea04af0c
CW
49 // Use label from saved search
50 $label = $this->savedSearch['label'] ?? '';
51 // Fall back on entity title as label
52 if (!strlen($label) && !empty($this->savedSearch['api_entity'])) {
53 $label = CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'title_plural');
54 }
55 $display = [
5c952e51 56 'id' => NULL,
ea04af0c 57 'name' => NULL,
5c952e51 58 'saved_search_id' => $this->savedSearch['id'] ?? NULL,
ea04af0c
CW
59 'label' => $label,
60 'type' => 'table',
5c952e51
CW
61 'type:label' => E::ts('Table'),
62 'type:name' => 'crm-search-display-table',
63 'type:icon' => 'fa-table',
ea04af0c
CW
64 'acl_bypass' => FALSE,
65 'settings' => [
ea04af0c
CW
66 'actions' => TRUE,
67 'limit' => \Civi::settings()->get('default_pager_size'),
68 'classes' => ['table', 'table-striped'],
69 'pager' => [
70 'show_count' => TRUE,
71 'expose_limit' => TRUE,
72 ],
73 'columns' => [],
74 ],
75 ];
5c952e51
CW
76 // Allow implicit-join-style selection of saved search fields
77 foreach ($this->savedSearch as $key => $val) {
78 $display['saved_search_id.' . $key] = $val;
79 }
ea04af0c
CW
80 foreach ($this->getSelectClause() as $key => $clause) {
81 $display['settings']['columns'][] = $this->configureColumn($clause, $key);
82 }
83 $display['settings']['columns'][] = [
84 'label' => '',
85 'type' => 'menu',
86 'icon' => 'fa-bars',
87 'size' => 'btn-xs',
88 'style' => 'secondary-outline',
89 'alignment' => 'text-right',
90 'links' => $this->getLinksMenu(),
91 ];
5c952e51 92 $result->exchangeArray($this->selectArray([$display]));
ea04af0c
CW
93 }
94
95 /**
96 * @param array{fields: array, expr: SqlExpression, dataType: string} $clause
97 * @param string $key
98 * @return array
99 */
100 private function configureColumn($clause, $key) {
101 $col = [
102 'type' => 'field',
103 'key' => $key,
104 'sortable' => !empty($clause['fields']),
105 'label' => $this->getColumnLabel($clause['expr']),
106 ];
107 $this->getColumnLink($col, $clause);
108 return $col;
109 }
110
111 /**
112 * @param \Civi\Api4\Query\SqlExpression $expr
113 * @return string
114 */
115 private function getColumnLabel(SqlExpression $expr) {
116 if ($expr instanceof SqlFunction) {
117 $args = [];
118 foreach ($expr->getArgs() as $arg) {
119 foreach ($arg['expr'] ?? [] as $ex) {
120 $args[] = $this->getColumnLabel($ex);
121 }
122 }
123 return '(' . $expr->getTitle() . ')' . ($args ? ' ' . implode(',', array_filter($args)) : '');
124 }
125 if ($expr instanceof SqlEquation) {
126 $args = [];
127 foreach ($expr->getArgs() as $arg) {
128 $args[] = $this->getColumnLabel($arg['expr']);
129 }
130 return '(' . implode(',', array_filter($args)) . ')';
131 }
132 elseif ($expr instanceof SqlField) {
133 $field = $this->getField($expr->getExpr());
134 $label = '';
135 if (!empty($field['explicit_join'])) {
136 $label = $this->getJoinLabel($field['explicit_join']) . ': ';
137 }
138 if (!empty($field['implicit_join'])) {
139 $field = $this->getField(substr($expr->getAlias(), 0, -1 - strlen($field['name'])));
140 }
141 return $label . $field['label'];
142 }
143 else {
144 return NULL;
145 }
146 }
147
148 /**
149 * @param string $joinAlias
150 * @return string
151 */
152 private function getJoinLabel($joinAlias) {
153 if (!isset($this->_joinMap)) {
154 $this->_joinMap = [];
155 $joinCount = [$this->savedSearch['api_entity'] => 1];
156 foreach ($this->savedSearch['api_params']['join'] ?? [] as $join) {
157 [$entityName, $alias] = explode(' AS ', $join[0]);
158 $num = '';
159 if (!empty($joinCount[$entityName])) {
160 $num = ' ' . (++$joinCount[$entityName]);
161 }
162 else {
163 $joinCount[$entityName] = 1;
164 }
165 $label = CoreUtil::getInfoItem($entityName, 'title');
166 $this->_joinMap[$alias] = $label . $num;
167 }
168 }
169 return $this->_joinMap[$joinAlias];
170 }
171
172 /**
173 * @param array $col
174 * @param array{fields: array, expr: SqlExpression, dataType: string} $clause
175 */
176 private function getColumnLink(&$col, $clause) {
177 if ($clause['expr'] instanceof SqlField || $clause['expr'] instanceof SqlFunctionGROUP_CONCAT) {
178 $field = $clause['fields'][0] ?? NULL;
179 if ($field &&
180 CoreUtil::getInfoItem($field['entity'], 'label_field') === $field['name'] &&
181 !empty(CoreUtil::getInfoItem($field['entity'], 'paths')['view'])
182 ) {
183 $col['link'] = [
184 'entity' => $field['entity'],
185 'join' => implode('.', array_filter([$field['explicit_join'], $field['implicit_join']])),
186 'action' => 'view',
187 ];
188 // Hack to support links to relationships
189 if ($col['link']['entity'] === 'RelationshipCache') {
190 $col['link']['entity'] = 'Relationship';
191 }
192 $col['title'] = E::ts('View %1', [1 => CoreUtil::getInfoItem($field['entity'], 'title')]);
193 }
194 }
195 }
196
197 /**
198 * return array[]
199 */
200 private function getLinksMenu() {
201 $menu = [];
202 $mainEntity = $this->savedSearch['api_entity'] ?? NULL;
203 if ($mainEntity && !$this->canAggregate(CoreUtil::getIdFieldName($mainEntity))) {
204 foreach (CoreUtil::getInfoItem($mainEntity, 'paths') as $action => $path) {
205 $link = $this->formatMenuLink($mainEntity, $action);
206 if ($link) {
207 $menu[] = $link;
208 }
209 }
210 }
211 $keys = ['entity' => TRUE, 'bridge' => TRUE];
212 foreach ($this->getJoins() as $join) {
213 if (!$this->canAggregate($join['alias'] . '.' . CoreUtil::getIdFieldName($join['entity']))) {
214 foreach (array_filter(array_intersect_key($join, $keys)) as $joinEntity) {
215 foreach (CoreUtil::getInfoItem($joinEntity, 'paths') as $action => $path) {
216 $link = $this->formatMenuLink($joinEntity, $action, $join['alias']);
217 if ($link) {
218 $menu[] = $link;
219 }
220 }
221 }
222 }
223 }
224 return $menu;
225 }
226
227 /**
228 * @param string $entity
229 * @param string $action
230 * @param string $joinAlias
231 * @return array|NULL
232 */
233 private function formatMenuLink(string $entity, string $action, string $joinAlias = NULL) {
234 if ($joinAlias && $entity === $this->getJoin($joinAlias)['entity']) {
235 $entityLabel = $this->getJoinLabel($joinAlias);
236 }
237 else {
238 $entityLabel = TRUE;
239 }
240 $link = Display::getEntityLinks($entity, $entityLabel)[$action] ?? NULL;
241 return $link ? $link + ['join' => $joinAlias] : NULL;
242 }
243
244}