Skip to content

Commit

Permalink
Afform - SearchKit support for calculated fields
Browse files Browse the repository at this point in the history
This allows the aggregated columns from a savedSearch to be used as filters on an afform with embedded search display
  • Loading branch information
colemanw committed Feb 17, 2021
1 parent 2b7821f commit cc55578
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 9 deletions.
6 changes: 3 additions & 3 deletions Civi/Api4/Query/Api4SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand All @@ -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();
Expand Down Expand Up @@ -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()) {
Expand Down
48 changes: 48 additions & 0 deletions ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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']];
Expand Down Expand Up @@ -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 [
[
Expand Down
11 changes: 11 additions & 0 deletions ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
var ctrl = this;
$scope.controls = {};
$scope.fieldList = [];
$scope.calcFieldList = [];
$scope.blockList = [];
$scope.blockTitles = [];
$scope.elementList = [];
Expand All @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions ext/afform/admin/ang/afGuiEditor/afGuiSearch.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
</div>
</div>
</div>
<div ng-if="calcFieldList.length">
<label>{{:: ts('Calculated Fields') }}</label>
<div ui-sortable="{update: buildPaletteLists, items: '&gt; div:not(.disabled)', connectWith: '[ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="calcFieldList">
<div ng-repeat="field in calcFieldList" ng-class="{disabled: fieldInUse(field.name)}">
{{:: field.defn.label }}
</div>
</div>
</div>
<div ng-repeat="fieldGroup in fieldList">
<div ng-if="fieldGroup.fields.length">
<label>{{:: fieldGroup.label }}</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
20 changes: 19 additions & 1 deletion ext/search/Civi/Api4/Action/SearchDisplay/Run.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down

0 comments on commit cc55578

Please sign in to comment.