From cc55578688a95230b35eb4f3bac8f99fa99a8b48 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 16 Feb 2021 15:59:35 -0500 Subject: [PATCH] Afform - SearchKit support for calculated fields This allows the aggregated columns from a savedSearch to be used as filters on an afform with embedded search display --- Civi/Api4/Query/Api4SelectQuery.php | 6 +-- .../Civi/Api4/Action/Afform/LoadAdminData.php | 48 +++++++++++++++++++ .../ang/afGuiEditor/afGuiSearch.component.js | 11 +++++ .../admin/ang/afGuiEditor/afGuiSearch.html | 8 ++++ .../elements/afGuiField.component.js | 11 +++-- .../Civi/Api4/Action/SearchDisplay/Run.php | 20 +++++++- 6 files changed, 95 insertions(+), 9 deletions(-) diff --git a/Civi/Api4/Query/Api4SelectQuery.php b/Civi/Api4/Query/Api4SelectQuery.php index e604e99bea50..f15db6f1d597 100644 --- a/Civi/Api4/Query/Api4SelectQuery.php +++ b/Civi/Api4/Query/Api4SelectQuery.php @@ -103,6 +103,9 @@ public function __construct($apiGet) { // Add ACLs first to avoid redundant subclauses $baoName = CoreUtil::getBAOFromApiName($this->getEntity()); $this->query->where($this->getAclClause(self::MAIN_TABLE_ALIAS, $baoName)); + + // Add explicit joins. Other joins implied by dot notation may be added later + $this->addExplicitJoins(); } /** @@ -113,8 +116,6 @@ public function __construct($apiGet) { * @throws \CRM_Core_Exception */ public function getSql() { - // Add explicit joins. Other joins implied by dot notation may be added later - $this->addExplicitJoins(); $this->buildSelectClause(); $this->buildWhereClause(); $this->buildOrderBy(); @@ -152,7 +153,6 @@ public function run() { * @throws \API_Exception */ public function getCount() { - $this->addExplicitJoins(); $this->buildWhereClause(); // If no having or groupBy, we only need to select count if (!$this->getHaving() && !$this->getGroupBy()) { diff --git a/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php b/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php index 929a91acab36..f8bd4cfafc3f 100644 --- a/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php +++ b/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php @@ -4,6 +4,8 @@ use Civi\AfformAdmin\AfformAdminMeta; use Civi\Api4\Afform; +use Civi\Api4\Entity; +use Civi\Api4\Query\SqlExpression; /** * This action is used by the Afform Admin extension to load metadata for the Admin GUI. @@ -176,6 +178,7 @@ public function _run(\Civi\Api4\Generic\Result $result) { ->addWhere('saved_search.name', '=', $displayTag['search-name']) ->addSelect('*', 'type:name', 'type:icon', 'saved_search.name', 'saved_search.api_entity', 'saved_search.api_params') ->execute()->first(); + $display['calc_fields'] = $this->getCalcFields($display['saved_search.api_entity'], $display['saved_search.api_params']); $info['search_displays'][] = $display; if ($newForm) { $info['definition']['layout'][0]['#children'][] = $displayTag + ['#tag' => $display['type:name']]; @@ -240,6 +243,51 @@ private function loadAvailableBlocks($entities, &$info, $where = []) { } } + /** + * @param string $apiEntity + * @param array $apiParams + * @return array + */ + private function getCalcFields($apiEntity, $apiParams) { + $calcFields = []; + $api = \Civi\API\Request::create($apiEntity, 'get', $apiParams); + $selectQuery = new \Civi\Api4\Query\Api4SelectQuery($api); + $joinMap = $joinCount = []; + foreach ($apiParams['join'] ?? [] as $join) { + [$entityName, $alias] = explode(' AS ', $join[0]); + $num = ''; + if (!empty($joinCount[$entityName])) { + $num = ' ' . (++$joinCount[$entityName]); + } + else { + $joinCount[$entityName] = 1; + } + $label = Entity::get(FALSE) + ->addWhere('name', '=', $entityName) + ->addSelect('title') + ->execute()->first()['title']; + $joinMap[$alias] = $label . $num; + } + + foreach ($apiParams['select'] ?? [] as $select) { + if (strstr($select, ' AS ')) { + $expr = SqlExpression::convert($select, TRUE); + $field = $expr->getFields() ? $selectQuery->getField($expr->getFields()[0]) : NULL; + $joinName = explode('.', $expr->getFields()[0] ?? '')[0]; + $label = $expr::getTitle() . ': ' . (isset($joinMap[$joinName]) ? $joinMap[$joinName] . ' ' : '') . $field['title']; + $calcFields[] = [ + '#tag' => 'af-field', + 'name' => $expr->getAlias(), + 'defn' => [ + 'label' => $label, + 'input_type' => 'Text', + ], + ]; + } + } + return $calcFields; + } + public function fields() { return [ [ diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js index c195a5e321f5..f8c282e5a919 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js @@ -13,6 +13,7 @@ var ctrl = this; $scope.controls = {}; $scope.fieldList = []; + $scope.calcFieldList = []; $scope.blockList = []; $scope.blockTitles = []; $scope.elementList = []; @@ -22,11 +23,21 @@ this.buildPaletteLists = function() { var search = $scope.controls.fieldSearch ? $scope.controls.fieldSearch.toLowerCase() : null; + buildCalcFieldList(search); buildFieldList(search); buildBlockList(search); buildElementList(search); }; + function buildCalcFieldList(search) { + $scope.calcFieldList.length = 0; + _.each(_.cloneDeep(ctrl.display.calc_fields), function(field) { + if (!search || _.contains(field.defn.label.toLowerCase(), search)) { + $scope.calcFieldList.push(field); + } + }); + } + function buildBlockList(search) { $scope.blockList.length = 0; $scope.blockTitles.length = 0; diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiSearch.html b/ext/afform/admin/ang/afGuiEditor/afGuiSearch.html index 1d9def8349ca..9115234d9a71 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiSearch.html +++ b/ext/afform/admin/ang/afGuiEditor/afGuiSearch.html @@ -21,6 +21,14 @@ +
+ +
+
+ {{:: field.defn.label }} +
+
+
diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js index 2c76c72bc2da..1534dafb29dd 100644 --- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js +++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiField.component.js @@ -26,13 +26,14 @@ $scope.meta = afGui.meta; }; - // $scope.getEntity = function() { - // return ctrl.editor ? ctrl.editor.getEntity(ctrl.container.getEntityName()) : {}; - // }; - // Returns the original field definition from metadata this.getDefn = function() { - return ctrl.editor ? afGui.getField(ctrl.container.getFieldEntityType(ctrl.node.name), ctrl.node.name) : {}; + var defn = afGui.getField(ctrl.container.getFieldEntityType(ctrl.node.name), ctrl.node.name); + return defn || { + label: ts('Untitled'), + requred: false, + input_attrs: [] + }; }; $scope.getOriginalLabel = function() { diff --git a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php index 1fd8d26c08bd..8028fc4d2ede 100644 --- a/ext/search/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search/Civi/Api4/Action/SearchDisplay/Run.php @@ -122,8 +122,26 @@ private function applyFilters() { foreach ($this->filters as $fieldName => $value) { if ($value) { $field = $this->getField($fieldName) ?? []; - $dataType = $field['data_type'] ?? NULL; + // If the field doesn't exist, it could be an aggregated column + if (!$field) { + // Not a real field but in the SELECT clause. It must be an aggregated column. Add to HAVING clause. + if (in_array($fieldName, $this->getSelectAliases())) { + if ($prefixWithWildcard) { + $this->savedSearch['api_params']['having'][] = [$fieldName, 'CONTAINS', $value]; + } + else { + $this->savedSearch['api_params']['having'][] = [$fieldName, 'LIKE', $value . '%']; + } + } + // Error - field doesn't exist and isn't a column alias + else { + // Maybe throw an exception? Or just log a warning? + } + continue; + } + + $dataType = $field['data_type']; if (!empty($field['serialize'])) { $this->savedSearch['api_params']['where'][] = [$fieldName, 'CONTAINS', $value]; }