From c123e9c5da3b0b256f04af4fe30204801a46b7a1 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sat, 15 Apr 2023 14:02:04 -0400 Subject: [PATCH] SearchKit - Display option values for field transformations This takes advantage of function option lists in SearchKit output, e.g. this will show Janurary instead of 1 for the Month-Only transformation. --- Civi/Api4/Query/SqlExpression.php | 2 +- Civi/Api4/Query/SqlFunction.php | 44 +++++++++++++++++-- ext/search_kit/ang/crmSearchAdmin.module.js | 6 ++- .../crmSearchFunction.component.js | 4 ++ .../phpunit/api/v4/Action/SqlFunctionTest.php | 6 +++ 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/Civi/Api4/Query/SqlExpression.php b/Civi/Api4/Query/SqlExpression.php index 6f3e9ee10fc3..f4192477d398 100644 --- a/Civi/Api4/Query/SqlExpression.php +++ b/Civi/Api4/Query/SqlExpression.php @@ -88,7 +88,7 @@ public static function convert(string $expression, $parseAlias = FALSE, $mustBe $className = 'SqlEquation'; } // If there are brackets but not the first character, we have a function - elseif ($bracketPos && $lastChar === ')') { + elseif ($bracketPos && preg_match('/^\w+\(.*\)(:[a-z]+)?$/', $expr)) { $fnName = substr($expr, 0, $bracketPos); if ($fnName !== strtoupper($fnName)) { throw new \CRM_Core_Exception('Sql function must be uppercase.'); diff --git a/Civi/Api4/Query/SqlFunction.php b/Civi/Api4/Query/SqlFunction.php index 7b00b6f2c95c..bf50b8eaae9d 100644 --- a/Civi/Api4/Query/SqlFunction.php +++ b/Civi/Api4/Query/SqlFunction.php @@ -30,6 +30,12 @@ abstract class SqlFunction extends SqlExpression { */ protected $args = []; + /** + * Pseudoconstant suffix (for functions with option lists) + * @var string + */ + private $suffix; + /** * Used for categorizing functions in the UI * @@ -47,7 +53,12 @@ abstract class SqlFunction extends SqlExpression { * Parse the argument string into an array of function arguments */ protected function initialize() { - $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1)); + $matches = []; + // Capture function argument string and possible suffix + preg_match('/[_A-Z]+\((.*)\)(:[a-z]+)?$/', $this->expr, $matches); + $arg = $matches[1]; + $this->setSuffix($matches[2] ?? NULL); + // Parse function arguments string, match to declared function params foreach ($this->getParams() as $idx => $param) { $prefix = NULL; $name = $param['name'] ?: ($idx + 1); @@ -96,7 +107,7 @@ protected function initialize() { } /** - * Change $dataType according to output of function + * Set $dataType and convert value by suffix * * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues * @param string $value @@ -107,6 +118,24 @@ public function formatOutputValue($value, &$dataType) { if (static::$dataType) { $dataType = static::$dataType; } + if (isset($value) && $this->suffix && $this->suffix !== 'id') { + $dataType = 'String'; + $option = $this->getOptions()[$value] ?? NULL; + // Option contains an array of suffix keys + if (is_array($option)) { + return $option[$this->suffix] ?? NULL; + } + // Flat arrays are name/value pairs + elseif ($this->suffix === 'label') { + return $option; + } + elseif ($this->suffix === 'name') { + return $value; + } + else { + return NULL; + } + } return $value; } @@ -150,7 +179,7 @@ private function renderArg($arg, Api4SelectQuery $query): string { * @inheritDoc */ public function getAlias(): string { - return $this->alias ?? $this->getName() . ':' . implode('_', $this->fields); + return $this->alias ?? $this->getName() . ':' . implode('_', $this->fields) . ($this->suffix ? ':' . $this->suffix : ''); } /** @@ -229,6 +258,15 @@ public function getType(): string { return 'SqlFunction'; } + /** + * @param string|null $suffix + */ + private function setSuffix(?string $suffix): void { + $this->suffix = $suffix ? + str_replace(':', '', $suffix) : + NULL; + } + /** * @return string */ diff --git a/ext/search_kit/ang/crmSearchAdmin.module.js b/ext/search_kit/ang/crmSearchAdmin.module.js index 54d332280984..08fc3704fe87 100644 --- a/ext/search_kit/ang/crmSearchAdmin.module.js +++ b/ext/search_kit/ang/crmSearchAdmin.module.js @@ -181,10 +181,12 @@ return {field: field, join: join}; } function parseFnArgs(info, expr) { - var fnName = expr.split('(')[0], - argString = expr.substr(fnName.length + 1, expr.length - fnName.length - 2); + var matches = /([_A-Z]+)\((.*)\)(:[a-z]+)?$/.exec(expr), + fnName = matches[1], + argString = matches[2]; info.fn = _.find(CRM.crmSearchAdmin.functions, {name: fnName || 'e'}); info.data_type = (info.fn && info.fn.data_type) || null; + info.suffix = matches[3]; function getKeyword(whitelist) { var keyword; diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.component.js index a9c7496cdd5e..bf4e16ab77e5 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.component.js @@ -186,6 +186,10 @@ ctrl.expr += args.join(''); ctrl.expr += ')'; if (ctrl.mode === 'select') { + // Add pseudoconstant suffix if function has an option list + if (ctrl.fn.options) { + ctrl.expr += ':label'; + } ctrl.expr += ' AS ' + makeAlias(); } } else { diff --git a/tests/phpunit/api/v4/Action/SqlFunctionTest.php b/tests/phpunit/api/v4/Action/SqlFunctionTest.php index ec13dcaaffc0..782d0ea780c3 100644 --- a/tests/phpunit/api/v4/Action/SqlFunctionTest.php +++ b/tests/phpunit/api/v4/Action/SqlFunctionTest.php @@ -233,6 +233,8 @@ public function testDateFunctions() { ->addSelect('YEAR(birth_date) AS year') ->addSelect('QUARTER(birth_date) AS quarter') ->addSelect('MONTH(birth_date) AS month') + ->addSelect('MONTH(birth_date):label AS month_name') + ->addSelect('MONTH(birth_date):label') ->addSelect('EXTRACT(YEAR_MONTH FROM birth_date) AS year_month') ->addWhere('last_name', '=', $lastName) ->addOrderBy('id') @@ -242,12 +244,16 @@ public function testDateFunctions() { $this->assertEquals(2009, $result[0]['year']); $this->assertEquals(4, $result[0]['quarter']); $this->assertEquals(11, $result[0]['month']); + $this->assertEquals('November', $result[0]['month_name']); + $this->assertEquals('November', $result[0]['MONTH:birth_date:label']); $this->assertEquals('200911', $result[0]['year_month']); $this->assertEquals(0, $result[1]['diff']); $this->assertEquals(2010, $result[1]['year']); $this->assertEquals(1, $result[1]['quarter']); $this->assertEquals(1, $result[1]['month']); + $this->assertEquals('January', $result[1]['month_name']); + $this->assertEquals('January', $result[1]['MONTH:birth_date:label']); $this->assertEquals('201001', $result[1]['year_month']); }