From fa5407b8e9a71b49a77a3a965890cc978c541be1 Mon Sep 17 00:00:00 2001 From: colemanw Date: Fri, 20 Feb 2026 15:09:04 -0500 Subject: [PATCH 1/4] SearchKit - Add generic crm-search-display directive Directive can load any viewable search. --- ext/search_kit/Civi/Search/Display.php | 23 ++++++++++++++-- ext/search_kit/ang/crmSearchDisplay.ang.php | 16 ++++++++--- ext/search_kit/ang/crmSearchDisplay.module.js | 4 +-- .../crmSearchDisplay.component.js | 27 +++++++++++++++++++ 4 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 ext/search_kit/ang/crmSearchDisplay/crmSearchDisplay.component.js diff --git a/ext/search_kit/Civi/Search/Display.php b/ext/search_kit/Civi/Search/Display.php index 90ab28603db8..a61cf3df161b 100644 --- a/ext/search_kit/Civi/Search/Display.php +++ b/ext/search_kit/Civi/Search/Display.php @@ -34,13 +34,20 @@ public static function getPartials($moduleName, $module) { /** * @return array */ - public static function getDisplayTypes(array $props):array { + public static function getDisplayTypes(array $props, bool $onlyViewable = FALSE): array { try { - return \Civi\Api4\SearchDisplay::getFields(FALSE) + if ($onlyViewable && !in_array('grouping', $props)) { + $props[] = 'grouping'; + } + $options = \Civi\Api4\SearchDisplay::getFields(FALSE) ->setLoadOptions(array_diff($props, ['tag'])) ->addWhere('name', '=', 'type') ->execute() ->first()['options']; + if ($onlyViewable) { + return array_filter($options, fn($type) => $type['grouping'] !== 'non-viewable'); + } + return $options; } catch (\Exception $e) { return []; @@ -79,4 +86,16 @@ public static function getEntityLinks(string $entity, $addLabel = FALSE, ?array return $links; } + /** + * Return settings for the crmSearchDisplay angular module. + * @return array + */ + public static function getModuleSettings(): array { + $viewableTypes = self::getDisplayTypes(['id', 'name'], TRUE); + + return [ + 'viewableDisplayTypes' => array_column($viewableTypes, 'name', 'id'), + ]; + } + } diff --git a/ext/search_kit/ang/crmSearchDisplay.ang.php b/ext/search_kit/ang/crmSearchDisplay.ang.php index 0dbdeb3526e2..f833f84f9531 100644 --- a/ext/search_kit/ang/crmSearchDisplay.ang.php +++ b/ext/search_kit/ang/crmSearchDisplay.ang.php @@ -1,5 +1,14 @@ [ 'ang/crmSearchDisplay.module.js', @@ -13,8 +22,9 @@ 'css/crmSearchDisplay.css', ], 'basePages' => [], - 'requires' => ['api4', 'ngSanitize'], + 'requires' => $requires, 'exports' => [ - 'crm-search-display-table' => 'E', + 'crm-search-display' => 'E', ], + 'settingsFactory' => ['\Civi\Search\Display', 'getModuleSettings'], ]; diff --git a/ext/search_kit/ang/crmSearchDisplay.module.js b/ext/search_kit/ang/crmSearchDisplay.module.js index beae3e52e125..952216871d69 100644 --- a/ext/search_kit/ang/crmSearchDisplay.module.js +++ b/ext/search_kit/ang/crmSearchDisplay.module.js @@ -1,7 +1,7 @@ (function(angular, $, _) { "use strict"; - // Declare module - angular.module('crmSearchDisplay', CRM.angRequires('crmSearchDisplay')); + // Note: We're not using CRM.angRequires here to avoid circular dependencies with search display types. See crmSearchDisplay.ang.php + angular.module('crmSearchDisplay', ['api4', 'ngSanitize']); })(angular, CRM.$, CRM._); diff --git a/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplay.component.js b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplay.component.js new file mode 100644 index 000000000000..4e9e0c26d8a8 --- /dev/null +++ b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplay.component.js @@ -0,0 +1,27 @@ +(function(angular, $, _) { + "use strict"; + + // Generic wrapper to render any display type + angular.module('crmSearchDisplay').component('crmSearchDisplay', { + bindings: { + type: '@', + apiEntity: '@', + search: '<', + display: '<', + settings: '<', + filters: '<', + }, + template: function($element) { + let html = ''; + const displayTypes = CRM.crmSearchDisplay.viewableDisplayTypes; + Object.entries(displayTypes).forEach(([type, directive]) => { + html += `<${directive} ng-if="$ctrl.type === '${type}'" api-entity="{{:: $ctrl.apiEntity }}" search="$ctrl.search" display="$ctrl.display" settings="$ctrl.settings" filters="$ctrl.filters">`; + }); + return html; + }, + controller: function($scope, $element) { + } + + }); + +})(angular, CRM.$, CRM._); From e49d96f6585e4e77cfbfd04b43b155e531b09e48 Mon Sep 17 00:00:00 2001 From: colemanw Date: Fri, 20 Feb 2026 15:21:24 -0500 Subject: [PATCH 2/4] SearchKit - Allow subsearch of different types Uses the new crm-search-display directive to enable non-table subsearches. --- ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php | 5 ++--- .../ang/crmSearchAdmin/crmSearchAdminSubsearch.component.js | 4 ++-- ext/search_kit/ang/crmSearchDisplay/colType/subsearch.html | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php index 49c9a280b7de..979c9b2c86e4 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/Run.php @@ -265,15 +265,14 @@ private function addSubsearchDisplaySettings(SearchDisplayRunResult $result): vo $displayName = $col['subsearch']['display'] ?? NULL; if ($searchName && $displayName) { $searchDisplay = SearchDisplay::get(FALSE) - ->addSelect('settings', 'saved_search_id.api_entity') + ->addSelect('settings', 'saved_search_id.api_entity', 'type') ->addWhere('name', '=', $displayName) ->addWhere('saved_search_id.name', '=', $searchName) ->execute()->first(); if ($searchDisplay) { $result->subsearch ??= []; $result->subsearch["{$searchName}.{$displayName}"] = [ - // Passing 'type' to support non-table displays might be nice but would add complexity - // 'type' => $searchDisplay['type:name'], + 'type' => $searchDisplay['type'], 'api_entity' => $searchDisplay['saved_search_id.api_entity'], 'settings' => $searchDisplay['settings'], ]; diff --git a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminSubsearch.component.js b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminSubsearch.component.js index 4acb77fbbf38..24aeba8bc657 100644 --- a/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminSubsearch.component.js +++ b/ext/search_kit/ang/crmSearchAdmin/crmSearchAdminSubsearch.component.js @@ -35,8 +35,8 @@ select: ['name', 'label'], where: [ ['saved_search_id.name', '=', searchName], - // For now this is the only type of embedded display we support - ['type:name', '=', 'crm-search-display-table'], + // Include all viewable display types + ['type', 'IN', Object.keys(CRM.crmSearchDisplay.viewableDisplayTypes)], ], }], savedSearch: ['SavedSearch', 'get', { diff --git a/ext/search_kit/ang/crmSearchDisplay/colType/subsearch.html b/ext/search_kit/ang/crmSearchDisplay/colType/subsearch.html index 48ad8946fb6f..7b29dee99cc6 100644 --- a/ext/search_kit/ang/crmSearchDisplay/colType/subsearch.html +++ b/ext/search_kit/ang/crmSearchDisplay/colType/subsearch.html @@ -5,7 +5,7 @@
- +
@@ -15,7 +15,7 @@
- +
@@ -24,5 +24,5 @@ {{:: colData.val }}
- + From 229bf3ef0546ea949094ba384c1e6f09238b1f0a Mon Sep 17 00:00:00 2001 From: colemanw Date: Fri, 20 Feb 2026 16:02:09 -0500 Subject: [PATCH 3/4] SearchKit - Simplify crmSearchPage template and requirements No need for dynamically generated templates when we can just use the one generic `crm-search-display` element. Just requiring crmSearchDisplay, the dependencies sort themselves out and we don't need to declare `basePages` for every display. --- ext/chart_kit/ang/crmChartKit.ang.php | 2 +- ext/search_kit/Civi/Search/Display.php | 12 ------------ ext/search_kit/ang/crmSearchAdmin.ang.php | 2 +- ext/search_kit/ang/crmSearchDisplayBatch.ang.php | 2 +- ext/search_kit/ang/crmSearchDisplayGrid.ang.php | 2 +- ext/search_kit/ang/crmSearchDisplayList.ang.php | 2 +- ext/search_kit/ang/crmSearchDisplayTable.ang.php | 2 +- ext/search_kit/ang/crmSearchDisplayTree.ang.php | 2 +- ext/search_kit/ang/crmSearchPage.ang.php | 6 ++++-- ext/search_kit/ang/crmSearchPage.module.js | 11 +---------- ext/search_kit/ang/crmSearchPage/crmSearchPage.html | 7 +++++++ 11 files changed, 19 insertions(+), 31 deletions(-) create mode 100644 ext/search_kit/ang/crmSearchPage/crmSearchPage.html diff --git a/ext/chart_kit/ang/crmChartKit.ang.php b/ext/chart_kit/ang/crmChartKit.ang.php index 5d9fab3e2a7b..93e79fec8f09 100644 --- a/ext/chart_kit/ang/crmChartKit.ang.php +++ b/ext/chart_kit/ang/crmChartKit.ang.php @@ -18,7 +18,7 @@ 'ui.bootstrap', 'crmSearchDisplay', ], - 'basePages' => ['civicrm/search', 'civicrm/admin/search'], + 'basePages' => [], 'bundles' => ['bootstrap3', 'chart_kit'], 'exports' => [ 'crm-search-display-chart-kit' => 'E', diff --git a/ext/search_kit/Civi/Search/Display.php b/ext/search_kit/Civi/Search/Display.php index a61cf3df161b..e6dc3fd3e587 100644 --- a/ext/search_kit/Civi/Search/Display.php +++ b/ext/search_kit/Civi/Search/Display.php @@ -19,18 +19,6 @@ */ class Display { - /** - * @return array - */ - public static function getPartials($moduleName, $module) { - $partials = []; - foreach (self::getDisplayTypes(['id', 'name']) as $type) { - $partials["~/$moduleName/displayType/{$type['id']}.html"] = - '<' . $type['name'] . ' api-entity="{{:: $ctrl.apiEntity }}" search="$ctrl.searchName" display="$ctrl.display.name" settings="$ctrl.display.settings" filters="$ctrl.filters">'; - } - return $partials; - } - /** * @return array */ diff --git a/ext/search_kit/ang/crmSearchAdmin.ang.php b/ext/search_kit/ang/crmSearchAdmin.ang.php index a548ab0c16cd..9d5270a94682 100644 --- a/ext/search_kit/ang/crmSearchAdmin.ang.php +++ b/ext/search_kit/ang/crmSearchAdmin.ang.php @@ -15,7 +15,7 @@ ], 'bundles' => ['bootstrap3'], 'basePages' => ['civicrm/admin/search'], - 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'api4', 'crmSearchTasks', 'crmRouteBinder', 'crmDialog', 'md5'], + 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'ui.sortable', 'ui.bootstrap', 'crmSearchDisplay', 'crmSearchTasks', 'crmRouteBinder', 'crmDialog', 'md5'], 'settingsFactory' => ['\Civi\Search\Admin', 'getAdminSettings'], 'permissions' => [ // Note the super permission "all CiviCRM permissions and ACLs" is used in the JS layer to determine if users can create search displays that bypass ACLs diff --git a/ext/search_kit/ang/crmSearchDisplayBatch.ang.php b/ext/search_kit/ang/crmSearchDisplayBatch.ang.php index 5dc62aad887d..72f6659ef8ff 100644 --- a/ext/search_kit/ang/crmSearchDisplayBatch.ang.php +++ b/ext/search_kit/ang/crmSearchDisplayBatch.ang.php @@ -9,7 +9,7 @@ 'ang/crmSearchDisplayBatch', ], 'css' => [], - 'basePages' => ['civicrm/search', 'civicrm/admin/search'], + 'basePages' => [], 'requires' => ['crmSearchDisplay', 'crmUi', 'ui.bootstrap', 'crmSearchTasks'], 'bundles' => ['bootstrap3'], 'permissions' => ['administer queues'], diff --git a/ext/search_kit/ang/crmSearchDisplayGrid.ang.php b/ext/search_kit/ang/crmSearchDisplayGrid.ang.php index 978734532c38..4b73ceb7de22 100644 --- a/ext/search_kit/ang/crmSearchDisplayGrid.ang.php +++ b/ext/search_kit/ang/crmSearchDisplayGrid.ang.php @@ -11,7 +11,7 @@ 'css' => [ 'css/crmSearchDisplayGrid.css', ], - 'basePages' => ['civicrm/search', 'civicrm/admin/search'], + 'basePages' => [], 'requires' => ['crmSearchDisplay', 'crmUi', 'ui.bootstrap', 'crmSearchTasks'], 'bundles' => ['bootstrap3'], 'exports' => [ diff --git a/ext/search_kit/ang/crmSearchDisplayList.ang.php b/ext/search_kit/ang/crmSearchDisplayList.ang.php index b34dc856f859..a7c328f0a5f1 100644 --- a/ext/search_kit/ang/crmSearchDisplayList.ang.php +++ b/ext/search_kit/ang/crmSearchDisplayList.ang.php @@ -8,7 +8,7 @@ 'partials' => [ 'ang/crmSearchDisplayList', ], - 'basePages' => ['civicrm/search', 'civicrm/admin/search'], + 'basePages' => [], 'requires' => ['crmSearchDisplay', 'crmUi', 'ui.bootstrap', 'crmSearchTasks'], 'bundles' => ['bootstrap3'], 'exports' => [ diff --git a/ext/search_kit/ang/crmSearchDisplayTable.ang.php b/ext/search_kit/ang/crmSearchDisplayTable.ang.php index dbbcca459514..2a003c6cdace 100644 --- a/ext/search_kit/ang/crmSearchDisplayTable.ang.php +++ b/ext/search_kit/ang/crmSearchDisplayTable.ang.php @@ -11,7 +11,7 @@ 'css' => [ 'css/crmSearchDisplayTable.css', ], - 'basePages' => ['civicrm/search', 'civicrm/admin/search'], + 'basePages' => [], 'requires' => ['crmSearchDisplay', 'crmUi', 'crmSearchTasks', 'ui.bootstrap', 'ui.sortable'], 'bundles' => ['bootstrap3'], 'exports' => [ diff --git a/ext/search_kit/ang/crmSearchDisplayTree.ang.php b/ext/search_kit/ang/crmSearchDisplayTree.ang.php index 0bd4498d982c..46d6ad2dc018 100644 --- a/ext/search_kit/ang/crmSearchDisplayTree.ang.php +++ b/ext/search_kit/ang/crmSearchDisplayTree.ang.php @@ -11,7 +11,7 @@ 'css' => [ 'css/crmSearchDisplayTree.css', ], - 'basePages' => ['civicrm/search', 'civicrm/admin/search'], + 'basePages' => [], 'requires' => ['crmSearchDisplay', 'crmUi', 'crmSearchTasks', 'ui.bootstrap', 'ui.sortable'], 'bundles' => ['bootstrap3'], 'exports' => [ diff --git a/ext/search_kit/ang/crmSearchPage.ang.php b/ext/search_kit/ang/crmSearchPage.ang.php index 5125ba731540..54442bdcd016 100644 --- a/ext/search_kit/ang/crmSearchPage.ang.php +++ b/ext/search_kit/ang/crmSearchPage.ang.php @@ -6,7 +6,9 @@ 'ang/crmSearchPage/*.js', 'ang/crmSearchPage/*/*.js', ], + 'partials' => [ + 'ang/crmSearchPage', + ], 'basePages' => ['civicrm/search'], - 'requires' => ['ngRoute', 'api4', 'crmUi'], - 'partialsCallback' => ['\Civi\Search\Display', 'getPartials'], + 'requires' => ['ngRoute', 'api4', 'crmUi', 'crmSearchDisplay'], ]; diff --git a/ext/search_kit/ang/crmSearchPage.module.js b/ext/search_kit/ang/crmSearchPage.module.js index 008352ee9739..29f2437fab30 100644 --- a/ext/search_kit/ang/crmSearchPage.module.js +++ b/ext/search_kit/ang/crmSearchPage.module.js @@ -8,16 +8,7 @@ // Load & render a SearchDisplay $routeProvider.when('/display/:savedSearchName/:displayName?', { controller: 'crmSearchPageDisplay', - template: '

{{:: $ctrl.display.label }}

\n' + - '
' + - // Edit link for authorized users - ' ' + - // Dynamic template generates the directive for each display type - // @see \Civi\Search\Display::getPartials() - '
' + - '
', + templateUrl: '~/crmSearchPage/crmSearchPage.html', resolve: { // Load saved search display info: function($route, crmApi4) { diff --git a/ext/search_kit/ang/crmSearchPage/crmSearchPage.html b/ext/search_kit/ang/crmSearchPage/crmSearchPage.html new file mode 100644 index 000000000000..192f9864fb04 --- /dev/null +++ b/ext/search_kit/ang/crmSearchPage/crmSearchPage.html @@ -0,0 +1,7 @@ +

{{:: $ctrl.display.label }}

+
+ + +
From e714f407d73611ef1836835f5b44bc1ce1911515 Mon Sep 17 00:00:00 2001 From: colemanw Date: Mon, 23 Feb 2026 09:41:45 -0500 Subject: [PATCH 4/4] Afform - Add support for generic `` tags. --- .../phpunit/Civi/Afform/AfformGetTest.php | 14 ++- .../ang/afsearchCampaignDashboard.aff.html | 2 +- .../SearchDisplay/AbstractRunAction.php | 2 +- .../Search/AfformSearchMetadataInjector.php | 90 ++++++++++--------- 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php b/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php index e4905763d4a7..95cc5c7ced2b 100644 --- a/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php +++ b/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php @@ -128,10 +128,20 @@ public function testAfformAutocomplete(): void { } public function testGetSearchDisplays() { + $exampleLayout = << + +
+ +
+ +< crm-search-display-table search-name='foo' display-name = 'bar-food' > +AFFORM; + Afform::create() ->addValue('name', $this->formName) ->addValue('title', 'Test Form') - ->addValue('layout', '
< crm-search-display-table search-name=\'foo\' display-name = \'bar-food\' >') + ->addValue('layout', $exampleLayout) ->setLayoutFormat('html') ->execute(); @@ -141,7 +151,7 @@ public function testGetSearchDisplays() { ->addWhere('search_displays', 'CONTAINS', 'foo.foo-bar') ->execute()->single(); - $this->assertEquals(['foo.foo-bar', 'foo.bar-food'], $result['search_displays']); + $this->assertEquals(['foo.foo-bar', 'genericTag', 'foo.bar-food'], $result['search_displays']); } public function testGetLayoutWithLegacyContactTypeConversion() { diff --git a/ext/civi_campaign/ang/afsearchCampaignDashboard.aff.html b/ext/civi_campaign/ang/afsearchCampaignDashboard.aff.html index a4dcb5a1772a..fc4cff25946c 100644 --- a/ext/civi_campaign/ang/afsearchCampaignDashboard.aff.html +++ b/ext/civi_campaign/ang/afsearchCampaignDashboard.aff.html @@ -6,7 +6,7 @@ - + diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index b73e50be0fcb..2e926a98b222 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -1781,7 +1781,7 @@ private function loadAfform() { } $displays = \CRM_Utils_Array::findAll( $fieldset, - ['#tag' => $this->display['type:name'], 'search-name' => $this->savedSearch['name'], 'display-name' => $this->display['name']] + ['search-name' => $this->savedSearch['name'], 'display-name' => $this->display['name']] ); if (!$displays) { continue; diff --git a/ext/search_kit/Civi/Search/AfformSearchMetadataInjector.php b/ext/search_kit/Civi/Search/AfformSearchMetadataInjector.php index a98a329c60a9..e37b039c965c 100644 --- a/ext/search_kit/Civi/Search/AfformSearchMetadataInjector.php +++ b/ext/search_kit/Civi/Search/AfformSearchMetadataInjector.php @@ -28,51 +28,57 @@ class AfformSearchMetadataInjector { public static function preprocess($e) { $changeSet = \Civi\Angular\ChangeSet::create('searchSettings') ->alterHtml(';\\.aff\\.html$;', function($doc, $path) { + // Display-type-specific tags e.g. `crm-search-display-table` $displayTags = array_column(\Civi\Search\Display::getDisplayTypes(['name']), 'name'); + // Include generic tag + $displayTags[] = 'crm-search-display'; - if ($displayTags) { - foreach (pq(implode(',', $displayTags), $doc) as $component) { - $searchName = pq($component)->attr('search-name'); - $displayName = pq($component)->attr('display-name'); - if ($searchName) { - // Fetch search display if name is provided - if (is_string($displayName) && strlen($displayName)) { - $searchDisplayGet = \Civi\Api4\SearchDisplay::get(FALSE) - ->addWhere('name', '=', $displayName) - ->addWhere('saved_search_id.name', '=', $searchName); - } - // Fall-back to the default display - else { - $displayName = NULL; - $searchDisplayGet = \Civi\Api4\SearchDisplay::getDefault(FALSE) - ->setSavedSearch($searchName); - } - try { - $display = $searchDisplayGet - ->addSelect('settings', 'saved_search_id.api_entity', 'saved_search_id.api_params') - ->execute()->single(); - $savedSearch = \CRM_Utils_Array::filterByPrefix($display, 'saved_search_id.'); - } - catch (\CRM_Core_Exception $e) { - return; - } - // Note: Should be kept in-sync with \Civi\Api4\Action\SearchDisplay\GetMarkup::doTask - pq($component)->attr('settings', htmlspecialchars(\CRM_Utils_JS::encode($display['settings'] ?? []), ENT_COMPAT)); - pq($component)->attr('api-entity', htmlspecialchars($savedSearch['api_entity'], ENT_COMPAT)); - pq($component)->attr('search', htmlspecialchars(\CRM_Utils_JS::encode($searchName), ENT_COMPAT)); - pq($component)->attr('display', htmlspecialchars(\CRM_Utils_JS::encode($displayName), ENT_COMPAT)); + foreach (pq(implode(',', $displayTags), $doc) as $component) { + $searchName = pq($component)->attr('search-name'); + $displayName = pq($component)->attr('display-name'); + if ($searchName) { + // Fetch search display if name is provided + if (is_string($displayName) && strlen($displayName)) { + $searchDisplayGet = \Civi\Api4\SearchDisplay::get(FALSE) + ->addWhere('name', '=', $displayName) + ->addWhere('saved_search_id.name', '=', $searchName); + } + // Fall-back to the default display + else { + $displayName = NULL; + $searchDisplayGet = \Civi\Api4\SearchDisplay::getDefault(FALSE) + ->setSavedSearch($searchName); + } + try { + $display = $searchDisplayGet + ->addSelect('type', 'settings', 'saved_search_id.api_entity', 'saved_search_id.api_params') + ->execute()->single(); + $savedSearch = \CRM_Utils_Array::filterByPrefix($display, 'saved_search_id.'); + } + catch (\CRM_Core_Exception $e) { + return; + } + // Note: Should be kept in-sync with \Civi\Api4\Action\SearchDisplay\GetMarkup::doTask + pq($component)->attr('settings', htmlspecialchars(\CRM_Utils_JS::encode($display['settings'] ?? []), ENT_COMPAT)); + pq($component)->attr('api-entity', htmlspecialchars($savedSearch['api_entity'], ENT_COMPAT)); + pq($component)->attr('search', htmlspecialchars(\CRM_Utils_JS::encode($searchName), ENT_COMPAT)); + pq($component)->attr('display', htmlspecialchars(\CRM_Utils_JS::encode($displayName), ENT_COMPAT)); + + // Add `type` if not included with a generic tag + if (pq($component)->is('crm-search-display') && pq($component)->attr('type') === NULL) { + pq($component)->attr('type', htmlspecialchars($display['type'], ENT_COMPAT)); + } - // Add entity names to the fieldset so that afform can populate field metadata - $fieldset = pq($component)->parents('[af-fieldset]'); - if ($fieldset->length) { - $entityList = FormDataModel::getSearchEntities($savedSearch); - $fieldset->attr('api-entities', htmlspecialchars(\CRM_Utils_JS::encode($entityList), ENT_COMPAT)); - // Add field metadata for aggregate fields because they are not in the schema. - // Normal entity fields will be handled by AfformMetadataInjector - foreach (Meta::getCalcFields($savedSearch['api_entity'], $savedSearch['api_params']) as $fieldInfo) { - foreach (pq("af-field[name='{$fieldInfo['name']}']", $doc) as $afField) { - \Civi\Afform\AfformMetadataInjector::setFieldMetadata($afField, $fieldInfo); - } + // Add entity names to the fieldset so that afform can populate field metadata + $fieldset = pq($component)->parents('[af-fieldset]'); + if ($fieldset->length) { + $entityList = FormDataModel::getSearchEntities($savedSearch); + $fieldset->attr('api-entities', htmlspecialchars(\CRM_Utils_JS::encode($entityList), ENT_COMPAT)); + // Add field metadata for aggregate fields because they are not in the schema. + // Normal entity fields will be handled by AfformMetadataInjector + foreach (Meta::getCalcFields($savedSearch['api_entity'], $savedSearch['api_params']) as $fieldInfo) { + foreach (pq("af-field[name='{$fieldInfo['name']}']", $doc) as $afField) { + \Civi\Afform\AfformMetadataInjector::setFieldMetadata($afField, $fieldInfo); } } }