Merge pull request #21762 from jitendrapurohit/job-alert
[civicrm-core.git] / ext / search_kit / Civi / Api4 / Action / SearchDisplay / GetDefault.php
1 <?php
2
3 namespace Civi\Api4\Action\SearchDisplay;
4
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;
16
17 /**
18 * Return the default results table for a saved search.
19 *
20 * @package Civi\Api4\Action\SearchDisplay
21 */
22 class GetDefault extends \Civi\Api4\Generic\AbstractAction {
23
24 use SavedSearchInspectorTrait;
25 use \Civi\Api4\Generic\Traits\ArrayQueryActionTrait;
26 use \Civi\Api4\Generic\Traits\SelectParamTrait;
27
28 /**
29 * Either the name of the savedSearch or an array containing the savedSearch definition (for preview mode)
30 * @var string|array|null
31 */
32 protected $savedSearch;
33
34 /**
35 * @var array
36 */
37 private $_joinMap;
38
39 /**
40 * @param \Civi\Api4\Generic\Result $result
41 * @throws UnauthorizedException
42 * @throws \API_Exception
43 */
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');
48 }
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');
56 }
57 $display = [
58 'id' => NULL,
59 'name' => NULL,
60 'saved_search_id' => $this->savedSearch['id'] ?? NULL,
61 'label' => $label,
62 'type' => 'table',
63 'acl_bypass' => FALSE,
64 'settings' => [
65 'actions' => TRUE,
66 'limit' => \Civi::settings()->get('default_pager_size'),
67 'classes' => ['table', 'table-striped'],
68 'pager' => [
69 'show_count' => TRUE,
70 'expose_limit' => TRUE,
71 ],
72 'sort' => [],
73 'columns' => [],
74 ],
75 ];
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');
79 if ($defaultSort) {
80 $display['settings']['sort'][] = [$defaultSort, 'ASC'];
81 }
82 }
83 foreach ($this->getSelectClause() as $key => $clause) {
84 $display['settings']['columns'][] = $this->configureColumn($clause, $key);
85 }
86 $display['settings']['columns'][] = [
87 'label' => '',
88 'type' => 'menu',
89 'icon' => 'fa-bars',
90 'size' => 'btn-xs',
91 'style' => 'secondary-outline',
92 'alignment' => 'text-right',
93 'links' => $this->getLinksMenu(),
94 ];
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.');
100 }
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];
106 }
107 }
108 $results = [$display];
109 // Replace pseudoconstants
110 FormattingUtil::formatOutputValues($results, $fields);
111 $result->exchangeArray($this->selectArray($results));
112 }
113
114 /**
115 * @param array{fields: array, expr: SqlExpression, dataType: string} $clause
116 * @param string $key
117 * @return array
118 */
119 private function configureColumn($clause, $key) {
120 $col = [
121 'type' => 'field',
122 'key' => $key,
123 'sortable' => !empty($clause['fields']),
124 'label' => $this->getColumnLabel($clause['expr']),
125 ];
126 $this->getColumnLink($col, $clause);
127 return $col;
128 }
129
130 /**
131 * @param \Civi\Api4\Query\SqlExpression $expr
132 * @return string
133 */
134 private function getColumnLabel(SqlExpression $expr) {
135 if ($expr instanceof SqlFunction) {
136 $args = [];
137 foreach ($expr->getArgs() as $arg) {
138 foreach ($arg['expr'] ?? [] as $ex) {
139 $args[] = $this->getColumnLabel($ex);
140 }
141 }
142 return '(' . $expr->getTitle() . ')' . ($args ? ' ' . implode(',', array_filter($args)) : '');
143 }
144 if ($expr instanceof SqlEquation) {
145 $args = [];
146 foreach ($expr->getArgs() as $arg) {
147 $args[] = $this->getColumnLabel($arg['expr']);
148 }
149 return '(' . implode(',', array_filter($args)) . ')';
150 }
151 elseif ($expr instanceof SqlField) {
152 $field = $this->getField($expr->getExpr());
153 $label = '';
154 if (!empty($field['explicit_join'])) {
155 $label = $this->getJoinLabel($field['explicit_join']) . ': ';
156 }
157 if (!empty($field['implicit_join'])) {
158 $field = $this->getField(substr($expr->getAlias(), 0, -1 - strlen($field['name'])));
159 }
160 return $label . $field['label'];
161 }
162 else {
163 return NULL;
164 }
165 }
166
167 /**
168 * @param string $joinAlias
169 * @return string
170 */
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]);
177 $num = '';
178 if (!empty($joinCount[$entityName])) {
179 $num = ' ' . (++$joinCount[$entityName]);
180 }
181 else {
182 $joinCount[$entityName] = 1;
183 }
184 $label = CoreUtil::getInfoItem($entityName, 'title');
185 $this->_joinMap[$alias] = $label . $num;
186 }
187 }
188 return $this->_joinMap[$joinAlias];
189 }
190
191 /**
192 * @param array $col
193 * @param array{fields: array, expr: SqlExpression, dataType: string} $clause
194 */
195 private function getColumnLink(&$col, $clause) {
196 if ($clause['expr'] instanceof SqlField || $clause['expr'] instanceof SqlFunctionGROUP_CONCAT) {
197 $field = $clause['fields'][0] ?? NULL;
198 if ($field &&
199 CoreUtil::getInfoItem($field['entity'], 'label_field') === $field['name'] &&
200 !empty(CoreUtil::getInfoItem($field['entity'], 'paths')['view'])
201 ) {
202 $col['link'] = [
203 'entity' => $field['entity'],
204 'join' => implode('.', array_filter([$field['explicit_join'], $field['implicit_join']])),
205 'action' => 'view',
206 ];
207 // Hack to support links to relationships
208 if ($col['link']['entity'] === 'RelationshipCache') {
209 $col['link']['entity'] = 'Relationship';
210 }
211 $col['title'] = E::ts('View %1', [1 => CoreUtil::getInfoItem($field['entity'], 'title')]);
212 }
213 }
214 }
215
216 /**
217 * return array[]
218 */
219 private function getLinksMenu() {
220 $menu = [];
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);
225 if ($link) {
226 $menu[] = $link;
227 }
228 }
229 }
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']);
236 if ($link) {
237 $menu[] = $link;
238 }
239 }
240 }
241 }
242 }
243 return $menu;
244 }
245
246 /**
247 * @param string $entity
248 * @param string $action
249 * @param string $joinAlias
250 * @return array|NULL
251 */
252 private function formatMenuLink(string $entity, string $action, string $joinAlias = NULL) {
253 if ($joinAlias && $entity === $this->getJoin($joinAlias)['entity']) {
254 $entityLabel = $this->getJoinLabel($joinAlias);
255 }
256 else {
257 $entityLabel = TRUE;
258 }
259 $link = Display::getEntityLinks($entity, $entityLabel)[$action] ?? NULL;
260 return $link ? $link + ['join' => $joinAlias] : NULL;
261 }
262
263 }