This allows the aggregated columns from a savedSearch to be used as filters on an afform with embedded search display
// 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();
}
/**
* @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();
* @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()) {
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.
->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']];
}
}
+ /**
+ * @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 [
[
var ctrl = this;
$scope.controls = {};
$scope.fieldList = [];
+ $scope.calcFieldList = [];
$scope.blockList = [];
$scope.blockTitles = [];
$scope.elementList = [];
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;
</div>
</div>
</div>
+ <div ng-if="calcFieldList.length">
+ <label>{{:: ts('Calculated Fields') }}</label>
+ <div ui-sortable="{update: buildPaletteLists, items: '> 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>
$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() {
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];
}