Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SearchKit - Add labels for joins #31664

Merged
merged 4 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CRM/Contact/BAO/SavedSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,21 @@ public static function self_hook_civicrm_pre(\Civi\Core\Event\PreEvent $event):
// Set by mysql
unset($event->params['modified_date']);

// Delete empty form values and save as null if completely empty
if (isset($event->params['form_values']) && is_array($event->params['form_values'])) {
// Exclude legacy smart groups by checking if api_entity is set
if (!empty($event->params['api_entity'])) {
foreach ($event->params['form_values'] as $key => $value) {
if (is_array($value) && !$value) {
unset($event->params['form_values'][$key]);
}
}
if (!$event->params['form_values']) {
$event->params['form_values'] = '';
}
}
}

// Flush angular caches to refresh search displays
if (isset($event->params['api_params'])) {
Civi::container()->get('angular')->clear();
Expand Down
14 changes: 9 additions & 5 deletions CRM/Upgrade/Incremental/SmartGroups.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,15 @@ public function renameFields($pairs) {
* @return mixed
*/
protected function getSearchesWithField($field) {
return civicrm_api3('SavedSearch', 'get', [
'options' => ['limit' => 0],
'form_values' => ['LIKE' => "%{$field}%"],
'return' => ['id', 'form_values'],
])['values'];
$apiGet = \Civi\Api4\SavedSearch::get(FALSE);
$apiGet->addSelect('id', 'form_values');
$apiGet->addWhere('form_values', 'LIKE', "%{$field}%");
// Avoid error if column hasn't been added yet by pending upgrades
if (version_compare(\CRM_Core_BAO_Domain::version(), '5.24', '>=')) {
// Exclude SearchKit searches
$apiGet->addWhere('api_entity', 'IS NULL');
}
return $apiGet->execute()->column(NULL, 'id');
}

/**
Expand Down
2 changes: 1 addition & 1 deletion Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ protected function getJoinLabel($joinAlias) {
$joinCount[$entityName] = 1;
}
$label = CoreUtil::getInfoItem($entityName, 'title');
$this->_joinMap[$alias] = $label . $num;
$this->_joinMap[$alias] = $this->savedSearch['form_values']['join'][$alias] ?? "$label$num";
}
}
return $this->_joinMap[$joinAlias];
Expand Down
2 changes: 1 addition & 1 deletion ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public function _run(\Civi\Api4\Generic\Result $result) {
->setSavedSearch($displayTag['search-name']);
}
$display = $displayGet
->addSelect('*', 'type:name', 'type:icon', 'saved_search_id.name', 'saved_search_id.label', 'saved_search_id.api_entity', 'saved_search_id.api_params')
->addSelect('*', 'type:name', 'type:icon', 'saved_search_id.name', 'saved_search_id.label', 'saved_search_id.api_entity', 'saved_search_id.api_params', 'saved_search_id.form_values')
->execute()->first();
if (!$display) {
continue;
Expand Down
18 changes: 10 additions & 8 deletions ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,23 @@
countEntity(mainEntity.entity);

_.each(ctrl.display.settings['saved_search_id.api_params'].join, function(join) {
var joinInfo = join[0].split(' AS '),
entity = afGui.getEntity(joinInfo[0]),
joinEntity = afGui.getEntity(join[2]);
const joinInfo = join[0].split(' AS ');
const entity = afGui.getEntity(joinInfo[0]);
const bridgeEntity = afGui.getEntity(join[2]);
const defaultLabel = entity.label + countEntity(entity.entity);
const formValues = ctrl.display.settings['saved_search_id.form_values'] || {};
entities.push({
name: entity.entity,
prefix: joinInfo[1] + '.',
label: entity.label + countEntity(entity.entity),
label: (formValues && formValues.join && formValues.join[joinInfo[1]]) || defaultLabel,
fields: entity.fields,
});
if (joinEntity) {
if (bridgeEntity) {
entities.push({
name: joinEntity.entity,
name: bridgeEntity.entity,
prefix: joinInfo[1] + '.',
label: joinEntity.label + countEntity(joinEntity.entity),
fields: _.omit(joinEntity.fields, _.keys(entity.fields)),
label: bridgeEntity.label + countEntity(bridgeEntity.entity),
fields: _.omit(bridgeEntity.fields, _.keys(entity.fields)),
});
}
});
Expand Down
38 changes: 23 additions & 15 deletions ext/search_kit/ang/crmSearchAdmin.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
savedSearch: function($route, crmApi4) {
var params = $route.current.params;
return crmApi4('SavedSearch', 'get', {
select: ['id', 'name', 'label', 'description', 'api_entity', 'api_params', 'is_template', 'expires_date', 'GROUP_CONCAT(DISTINCT entity_tag.tag_id) AS tag_id'],
select: ['id', 'name', 'label', 'description', 'api_entity', 'api_params', 'form_values', 'is_template', 'expires_date', 'GROUP_CONCAT(DISTINCT entity_tag.tag_id) AS tag_id'],
where: [['id', '=', params.id]],
join: [
['EntityTag AS entity_tag', 'LEFT', ['entity_tag.entity_table', '=', '"civicrm_saved_search"'], ['id', '=', 'entity_tag.entity_id']],
Expand All @@ -51,7 +51,7 @@
savedSearch: function($route, crmApi4) {
var params = $route.current.params;
return crmApi4('SavedSearch', 'get', {
select: ['label', 'description', 'api_entity', 'api_params', 'is_template', 'expires_date', 'GROUP_CONCAT(DISTINCT entity_tag.tag_id) AS tag_id'],
select: ['label', 'description', 'api_entity', 'api_params', 'form_values', 'is_template', 'expires_date', 'GROUP_CONCAT(DISTINCT entity_tag.tag_id) AS tag_id'],
where: [['id', '=', params.id]],
join: [
['EntityTag AS entity_tag', 'LEFT', ['entity_tag.entity_table', '=', '"civicrm_saved_search"'], ['id', '=', 'entity_tag.entity_id']],
Expand Down Expand Up @@ -160,11 +160,11 @@
}
}
// Get join metadata matching a given expression like "Email AS Contact_Email_contact_id_01"
function getJoin(fullNameOrAlias) {
function getJoin(savedSearch, fullNameOrAlias) {
var alias = _.last(fullNameOrAlias.split(' AS ')),
path = alias,
baseEntity = searchEntity,
label = [],
labels = [],
join,
result;
while (path.length) {
Expand All @@ -177,13 +177,20 @@
}
path = path.replace(join.alias + '_', '');
var num = parseInt(path.substr(0, 2), 10);
label.push(join.label + (num > 1 ? ' ' + num : ''));
labels.push(join.label + (num > 1 ? ' ' + num : ''));
path = path.replace(/^\d\d_?/, '');
if (path.length) {
baseEntity = join.entity;
}
}
result = _.assign(_.cloneDeep(join), {label: label.join(' - '), alias: alias, baseEntity: baseEntity});
const defaultLabel = labels.join(' - ');
result = _.assign(_.cloneDeep(join), {
label: (savedSearch && savedSearch.form_values && savedSearch.form_values.join && savedSearch.form_values.join[alias]) || defaultLabel,
defaultLabel: defaultLabel,
alias: alias,
baseEntity: baseEntity,
icon: getEntity(join.entity).icon,
});
// Add the numbered suffix to the join conditions
// If this is a deep join, also add the base entity prefix
var prefix = alias.replace(new RegExp('_?' + join.alias + '_?\\d?\\d?$'), '');
Expand Down Expand Up @@ -212,7 +219,7 @@
field;
// If 2 or more segments, the first might be the name of a join
if (dotSplit.length > 1) {
join = getJoin(dotSplit[0]);
join = getJoin(null, dotSplit[0]);
if (join) {
dotSplit.shift();
entityName = join.entity;
Expand Down Expand Up @@ -361,15 +368,16 @@
}
return info;
}
function getDefaultLabel(col) {
function getDefaultLabel(col, savedSearch) {
var info = parseExpr(col),
label = '';
if (info.fn) {
label = '(' + info.fn.title + ')';
}
_.each(info.args, function(arg) {
if (arg.join) {
label += (label ? ' ' : '') + arg.join.label + ':';
let join = getJoin(savedSearch, arg.join.alias);
label += (label ? ' ' : '') + join.label + ':';
}
if (arg.field) {
label += (label ? ' ' : '') + arg.field.label;
Expand All @@ -379,7 +387,7 @@
});
return label;
}
function fieldToColumn(fieldExpr, defaults) {
function fieldToColumn(fieldExpr, defaults, savedSearch) {
var info = parseExpr(fieldExpr),
field = (_.findWhere(info.args, {type: 'field'}) || {}).field || {},
values = _.merge({
Expand All @@ -388,7 +396,7 @@
dataType: (info.fn && info.fn.data_type) || field.data_type
}, defaults);
if (defaults.label === true) {
values.label = getDefaultLabel(fieldExpr);
values.label = getDefaultLabel(fieldExpr, savedSearch);
}
if (defaults.sortable) {
values.sortable = field.type && field.type !== 'Pseudo';
Expand Down Expand Up @@ -448,11 +456,11 @@
return null;
},
// Find all possible search columns that could serve as contact_id for a smart group
getSmartGroupColumns: function(api_entity, api_params) {
var joins = _.pluck((api_params.join || []), 0);
return _.transform([api_entity].concat(joins), function(columns, joinExpr) {
getSmartGroupColumns: function(savedSearch) {
var joins = _.pluck((savedSearch.api_params.join || []), 0);
return _.transform([savedSearch.api_entity].concat(joins), function(columns, joinExpr) {
var joinName = joinExpr.split(' AS '),
joinInfo = joinName[1] ? getJoin(joinName[1]) : {entity: joinName[0]},
joinInfo = joinName[1] ? getJoin(savedSearch, joinName[1]) : {entity: joinName[0]},
entity = getEntity(joinInfo.entity),
prefix = joinInfo.alias ? joinInfo.alias + '.' : '';
_.each(entity.fields, function(field) {
Expand Down
5 changes: 4 additions & 1 deletion ext/search_kit/ang/crmSearchAdmin/crmSearch-for.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
<fieldset ng-repeat="join in $ctrl.savedSearch.api_params.join" class="crm-search-join">
<div class="form-inline">
<select class="form-control" ng-model="join[1]" ng-change="$ctrl.changeJoinType(join)" ng-options="o.k as o.v for o in ::joinTypes" ></select>
<input id="crm-search-join-{{ $index }}" class="form-control huge" ng-model="join[0]" crm-ui-select="{placeholder: ' ', data: getJoinEntities}" disabled >
<div class="input-group">
<span class="input-group-addon" title="{{:: getJoin(join[0]).defaultLabel }}"><i class="crm-i {{:: getJoin(join[0]).icon }}"></i></span>
<input class="form-control crm-search-join-label huge" ng-model="$ctrl.getSetJoinLabel(join[0])" ng-model-options="{getterSetter: true, updateOn: 'blur'}" placeholder="{{:: getJoin(join[0]).defaultLabel }}" title="{{:: ts('Optional label for this join') }}">
</div>
<button type="button" class="btn btn-xs btn-danger-outline" ng-click="$ctrl.removeJoin($index)" title="{{:: ts('Remove join') }}">
<i class="crm-i fa-trash" aria-hidden="true"></i>
</button>
Expand Down
45 changes: 33 additions & 12 deletions ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
this.entityTitle = searchMeta.getEntity(this.savedSearch.api_entity).title_plural;

this.savedSearch.displays = this.savedSearch.displays || [];
this.savedSearch.form_values = this.savedSearch.form_values || {};
this.savedSearch.form_values.join = this.savedSearch.form_values.join || {};
this.savedSearch.groups = this.savedSearch.groups || [];
this.savedSearch.tag_id = this.savedSearch.tag_id || [];
this.groupExists = !!this.savedSearch.groups.length;
Expand Down Expand Up @@ -129,6 +131,8 @@
});
}

$scope.getJoin = _.wrap(this.savedSearch, searchMeta.getJoin);

$scope.mainEntitySelect = searchMeta.getPrimaryAndSecondaryEntitySelect();

$scope.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect);
Expand Down Expand Up @@ -289,7 +293,7 @@
$scope.selectTab = function(tab) {
if (tab === 'group') {
loadFieldOptions('Group');
$scope.smartGroupColumns = searchMeta.getSmartGroupColumns(ctrl.savedSearch.api_entity, ctrl.savedSearch.api_params);
$scope.smartGroupColumns = searchMeta.getSmartGroupColumns(ctrl.savedSearch);
var smartGroupColumns = _.map($scope.smartGroupColumns, 'id');
if (smartGroupColumns.length && !_.includes(smartGroupColumns, ctrl.savedSearch.api_params.select[0])) {
ctrl.savedSearch.api_params.select.unshift(smartGroupColumns[0]);
Expand All @@ -316,12 +320,10 @@

function getExistingJoins() {
return _.transform(ctrl.savedSearch.api_params.join || [], function(joins, join) {
joins[join[0].split(' AS ')[1]] = searchMeta.getJoin(join[0]);
joins[join[0].split(' AS ')[1]] = searchMeta.getJoin(ctrl.savedSearch, join[0]);
}, {});
}

$scope.getJoin = searchMeta.getJoin;

$scope.getJoinEntities = function() {
var existingJoins = getExistingJoins();

Expand Down Expand Up @@ -362,8 +364,8 @@
this.addJoin = function(value) {
if (value) {
ctrl.savedSearch.api_params.join = ctrl.savedSearch.api_params.join || [];
var join = searchMeta.getJoin(value),
entity = searchMeta.getEntity(join.entity),
var join = searchMeta.getJoin(ctrl.savedSearch, value),
entity = searchMeta.getEntity(ctrl.savedSearch, join.entity),
params = [value, $scope.controls.joinType || 'LEFT'];
_.each(_.cloneDeep(join.conditions), function(condition) {
params.push(condition);
Expand All @@ -388,9 +390,27 @@
}
};

// Factory returns a getter-setter function for ngModel
this.getSetJoinLabel = function(joinName) {
return _.wrap(joinName, getSetJoinLabel);
};

function getSetJoinLabel(joinName, value) {
const joinInfo = searchMeta.getJoin(ctrl.savedSearch, joinName);
const alias = joinInfo.alias;
// Setter
if (arguments.length > 1) {
ctrl.savedSearch.form_values.join[alias] = value;
if (!value || value === joinInfo.defaultLabel) {
delete ctrl.savedSearch.form_values.join[alias];
}
}
return ctrl.savedSearch.form_values.join[alias] || joinInfo.defaultLabel;
}

// Remove an explicit join + all SELECT, WHERE & other JOINs that use it
this.removeJoin = function(index) {
var alias = searchMeta.getJoin(ctrl.savedSearch.api_params.join[index][0]).alias;
var alias = searchMeta.getJoin(ctrl.savedSearch, ctrl.savedSearch.api_params.join[index][0]).alias;
ctrl.clearParam('join', index);
removeJoinStuff(alias);
};
Expand All @@ -404,16 +424,17 @@
return clauseUsesJoin(clause, alias);
});
_.eachRight(ctrl.savedSearch.api_params.join, function(item, i) {
var joinAlias = searchMeta.getJoin(item[0]).alias;
var joinAlias = searchMeta.getJoin(ctrl.savedSearch, item[0]).alias;
if (joinAlias !== alias && joinAlias.indexOf(alias) === 0) {
ctrl.removeJoin(i);
}
});
delete ctrl.savedSearch.form_values.join[alias];
}

this.changeJoinType = function(join) {
if (join[1] === 'EXCLUDE') {
removeJoinStuff(searchMeta.getJoin(join[0]).alias);
removeJoinStuff(searchMeta.getJoin(ctrl.savedSearch, join[0]).alias);
}
};

Expand Down Expand Up @@ -639,7 +660,7 @@
result = [];

function addJoin(join) {
var joinInfo = searchMeta.getJoin(join),
let joinInfo = searchMeta.getJoin(ctrl.savedSearch, join),
joinEntity = searchMeta.getEntity(joinInfo.entity);
result.push({
text: joinInfo.label,
Expand Down Expand Up @@ -702,7 +723,7 @@

// Join entities + bridge entities
_.each(ctrl.savedSearch.api_params.join, function(join) {
var joinInfo = searchMeta.getJoin(join[0]);
var joinInfo = searchMeta.getJoin(ctrl.savedSearch, join[0]);
entitiesToLoad.push(joinInfo.entity);
if (joinInfo.bridge) {
entitiesToLoad.push(joinInfo.bridge);
Expand Down Expand Up @@ -733,7 +754,7 @@
});
// Links to explicitly joined entities
_.each(ctrl.savedSearch.api_params.join, function(joinClause) {
var join = searchMeta.getJoin(joinClause[0]),
var join = searchMeta.getJoin(ctrl.savedSearch, joinClause[0]),
joinEntity = searchMeta.getEntity(join.entity),
bridgeEntity = _.isString(joinClause[2]) ? searchMeta.getEntity(joinClause[2]) : null;
_.each(_.cloneDeep(joinEntity.links), function(link) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@

this.getFieldLabel = function(key) {
var expr = ctrl.getExprFromSelect(selectToKey(key));
return searchMeta.getDefaultLabel(expr);
return searchMeta.getDefaultLabel(expr, ctrl.savedSearch);
};

this.getColLabel = function(col) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
ctrl.crmSearchAdmin.clearParam('select', index);
};

$scope.getColumnLabel = function(index) {
return searchMeta.getDefaultLabel(ctrl.search.api_params.select[index], ctrl.search);
};

}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</th>
<th ng-repeat="item in $ctrl.search.api_params.select" ng-click="$ctrl.setSort($ctrl.settings.columns[$index], $event)" title="{{$index || !$ctrl.crmSearchAdmin.groupExists ? ts('Drag to reorder columns, click to sort results (shift-click to sort by multiple).') : ts('Column reserved for smart group.')}}">
<i ng-if=":: $ctrl.isSortable($ctrl.settings.columns[$index])" class="crm-i {{ $ctrl.getSort($ctrl.settings.columns[$index]) }}"></i>
<span ng-class="{'crm-draggable': $index || !$ctrl.crmSearchAdmin.groupExists}">{{ $ctrl.settings.columns[$index].label }}</span>
<span ng-class="{'crm-draggable': $index || !$ctrl.crmSearchAdmin.groupExists}">{{ getColumnLabel($index) }}</span>
<span ng-switch="$index || !$ctrl.crmSearchAdmin.groupExists ? 'sortable' : 'locked'">
<i ng-switch-when="locked" class="crm-i fa-lock" aria-hidden="true"></i>
<a href ng-switch-default class="crm-hover-button" title="{{:: ts('Clear') }}" ng-click="removeColumn($index); $event.stopPropagation();"><i class="crm-i fa-times" aria-hidden="true"></i></a>
Expand Down
Loading