* @return array{val: mixed, links: array, edit: array, label: string, title: string, image: array, cssClass: string}
*/
private function formatColumn($column, $data) {
- $column += ['rewrite' => NULL, 'label' => NULL];
+ $column += ['rewrite' => NULL, 'label' => NULL, 'key' => ''];
$out = [];
switch ($column['type']) {
case 'field':
$out['cssClass'] = implode(' ', $cssClass);
}
if (!empty($column['icons'])) {
- $out['icons'] = $this->getColumnIcons($column['icons'], $data);
+ $out['icons'] = $this->getColumnIcons($column, $data, $out);
}
return $out;
}
* If more than one per side is given, latter icons are treated as fallbacks
* and only shown if prior ones are missing.
*
- * @param array{icon: string, field: string, if: array, side: string}[] $icons
+ * @param array $column
* @param array $data
+ * @param array $out
* @return array
*/
- protected function getColumnIcons(array $icons, array $data) {
+ protected function getColumnIcons(array $column, array $data, array $out): array {
+ // Column is either outputting an array of links, or a plain value
+ // Links are always an array. Value could be, if field is multivalued or aggregated.
+ $value = $out['links'] ?? $out['val'] ?? NULL;
+ // Get 0-indexed keys of the values (pad so we have at least one)
+ $keys = array_pad(array_keys(array_values((array) $value)), 1, 0);
$result = [];
- // Reverse order so latter icons become fallbacks and earlier ones take priority
- foreach (array_reverse($icons) as $icon) {
- $iconClass = $icon['icon'] ?? NULL;
- if (!$iconClass && !empty($icon['field']) && !empty($data[$icon['field']])) {
- // Icon field may be multivalued e.g. contact_sub_type
- $iconClass = \CRM_Utils_Array::first(array_filter((array) $data[$icon['field']]));
+ foreach (['left', 'right'] as $side) {
+ foreach ($keys as $index) {
+ $result[$side][$index] = $this->getColumnIcon($column['icons'], $side, $index, $data, is_array($value));
}
- if ($iconClass) {
- $condition = $this->getRuleCondition($icon['if'] ?? []);
- if (!is_null($condition[0]) && !(self::filterCompare($data, $condition))) {
+ // Drop if empty
+ if (!array_filter($result[$side])) {
+ unset($result[$side]);
+ }
+ }
+ return $result;
+ }
+
+ private function getColumnIcon(array $icons, string $side, int $index, array $data, bool $isMulti): ?string {
+ // Latter icons are fallbacks, earlier ones take priority
+ foreach ($icons as $icon) {
+ $iconClass = NULL;
+ $icon += ['side' => 'left', 'icon' => NULL];
+ if ($icon['side'] !== $side) {
+ continue;
+ }
+ $iconField = !empty($icon['field']) ? $this->renameIfAggregate($icon['field']) : NULL;
+ if (!empty($iconField) && !empty($data[$iconField])) {
+ // Icon field may be multivalued e.g. contact_sub_type, or it may be aggregated
+ // If both base field and icon field are multivalued, use corresponding index
+ if ($isMulti && is_array($data[$iconField])) {
+ $iconClass = $data[$iconField][$index] ?? NULL;
+ }
+ // Otherwise get a single value
+ else {
+ $iconClass = \CRM_Utils_Array::first(array_filter((array) $data[$iconField]));
+ }
+ }
+ $iconClass = $iconClass ?? $icon['icon'];
+ if ($iconClass && !empty($icon['if'])) {
+ $condition = $this->getRuleCondition($icon['if'], $isMulti);
+ if (!is_null($condition[0]) && !(self::filterCompare($data, $condition, $isMulti ? $index : NULL))) {
continue;
}
- $side = $icon['side'] ?? 'left';
- $result[$side] = ['class' => $iconClass, 'side' => $side];
+ }
+ if ($iconClass) {
+ return $iconClass;
}
}
- return array_values($result);
+ return NULL;
}
/**
* Returns the condition of a cssRules
*
* @param array $clause
+ * @param bool $isMulti
* @return array
*/
- protected function getRuleCondition($clause) {
+ protected function getRuleCondition($clause, bool $isMulti = FALSE): array {
$fieldKey = $clause[0] ?? NULL;
- // For fields used in group by, add aggregation and change operator to CONTAINS
- // NOTE: This doesn't support any other operators for aggregated fields.
+ // For fields used in group by, add aggregation and change comparison operator to CONTAINS
if ($fieldKey && $this->canAggregate($fieldKey)) {
- $clause[1] = 'CONTAINS';
- $fieldKey = 'GROUP_CONCAT_' . str_replace(['.', ':'], '_', $clause[0]);
+ if (!$isMulti && !empty($clause[1]) && !in_array($clause[1], ['IS EMPTY', 'IS NOT EMPTY'], TRUE)) {
+ $clause[1] = 'CONTAINS';
+ }
+ $fieldKey = $this->renameIfAggregate($fieldKey);
}
return [$fieldKey, $clause[1] ?? 'IS NOT EMPTY', $clause[2] ?? NULL];
}
$fieldKey = $clause[1] ?? NULL;
if ($fieldKey) {
// For fields used in group by, add aggregation
- $select[] = $this->canAggregate($fieldKey) ? "GROUP_CONCAT($fieldKey) AS GROUP_CONCAT_" . str_replace(['.', ':'], '_', $fieldKey) : $fieldKey;
+ $select[] = $this->renameIfAggregate($fieldKey, TRUE);
}
}
return $select;
$select = [];
foreach ($icons as $icon) {
if (!empty($icon['field'])) {
- $select[] = $icon['field'];
+ $select[] = $this->renameIfAggregate($icon['field'], TRUE);
}
$fieldKey = $icon['if'][0] ?? NULL;
if ($fieldKey) {
// For fields used in group by, add aggregation
- $select[] = $this->canAggregate($fieldKey) ? "GROUP_CONCAT($fieldKey) AS GROUP_CONCAT_" . str_replace(['.', ':'], '_', $fieldKey) : $fieldKey;
+ $select[] = $this->renameIfAggregate($fieldKey, TRUE);
}
}
return $select;
}
private function formatLink(array $link, array $data, string $text = NULL, $index = 0): ?array {
- $link = $this->getLinkInfo($link, $data, $index);
+ $link = $this->getLinkInfo($link);
if (!$this->checkLinkAccess($link, $data, $index)) {
return NULL;
}
$path = $this->replaceTokens($link['path'], $data, 'url', $index);
if ($path) {
$link['url'] = $this->getUrl($path);
- $keys = ['url', 'text', 'title', 'target', 'style', 'icon'];
+ $keys = ['url', 'text', 'title', 'target', 'style'];
}
else {
- $keys = ['task', 'text', 'title', 'style', 'icon'];
+ $keys = ['task', 'text', 'title', 'style'];
}
$link = array_intersect_key($link, array_flip($keys));
return array_filter($link);
}
/**
- * @param array{path: string, entity: string, action: string, task: string, join: string, target: string, style: string, icon: string, title: string, text: string} $link
- * @return array{path: string, entity: string, action: string, task: string, join: string, target: string, style: string, icon: string, title: string, text: string, prefix: string}
+ * @param array{path: string, entity: string, action: string, task: string, join: string, target: string, style: string, title: string, text: string} $link
+ * @return array{path: string, entity: string, action: string, task: string, join: string, target: string, style: string, title: string, text: string, prefix: string}
*/
private function getLinkInfo(array $link): array {
$link += [
if (!$this->getSelectExpression($expr)) {
// Tokens for aggregated columns start with 'GROUP_CONCAT_'
if (strpos($expr, 'GROUP_CONCAT_') === 0) {
- $expr = 'GROUP_CONCAT(' . $this->getJoinFromAlias(explode('_', $expr, 3)[2]) . ') AS ' . $expr;
+ $expr = 'GROUP_CONCAT(UNIQUE ' . $this->getJoinFromAlias(explode('_', $expr, 3)[2]) . ') AS ' . $expr;
}
$this->_apiParams['select'][] = $expr;
// Force-reset cache so it gets rebuilt with the new select param
->execute();
// Icon based on activity type
- $this->assertEquals([['class' => 'fa-slideshare', 'side' => 'left']], $result[0]['columns'][0]['icons']);
+ $this->assertEquals(['left' => ['fa-slideshare']], $result[0]['columns'][0]['icons']);
// Activity type icon + conditional icon based on status
- $this->assertEquals([['class' => 'fa-star', 'side' => 'right'], ['class' => 'fa-phone', 'side' => 'left']], $result[1]['columns'][0]['icons']);
+ $this->assertEquals(['right' => ['fa-star'], 'left' => ['fa-phone']], $result[1]['columns'][0]['icons']);
}
/**
// Contacts will be returned in order by sort_name
$this->assertEquals('Both', $result[0]['columns'][0]['val']);
- $this->assertEquals('fa-star', $result[0]['columns'][0]['icons'][0]['class']);
+ $this->assertEquals('fa-star', $result[0]['columns'][0]['icons']['left'][0]);
$this->assertEquals('No icon', $result[1]['columns'][0]['val']);
- $this->assertEquals('fa-user', $result[1]['columns'][0]['icons'][0]['class']);
+ $this->assertEquals('fa-user', $result[1]['columns'][0]['icons']['left'][0]);
$this->assertEquals('Starry', $result[2]['columns'][0]['val']);
- $this->assertEquals('fa-star', $result[2]['columns'][0]['icons'][0]['class']);
+ $this->assertEquals('fa-star', $result[2]['columns'][0]['icons']['left'][0]);
}
public function testKeyIsReturned(): void {
$this->assertEquals($id, $result[0]['key']);
}
+ public function testRunWithEntityFile(): void {
+ $cid = $this->createTestRecord('Contact')['id'];
+ $notes = $this->saveTestRecords('Note', [
+ 'records' => 2,
+ 'defaults' => ['entity_table' => 'civicrm_contact', 'entity_id' => $cid],
+ ])->column('id');
+
+ foreach (['text/plain' => 'txt', 'image/png' => 'png', 'image/foo' => 'foo'] as $mimeType => $ext) {
+ // FIXME: Use api4 when available
+ civicrm_api3('Attachment', 'create', [
+ 'entity_table' => 'civicrm_note',
+ 'entity_id' => $notes[0],
+ 'name' => 'test_file.' . $ext,
+ 'mime_type' => $mimeType,
+ 'content' => 'hello',
+ ])['id'];
+ }
+
+ $params = [
+ 'checkPermissions' => FALSE,
+ 'debug' => TRUE,
+ 'return' => 'page:1',
+ 'savedSearch' => [
+ 'api_entity' => 'Note',
+ 'api_params' => [
+ 'version' => 4,
+ 'select' => [
+ 'id',
+ 'subject',
+ 'note',
+ 'note_date',
+ 'modified_date',
+ 'contact_id.sort_name',
+ 'GROUP_CONCAT(UNIQUE Note_EntityFile_File_01.file_name) AS GROUP_CONCAT_Note_EntityFile_File_01_file_name',
+ 'GROUP_CONCAT(UNIQUE Note_EntityFile_File_01.url) AS GROUP_CONCAT_Note_EntityFile_File_01_url',
+ 'GROUP_CONCAT(UNIQUE Note_EntityFile_File_01.icon) AS GROUP_CONCAT_Note_EntityFile_File_01_icon',
+ ],
+ 'where' => [
+ ['entity_id', 'IN', [$cid]],
+ ['entity_table:name', '=', 'Contact'],
+ ],
+ 'groupBy' => [
+ 'id',
+ ],
+ 'join' => [
+ [
+ 'File AS Note_EntityFile_File_01',
+ 'LEFT',
+ 'EntityFile',
+ [
+ 'id',
+ '=',
+ 'Note_EntityFile_File_01.entity_id',
+ ],
+ [
+ 'Note_EntityFile_File_01.entity_table',
+ '=',
+ "'civicrm_note'",
+ ],
+ ],
+ ],
+ 'having' => [],
+ ],
+ ],
+ 'display' => [
+ 'type' => 'table',
+ 'label' => '',
+ 'settings' => [
+ 'limit' => 20,
+ 'pager' => TRUE,
+ 'actions' => TRUE,
+ 'columns' => [
+ [
+ 'type' => 'field',
+ 'key' => 'id',
+ 'dataType' => 'Integer',
+ 'label' => 'ID',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'GROUP_CONCAT_Note_EntityFile_File_01_file_name',
+ 'dataType' => 'String',
+ 'label' => ts('Attachments'),
+ 'sortable' => TRUE,
+ 'link' => [
+ 'path' => '[GROUP_CONCAT_Note_EntityFile_File_01_url]',
+ 'entity' => '',
+ 'action' => '',
+ 'join' => '',
+ 'target' => '',
+ ],
+ 'icons' => [
+ [
+ 'field' => 'Note_EntityFile_File_01.icon',
+ 'side' => 'left',
+ ],
+ [
+ 'icon' => 'fa-search',
+ 'side' => 'right',
+ 'if' => ['Note_EntityFile_File_01.is_image'],
+ ],
+ ],
+ ],
+ ],
+ 'sort' => [
+ ['id', 'ASC'],
+ ],
+ ],
+ ],
+ 'afform' => NULL,
+ ];
+
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(2, $result);
+ $this->assertEquals(['fa-file-text-o', 'fa-file-image-o', 'fa-file-image-o'], $result[0]['columns'][1]['icons']['left']);
+ $this->assertEquals([NULL, 'fa-search', NULL], $result[0]['columns'][1]['icons']['right']);
+ }
+
/**
* Returns all contacts in VIEW mode but only specified contact for EDIT.
*