return static::$category;
}
+ /**
+ * All functions return 'SqlFunction' as their type.
+ *
+ * To get the function name @see SqlFunction::getName()
+ * @return string
+ */
+ public function getType(): string {
+ return 'SqlFunction';
+ }
+
/**
* @return string
*/
*/
private $_afform;
+ /**
+ * @var array
+ */
+ private $_selectClause;
+
/**
* @param \Civi\Api4\Generic\Result $result
* @throws UnauthorizedException
abstract protected function processResult(\Civi\Api4\Generic\Result $result);
/**
- * Transform each value returned by the API into 'raw' and 'view' properties
+ * Transforms each row into an array of raw data and an array of formatted columns
+ *
* @param \Civi\Api4\Generic\Result $result
- * @return array
+ * @return array{data: array, columns: array}[]
*/
protected function formatResult(\Civi\Api4\Generic\Result $result): array {
- $select = [];
- foreach ($this->savedSearch['api_params']['select'] as $selectExpr) {
- $expr = SqlExpression::convert($selectExpr, TRUE);
- $item = [
- 'fields' => [],
- 'dataType' => $expr->getDataType(),
- ];
- foreach ($expr->getFields() as $field) {
- $item['fields'][] = $this->getField($field);
- }
- if (!isset($item['dataType']) && $item['fields']) {
- $item['dataType'] = $item['fields'][0]['data_type'];
+ $rows = [];
+ foreach ($result as $index => $row) {
+ $data = $columns = [];
+ foreach ($this->getSelectClause() as $key => $item) {
+ $data[$key] = $this->getValue($key, $row, $index);
}
- $select[$expr->getAlias()] = $item;
- }
- $formatted = [];
- foreach ($result as $index => $data) {
- $row = [];
- foreach ($select as $key => $item) {
- $row[$key] = $this->getValue($key, $data, $item['dataType'], $index);
+ foreach ($this->display['settings']['columns'] as $column) {
+ $columns[] = $this->formatColumn($column, $data);
}
- $formatted[] = $row;
+ $rows[] = [
+ 'data' => $data,
+ 'columns' => $columns,
+ ];
}
- return $formatted;
+ return $rows;
}
/**
- * @param $key
- * @param $data
- * @param $dataType
- * @param $index
- * @return array
+ * @param string $key
+ * @param array $data
+ * @param int $rowIndex
+ * @return mixed
*/
- private function getValue($key, $data, $dataType, $index) {
+ private function getValue($key, $data, $rowIndex) {
// Get value from api result unless this is a pseudo-field which gets a calculated value
switch ($key) {
case 'result_row_num':
- $raw = $index + 1 + ($this->savedSearch['api_params']['offset'] ?? 0);
- break;
+ return $rowIndex + 1 + ($this->savedSearch['api_params']['offset'] ?? 0);
case 'user_contact_id':
- $raw = \CRM_Core_Session::getLoggedInContactID();
- break;
+ return \CRM_Core_Session::getLoggedInContactID();
default:
- $raw = $data[$key] ?? NULL;
+ return $data[$key] ?? NULL;
+ }
+ }
+
+ /**
+ * @param $column
+ * @param $data
+ * @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];
+ $out = $cssClass = [];
+ switch ($column['type']) {
+ case 'field':
+ if (isset($column['image']) && is_array($column['image'])) {
+ $out['img'] = $this->formatImage($column, $data);
+ $out['val'] = $this->replaceTokens($column['image']['alt'] ?? NULL, $data, 'view');
+ }
+ elseif ($column['rewrite']) {
+ $out['val'] = $this->replaceTokens($column['rewrite'], $data, 'view');
+ }
+ else {
+ $out['val'] = $this->formatViewValue($column['key'], $data[$column['key']] ?? NULL);
+ }
+ if ($this->hasValue($column['label']) && (!empty($column['forceLabel']) || $this->hasValue($out['val']))) {
+ $out['label'] = $this->replaceTokens($column['label'], $data, 'view');
+ }
+ if (isset($column['title']) && strlen($column['title'])) {
+ $out['title'] = $this->replaceTokens($column['title'], $data, 'view');
+ }
+ if (!empty($column['link']['path'])) {
+ $out['links'] = $this->formatFieldLinks($column, $data, $out['val']);
+ }
+ elseif (!empty($column['editable']) && !$column['rewrite']) {
+ $out['edit'] = $this->formatEditableColumn($column, $data);
+ }
+ break;
+
+ case 'links':
+ case 'buttons':
+ case 'menu':
+ $out = $this->formatLinksColumn($column, $data);
+ break;
+ }
+ if (!empty($column['alignment'])) {
+ $cssClass[] = $column['alignment'];
+ }
+ if ($cssClass) {
+ $out['cssClass'] = implode(' ', $cssClass);
+ }
+ return $out;
+ }
+
+ /**
+ * Format a field value as links
+ * @param $column
+ * @param $data
+ * @param $value
+ * @return array{text: string, url: string, target: string}[]
+ */
+ private function formatFieldLinks($column, $data, $value): array {
+ $links = [];
+ if (!empty($column['image'])) {
+ $value = [''];
+ }
+ foreach ((array) $value as $index => $val) {
+ $path = $this->replaceTokens($column['link']['path'], $data, 'url', $index);
+ if ($path) {
+ $link = [
+ 'text' => $val,
+ 'url' => $this->getUrl($path),
+ ];
+ if (!empty($column['link']['target'])) {
+ $link['target'] = $column['link']['target'];
+ }
+ $links[] = $link;
+ }
}
+ return $links;
+ }
+
+ /**
+ * Format links for a menu/buttons/links column
+ * @param $column
+ * @param $data
+ * @return array{text: string, url: string, target: string, style: string, icon: string}[]
+ */
+ private function formatLinksColumn($column, $data): array {
+ $out = ['links' => []];
+ if (isset($column['text'])) {
+ $out['text'] = $this->replaceTokens($column['text'], $data, 'view');
+ }
+ foreach ($column['links'] as $item) {
+ $path = $this->replaceTokens($item['path'], $data, 'url');
+ if ($path) {
+ $link = [
+ 'text' => $this->replaceTokens($item['text'] ?? '', $data, 'view'),
+ 'url' => $this->getUrl($path),
+ ];
+ foreach (['target', 'style', 'icon'] as $prop) {
+ if (!empty($item[$prop])) {
+ $link[$prop] = $item[$prop];
+ }
+ }
+ $out['links'][] = $link;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * @param string $path
+ * @return string
+ */
+ private function getUrl(string $path) {
+ if ($path[0] === '/' || strpos($path, 'http://') || strpos($path, 'https://')) {
+ return $path;
+ }
+ // Use absolute urls when downloading spreadsheet
+ $absolute = $this->getActionName() === 'download';
+ return \CRM_Utils_System::url($path, NULL, $absolute, NULL, FALSE);
+ }
+
+ private function formatEditableColumn($column, $data) {
+
+ }
+
+ private function formatImage($column, $data) {
+ $tokenExpr = $column['rewrite'] ?: '[' . $column['key'] . ']';
return [
- 'raw' => $raw,
- 'view' => $this->formatViewValue($dataType, $raw),
+ 'url' => $this->replaceTokens($tokenExpr, $data, 'url'),
+ 'height' => $column['image']['height'] ?? NULL,
+ 'width' => $column['image']['width'] ?? NULL,
];
}
return $this->_selectQuery->getField($fieldName, FALSE);
}
+ /**
+ * Returns the select clause enhanced with metadata
+ *
+ * @return array
+ */
+ protected function getSelectClause() {
+ if (!isset($this->_selectClause)) {
+ $this->_selectClause = [];
+ foreach ($this->savedSearch['api_params']['select'] as $selectExpr) {
+ $expr = SqlExpression::convert($selectExpr, TRUE);
+ $item = [
+ 'fields' => [],
+ 'type' => $expr->getType(),
+ 'dataType' => $expr->getDataType(),
+ ];
+ foreach ($expr->getFields() as $fieldName) {
+ $fieldMeta = $this->getField($fieldName);
+ if ($fieldMeta) {
+ $item['fields'][] = $fieldMeta;
+ }
+ }
+ if (!isset($item['dataType']) && $item['fields']) {
+ $item['dataType'] = $item['fields'][0]['data_type'];
+ }
+ $this->_selectClause[$expr->getAlias()] = $item;
+ }
+ }
+ return $this->_selectClause;
+ }
+
+ /**
+ * @param string $key
+ * @return array{fields: array, dataType: string}|NULL
+ */
+ protected function getSelectExpression($key) {
+ return $this->getSelectClause()[$key] ?? NULL;
+ }
+
+ /**
+ * @param string $tokenExpr
+ * @param array $data
+ * @param string $format view|raw|url
+ * @param int $index
+ * @return string
+ */
+ private function replaceTokens($tokenExpr, $data, $format, $index = 0) {
+ foreach ($this->getTokens($tokenExpr) as $token) {
+ $val = $data[$token] ?? NULL;
+ if (isset($val) && $format === 'view') {
+ $val = $this->formatViewValue($token, $val);
+ }
+ $replacement = is_array($val) ? $val[$index] ?? '' : $val;
+ // A missing token value in a url invalidates it
+ if ($format === 'url' && (!isset($replacement) || $replacement === '')) {
+ return NULL;
+ }
+ $tokenExpr = str_replace('[' . $token . ']', $replacement, $tokenExpr);
+ }
+ return $tokenExpr;
+ }
+
/**
* Format raw field value according to data type
- * @param $dataType
+ * @param string $key
* @param mixed $rawValue
* @return array|string
*/
- protected function formatViewValue($dataType, $rawValue) {
+ protected function formatViewValue($key, $rawValue) {
if (is_array($rawValue)) {
- return array_map(function($val) use ($dataType) {
- return $this->formatViewValue($dataType, $val);
+ return array_map(function($val) use ($key) {
+ return $this->formatViewValue($key, $val);
}, $rawValue);
}
+ $dataType = $this->getSelectExpression($key)['dataType'] ?? NULL;
+
$formatted = $rawValue;
switch ($dataType) {
$defaultSort = $this->display['settings']['sort'] ?? [];
$currentSort = $this->sort;
- // Validate that requested sort fields are part of the SELECT
+ // Verify requested sort corresponds to sortable columns
foreach ($this->sort as $item) {
- if (!in_array($item[0], $this->getSelectAliases())) {
+ $column = array_column($this->display['settings']['columns'], NULL, 'key')[$item[0]] ?? NULL;
+ if (!$column || (isset($column['sortable']) && !$column['sortable'])) {
$currentSort = NULL;
}
}
}
}
// Add fields referenced via token
- $tokens = [];
- preg_match_all('/\\[([^]]+)\\]/', $possibleTokens, $tokens);
+ $tokens = $this->getTokens($possibleTokens);
// Only add fields not already in SELECT clause
- $additions = array_diff(array_merge($additions, $tokens[1]), $existing);
+ $additions = array_diff(array_merge($additions, $tokens), $existing);
// Tokens for aggregated columns start with 'GROUP_CONCAT_'
foreach ($additions as $index => $alias) {
if (strpos($alias, 'GROUP_CONCAT_') === 0) {
$additions[$index] = 'GROUP_CONCAT(' . $this->getJoinFromAlias(explode('_', $alias, 3)[2]) . ') AS ' . $alias;
}
}
+ $this->_selectClause = NULL;
$apiParams['select'] = array_unique(array_merge($apiParams['select'], $additions));
}
+ /**
+ * @param string $str
+ */
+ private function getTokens($str) {
+ $tokens = [];
+ preg_match_all('/\\[([^]]+)\\]/', $str, $tokens);
+ return array_unique($tokens[1]);
+ }
+
/**
* Given an alias like Contact_Email_01_location_type_id
* this will return Contact_Email_01.location_type_id
$rows = $this->formatResult($apiResult);
$columns = [];
- foreach ($this->display['settings']['columns'] as $col) {
+ foreach ($this->display['settings']['columns'] as $index => $col) {
$col += ['type' => NULL, 'label' => '', 'rewrite' => FALSE];
if ($col['type'] === 'field' && !empty($col['key'])) {
- $columns[] = $col;
+ $columns[$index] = $col;
}
}
switch ($this->format) {
case 'array':
- $result[] = $columns;
+ $result[] = array_column($columns, 'label');
foreach ($rows as $data) {
- $row = [];
- foreach ($columns as $col) {
- $row[] = $this->formatColumnValue($col, $data);
- }
+ $row = array_column(array_intersect_key($data['columns'], $columns), 'val');
$result[] = $row;
}
return;
$csv->insertOne(array_column($columns, 'label'));
foreach ($rows as $data) {
- $row = [];
- foreach ($columns as $col) {
- $row[] = $this->formatColumnValue($col, $data);
+ $row = array_column(array_intersect_key($data['columns'], $columns), 'val');
+ foreach ($row as &$val) {
+ if (is_array($val)) {
+ $val = implode(', ', $val);
+ }
}
$csv->insertOne($row);
}
$sheet = $document->getActiveSheet();
// Header row
- foreach ($columns as $index => $col) {
+ foreach (array_values($columns) as $index => $col) {
$sheet->setCellValueByColumnAndRow($index + 1, 1, $col['label']);
}
foreach ($rows as $rowNum => $data) {
+ $colNum = 1;
foreach ($columns as $index => $col) {
- $sheet->setCellValueByColumnAndRow($index + 1, $rowNum + 2, $this->formatColumnValue($col, $data));
+ $sheet->setCellValueByColumnAndRow($colNum++, $rowNum + 2, $this->formatColumnValue($col, $data['columns'][$index]));
}
}
* Returns final formatted column value
*
* @param array $col
- * @param array $data
+ * @param array $value
* @return string
*/
- protected function formatColumnValue(array $col, array $data) {
- $val = $col['rewrite'] ?: $data[$col['key']]['view'] ?? '';
- if ($col['rewrite']) {
- foreach ($data as $k => $v) {
- $val = str_replace("[$k]", $v['view'], $val);
- }
- }
+ protected function formatColumnValue(array $col, array $value) {
+ $val = $value['val'] ?? '';
return is_array($val) ? implode(', ', $val) : $val;
}
};
ctrl.settings = ctrl.display.settings;
setLabel();
+ ctrl.results = null;
+ ctrl.rowCount = null;
+ ctrl.page = 1;
}
function setLabel() {
this.$onInit = function() {
buildSettings();
this.initializeDisplay($scope, $element);
- $scope.$watch('$ctrl.search.api_entity', buildSettings);
$scope.$watch('$ctrl.search.api_params', buildSettings, true);
$scope.$watch('$ctrl.search.label', setLabel);
};
-<div class="btn-group" ng-if=":: row.display_name.raw">
+<div class="btn-group" ng-if=":: row.data.display_name">
<button type="button" ng-click="$ctrl.loadAfforms(); row.openAfformMenu = true;" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ $ctrl.afforms ? (row.afform_count === 1 ? ts('1 Form') : ts('%1 Forms', {1: row.afform_count})) : ts('Forms...') }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" ng-if=":: row.openAfformMenu">
- <li ng-repeat="display_name in row.display_name.raw" ng-if="::$ctrl.afformAdminEnabled">
- <a target="_blank" href="{{:: $ctrl.afformPath + '#/create/search/' + row.name.raw + '.' + display_name }}">
- <i class="fa fa-plus"></i> {{:: ts('Create form for %1', {1: row.display_label.raw[$index]}) }}
+ <li ng-repeat="display_name in row.data.display_name" ng-if="::$ctrl.afformAdminEnabled">
+ <a target="_blank" href="{{:: $ctrl.afformPath + '#/create/search/' + row.data.name + '.' + display_name }}">
+ <i class="fa fa-plus"></i> {{:: ts('Create form for %1', {1: row.data.display_label[$index]}) }}
</a>
</li>
<li class="divider" role="separator" ng-if="::$ctrl.afformAdminEnabled"></li>
<li ng-if="!row.afform_count" class="disabled">
<a href>
<i ng-if="!$ctrl.afforms" class="crm-i fa-spinner fa-spin"></i>
- <em ng-if="$ctrl.afforms && !$ctrl.afforms[row.name.raw]">{{:: ts('None Found') }}</em>
+ <em ng-if="$ctrl.afforms && !$ctrl.afforms[row.data.name]">{{:: ts('None Found') }}</em>
</a>
</li>
- <li ng-if="$ctrl.afforms" ng-repeat="afform in $ctrl.afforms[row.name.raw]" title="{{:: $ctrl.afformAdminEnabled ? ts('Edit form') : '' }}">
+ <li ng-if="$ctrl.afforms" ng-repeat="afform in $ctrl.afforms[row.data.name]" title="{{:: $ctrl.afformAdminEnabled ? ts('Edit form') : '' }}">
<a target="_blank" ng-href="{{:: afform.link }}">
<i class="crm-i {{:: $ctrl.afformAdminEnabled ? 'fa-pencil-square-o' : 'fa-list-alt' }}"></i>
{{:: afform.title }}
-<a class="btn btn-xs btn-default" href="#/edit/{{:: row.id.raw }}" ng-if="row.permissionToEdit">
+<a class="btn btn-xs btn-default" href="#/edit/{{:: row.data.id }}" ng-if="row.permissionToEdit">
{{:: ts('Edit') }}
</a>
-<a class="btn btn-xs btn-default" href="#/create/{{:: row.api_entity.raw + '?params=' + $ctrl.encode(row.api_params.raw) }}">
+<a class="btn btn-xs btn-default" href="#/create/{{:: row.data.api_entity + '?params=' + $ctrl.encode(row.data.api_params) }}">
{{:: ts('Clone') }}
</a>
<a href class="btn btn-xs btn-danger" ng-click="$ctrl.confirmDelete(row)">
this.onPostRun.push(function(result) {
_.each(result, function(row) {
- row.permissionToEdit = CRM.checkPerm('all CiviCRM permissions and ACLs') || !_.includes(row.display_acl_bypass.raw, true);
+ row.permissionToEdit = CRM.checkPerm('all CiviCRM permissions and ACLs') || !_.includes(row.data.display_acl_bypass, true);
// Saves rendering cycles to not show an empty menu of search displays
- if (!row.display_name.raw) {
+ if (!row.data.display_name) {
row.openDisplayMenu = false;
}
});
function getConfirmationMsg() {
var msg = '<h4>' + _.escape(ts('Permanently delete this saved search?')) + '</h4>' +
'<ul>';
- if (search.display_label.view && search.display_label.view.length === 1) {
+ if (search.data.display_label && search.data.display_label.length === 1) {
msg += '<li>' + _.escape(ts('Includes 1 display which will also be deleted.')) + '</li>';
- } else if (search.display_label.view && search.display_label.view.length > 1) {
- msg += '<li>' + _.escape(ts('Includes %1 displays which will also be deleted.', {1: search.display_label.view.length})) + '</li>';
+ } else if (search.data.display_label && search.data.display_label.length > 1) {
+ msg += '<li>' + _.escape(ts('Includes %1 displays which will also be deleted.', {1: search.data.display_label.length})) + '</li>';
}
- _.each(search.groups.view, function(smartGroup) {
+ _.each(search.data.groups, function(smartGroup) {
msg += '<li class="crm-error"><i class="crm-i fa-exclamation-triangle"></i> ' + _.escape(ts('Smart group "%1" will also be deleted.', {1: smartGroup})) + '</li>';
});
if (search.afform_count) {
- _.each(ctrl.afforms[search.name.raw], function(afform) {
+ _.each(ctrl.afforms[search.data.name], function(afform) {
msg += '<li class="crm-error"><i class="crm-i fa-exclamation-triangle"></i> ' + _.escape(ts('Form "%1" will also be deleted because it contains an embedded display from this search.', {1: afform.title})) + '</li>';
});
}
}
var dialog = CRM.confirm({
- title: ts('Delete %1', {1: search.label.view}),
+ title: ts('Delete %1', {1: search.data.label}),
message: getConfirmationMsg(),
}).on('crmConfirm:yes', function() {
$scope.$apply(function() {
this.deleteSearch = function(search) {
crmStatus({start: ts('Deleting...'), success: ts('Search Deleted')},
- crmApi4('SavedSearch', 'delete', {where: [['id', '=', search.id.raw]]}).then(function() {
+ crmApi4('SavedSearch', 'delete', {where: [['id', '=', search.data.id]]}).then(function() {
ctrl.rowCount = null;
ctrl.runSearch();
})
function updateAfformCounts() {
_.each(ctrl.results, function(row) {
- row.afform_count = ctrl.afforms && ctrl.afforms[row.name.raw] && ctrl.afforms[row.name.raw].length || 0;
+ row.afform_count = ctrl.afforms && ctrl.afforms[row.data.name] && ctrl.afforms[row.data.name].length || 0;
});
}
<div class="btn-group">
- <button type="button" disabled ng-if="!row.display_name.raw" class="btn btn-xs dropdown-toggle btn-primary-outline">
+ <button type="button" disabled ng-if="!row.data.display_name" class="btn btn-xs dropdown-toggle btn-primary-outline">
{{:: ts('0 Displays') }}
</button>
- <button type="button" ng-if=":: row.display_name.raw" ng-click="row.openDisplayMenu = true" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- {{:: row.display_name.raw.length === 1 ? ts('1 Display') : ts('%1 Displays', {1: row.display_name.raw.length}) }} <span class="caret"></span>
+ <button type="button" ng-if=":: row.data.display_name" ng-click="row.openDisplayMenu = true" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{:: row.data.display_name.length === 1 ? ts('1 Display') : ts('%1 Displays', {1: row.data.display_name.length}) }} <span class="caret"></span>
</button>
<ul class="dropdown-menu" ng-if=":: row.openDisplayMenu">
- <li ng-repeat="display_name in row.display_name.raw" ng-class="{disabled: row.display_acl_bypass.raw[$index]}" title="{{:: row.display_acl_bypass.raw[$index] ? ts('Display has permissions disabled') : ts('View display') }}">
- <a ng-href="{{:: row.display_acl_bypass.raw[$index] ? '' : $ctrl.searchDisplayPath + '#/display/' + row.name.raw + '/' + display_name }}" target="_blank">
+ <li ng-repeat="display_name in row.data.display_name" ng-class="{disabled: row.data.display_acl_bypass[$index]}" title="{{:: row.data.display_acl_bypass[$index] ? ts('Display has permissions disabled') : ts('View display') }}">
+ <a ng-href="{{:: row.data.display_acl_bypass[$index] ? '' : $ctrl.searchDisplayPath + '#/display/' + row.data.name + '/' + display_name }}" target="_blank">
<i class="fa {{:: row.display_icon.rw[$index] }}"></i>
- {{:: row.display_label.raw[$index] }}
+ {{:: row.data.display_label[$index] }}
</a>
</li>
</ul>
-<crm-search-admin-tags tag-ids="row.tag_id.raw" saved-search-id="row.id.raw" class="btn-group btn-group-xs"></crm-search-admin-tags>
+<crm-search-admin-tags tag-ids="row.data.tag_id" saved-search-id="row.data.id" class="btn-group btn-group-xs"></crm-search-admin-tags>
-<span ng-repeat="item in col.links">
- <a class="btn {{:: col.size }} btn-{{:: item.style }}" target="{{:: item.target }}" href="{{:: $ctrl.getUrl(item.path, row) }}">
- <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
- {{:: $ctrl.replaceTokens(item.text, row) }}
+<span ng-repeat="link in colData.links">
+ <a class="btn {{:: $ctrl.settings.columns[colIndex].size }} btn-{{:: link.style }}" target="{{:: link.target }}" href="{{:: link.url }}">
+ <i ng-if=":: link.icon" class="crm-i {{:: link.icon }}"></i>
+ {{:: link.text }}
</a>
</span>
-<crm-search-display-editable row="row" col="col" on-success="$ctrl.runSearch(row)" cancel="$ctrl.editing = null;" ng-if="col.editable && $ctrl.editing && $ctrl.editing[0] === rowIndex && $ctrl.editing[1] === col.key"></crm-search-display-editable>
-<span ng-if="::!col.link && !col.image" ng-class="{'crm-editable-enabled': col.editable && !$ctrl.editing && row[col.editable.id]}" ng-click="col.editable && !$ctrl.editing && ($ctrl.editing = [rowIndex, col.key])">
- {{:: $ctrl.formatFieldValue(row, col) }}
+<crm-search-display-editable row="row" col="$ctrl.settings.columns[colIndex]" on-success="$ctrl.runSearch(row)" cancel="$ctrl.editing = null;" ng-if="col.editable && $ctrl.editing && $ctrl.editing[0] === rowIndex && $ctrl.editing[1] === col.key"></crm-search-display-editable>
+<span ng-if="::!colData.links && !colData.img" ng-class="{'crm-editable-enabled': colData.edit && !$ctrl.editing}" ng-click="col.edit && !$ctrl.editing && ($ctrl.editing = [rowIndex, col.key])">
+ {{:: $ctrl.formatFieldValue(colData) }}
</span>
-<span ng-if="::col.link">
- <span ng-repeat="link in $ctrl.getLinks(row, col)">
- <a target="{{:: col.link.target }}" href="{{:: link.url }}">
- <span ng-if=":: col.image && $ctrl.formatFieldValue(row, col).length">
- <img ng-src="{{:: $ctrl.formatFieldValue(row, col) }}" alt="{{:: $ctrl.replaceTokens(col.image.alt, row) }}" height="{{:: col.image.height }}" width="{{:: col.image.width }}"/>
+<span ng-if="::colData.links">
+ <span ng-repeat="link in colData.links">
+ <a target="{{:: link.target }}" href="{{:: link.url }}">
+ <span ng-if=":: colData.img">
+ <img ng-src="{{:: colData.img.src }}" alt="{{:: colData.val }}" height="{{:: colData.img.height }}" width="{{:: colData.img.width }}"/>
</span>
- {{:: link.value }}</a><span ng-if="!$last">,
+ {{:: link.text }}</a><span ng-if="!$last">,
</span>
</span>
</span>
-<span ng-if=":: !col.link && col.image && $ctrl.formatFieldValue(row, col).length">
- <img ng-src="{{:: $ctrl.formatFieldValue(row, col) }}" alt="{{:: $ctrl.replaceTokens(col.image.alt, row, $ctrl.settings.columns) }}" height="{{:: col.image.height }}" width="{{:: col.image.width }}"/>
+<span ng-if=":: !colData.links && colData.img">
+ <img ng-src="{{:: colData.img.src }}" alt="{{:: colData.val }}" height="{{:: colData.img.height }}" width="{{:: colData.img.width }}"/>
</span>
-<div ng-include="col.path"></div>
+<div ng-include="$ctrl.settings.columns[colIndex].path"></div>
-<span ng-repeat="item in col.links">
- <a class="text-{{:: item.style }}" target="{{:: item.target }}" href="{{:: $ctrl.getUrl(item.path, row) }}">
- <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
- {{:: $ctrl.replaceTokens(item.text, row) }}
+<span ng-repeat="link in colData.links">
+ <a class="text-{{:: link.style }}" target="{{:: link.target }}" href="{{:: link.url }}">
+ <i ng-if=":: link.icon" class="crm-i {{:: link.icon }}"></i>
+ {{:: link.text }}
</a>
</span>
<div class="btn-group">
- <button type="button" class="dropdown-toggle {{:: col.size }} btn-{{:: col.style }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-click="col.open = true">
- <i ng-if=":: col.icon" class="crm-i {{:: col.icon }}"></i>
- {{:: $ctrl.replaceTokens(col.text, row) }}
+ <button type="button" class="dropdown-toggle {{:: $ctrl.settings.columns[colIndex].size }} btn-{{:: $ctrl.settings.columns[colIndex].style }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-click="colData.open = true">
+ <i ng-if=":: $ctrl.settings.columns[colIndex].icon" class="crm-i {{:: $ctrl.settings.columns[colIndex].icon }}"></i>
+ {{:: colData.text }}
</button>
- <ul class="dropdown-menu {{ col.alignment === 'text-right' ? 'dropdown-menu-right' : '' }}" ng-if=":: col.open">
- <li ng-repeat="item in col.links" class="bg-{{:: item.style }}">
- <a href="{{:: $ctrl.getUrl(item.path, row) }}" target="{{:: item.target }}">
- <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
- {{:: $ctrl.replaceTokens(item.text, row) }}
+ <ul class="dropdown-menu {{ $ctrl.settings.columns[colIndex].alignment === 'text-right' ? 'dropdown-menu-right' : '' }}" ng-if=":: colData.open">
+ <li ng-repeat="link in colData.links" class="bg-{{:: link.style }}">
+ <a href="{{:: link.url }}" target="{{:: link.target }}">
+ <i ng-if=":: link.icon" class="crm-i {{:: link.icon }}"></i>
+ {{:: link.text }}
</a>
</li>
</ul>
// Trait provides base methods and properties common to all search display types
angular.module('crmSearchDisplay').factory('searchDisplayBaseTrait', function(crmApi4) {
var ts = CRM.ts('org.civicrm.search_kit'),
- runCount = 0,
- seed = Date.now();
-
- // Replace tokens keyed to rowData.
- // Pass view=true to replace with view value, otherwise raw value is used.
- function replaceTokens(str, rowData, view, index) {
- if (!str) {
- return '';
- }
- _.each(rowData, function(value, key) {
- if (str.indexOf('[' + key + ']') >= 0) {
- var val = view ? value.view : value.raw,
- replacement = angular.isArray(val) ? val[index || 0] : val;
- str = str.replace(new RegExp(_.escapeRegExp('[' + key + ']', 'g')), replacement);
- }
- });
- return str;
- }
-
- function getUrl(link, rowData, index) {
- var url = replaceTokens(link, rowData, false, index);
- if (url.slice(0, 1) !== '/' && url.slice(0, 4) !== 'http') {
- url = CRM.url(url);
- }
- return url;
- }
-
- // Returns display value for a single column in a row
- function formatDisplayValue(rowData, key, columns) {
- var column = _.findWhere(columns, {key: key}),
- displayValue = column.rewrite ? replaceTokens(column.rewrite, rowData, columns) : getValue(rowData[key], 'view');
- return angular.isArray(displayValue) ? displayValue.join(', ') : displayValue;
- }
-
- // Returns value and url for a column formatted as link(s)
- function formatLinks(rowData, key, columns) {
- var column = _.findWhere(columns, {key: key}),
- value = column.image ? '' : getValue(rowData[key], 'view'),
- values = angular.isArray(value) ? value : [value],
- links = [];
- _.each(values, function(value, index) {
- links.push({
- value: value,
- url: getUrl(column.link.path, rowData, index)
- });
- });
- return links;
- }
+ runCount = 0;
// Get value from column data, specify either 'raw' or 'view'
function getValue(data, ret) {
return {
page: 1,
rowCount: null,
- getUrl: getUrl,
// Arrays may contain callback functions for various events
onChangeFilters: [],
onPreRun: [],
});
});
},
- replaceTokens: function(value, row) {
- return replaceTokens(value, row, this.settings.columns);
- },
- getLinks: function(rowData, col) {
- rowData._links = rowData._links || {};
- if (!(col.key in rowData._links)) {
- rowData._links[col.key] = formatLinks(rowData, col.key, this.settings.columns);
- }
- return rowData._links[col.key];
- },
- formatFieldValue: function(rowData, col) {
- return formatDisplayValue(rowData, col.key, this.settings.columns);
+ formatFieldValue: function(colData) {
+ return angular.isArray(colData.val) ? colData.val.join(', ') : colData.val;
}
};
});
<div ng-repeat="(rowIndex, row) in $ctrl.results">
- <div ng-repeat="col in $ctrl.settings.columns" title="{{:: $ctrl.replaceTokens(col.title, row) }}" class="{{:: col.break ? '' : 'crm-inline-block' }}">
- <label ng-if=":: col.label && (col.type !== 'field' || col.forceLabel || row[col.key])">
- {{:: $ctrl.replaceTokens(col.label, row) }}
+ <div ng-repeat="(colIndex, colData) in row.columns" title="{{:: colData.title }}" class="{{:: colData.cssClass }} {{:: $ctrl.settings.columns[colIndex].break ? '' : 'crm-inline-block' }}">
+ <label ng-if=":: colData.label">
+ {{:: colData.label }}
</label>
- <span ng-include="'~/crmSearchDisplay/colType/' + col.type + '.html'"></span>
+ <span ng-include="'~/crmSearchDisplay/colType/' + $ctrl.settings.columns[colIndex].type + '.html'"></span>
</div>
</div>
<li ng-repeat="(rowIndex, row) in $ctrl.results">
- <div ng-repeat="col in $ctrl.settings.columns" title="{{:: $ctrl.replaceTokens(col.title, row) }}" class="{{:: col.break ? '' : 'crm-inline-block' }}">
- <label ng-if=":: col.label && (col.type !== 'field' || col.forceLabel || row[col.key])">
- {{:: $ctrl.replaceTokens(col.label, row) }}
+ <div ng-repeat="(colIndex, colData) in row.columns" title="{{:: colData.title }}" class="{{:: colData.cssClass }} {{:: $ctrl.settings.columns[colIndex].break ? '' : 'crm-inline-block' }}">
+ <label ng-if=":: colData.label">
+ {{:: colData.label }}
</label>
- <span ng-include="'~/crmSearchDisplay/colType/' + col.type + '.html'"></span>
+ <span ng-include="'~/crmSearchDisplay/colType/' + $ctrl.settings.columns[colIndex].type + '.html'"></span>
</div>
</li>
<td ng-if=":: $ctrl.settings.actions">
<input type="checkbox" ng-checked="$ctrl.isRowSelected(row)" ng-click="$ctrl.selectRow(row)" ng-disabled="!(!$ctrl.loadingAllRows && row.id)">
</td>
- <td ng-repeat="col in $ctrl.settings.columns" ng-include="'~/crmSearchDisplay/colType/' + col.type + '.html'" title="{{:: $ctrl.replaceTokens(col.title, row) }}" class="{{:: col.alignment }}">
+ <td ng-repeat="(colIndex, colData) in row.columns" ng-include="'~/crmSearchDisplay/colType/' + $ctrl.settings.columns[colIndex].type + '.html'" title="{{:: colData.title }}" class="{{:: colData.cssClass }}">
</td>
</tr>
<tr ng-if="$ctrl.rowCount === 0">
// Toggle row selection
selectRow: function(row) {
- var index = this.selectedRows.indexOf(row.id.raw);
+ var index = this.selectedRows.indexOf(row.data.id);
if (index < 0) {
- this.selectedRows.push(row.id.raw);
+ this.selectedRows.push(row.data.id);
this.allRowsSelected = (this.rowCount === this.selectedRows.length);
} else {
this.allRowsSelected = false;
// @return bool
isRowSelected: function(row) {
- return this.allRowsSelected || _.includes(this.selectedRows, row.id.raw);
+ return this.allRowsSelected || _.includes(this.selectedRows, row.data.id);
},
refreshAfterTask: function() {
onPostRun: [function(results, status, editedRow) {
if (editedRow && status === 'success') {
// If edited row disappears (because edits cause it to not meet search criteria), deselect it
- var index = this.selectedRows.indexOf(editedRow.id.raw);
- if (index > -1 && !_.findWhere(results, {id: editedRow.id.raw})) {
+ var index = this.selectedRows.indexOf(editedRow.data.id);
+ if (index > -1 && !_.findWhere(results, {id: editedRow.data.id})) {
this.selectedRows.splice(index, 1);
}
}
$download = (array) civicrm_api4('SearchDisplay', 'download', $params);
$header = array_shift($download);
- $this->assertEquals('First Last', $header[0]['label']);
+ $this->assertEquals('First Last', $header[0]);
foreach ($download as $rowNum => $data) {
$this->assertEquals($sampleData[$rowNum]['first_name'] . ' ' . $lastName, $data[0]);
'dataType' => 'String',
'type' => 'field',
],
+ [
+ 'key' => 'is_deceased',
+ 'label' => 'Deceased',
+ 'dataType' => 'Boolean',
+ 'type' => 'field',
+ ],
],
'sort' => [
['id', 'ASC'],
$params['filters']['first_name'] = ['One', 'Two'];
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(2, $result);
- $this->assertEquals('One', $result[0]['first_name']['raw']);
- $this->assertEquals('Two', $result[1]['first_name']['raw']);
+ $this->assertEquals('One', $result[0]['data']['first_name']);
+ $this->assertEquals('Two', $result[1]['data']['first_name']);
// Raw value should be boolean, view value should be string
- $this->assertEquals(FALSE, $result[0]['is_deceased']['raw']);
- $this->assertEquals(ts('No'), $result[0]['is_deceased']['view']);
+ $this->assertEquals(FALSE, $result[0]['data']['is_deceased']);
+ $this->assertEquals(ts('No'), $result[0]['columns'][4]['val']);
- $params['filters'] = ['last_name' => $lastName, 'id' => ['>' => $result[0]['id']['raw'], '<=' => $result[1]['id']['raw'] + 1]];
+ $params['filters'] = ['last_name' => $lastName, 'id' => ['>' => $result[0]['data']['id'], '<=' => $result[1]['data']['id'] + 1]];
$params['sort'] = [['first_name', 'ASC']];
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(2, $result);
- $this->assertEquals('Three', $result[0]['first_name']['raw']);
- $this->assertEquals('Two', $result[1]['first_name']['raw']);
+ $this->assertEquals('Three', $result[0]['data']['first_name']);
+ $this->assertEquals('Two', $result[1]['data']['first_name']);
$params['filters'] = ['last_name' => $lastName, 'contact_sub_type:label' => ['Tester', 'Bot']];
$result = civicrm_api4('SearchDisplay', 'run', $params);
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(2, $result);
- $this->assertNotEmpty($result->first()['display_name']['raw']);
+ $this->assertNotEmpty($result->first()['data']['display_name']);
// Assert that display name was added to the search due to the link token
- $this->assertNotEmpty($result->first()['sort_name']['raw']);
+ $this->assertNotEmpty($result->first()['data']['sort_name']);
// These items are not part of the search, but will be added via links
$this->assertArrayNotHasKey('contact_type', $result->first());
],
];
$result = civicrm_api4('SearchDisplay', 'run', $params);
- $this->assertEquals('Individual', $result->first()['contact_type']['raw']);
- $this->assertEquals('Unit test', $result->first()['source']['raw']);
- $this->assertEquals($lastName, $result->first()['last_name']['raw']);
+ $this->assertEquals('Individual', $result->first()['data']['contact_type']);
+ $this->assertEquals('Unit test', $result->first()['data']['source']);
+ $this->assertEquals($lastName, $result->first()['data']['last_name']);
}
/**
$this->cleanupCachedPermissions();
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(1, $result);
- $this->assertEquals($sampleData['Two'], $result[0]['id']['raw']);
+ $this->assertEquals($sampleData['Two'], $result[0]['data']['id']);
$hooks->setHook('civicrm_aclWhereClause', [$this, 'aclWhereGreaterThan']);
$this->cleanupCachedPermissions();
$result = civicrm_api4('SearchDisplay', 'run', $params);
$this->assertCount(2, $result);
- $this->assertEquals($sampleData['Three'], $result[0]['id']['raw']);
- $this->assertEquals($sampleData['Four'], $result[1]['id']['raw']);
+ $this->assertEquals($sampleData['Three'], $result[0]['data']['id']);
+ $this->assertEquals($sampleData['Four'], $result[1]['data']['id']);
}
public function testWithACLBypass() {