savedSearch) && $this->checkPermissions && !\CRM_Core_Permission::check('administer CiviCRM data')) { throw new UnauthorizedException('Access denied'); } $this->loadSavedSearch(); $this->expandSelectClauseWildcards(); // Use label from saved search $label = $this->savedSearch['label'] ?? ''; // Fall back on entity title as label if (!strlen($label) && !empty($this->savedSearch['api_entity'])) { $label = CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'title_plural'); } $display = [ 'id' => NULL, 'name' => NULL, 'saved_search_id' => $this->savedSearch['id'] ?? NULL, 'label' => $label, 'type' => 'table', 'acl_bypass' => FALSE, 'settings' => [ 'actions' => TRUE, 'limit' => \Civi::settings()->get('default_pager_size'), 'classes' => ['table', 'table-striped'], 'pager' => [ 'show_count' => TRUE, 'expose_limit' => TRUE, ], 'sort' => [], 'columns' => [], ], ]; // Supply default sort if no orderBy given in api params if (!empty($this->savedSearch['api_entity']) && empty($this->savedSearch['api_params']['orderBy'])) { $defaultSort = CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'order_by'); if ($defaultSort) { $display['settings']['sort'][] = [$defaultSort, 'ASC']; } } foreach ($this->getSelectClause() as $key => $clause) { $display['settings']['columns'][] = $this->configureColumn($clause, $key); } $display['settings']['columns'][] = [ 'label' => '', 'type' => 'menu', 'icon' => 'fa-bars', 'size' => 'btn-xs', 'style' => 'secondary-outline', 'alignment' => 'text-right', 'links' => $this->getLinksMenu(), ]; $fields = $this->entityFields(); // Allow implicit-join-style selection of saved search fields if ($this->savedSearch) { $display += \CRM_Utils_Array::prefixKeys($this->savedSearch, 'saved_search_id.'); $fields += \CRM_Utils_Array::prefixKeys(SavedSearch::get()->entityFields(), 'saved_search_id.'); } // Fill pseudoconstant keys with raw values for replacement foreach ($this->select as $fieldExpr) { [$fieldName, $suffix] = array_pad(explode(':', $fieldExpr), 2, NULL); if ($suffix && array_key_exists($fieldName, $display)) { $display[$fieldExpr] = $display[$fieldName]; } } $results = [$display]; // Replace pseudoconstants FormattingUtil::formatOutputValues($results, $fields); $result->exchangeArray($this->selectArray($results)); } /** * @param array{fields: array, expr: SqlExpression, dataType: string} $clause * @param string $key * @return array */ private function configureColumn($clause, $key) { $col = [ 'type' => 'field', 'key' => $key, 'sortable' => !empty($clause['fields']), 'label' => $this->getColumnLabel($clause['expr']), ]; $this->getColumnLink($col, $clause); return $col; } /** * @param \Civi\Api4\Query\SqlExpression $expr * @return string */ private function getColumnLabel(SqlExpression $expr) { if ($expr instanceof SqlFunction) { $args = []; foreach ($expr->getArgs() as $arg) { foreach ($arg['expr'] ?? [] as $ex) { $args[] = $this->getColumnLabel($ex); } } return '(' . $expr->getTitle() . ')' . ($args ? ' ' . implode(',', array_filter($args)) : ''); } if ($expr instanceof SqlEquation) { $args = []; foreach ($expr->getArgs() as $arg) { $args[] = $this->getColumnLabel($arg['expr']); } return '(' . implode(',', array_filter($args)) . ')'; } elseif ($expr instanceof SqlField) { $field = $this->getField($expr->getExpr()); $label = ''; if (!empty($field['explicit_join'])) { $label = $this->getJoinLabel($field['explicit_join']) . ': '; } if (!empty($field['implicit_join']) && empty($field['custom_field_id'])) { $field = $this->getField(substr($expr->getAlias(), 0, -1 - strlen($field['name']))); } return $label . $field['label']; } else { return NULL; } } /** * @param string $joinAlias * @return string */ private function getJoinLabel($joinAlias) { if (!isset($this->_joinMap)) { $this->_joinMap = []; $joinCount = [$this->savedSearch['api_entity'] => 1]; foreach ($this->savedSearch['api_params']['join'] ?? [] as $join) { [$entityName, $alias] = explode(' AS ', $join[0]); $num = ''; if (!empty($joinCount[$entityName])) { $num = ' ' . (++$joinCount[$entityName]); } else { $joinCount[$entityName] = 1; } $label = CoreUtil::getInfoItem($entityName, 'title'); $this->_joinMap[$alias] = $label . $num; } } return $this->_joinMap[$joinAlias]; } /** * @param array $col * @param array{fields: array, expr: SqlExpression, dataType: string} $clause */ private function getColumnLink(&$col, $clause) { if ($clause['expr'] instanceof SqlField || $clause['expr'] instanceof SqlFunctionGROUP_CONCAT) { $field = $clause['fields'][0] ?? NULL; if ($field && CoreUtil::getInfoItem($field['entity'], 'label_field') === $field['name'] && !empty(CoreUtil::getInfoItem($field['entity'], 'paths')['view']) ) { $col['link'] = [ 'entity' => $field['entity'], 'join' => implode('.', array_filter([$field['explicit_join'], $field['implicit_join']])), 'action' => 'view', ]; // Hack to support links to relationships if ($col['link']['entity'] === 'RelationshipCache') { $col['link']['entity'] = 'Relationship'; } $col['title'] = E::ts('View %1', [1 => CoreUtil::getInfoItem($field['entity'], 'title')]); } } } /** * return array[] */ private function getLinksMenu() { $menu = []; $mainEntity = $this->savedSearch['api_entity'] ?? NULL; if ($mainEntity && !$this->canAggregate(CoreUtil::getIdFieldName($mainEntity))) { foreach (CoreUtil::getInfoItem($mainEntity, 'paths') as $action => $path) { $link = $this->formatMenuLink($mainEntity, $action); if ($link) { $menu[] = $link; } } } $keys = ['entity' => TRUE, 'bridge' => TRUE]; foreach ($this->getJoins() as $join) { if (!$this->canAggregate($join['alias'] . '.' . CoreUtil::getIdFieldName($join['entity']))) { foreach (array_filter(array_intersect_key($join, $keys)) as $joinEntity) { foreach (CoreUtil::getInfoItem($joinEntity, 'paths') as $action => $path) { $link = $this->formatMenuLink($joinEntity, $action, $join['alias']); if ($link) { $menu[] = $link; } } } } } return $menu; } /** * @param string $entity * @param string $action * @param string $joinAlias * @return array|NULL */ private function formatMenuLink(string $entity, string $action, string $joinAlias = NULL) { if ($joinAlias && $entity === $this->getJoin($joinAlias)['entity']) { $entityLabel = $this->getJoinLabel($joinAlias); } else { $entityLabel = TRUE; } $link = Display::getEntityLinks($entity, $entityLabel)[$action] ?? NULL; return $link ? $link + ['join' => $joinAlias] : NULL; } }