diff --git a/ext/afform/mock/ang/testMultipleSearchForm.aff.html b/ext/afform/mock/ang/testMultipleSearchForm.aff.html
new file mode 100644
index 000000000000..3d60563fafa1
--- /dev/null
+++ b/ext/afform/mock/ang/testMultipleSearchForm.aff.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/ext/afform/mock/ang/testMultipleSearchForm.aff.json b/ext/afform/mock/ang/testMultipleSearchForm.aff.json
new file mode 100644
index 000000000000..609e0f5033cf
--- /dev/null
+++ b/ext/afform/mock/ang/testMultipleSearchForm.aff.json
@@ -0,0 +1,6 @@
+{
+ "type": "search",
+ "title": "TestMultipleSearchForm",
+ "server_route": "",
+ "permission": "access CiviCRM"
+}
diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php
index abf81cd7d53c..6d63a56d8ff1 100644
--- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php
+++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php
@@ -999,7 +999,7 @@ private function getAfformFilters() {
}
// Get afform field filters
$filterKeys = array_column(\CRM_Utils_Array::findAll(
- $afform['layout'] ?? [],
+ $afform['searchDisplay']['fieldset'],
['#tag' => 'af-field']
), 'name');
// Get filters passed into search display directive from Afform markup
@@ -1028,24 +1028,35 @@ private function getAfformFilters() {
*
* Verifies the searchDisplay is embedded in the afform and the user has permission to view it.
*
- * @return array|false|null
+ * @return array|false
*/
private function loadAfform() {
// Only attempt to load afform once.
if ($this->afform && !isset($this->_afform)) {
$this->_afform = FALSE;
// Permission checks are enabled in this api call to ensure the user has permission to view the form
- $afform = \Civi\Api4\Afform::get()
+ $afform = \Civi\Api4\Afform::get($this->getCheckPermissions())
->addWhere('name', '=', $this->afform)
- ->setLayoutFormat('shallow')
+ ->setLayoutFormat('deep')
->execute()->first();
+ if (empty($afform['layout'])) {
+ return FALSE;
+ }
+ // Get all search display fieldsets (which will have an empty value for the af-fieldset attribute)
+ $fieldsets = \CRM_Utils_Array::findAll($afform['layout'], ['af-fieldset' => '']);
+ // As a fallback, search the entire afform in case the search display is not in a fieldset
+ $fieldsets['form'] = $afform['layout'];
// Validate that the afform contains this search display
- $afform['searchDisplay'] = \CRM_Utils_Array::findAll(
- $afform['layout'] ?? [],
- ['#tag' => "{$this->display['type:name']}", 'display-name' => $this->display['name']]
- )[0] ?? NULL;
- if ($afform['searchDisplay']) {
- $this->_afform = $afform;
+ foreach ($fieldsets as $key => $fieldset) {
+ $afform['searchDisplay'] = \CRM_Utils_Array::findAll(
+ $fieldset,
+ ['#tag' => $this->display['type:name'], 'search-name' => $this->savedSearch['name'], 'display-name' => $this->display['name']]
+ )[0] ?? NULL;
+ if ($afform['searchDisplay']) {
+ // Set the fieldset for this display (if it is in one and we haven't fallen back to the whole form)
+ $afform['searchDisplay']['fieldset'] = $key === 'form' ? [] : $fieldset;
+ return $this->_afform = $afform;
+ }
}
}
return $this->_afform;
diff --git a/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchAfformTest.php b/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchAfformTest.php
index 577589455467..15d5425f39c7 100644
--- a/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchAfformTest.php
+++ b/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchAfformTest.php
@@ -5,6 +5,7 @@
use Civi\Api4\Afform;
use Civi\Api4\Contact;
use Civi\Api4\Email;
+use Civi\Api4\Phone;
use Civi\Api4\SavedSearch;
use Civi\Api4\SearchDisplay;
use Civi\Api4\Utils\CoreUtil;
@@ -163,6 +164,175 @@ public function testRunWithAfform() {
$this->assertCount(1, $result);
}
+ public function testRunMultipleSearchForm() {
+ $email = uniqid('tester@');
+
+ Contact::create(FALSE)
+ ->addValue('first_name', 'tester')
+ ->addValue('last_name', __FUNCTION__)
+ ->addValue('source', 'afform_multi_test')
+ ->addChain('emails', Email::save()
+ ->addDefault('contact_id', '$id')
+ ->addRecord(['email' => $email, 'location_type_id:name' => 'Home'])
+ ->addRecord(['email' => $email, 'location_type_id:name' => 'Work'])
+ )
+ ->addChain('phones', Phone::save()
+ ->addDefault('contact_id', '$id')
+ ->addRecord(['phone' => '123-4567', 'location_type_id:name' => 'Home'])
+ ->addRecord(['phone' => '234-5678', 'location_type_id:name' => 'Work'])
+ )
+ ->execute();
+
+ Contact::create(FALSE)
+ ->addValue('first_name', 'tester2')
+ ->addValue('last_name', __FUNCTION__)
+ ->addValue('source', 'afform_multi_test')
+ ->addChain('emails', Email::save()
+ ->addDefault('contact_id', '$id')
+ ->addRecord(['email' => 'other@test.com', 'location_type_id:name' => 'Other'])
+ )
+ ->addChain('phones', Phone::save()
+ ->addDefault('contact_id', '$id')
+ ->addRecord(['phone' => '123-4567', 'location_type_id:name' => 'Home'])
+ ->addRecord(['phone' => '234-5678', 'location_type_id:name' => 'Work'])
+ )
+ ->execute();
+
+ // Decoy contact just to make sure we don't get false-positives
+ Contact::create(FALSE)
+ ->addValue('first_name', 'tester3')
+ ->addValue('last_name', 'nobody')
+ ->addValue('source', 'decoy')
+ ->addChain('emails', Email::save()
+ ->addDefault('contact_id', '$id')
+ ->addRecord(['email' => $email, 'location_type_id:name' => 'Home'])
+ )
+ ->addChain('phones', Phone::save()
+ ->addDefault('contact_id', '$id')
+ ->addRecord(['phone' => '123-4567', 'location_type_id:name' => 'Home'])
+ ->addRecord(['phone' => '234-5678', 'location_type_id:name' => 'Work'])
+ )
+ ->execute();
+
+ $contactEmailSearch = SavedSearch::create(FALSE)
+ ->setValues([
+ 'name' => 'TestContactEmailSearch',
+ 'label' => 'TestContactEmailSearch',
+ 'api_entity' => 'Contact',
+ 'api_params' => [
+ 'version' => 4,
+ 'select' => [
+ 'id',
+ 'display_name',
+ 'GROUP_CONCAT(DISTINCT Contact_Email_contact_id_01.email) AS GROUP_CONCAT_Contact_Email_contact_id_01_email',
+ ],
+ 'orderBy' => [],
+ 'where' => [
+ ['contact_type:name', '=', 'Individual'],
+ ],
+ 'groupBy' => ['id'],
+ 'join' => [
+ [
+ 'Email AS Contact_Email_contact_id_01',
+ 'LEFT',
+ ['id', '=', 'Contact_Email_contact_id_01.contact_id'],
+ ],
+ ],
+ 'having' => [],
+ ],
+ ])
+ ->execute()->first();
+
+ $contactEmailDisplay = SearchDisplay::create(FALSE)
+ ->setValues([
+ 'name' => 'TestContactEmailDisplay',
+ 'label' => 'TestContactEmailDisplay',
+ 'saved_search_id.name' => 'TestContactEmailSearch',
+ 'type' => 'table',
+ 'settings' => [
+ 'limit' => 50,
+ 'pager' => TRUE,
+ 'columns' => [
+ [
+ 'key' => 'id',
+ 'label' => 'Contact ID',
+ 'dataType' => 'Integer',
+ 'type' => 'field',
+ ],
+ [
+ 'key' => 'display_name',
+ 'label' => 'Display Name',
+ 'dataType' => 'String',
+ 'type' => 'field',
+ ],
+ [
+ 'key' => 'GROUP_CONCAT_Contact_Email_contact_id_01_email',
+ 'label' => 'Emails',
+ 'dataType' => 'String',
+ 'type' => 'field',
+ ],
+ ],
+ ],
+ 'acl_bypass' => FALSE,
+ ])
+ ->execute()->first();
+
+ foreach (['Email', 'Phone'] as $entity) {
+ SavedSearch::create(FALSE)
+ ->setValues([
+ 'name' => 'TestSearchFor' . $entity,
+ 'label' => 'TestSearchFor' . $entity,
+ 'api_entity' => $entity,
+ 'api_params' => [
+ 'version' => 4,
+ 'select' => [
+ 'id',
+ 'contact_id.display_name',
+ ],
+ 'orderBy' => [],
+ 'where' => [],
+ 'groupBy' => [],
+ 'join' => [],
+ 'having' => [],
+ ],
+ ])
+ ->execute();
+ }
+
+ $params = [
+ 'return' => 'page:1',
+ 'display' => NULL,
+ 'afform' => 'testMultipleSearchForm',
+ ];
+
+ // This filter will not work because the search display is not within an
+ $params['savedSearch'] = 'TestSearchForPhone';
+ $params['filters'] = ['location_type_id' => 1];
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(4, $result);
+
+ $params['savedSearch'] = 'TestSearchForEmail';
+ $params['filters'] = ['location_type_id' => 1, 'contact_id.display_name' => __FUNCTION__];
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(1, $result);
+
+ // Email filter will not work because it's in the wrong fieldset on the form
+ $params['filters'] = ['email' => $email, 'contact_id.display_name' => __FUNCTION__];
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(3, $result);
+
+ // No filters will work; they are in the fieldset belonging to the non-default display
+ $params['savedSearch'] = 'TestContactEmailSearch';
+ $params['filters'] = ['source' => 'afform_multi_test', 'Contact_Email_contact_id_01.location_type_id' => 1];
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertGreaterThanOrEqual(3, $result->count());
+
+ // Now the filters will work because they are in the fieldset for this display
+ $params['display'] = 'TestContactEmailDisplay';
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(1, $result);
+ }
+
public function testSearchReferencesToAfform() {
$search = SavedSearch::create(FALSE)
->setValues([