From f28a6f1829406d468b18b30d04bb1e16ed2d0dd6 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sun, 31 Oct 2021 12:29:00 -0400 Subject: [PATCH] SearchKit - Conditional style rules per-row & per-field --- .../Generic/Traits/ArrayQueryActionTrait.php | 4 +- css/civicrm.css | 6 +- .../SearchDisplay/AbstractRunAction.php | 76 +++++++++- ext/search_kit/ang/crmSearchAdmin.module.js | 6 +- .../crmSearchAdminLinkSelect.component.js | 8 ++ .../crmSearchAdminLinkSelect.html | 4 +- .../displays/colType/field.html | 1 + .../common/searchAdminCssRules.component.js | 69 +++++++++ .../displays/common/searchAdminCssRules.html | 48 +++++++ .../searchAdminDisplayTable.component.js | 2 +- .../displays/searchAdminDisplayTable.html | 1 + .../crmSearchDisplayGridItems.html | 2 +- .../crmSearchDisplayListItems.html | 2 +- .../crmSearchDisplayTableBody.html | 4 +- ext/search_kit/css/crmSearchAdmin.css | 5 + .../api/v4/SearchDisplay/SearchRunTest.php | 136 +++++++++++++++++- 16 files changed, 358 insertions(+), 16 deletions(-) create mode 100644 ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminCssRules.component.js create mode 100644 ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminCssRules.html diff --git a/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php b/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php index 467fd6054c..70a53af8b3 100644 --- a/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php +++ b/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php @@ -87,7 +87,7 @@ trait ArrayQueryActionTrait { return $result; default: - return $this->filterCompare($row, $filters); + return self::filterCompare($row, $filters); } } @@ -97,7 +97,7 @@ trait ArrayQueryActionTrait { * @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.'); } diff --git a/css/civicrm.css b/css/civicrm.css index fcac87c112..163095e9d0 100644 --- a/css/civicrm.css +++ b/css/civicrm.css @@ -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; } @@ -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, diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index 4bc6287afd..a2ab258ce8 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -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; @@ -123,9 +124,11 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { 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]; @@ -162,7 +165,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { */ 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'])) { @@ -201,6 +204,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { $out = $this->formatLinksColumn($column, $data); break; } + $cssClass = $this->getCssStyles($column['cssRules'] ?? [], $data); if (!empty($column['alignment'])) { $cssClass[] = $column['alignment']; } @@ -210,6 +214,66 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { 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 @@ -635,6 +699,10 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { 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 @@ -655,6 +723,10 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { $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); @@ -697,7 +769,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { } } } - return $result; + return $result ?: $alias; } /** diff --git a/ext/search_kit/ang/crmSearchAdmin.module.js b/ext/search_kit/ang/crmSearchAdmin.module.js index ee6bb116f5..b920e7ba90 100644 --- a/ext/search_kit/ang/crmSearchAdmin.module.js +++ b/ext/search_kit/ang/crmSearchAdmin.module.js @@ -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], @@ -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 { @@ -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, diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js index bf7c68d4ba..8291b3af39 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js @@ -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 () { diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html index cd4f85f2b7..1fa7efac6a 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html @@ -1,10 +1,10 @@
- -
+ diff --git a/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminCssRules.component.js b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminCssRules.component.js new file mode 100644 index 0000000000..b19ade12fd --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminCssRules.component.js @@ -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._); diff --git a/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminCssRules.html b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminCssRules.html new file mode 100644 index 0000000000..cf9e7db745 --- /dev/null +++ b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminCssRules.html @@ -0,0 +1,48 @@ +
+ +
+ +
+ + + +
+
+ + + + + + +
+
+ +
+ + +
+
diff --git a/ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js b/ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js index 7c45809d27..78119bbad1 100644 --- a/ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js @@ -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 diff --git a/ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html b/ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html index b3234eadaa..a3090f9203 100644 --- a/ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html +++ b/ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html @@ -18,6 +18,7 @@
+
diff --git a/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGridItems.html b/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGridItems.html index a413ad1cf6..805a432aa2 100644 --- a/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGridItems.html +++ b/ext/search_kit/ang/crmSearchDisplayGrid/crmSearchDisplayGridItems.html @@ -1,4 +1,4 @@ -
+