diff --git a/CRM/Contact/BAO/SavedSearch.php b/CRM/Contact/BAO/SavedSearch.php index 5dcc905d8a4..9fc320aff14 100644 --- a/CRM/Contact/BAO/SavedSearch.php +++ b/CRM/Contact/BAO/SavedSearch.php @@ -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(); diff --git a/CRM/Upgrade/Incremental/SmartGroups.php b/CRM/Upgrade/Incremental/SmartGroups.php index 862eefe79d2..d4e52135d13 100644 --- a/CRM/Upgrade/Incremental/SmartGroups.php +++ b/CRM/Upgrade/Incremental/SmartGroups.php @@ -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'); } /** diff --git a/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php b/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php index b44649e9b4e..e0518dc5fb9 100644 --- a/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php +++ b/Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php @@ -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]; diff --git a/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php b/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php index fcebff5fa33..1005a3788b0 100644 --- a/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php +++ b/ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php @@ -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; diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js index 7cd0eb8b870..1228c8e8302 100644 --- a/ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js +++ b/ext/afform/admin/ang/afGuiEditor/afGuiSearch.component.js @@ -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)), }); } }); diff --git a/ext/search_kit/ang/crmSearchAdmin.module.js b/ext/search_kit/ang/crmSearchAdmin.module.js index cd43783846d..c0211129bbc 100644 --- a/ext/search_kit/ang/crmSearchAdmin.module.js +++ b/ext/search_kit/ang/crmSearchAdmin.module.js @@ -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']], @@ -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']], @@ -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) { @@ -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?$'), ''); @@ -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; @@ -361,7 +368,7 @@ } return info; } - function getDefaultLabel(col) { + function getDefaultLabel(col, savedSearch) { var info = parseExpr(col), label = ''; if (info.fn) { @@ -369,7 +376,8 @@ } _.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; @@ -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({ @@ -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'; @@ -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) { diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearch-for.html b/ext/search_kit/ang/crmSearchAdmin/crmSearch-for.html index deaa53466d6..26f39623c82 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearch-for.html +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearch-for.html @@ -6,7 +6,10 @@
- +
+ + +
diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js index d8d0d372333..ea9ccea2efa 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js @@ -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; @@ -129,6 +131,8 @@ }); } + $scope.getJoin = _.wrap(this.savedSearch, searchMeta.getJoin); + $scope.mainEntitySelect = searchMeta.getPrimaryAndSecondaryEntitySelect(); $scope.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect); @@ -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]); @@ -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(); @@ -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); @@ -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); }; @@ -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); } }; @@ -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, @@ -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); @@ -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) { diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js index 7e280a76c02..ebd3fa08cbb 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js @@ -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) { diff --git a/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js b/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js index b44da215eec..3be50ff7ab0 100644 --- a/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.component.js @@ -74,6 +74,10 @@ ctrl.crmSearchAdmin.clearParam('select', index); }; + $scope.getColumnLabel = function(index) { + return searchMeta.getDefaultLabel(ctrl.search.api_params.select[index], ctrl.search); + }; + } }); diff --git a/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html b/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html index 0a78256345a..53f73d33f32 100644 --- a/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html +++ b/ext/search_kit/ang/crmSearchAdmin/resultsTable/crmSearchAdminResultsTable.html @@ -11,7 +11,7 @@ - {{ $ctrl.settings.columns[$index].label }} + {{ getColumnLabel($index) }} diff --git a/tests/phpunit/CRM/Upgrade/Incremental/BaseTest.php b/tests/phpunit/CRM/Upgrade/Incremental/BaseTest.php index 30063aceabd..f3f4cfff372 100644 --- a/tests/phpunit/CRM/Upgrade/Incremental/BaseTest.php +++ b/tests/phpunit/CRM/Upgrade/Incremental/BaseTest.php @@ -210,7 +210,8 @@ public function testSmartGroupMultipleRelativeDateConversions(): void { * Test upgrading multiple Event smart groups of different formats */ public function testMultipleEventSmartGroupDateConversions(): void { - $this->callAPISuccess('SavedSearch', 'create', [ + $savedSearchIds = []; + $savedSearchIds[] = $this->callAPISuccess('SavedSearch', 'create', [ 'form_values' => [ ['event_start_date_low', '=', '20191001000000'], ['event_end_date_high', '=', '20191031235959'], @@ -218,26 +219,26 @@ public function testMultipleEventSmartGroupDateConversions(): void { 'event' => 'this.month', ], ], - ]); - $this->callAPISuccess('SavedSearch', 'create', [ + ])['id']; + $savedSearchIds[] = $this->callAPISuccess('SavedSearch', 'create', [ 'form_values' => [ ['event_start_date_low', '=', '20191001000000'], ], - ]); - $this->callAPISuccess('SavedSearch', 'create', [ + ])['id']; + $savedSearchIds[] = $this->callAPISuccess('SavedSearch', 'create', [ 'form_values' => [ 'event_start_date_low' => '20191001000000', 'event_end_date_high' => '20191031235959', 'event_relative' => 'this.month', ], - ]); - $this->callAPISuccess('SavedSearch', 'create', [ + ])['id']; + $savedSearchIds[] = $this->callAPISuccess('SavedSearch', 'create', [ 'form_values' => [ 'event_start_date_low' => '10/01/2019', 'event_end_date_high' => '', 'event_relative' => '0', ], - ]); + ])['id']; $smartGroupConversionObject = new CRM_Upgrade_Incremental_SmartGroups(); $smartGroupConversionObject->renameFields([ ['old' => 'event_start_date_low', 'new' => 'event_low'], @@ -249,23 +250,25 @@ public function testMultipleEventSmartGroupDateConversions(): void { ], ]); $expectedResults = [ - 1 => [ + $savedSearchIds[0] => [ 'relative_dates' => [], 2 => ['event_relative', '=', 'this.month'], ], - 2 => [ + $savedSearchIds[1] => [ 0 => ['event_low', '=', '2019-10-01 00:00:00'], 1 => ['event_relative', '=', 0], ], - 3 => [ + $savedSearchIds[2] => [ 'event_relative' => 'this.month', ], - 4 => [ + $savedSearchIds[3] => [ 'event_relative' => 0, 'event_low' => '2019-10-01 00:00:00', ], ]; - $savedSearches = $this->callAPISuccess('SavedSearch', 'get', []); + $savedSearches = $this->callAPISuccess('SavedSearch', 'get', [ + 'id' => ['IN' => $savedSearchIds], + ]); foreach ($savedSearches['values'] as $id => $savedSearch) { $this->assertEquals($expectedResults[$id], $savedSearch['form_values']); }