Skip to content

Commit

Permalink
Merge pull request #21947 from colemanw/cssRules
Browse files Browse the repository at this point in the history
SearchKit - Conditional style rules for rows/cells
  • Loading branch information
eileenmcnaughton authored Nov 4, 2021
2 parents 39bc566 + f28a6f1 commit 826ba50
Show file tree
Hide file tree
Showing 16 changed files with 358 additions and 16 deletions.
4 changes: 2 additions & 2 deletions Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private function walkFilters($row, $filters) {
return $result;

default:
return $this->filterCompare($row, $filters);
return self::filterCompare($row, $filters);
}
}

Expand All @@ -97,7 +97,7 @@ private function walkFilters($row, $filters) {
* @return bool
* @throws \Civi\API\Exception\NotImplementedException
*/
private function filterCompare($row, $condition) {
public static function filterCompare($row, $condition) {
if (!is_array($condition)) {
throw new NotImplementedException('Unexpected where syntax; expecting array.');
}
Expand Down
6 changes: 5 additions & 1 deletion css/civicrm.css
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,10 @@ input.crm-form-entityref {
font-weight: bold;
}

.crm-container .font-bold {
font-weight: bold !important;
}

.crm-container .font-italic {
font-style: italic;
}
Expand Down Expand Up @@ -3867,7 +3871,7 @@ span.crm-status-icon {
}

.crm-container .strikethrough {
text-decoration: line-through;
text-decoration: line-through !important;
}

.crm-container input.ng-invalid.ng-dirty,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Civi\Api4\Action\SearchDisplay;

use Civi\API\Exception\UnauthorizedException;
use Civi\Api4\Generic\Traits\ArrayQueryActionTrait;
use Civi\Api4\SearchDisplay;
use Civi\Api4\Utils\CoreUtil;

Expand Down Expand Up @@ -123,9 +124,11 @@ protected function formatResult(\Civi\Api4\Generic\Result $result): array {
foreach ($this->display['settings']['columns'] as $column) {
$columns[] = $this->formatColumn($column, $data);
}
$style = $this->getCssStyles($this->display['settings']['cssRules'] ?? [], $data);
$row = [
'data' => $data,
'columns' => $columns,
'cssClass' => implode(' ', $style),
];
if (isset($data[$keyName])) {
$row['key'] = $data[$keyName];
Expand Down Expand Up @@ -162,7 +165,7 @@ private function getValue($key, $data, $rowIndex) {
*/
private function formatColumn($column, $data) {
$column += ['rewrite' => NULL, 'label' => NULL];
$out = $cssClass = [];
$out = [];
switch ($column['type']) {
case 'field':
if (isset($column['image']) && is_array($column['image'])) {
Expand Down Expand Up @@ -201,6 +204,7 @@ private function formatColumn($column, $data) {
$out = $this->formatLinksColumn($column, $data);
break;
}
$cssClass = $this->getCssStyles($column['cssRules'] ?? [], $data);
if (!empty($column['alignment'])) {
$cssClass[] = $column['alignment'];
}
Expand All @@ -210,6 +214,66 @@ private function formatColumn($column, $data) {
return $out;
}

/**
* Evaluates conditional style rules
*
* Rules are in the format ['css class', 'field_name', 'OPERATOR', 'value']
*
* @param array[] $styleRules
* @param array $data
* @return array
*/
protected function getCssStyles(array $styleRules, array $data) {
$classes = [];
foreach ($styleRules as $clause) {
$cssClass = $clause[0] ?? '';
if ($cssClass) {
$condition = $this->getCssRuleCondition($clause);
if (is_null($condition[0]) || (ArrayQueryActionTrait::filterCompare($data, $condition))) {
$classes[] = $cssClass;
}
}
}
return $classes;
}

/**
* Returns the condition of a cssRules
*
* @param array $clause
* @return array
*/
protected function getCssRuleCondition($clause) {
$fieldKey = $clause[1] ?? NULL;
// For fields used in group by, add aggregation and change operator from = to CONTAINS
// FIXME: This assumes the operator is always set to '=', which so far is all the admin UI supports.
// That's only a safe assumption as long as the admin UI doesn't have an operator selector.
// @see ang/crmSearchAdmin/displays/common/searchAdminCssRules.html
if ($fieldKey && $this->canAggregate($fieldKey)) {
$clause[2] = 'CONTAINS';
$fieldKey = 'GROUP_CONCAT_' . str_replace(['.', ':'], '_', $clause[1]);
}
return [$fieldKey, $clause[2] ?? 'IS NOT EMPTY', $clause[3] ?? NULL];
}

/**
* Return fields needed for the select clause by a set of css rules
*
* @param array $cssRules
* @return array
*/
protected function getCssRulesSelect($cssRules) {
$select = [];
foreach ($cssRules as $clause) {
$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;
}
}
return $select;
}

/**
* Format a field value as links
* @param $column
Expand Down Expand Up @@ -635,6 +699,10 @@ protected function augmentSelectClause(&$apiParams): void {
if (!empty($this->display['settings']['actions'])) {
$additions = CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'primary_key');
}
// Add style conditions for the display
foreach ($this->getCssRulesSelect($this->display['settings']['cssRules'] ?? []) as $addition) {
$additions[] = $addition;
}
$possibleTokens = '';
foreach ($this->display['settings']['columns'] as $column) {
// Collect display values in which a token is allowed
Expand All @@ -655,6 +723,10 @@ protected function augmentSelectClause(&$apiParams): void {
$additions[] = $editable['id_path'];
}
}
// Add style conditions for the column
foreach ($this->getCssRulesSelect($column['cssRules'] ?? []) as $addition) {
$additions[] = $addition;
}
}
// Add fields referenced via token
$tokens = $this->getTokens($possibleTokens);
Expand Down Expand Up @@ -697,7 +769,7 @@ protected function getJoinFromAlias(string $alias) {
}
}
}
return $result;
return $result ?: $alias;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions ext/search_kit/ang/crmSearchAdmin.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@
}
if (field) {
field.baseEntity = entityName;
return {field: field, join: join};
}
return {field: field, join: join};
}
function parseFnArgs(info, expr) {
var fnName = expr.split('(')[0],
Expand Down Expand Up @@ -224,7 +224,7 @@
};
} else if (arg) {
var fieldAndJoin = getFieldAndJoin(arg, searchEntity);
if (fieldAndJoin) {
if (fieldAndJoin.field) {
var split = arg.split(':'),
prefixPos = split[0].lastIndexOf(fieldAndJoin.field.name);
return {
Expand Down Expand Up @@ -294,7 +294,7 @@
return {
getEntity: getEntity,
getField: function(fieldName, entityName) {
return getFieldAndJoin(fieldName, entityName).field;
return getFieldAndJoin(fieldName, entityName || searchEntity).field;
},
getJoin: getJoin,
parseExpr: parseExpr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
ctrl = this;

this.$onInit = function() {
$element.on('hidden.bs.dropdown', function() {
$scope.$apply(function() {
ctrl.menuOpen = false;
});
});
};

this.setValue = function(val) {
if (val.path) {
$timeout(function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div class="crm-flex-1 input-group" >
<input type="text" class="form-control" ng-if="!$ctrl.link.action" ng-model="$ctrl.link.path" ng-model-options="{updateOn: 'blur'}" ng-change="$ctrl.onChange({newLink: $ctrl.link})">
<div class="input-group-btn" style="{{ $ctrl.link.action ? '' : 'width:27px' }}">
<button type="button" class="btn btn-sm btn-secondary-outline dropdown-toggle" style="min-width: 200px; text-align: left;" ng-if="$ctrl.link.action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button type="button" ng-click="$ctrl.menuOpen = true;" class="btn btn-sm btn-secondary-outline dropdown-toggle crm-search-admin-combo-button" ng-if="$ctrl.link.action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ $ctrl.getLink().text }}
</button>
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button type="button" ng-click="$ctrl.menuOpen = true;" class="btn btn-sm btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu {{ $ctrl.link.action ? '' : 'dropdown-menu-right' }}" style="min-width: 223px;">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@
{{:: ts('In-Place Edit') }}
</label>
</div>
<search-admin-css-rules label="{{:: ts('Style') }}" item="col" default="col.key"></search-admin-css-rules>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
(function(angular, $, _) {
"use strict";

angular.module('crmSearchAdmin').component('searchAdminCssRules', {
bindings: {
item: '<',
default: '<',
label: '@',
},
require: {
crmSearchAdmin: '^crmSearchAdmin'
},
templateUrl: '~/crmSearchAdmin/displays/common/searchAdminCssRules.html',
controller: function($scope, $element, searchMeta) {
var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
ctrl = this;

this.getField = searchMeta.getField;

this.styles = _.transform(_.cloneDeep(CRM.crmSearchAdmin.styles), function(styles, style) {
if (style.key !== 'default' && style.key !== 'secondary') {
styles['bg-' + style.key] = style.value;
}
}, {});
this.styles.disabled = ts('Disabled');
this.styles['font-bold'] = ts('Bold');
this.styles['font-italic'] = ts('Italic');
this.styles.strikethrough = ts('Strikethrough');

this.fields = function() {
return {results: ctrl.crmSearchAdmin.getAllFields(':name', ['Field', 'Custom', 'Extra'])};
};

this.$onInit = function() {
$element.on('hidden.bs.dropdown', function() {
$scope.$apply(function() {
ctrl.menuOpen = false;
});
});
};

this.onSelectField = function(clause) {
if (clause[1]) {
clause[2] = '=';
clause.length = 3;
} else {
clause.length = 1;
}
};

this.addClause = function(style) {
var clause = [style];
if (ctrl.default && ctrl.getField(ctrl.default)) {
clause.push(ctrl.default, '=');
}
this.item.cssRules = this.item.cssRules || [];
this.item.cssRules.push(clause);
};

this.showMore = function() {
return !this.item.cssRules || !this.item.cssRules.length || _.last(this.item.cssRules)[1];
};



}
});

})(angular, CRM.$, CRM._);
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<div class="form-inline" ng-repeat="clause in $ctrl.item.cssRules">
<label>{{:: $ctrl.label }}</label>
<div class="input-group">
<input type="text" class="form-control" ng-if="!$ctrl.styles[clause[0]]" placeholder="{{:: ts('CSS class') }}" ng-model="clause[0]" ng-model-options="{updateOn: 'blur'}">
<div class="input-group-btn" style="{{ $ctrl.styles[clause[0]] ? '' : 'width:27px' }}">
<button type="button" ng-click="$ctrl.menuOpen = true" ng-if="$ctrl.styles[clause[0]]" class="btn btn-sm dropdown-toggle crm-search-admin-combo-button {{ clause[0].replace('bg-', 'btn-') }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ $ctrl.styles[clause[0]] }}
</button>
<button type="button" ng-click="$ctrl.menuOpen = true" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">{{:: $ctrl.label }}</span> <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" ng-if="$ctrl.menuOpen">
<li ng-repeat="(key, label) in $ctrl.styles">
<a href class="{{:: key }}" ng-click="clause[0] = key">{{:: label }}</a>
</li>
<li class="divider" role="separator"></li>
<li>
<a href ng-click="clause[0] = ''">{{:: ts('Other') }}</a>
</li>
</ul>
</div>
</div>
<label>{{:: ts('If') }}</label>
<input class="form-control collapsible-optgroups" ng-model="clause[1]" crm-ui-select="::{data: $ctrl.fields, allowClear: true, placeholder: ts('Always')}" ng-change="$ctrl.onSelectField(clause)" />
<!-- TODO: Support operators other than '=' as clause[2] -->
<label ng-if="clause[1]">{{:: ts('Is') }}</label>
<crm-search-input ng-if="clause[1]" ng-model="clause[3]" field="$ctrl.getField(clause[1])" option-key="'name'" op="clause[1]" format="$ctrl.format" class="form-group"></crm-search-input>
<button type="button" class="btn-xs btn-danger-outline" ng-click="$ctrl.item.cssRules.splice($index);" title="{{:: ts('Remove style') }}">
<i class="crm-i fa-ban"></i>
</button>
</div>
<div class="form-inline" ng-if="$ctrl.showMore()" title="{{:: ts('Set background color or text style based on a field value') }}">
<label>{{:: $ctrl.label }}</label>
<div class="btn-group">
<button type="button" ng-click="$ctrl.menuOpen = true" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span>{{ $ctrl.item.cssRules && $ctrl.item.cssRules.length ? ts('Add') : ts('None') }}</span> <span class="caret"></span>
</button>
<ul class="dropdown-menu" ng-if="$ctrl.menuOpen">
<li ng-repeat="(key, label) in $ctrl.styles">
<a href class="{{:: key }}" ng-click="$ctrl.addClause(key)">{{:: label }}</a>
</li>
<li class="divider" role="separator"></li>
<li>
<a href ng-click="$ctrl.addClause('')">{{:: ts('Other') }}</a>
</li>
</ul>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
{name: 'table-striped', label: ts('Even/Odd Stripes')}
];

// Check if array conatains item
// Check if array contains item
this.includes = _.includes;

// Add or remove an item from an array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
</label>
</div>
</div>
<search-admin-css-rules label="{{:: ts('Row Style') }}" item="$ctrl.display.settings"></search-admin-css-rules>
<search-admin-pager-config display="$ctrl.display"></search-admin-pager-config>
</fieldset>
<fieldset class="crm-search-admin-edit-columns-wrapper">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div ng-repeat="(rowIndex, row) in $ctrl.results">
<div ng-repeat="(rowIndex, row) in $ctrl.results" class="{{:: row.cssClass }}">
<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 }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<li ng-repeat="(rowIndex, row) in $ctrl.results">
<li ng-repeat="(rowIndex, row) in $ctrl.results" class="{{:: row.cssClass }}">
<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 }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<tr ng-repeat="(rowIndex, row) in $ctrl.results">
<td ng-if=":: $ctrl.settings.actions">
<td ng-if=":: $ctrl.settings.actions" class="{{:: row.cssClass }}">
<input type="checkbox" ng-checked="$ctrl.isRowSelected(row)" ng-click="$ctrl.selectRow(row)" ng-disabled="!!$ctrl.loadingAllRows">
</td>
<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 ng-repeat="(colIndex, colData) in row.columns" ng-include="'~/crmSearchDisplay/colType/' + $ctrl.settings.columns[colIndex].type + '.html'" title="{{:: colData.title }}" class="{{:: row.cssClass }} {{:: colData.cssClass }}">
</td>
</tr>
<tr ng-if="$ctrl.rowCount === 0">
Expand Down
Loading

0 comments on commit 826ba50

Please sign in to comment.