Skip to content

Commit

Permalink
Merge pull request #21535 from colemanw/concat_ws
Browse files Browse the repository at this point in the history
Add CONCAT_WS fn to APIv4 and UI support in SearchKit
  • Loading branch information
eileenmcnaughton authored Sep 19, 2021
2 parents e0b149e + 2929fd3 commit 7aac971
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 14 deletions.
4 changes: 4 additions & 0 deletions Civi/Api4/Query/SqlFunctionCOALESCE.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ protected static function params(): array {
[
'max_expr' => 99,
'optional' => FALSE,
'ui_defaults' => [
['type' => 'SqlField', 'placeholder' => ts('If')],
['type' => 'SqlField', 'placeholder' => ts('Else')],
],
],
];
}
Expand Down
7 changes: 5 additions & 2 deletions Civi/Api4/Query/SqlFunctionCONCAT.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ protected static function params(): array {
'max_expr' => 99,
'optional' => FALSE,
'must_be' => ['SqlField', 'SqlString'],
'ui_defaults' => [
['placeholder' => ts('Plus')],
],
],
];
}
Expand All @@ -34,14 +37,14 @@ protected static function params(): array {
* @return string
*/
public static function getTitle(): string {
return ts('Combine text');
return ts('Combine if');
}

/**
* @return string
*/
public static function getDescription(): string {
return ts('Multiple values concatenated into a single string.');
return ts('Joined text, only if all values are not null.');
}

}
51 changes: 51 additions & 0 deletions Civi/Api4/Query/SqlFunctionCONCAT_WS.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

namespace Civi\Api4\Query;

/**
* Sql function
*/
class SqlFunctionCONCAT_WS extends SqlFunction {

protected static $category = self::CATEGORY_STRING;

protected static $dataType = 'String';

protected static function params(): array {
return [
[
'max_expr' => 99,
'optional' => FALSE,
'must_be' => ['SqlField', 'SqlString'],
'ui_defaults' => [
['type' => 'SqlString', 'placeholder' => ts('Separator')],
['type' => 'SqlField', 'placeholder' => ts('Plus')],
],
],
];
}

/**
* @return string
*/
public static function getTitle(): string {
return ts('Combine text');
}

/**
* @return string
*/
public static function getDescription(): string {
return ts('Every non-null value joined by a separator.');
}

}
4 changes: 4 additions & 0 deletions Civi/Api4/Query/SqlFunctionGREATEST.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ protected static function params(): array {
'max_expr' => 99,
'min_expr' => 2,
'optional' => FALSE,
'ui_defaults' => [
['type' => 'SqlField', 'placeholder' => ts('If')],
['type' => 'SqlField', 'placeholder' => ts('Else')],
],
],
];
}
Expand Down
5 changes: 5 additions & 0 deletions Civi/Api4/Query/SqlFunctionIF.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ protected static function params(): array {
'min_expr' => 3,
'max_expr' => 3,
'optional' => FALSE,
'ui_defaults' => [
['type' => 'SqlField', 'placeholder' => ts('If')],
['type' => 'SqlField', 'placeholder' => ts('Then')],
['type' => 'SqlField', 'placeholder' => ts('Else')],
],
],
];
}
Expand Down
4 changes: 4 additions & 0 deletions Civi/Api4/Query/SqlFunctionLEAST.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ protected static function params(): array {
'max_expr' => 99,
'min_expr' => 2,
'optional' => FALSE,
'ui_defaults' => [
['type' => 'SqlField', 'placeholder' => ts('If')],
['type' => 'SqlField', 'placeholder' => ts('Else')],
],
],
];
}
Expand Down
4 changes: 4 additions & 0 deletions Civi/Api4/Query/SqlFunctionNULLIF.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ protected static function params(): array {
'min_expr' => 2,
'max_expr' => 2,
'optional' => FALSE,
'ui_defaults' => [
['type' => 'SqlField', 'placeholder' => ts('Preferred')],
['type' => 'SqlField', 'placeholder' => ts('Alternate')],
],
],
];
}
Expand Down
5 changes: 5 additions & 0 deletions Civi/Api4/Query/SqlFunctionREPLACE.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ protected static function params(): array {
'max_expr' => 3,
'optional' => FALSE,
'must_be' => ['SqlString', 'SqlField'],
'ui_defaults' => [
['type' => 'SqlField', 'placeholder' => ts('Source')],
['type' => 'SqlString', 'placeholder' => ts('Find')],
['type' => 'SqlString', 'placeholder' => ts('Replace')],
],
],
];
}
Expand Down
4 changes: 4 additions & 0 deletions Civi/Api4/Query/SqlFunctionROUND.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ protected static function params(): array {
'min_expr' => 1,
'max_expr' => 2,
'must_be' => ['SqlNumber', 'SqlField'],
'ui_defaults' => [
['type' => 'SqlField', 'placeholder' => ts('Number')],
['type' => 'SqlNumber', 'placeholder' => ts('Decimals')],
],
],
];
}
Expand Down
39 changes: 32 additions & 7 deletions ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
ctrl = this;

var defaultUiDefaults = {type: 'SqlField', placeholder: ts('Select')};

var allTypes = {
aggregate: ts('Aggregate'),
comparison: ts('Comparison'),
Expand All @@ -29,14 +31,15 @@

this.$onInit = function() {
var info = searchMeta.parseExpr(ctrl.expr);
ctrl.fieldArg = _.findWhere(info.args, {type: 'field'});
ctrl.args = info.args;
ctrl.fn = info.fn;
ctrl.fnName = !info.fn ? '' : info.fn.name;
initFunction();
};

this.addArg = function(exprType) {
exprType = exprType || ctrl.fn.params[0].must_be[0];
exprType = exprType || ctrl.getUiDefault(ctrl.args.length).type;
ctrl.args.push({
type: ctrl.exprTypes[exprType].type,
value: exprType === 'SqlNumber' ? 0 : ''
Expand All @@ -55,23 +58,32 @@
ctrl.modifierName = null;
ctrl.modifier = null;
}
// Push args to reach the minimum
while (ctrl.args.length < ctrl.fn.params[0].min_expr) {
ctrl.addArg();
}
}

this.getUiDefault = function(index) {
if (ctrl.fn.params[0].ui_defaults) {
return ctrl.fn.params[0].ui_defaults[index] || _.last(ctrl.fn.params[0].ui_defaults);
}
defaultUiDefaults.type = ctrl.fn.params[0].must_be[0];
return defaultUiDefaults;
};

// On-demand options for dropdown function selector
this.getFunctions = function() {
var allowedTypes = [], functions = [];
if (ctrl.expr && ctrl.args[0] && ctrl.args[0].field) {
if (ctrl.expr && ctrl.fieldArg) {
if (ctrl.crmSearchAdmin.canAggregate(ctrl.expr)) {
allowedTypes.push('aggregate');
} else {
allowedTypes.push('comparison', 'string');
if (_.includes(['Integer', 'Float', 'Date', 'Timestamp'], ctrl.args[0].field.data_type)) {
if (_.includes(['Integer', 'Float', 'Date', 'Timestamp'], ctrl.fieldArg.field.data_type)) {
allowedTypes.push('math');
}
if (_.includes(['Date', 'Timestamp'], ctrl.args[0].field.data_type)) {
if (_.includes(['Date', 'Timestamp'], ctrl.fieldArg.field.data_type)) {
allowedTypes.push('date');
}
}
Expand Down Expand Up @@ -99,8 +111,21 @@

this.selectFunction = function() {
ctrl.fn = _.find(CRM.crmSearchAdmin.functions, {name: ctrl.fnName});
ctrl.args.length = 1;
initFunction();
ctrl.args = [ctrl.fieldArg];
if (ctrl.fn) {
var exprType, pos = 0,
uiDefaults = ctrl.fn.params[0].ui_defaults || [];
// Add non-field args to the beginning if needed
while (uiDefaults[pos] && uiDefaults[pos].type && uiDefaults[pos].type !== 'SqlField') {
exprType = uiDefaults[pos].type;
ctrl.args.splice(pos, 0, {
type: ctrl.exprTypes[exprType].type,
value: exprType === 'SqlNumber' ? 0 : ''
});
++pos;
}
initFunction();
}
ctrl.writeExpr();
};

Expand All @@ -112,7 +137,7 @@
this.changeArg = function(index) {
var val = ctrl.args[index].value;
// Delete empty value
if (!val && ctrl.args.length > ctrl.fn.params[0].min_expr) {
if (index && !val && ctrl.args.length > ctrl.fn.params[0].min_expr) {
ctrl.args.splice(index, 1);
}
ctrl.writeExpr();
Expand Down
10 changes: 5 additions & 5 deletions ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<div class="form-inline">
<input class="form-control" style="width: 15em;" ng-model="$ctrl.fnName" crm-ui-select="{data: $ctrl.getFunctions, placeholder: ts('Function')}" ng-change="$ctrl.selectFunction()">
<label>{{ $ctrl.args[0].field.label }}</label>
<label>{{ $ctrl.fieldArg.field.label }}</label>
<label ng-if="$ctrl.modifierName">
<input type="checkbox" ng-checked="!!$ctrl.modifier" ng-click="$ctrl.toggleModifier()">
{{ $ctrl.modifierLabel }}
</label>
<div class="form-group" ng-repeat="arg in $ctrl.args" ng-if="$index">
<div class="form-group" ng-repeat="arg in $ctrl.args" ng-if="arg !== $ctrl.fieldArg">
<span ng-switch="arg.type">
<input ng-switch-when="number" class="form-control" type="number" ng-model="arg.value" ng-change="$ctrl.changeArg($index)" ng-model-options="{updateOn: 'blur'}">
<input ng-switch-when="string" class="form-control" ng-model="arg.value" ng-change="$ctrl.changeArg($index)" ng-model-options="{updateOn: 'blur'}">
<input ng-switch-default class="form-control" ng-model="arg.value" crm-ui-select="{data: $ctrl.getFields, placeholder: ts('Field')}" ng-change="$ctrl.changeArg($index)">
<input ng-switch-when="number" class="form-control" type="number" ng-model="arg.value" placeholder="{{ $ctrl.getUiDefault($index).placeholder }}" ng-change="$ctrl.changeArg($index)" ng-model-options="{updateOn: 'blur'}">
<input ng-switch-when="string" class="form-control" ng-model="arg.value" placeholder="{{ $ctrl.getUiDefault($index).placeholder }}" ng-change="$ctrl.changeArg($index)" ng-trim="false" ng-model-options="{updateOn: 'blur'}">
<input ng-switch-default class="form-control" ng-model="arg.value" crm-ui-select="{data: $ctrl.getFields, placeholder: $ctrl.getUiDefault($index).placeholder}" ng-change="$ctrl.changeArg($index)">
</span>
</div>
<div class="btn-group" ng-if="$ctrl.args.length < $ctrl.fn.params[0].max_expr">
Expand Down
16 changes: 16 additions & 0 deletions tests/phpunit/api/v4/Action/SqlFunctionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,22 @@ public function testComparisonFunctions() {
$this->assertEquals(FALSE, $result[$aids[2]]['duration_isnull']);
}

public function testStringFunctions() {
$sampleData = [
['first_name' => 'abc', 'middle_name' => 'q', 'last_name' => 'tester1', 'source' => '123'],
];
$cid = Contact::save(FALSE)
->setRecords($sampleData)
->execute()->first()['id'];

$result = Contact::get(FALSE)
->addWhere('id', '=', $cid)
->addSelect('CONCAT_WS("|", first_name, middle_name, last_name) AS concat_ws')
->execute()->first();

$this->assertEquals('abc|q|tester1', $result['concat_ws']);
}

public function testIncorrectNumberOfArguments() {
try {
Activity::get(FALSE)
Expand Down

0 comments on commit 7aac971

Please sign in to comment.