diff --git a/frontend/drupal9/web/modules/contrib/ctools/README.txt b/frontend/drupal9/web/modules/contrib/ctools/README.txt
index 6439633f8..5bc0a8a97 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/README.txt
+++ b/frontend/drupal9/web/modules/contrib/ctools/README.txt
@@ -74,7 +74,7 @@ INSTALLATION
* Install the Chaos Tool Suite module as you would normally install a
contributed Drupal module. Visit
- https://www.drupal.org/docs/8/extending-drupal-8/installing-drupal-8-modules
+ https://www.drupal.org/docs/extending-drupal/installing-modules
for further information.
diff --git a/frontend/drupal9/web/modules/contrib/ctools/ctools.info.yml b/frontend/drupal9/web/modules/contrib/ctools/ctools.info.yml
index 9dcfdb9d7..91044b37c 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/ctools.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/ctools.info.yml
@@ -4,7 +4,7 @@ description: 'Provides a number of utility and helper APIs for Drupal developers
package: Chaos tool suite
core_version_requirement: ^8.8 || ^9
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/ctools.module b/frontend/drupal9/web/modules/contrib/ctools/ctools.module
index 0bdf4ee0a..80229fb78 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/ctools.module
+++ b/frontend/drupal9/web/modules/contrib/ctools/ctools.module
@@ -5,8 +5,10 @@
* Provides utility and helper APIs for Drupal developers and site builders.
*/
-use Drupal\Core\Url;
+use Drupal\Core\Entity\Plugin\Condition\EntityBundle as CoreEntityBundle;
use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Url;
+use Drupal\ctools\Plugin\Condition\EntityBundle;
/**
* Implements hook_theme().
@@ -47,7 +49,7 @@ function ctools_theme($existing, $type, $theme, $path) {
* @param $variables
*/
function template_preprocess_ctools_wizard_trail(&$variables) {
- /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface */
+ /** @var \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface $wizard */
$wizard = $variables['wizard'];
$cached_values = $variables['cached_values'];
$trail = $variables['trail'];
@@ -64,7 +66,7 @@ function template_preprocess_ctools_wizard_trail(&$variables) {
* @param $variables
*/
function template_preprocess_ctools_wizard_trail_links(&$variables) {
- /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface */
+ /** @var \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface $wizard */
$wizard = $variables['wizard'];
$cached_values = $variables['cached_values'];
$trail = $variables['trail'];
@@ -91,8 +93,16 @@ function ctools_condition_info_alter(&$definitions) {
if (isset($definitions['node_type']) && $definitions['node_type']['class'] == 'Drupal\node\Plugin\Condition\NodeType') {
$definitions['node_type']['class'] = 'Drupal\ctools\Plugin\Condition\NodeType';
}
-}
+ // Replace all generic entity bundle conditions classes if they are unaltered,
+ // these exist in Drupal 9.3+.
+ foreach ($definitions as $id => $definition) {
+ if (strpos($id, 'entity_bundle:') === 0 && $definition['class'] == CoreEntityBundle::class) {
+ $definitions[$id]['class'] = EntityBundle::class;
+ }
+ }
+
+}
/**
* Implements hook_help().
@@ -115,8 +125,9 @@ function ctools_help($route_name, RouteMatchInterface $route_match) {
$output .= '
' . t('Modal dialog -- tool to make it simple to put a form in a modal dialog.') . ' ';
$output .= '' . t('Dependent -- a simple form widget to make form items appear and disappear based upon the selections in another item.') . ' ';
$output .= '' . t('Content -- pluggable content types used as panes in Panels and other modules like Dashboard.') . ' ';
- $output .= '' . t('Form wizard -- an API to make multi-step forms much easier.') . ' ';+ $output .= '' . t('CSS tools -- tools to cache and sanitize CSS easily to make user-input CSS safe.') . ' ';
+ $output .= '' . t('Form wizard -- an API to make multi-step forms much easier.') . ' ';
+ $output .= '' . t('CSS tools -- tools to cache and sanitize CSS easily to make user-input CSS safe.') . ' ';
$output .= '';
- return $output;
+ return $output;
}
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/ctools_block.info.yml b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/ctools_block.info.yml
index 5a3bb9d28..ca7e4cb37 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/ctools_block.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/ctools_block.info.yml
@@ -6,7 +6,7 @@ core_version_requirement: ^8.8 || ^9
dependencies:
- ctools:ctools
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/ctools_block.module b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/ctools_block.module
index 47a3a1ac4..74dc1dcfa 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/ctools_block.module
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/ctools_block.module
@@ -11,8 +11,7 @@
* In general, users should be using the core block types instead.
*/
function ctools_block_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
- $moduleHandler = \Drupal::service('module_handler');
- if ($moduleHandler->moduleExists('layout_builder')) {
+ if ($consumer == 'layout_builder') {
foreach ($definitions as $label => $definition) {
if ($definition['provider'] == 'ctools_block') {
unset($definitions[$label]);
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Block/EntityField.php b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Block/EntityField.php
index 1fc1e1c35..23e8da420 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Block/EntityField.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Block/EntityField.php
@@ -108,7 +108,7 @@ class EntityField extends BlockBase implements ContextAwarePluginInterface, Cont
$this->formatterManager = $formatter_manager;
// Get the entity type and field name from the plugin id.
- list (, $entity_type_id, $field_name) = explode(':', $plugin_id);
+ [, $entity_type_id, $field_name] = explode(':', $plugin_id);
$this->entityTypeId = $entity_type_id;
$this->fieldName = $field_name;
@@ -190,7 +190,7 @@ class EntityField extends BlockBase implements ContextAwarePluginInterface, Cont
return [
'formatter' => [
'label' => 'above',
- 'type' => isset($field_type_definition['default_formatter']) ? $field_type_definition['default_formatter'] : '',
+ 'type' => $field_type_definition['default_formatter'] ?? '',
'settings' => [],
'third_party_settings' => [],
'weight' => 0,
@@ -316,7 +316,12 @@ class EntityField extends BlockBase implements ContextAwarePluginInterface, Cont
*/
protected function getFieldStorageDefinition() {
if (empty($this->fieldStorageDefinition)) {
- $field_definitions = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId);
+ // Some base fields have no storage.
+ $field_definitions = array_merge(
+ $this->entityFieldManager->getBaseFieldDefinitions($this->entityTypeId),
+ $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId)
+ );
+
$this->fieldStorageDefinition = $field_definitions[$this->fieldName];
}
return $this->fieldStorageDefinition;
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Deriver/EntityFieldDeriver.php b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Deriver/EntityFieldDeriver.php
index 62aebb33c..aecfafce4 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Deriver/EntityFieldDeriver.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Deriver/EntityFieldDeriver.php
@@ -16,7 +16,13 @@ class EntityFieldDeriver extends EntityDeriverBase {
public function getDerivativeDefinitions($base_plugin_definition) {
$entity_type_labels = $this->entityTypeRepository->getEntityTypeLabels();
foreach ($this->entityFieldManager->getFieldMap() as $entity_type_id => $entity_field_map) {
- foreach ($this->entityFieldManager->getFieldStorageDefinitions($entity_type_id) as $field_storage_definition) {
+ // Some base fields have no storage.
+ /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_storage_definitions */
+ $field_storage_definitions = array_merge(
+ $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id),
+ $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id)
+ );
+ foreach ($field_storage_definitions as $field_storage_definition) {
$field_name = $field_storage_definition->getName();
// The blocks are based on fields. However, we are looping through field
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/ctools_block_field_test.info.yml b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/ctools_block_field_test.info.yml
index 09643042d..4c9b77242 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/ctools_block_field_test.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/ctools_block_field_test.info.yml
@@ -12,7 +12,7 @@ dependencies:
- drupal:user
features: true
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/tests/src/Functional/EntityFieldBlockTest.php b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/tests/src/Functional/EntityFieldBlockTest.php
index 40d0b7e4e..56baf0dad 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/tests/src/Functional/EntityFieldBlockTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_block/tests/src/Functional/EntityFieldBlockTest.php
@@ -14,7 +14,7 @@ class EntityFieldBlockTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
- public static $modules = ['block', 'ctools_block', 'ctools_block_field_test'];
+ protected static $modules = ['block', 'ctools_block', 'ctools_block_field_test'];
/**
* {@inheritdoc}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/ctools_entity_mask.info.yml b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/ctools_entity_mask.info.yml
index 735696531..133eb685f 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/ctools_entity_mask.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/ctools_entity_mask.info.yml
@@ -3,7 +3,7 @@ core_version_requirement: ^8.8 || ^9
type: module
description: 'Allows an entity type to borrow the fields and display configuration of another entity type.'
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/modules/entity_mask_test/entity_mask_test.info.yml b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/modules/entity_mask_test/entity_mask_test.info.yml
index 32bdc7e0e..8e9ccd02c 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/modules/entity_mask_test/entity_mask_test.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/modules/entity_mask_test/entity_mask_test.info.yml
@@ -7,7 +7,7 @@ dependencies:
- drupal:image
- drupal:text
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/src/Functional/DisplayTest.php b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/src/Functional/DisplayTest.php
index 506c8b315..b65d66da9 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/src/Functional/DisplayTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/src/Functional/DisplayTest.php
@@ -21,7 +21,7 @@ class DisplayTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
- public static $modules = [
+ protected static $modules = [
'block',
'block_content',
'ctools_entity_mask',
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/src/Kernel/EntityMaskTest.php b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/src/Kernel/EntityMaskTest.php
index 42f3aa970..f4d14a392 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/src/Kernel/EntityMaskTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_entity_mask/tests/src/Kernel/EntityMaskTest.php
@@ -16,7 +16,7 @@ class EntityMaskTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
- public static $modules = [
+ protected static $modules = [
'block',
'block_content',
'ctools_entity_mask',
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/ctools_views.info.yml b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/ctools_views.info.yml
index 50951d536..33157d675 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/ctools_views.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/ctools_views.info.yml
@@ -8,7 +8,7 @@ dependencies:
- drupal:block
- drupal:views
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/src/Plugin/Display/Block.php b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/src/Plugin/Display/Block.php
index 65406dee8..f85d03288 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/src/Plugin/Display/Block.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/src/Plugin/Display/Block.php
@@ -2,14 +2,10 @@
namespace Drupal\ctools_views\Plugin\Display;
-use Drupal\Core\Block\BlockManagerInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\Block\ViewsBlock;
use Drupal\views\Plugin\views\display\Block as CoreBlock;
-use Drupal\views\Plugin\ViewsHandlerManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\Request;
/**
* Provides a Block display plugin.
@@ -32,44 +28,18 @@ class Block extends CoreBlock {
*/
protected $request;
- /**
- * Constructs a new Block instance.
- *
- * @param array $configuration
- * A configuration array containing information about the plugin instance.
- * @param string $plugin_id
- * The plugin_id for the plugin instance.
- * @param mixed $plugin_definition
- * The plugin implementation definition.
- * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
- * The entity manager.
- * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
- * The block manager.
- * @param \Drupal\views\Plugin\ViewsHandlerManager $filter_manager
- * The views filter plugin manager.
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The current request.
- */
- public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, BlockManagerInterface $block_manager, ViewsHandlerManager $filter_manager, Request $request) {
- parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $block_manager);
-
- $this->filterManager = $filter_manager;
- $this->request = $request;
- }
-
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
- return new static(
- $configuration,
- $plugin_id,
- $plugin_definition,
- $container->get('entity_type.manager'),
- $container->get('plugin.manager.block'),
- $container->get('plugin.manager.views.filter'),
- $container->get('request_stack')->getCurrentRequest()
- );
+ /**
+ * @var \Drupal\ctools_views\Plugin\Display\Block
+ */
+ $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+ $instance->filterManager = $container->get('plugin.manager.views.filter');
+ $instance->request = $container->get('request_stack')->getCurrentRequest();
+
+ return $instance;
}
/**
@@ -141,7 +111,7 @@ class Block extends CoreBlock {
$form['override']['pager_offset'] = [
'#type' => 'number',
'#title' => $this->t('Pager offset'),
- '#default_value' => isset($block_configuration['pager_offset']) ? $block_configuration['pager_offset'] : 0,
+ '#default_value' => $block_configuration['pager_offset'] ?? 0,
'#description' => $this->t('For example, set this to 3 and the first 3 items will not be displayed.'),
];
}
@@ -157,7 +127,7 @@ class Block extends CoreBlock {
'#type' => 'radios',
'#title' => $this->t('Pager'),
'#options' => $pager_options,
- '#default_value' => isset($block_configuration['pager']) ? $block_configuration['pager'] : 'view',
+ '#default_value' => $block_configuration['pager'] ?? 'view',
];
}
@@ -245,7 +215,7 @@ class Block extends CoreBlock {
if (!empty($allow_settings['disable_filters'])) {
$items = [];
foreach ((array) $this->getOption('filters') as $filter_name => $item) {
- $item['value'] = isset($block_configuration["filter"][$filter_name]['value']) ? $block_configuration["filter"][$filter_name]['value'] : '';
+ $item['value'] = $block_configuration["filter"][$filter_name]['value'] ?? '';
$items[$filter_name] = $item;
}
$this->setOption('filters', $items);
@@ -390,7 +360,7 @@ class Block extends CoreBlock {
$allow_settings = array_filter($this->getOption('allow'));
$config = $block->getConfiguration();
- list(, $display_id) = explode('-', $block->getDerivativeId(), 2);
+ [, $display_id] = explode('-', $block->getDerivativeId(), 2);
// Change pager offset settings based on block configuration.
if (!empty($allow_settings['offset']) && isset($config['pager_offset'])) {
@@ -459,19 +429,6 @@ class Block extends CoreBlock {
return $config['value'][$filter['expose']['identifier']];
}
- /**
- * {@inheritdoc}
- */
- public function usesExposed() {
- $filters = $this->getHandlers('filter');
- foreach ($filters as $filter) {
- if ($filter->isExposed() && !empty($filter->exposedInfo())) {
- return TRUE;
- }
- }
- return FALSE;
- }
-
/**
* Exposed widgets.
*
@@ -501,8 +458,8 @@ class Block extends CoreBlock {
* Return the more weight
*/
public static function sortFieldsByWeight($a, $b) {
- $a_weight = isset($a['weight']) ? $a['weight'] : 0;
- $b_weight = isset($b['weight']) ? $b['weight'] : 0;
+ $a_weight = $a['weight'] ?? 0;
+ $b_weight = $b['weight'] ?? 0;
if ($a_weight == $b_weight) {
return 0;
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/ctools_views_test_views.info.yml b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/ctools_views_test_views.info.yml
index 46e4c59d8..e33eb6ea1 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/ctools_views_test_views.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/ctools_views_test_views.info.yml
@@ -12,7 +12,7 @@ dependencies:
- drupal:node
- drupal:taxonomy
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/tests/src/Functional/CToolsViewsBasicViewBlockTest.php b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/tests/src/Functional/CToolsViewsBasicViewBlockTest.php
index 5c22860d1..23ba31cca 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/tests/src/Functional/CToolsViewsBasicViewBlockTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/modules/ctools_views/tests/src/Functional/CToolsViewsBasicViewBlockTest.php
@@ -21,7 +21,7 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
*
* @var array
*/
- public static $modules = ['ctools_views', 'ctools_views_test_views'];
+ protected static $modules = ['ctools_views', 'ctools_views_test_views'];
/**
* Views used by this test.
@@ -65,7 +65,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit = [];
$edit['region'] = 'sidebar_first';
$edit['settings[override][items_per_page]'] = 0;
- $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme, $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme);
+ $this->submitForm($edit, $this->t('Save block'));
// Assert items per page default settings.
$this->drupalGet('');
@@ -78,7 +79,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit = [];
$edit['region'] = 'sidebar_first';
$edit['settings[override][items_per_page]'] = 2;
- $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager', $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager');
+ $this->submitForm($edit, $this->t('Save block'));
$block = $this->storage->load('views_block__ctools_views_test_view_block_pager');
$config = $block->getPlugin()->getConfiguration();
@@ -110,7 +112,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit = [];
$edit['region'] = 'sidebar_first';
$edit['settings[override][items_per_page]'] = 0;
- $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme, $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme);
+ $this->submitForm($edit, $this->t('Save block'));
// Assert pager offset default settings.
$this->drupalGet('');
@@ -125,7 +128,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit['region'] = 'sidebar_first';
$edit['settings[override][items_per_page]'] = 0;
$edit['settings[override][pager_offset]'] = 1;
- $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager', $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager');
+ $this->submitForm($edit, $this->t('Save block'));
$block = $this->storage->load('views_block__ctools_views_test_view_block_pager');
$config = $block->getPlugin()->getConfiguration();
@@ -156,7 +160,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit = [];
$edit['region'] = 'sidebar_first';
$edit['settings[override][items_per_page]'] = 0;
- $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme, $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme);
+ $this->submitForm($edit, $this->t('Save block'));
// Assert pager default settings.
$this->drupalGet('');
@@ -168,7 +173,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit['region'] = 'sidebar_first';
$edit['settings[override][items_per_page]'] = 0;
$edit['settings[override][pager]'] = 'some';
- $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager', $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager');
+ $this->submitForm($edit, $this->t('Save block'));
$block = $this->storage->load('views_block__ctools_views_test_view_block_pager');
$config = $block->getPlugin()->getConfiguration();
@@ -184,7 +190,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit['region'] = 'sidebar_first';
$edit['settings[override][items_per_page]'] = 0;
$edit['settings[override][pager]'] = 'none';
- $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager', $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager');
+ $this->submitForm($edit, $this->t('Save block'));
$block = $this->storage->load('views_block__ctools_views_test_view_block_pager');
$config = $block->getPlugin()->getConfiguration();
@@ -209,7 +216,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
// Add block to sidebar_first region with default settings.
$edit = [];
$edit['region'] = 'sidebar_first';
- $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_fields/' . $default_theme, $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_fields/' . $default_theme);
+ $this->submitForm($edit, $this->t('Save block'));
// Assert hide_fields default settings.
$this->drupalGet('');
@@ -219,7 +227,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit = [];
$edit['region'] = 'sidebar_first';
$edit['settings[override][order_fields][id][hide]'] = 1;
- $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_fields', $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/manage/views_block__ctools_views_test_view_block_fields');
+ $this->submitForm($edit, $this->t('Save block'));
$block = $this->storage->load('views_block__ctools_views_test_view_block_fields');
$config = $block->getPlugin()->getConfiguration();
@@ -244,7 +253,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
// Add block to sidebar_first region with default settings.
$edit = [];
$edit['region'] = 'sidebar_first';
- $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_fields/' . $default_theme, $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_fields/' . $default_theme);
+ $this->submitForm($edit, $this->t('Save block'));
// Assert sort_fields default settings.
$this->drupalGet('');
@@ -261,7 +271,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit['settings[override][order_fields][created][weight]'] = -47;
$edit['settings[override][order_fields][id][weight]'] = -46;
$edit['settings[override][order_fields][name_1][weight]'] = -45;
- $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_fields', $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/manage/views_block__ctools_views_test_view_block_fields');
+ $this->submitForm($edit, $this->t('Save block'));
$block = $this->storage->load('views_block__ctools_views_test_view_block_fields');
$config = $block->getPlugin()->getConfiguration();
@@ -298,7 +309,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
// Add block to sidebar_first region with default settings.
$edit = [];
$edit['region'] = 'sidebar_first';
- $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme, $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme);
+ $this->submitForm($edit, $this->t('Save block'));
// Assert disable_filters default settings.
$this->drupalGet('');
@@ -311,7 +323,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit['region'] = 'sidebar_first';
$edit['settings[override][filters][status][disable]'] = 1;
$edit['settings[override][filters][job][disable]'] = 1;
- $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_filter', $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/manage/views_block__ctools_views_test_view_block_filter');
+ $this->submitForm($edit, $this->t('Save block'));
$block = $this->storage->load('views_block__ctools_views_test_view_block_filter');
$config = $block->getPlugin()->getConfiguration();
@@ -337,7 +350,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
// Add block to sidebar_first region with default settings.
$edit = [];
$edit['region'] = 'sidebar_first';
- $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_sort/' . $default_theme, $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_sort/' . $default_theme);
+ $this->submitForm($edit, $this->t('Save block'));
// Assert configure_sorts default settings.
$this->drupalGet('');
@@ -350,7 +364,8 @@ class CToolsViewsBasicViewBlockTest extends UITestBase {
$edit = [];
$edit['region'] = 'sidebar_first';
$edit['settings[override][sort][id][order]'] = 'DESC';
- $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_sort', $edit, $this->t('Save block'));
+ $this->drupalGet('admin/structure/block/manage/views_block__ctools_views_test_view_block_sort');
+ $this->submitForm($edit, $this->t('Save block'));
$block = $this->storage->load('views_block__ctools_views_test_view_block_sort');
$config = $block->getPlugin()->getConfiguration();
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Access/AccessInterface.php b/frontend/drupal9/web/modules/contrib/ctools/src/Access/AccessInterface.php
index 1e12fd29c..898c9957b 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Access/AccessInterface.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Access/AccessInterface.php
@@ -4,6 +4,14 @@ namespace Drupal\ctools\Access;
use Drupal\Core\Session\AccountInterface;
+/**
+ * Ctools Access Interface.
+ */
interface AccessInterface {
+
+ /**
+ * Provides the access method for accounts.
+ */
public function access(AccountInterface $account);
+
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Access/TempstoreAccess.php b/frontend/drupal9/web/modules/contrib/ctools/src/Access/TempstoreAccess.php
index e18eb95a9..bb94208b8 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Access/TempstoreAccess.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Access/TempstoreAccess.php
@@ -10,7 +10,9 @@ use Drupal\ctools\Access\AccessInterface as CToolsAccessInterface;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Symfony\Component\Routing\Route;
-
+/**
+ * Tempstore Access for ctools.
+ */
class TempstoreAccess implements CoreAccessInterface {
/**
@@ -20,17 +22,29 @@ class TempstoreAccess implements CoreAccessInterface {
*/
protected $tempstore;
-
+ /**
+ * Constructor for access to shared tempstore.
+ */
public function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
-
+ /**
+ * Retreive the tempstore factory.
+ */
protected function getTempstore() {
return $this->tempstore;
}
-
+ /**
+ * Access method to find if user has access to a particular tempstore.
+ *
+ * @param \Symfony\Component\Routing\Route $route
+ * @param \Drupal\Core\Routing\RouteMatchInterface $match
+ * @param \Drupal\Core\Session\AccountInterface $account
+ *
+ * @return \Drupal\Core\Access\AccessResultAllowed|\Drupal\Core\Access\AccessResultForbidden
+ */
public function access(Route $route, RouteMatchInterface $match, AccountInterface $account) {
$tempstore_id = $match->getParameter('tempstore_id') ? $match->getParameter('tempstore_id') : $route->getDefault('tempstore_id');
$id = $match->getParameter($route->getRequirement('_ctools_access'));
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Ajax/OpenModalWizardCommand.php b/frontend/drupal9/web/modules/contrib/ctools/src/Ajax/OpenModalWizardCommand.php
index 6f0303279..7311e6ed8 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Ajax/OpenModalWizardCommand.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Ajax/OpenModalWizardCommand.php
@@ -4,10 +4,14 @@ namespace Drupal\ctools\Ajax;
use Drupal\Core\Ajax\OpenModalDialogCommand;
-
+/**
+ *
+ */
class OpenModalWizardCommand extends OpenModalDialogCommand {
-
+ /**
+ *
+ */
public function __construct($object, $tempstore_id, array $parameters = [], array $dialog_options = [], $settings = NULL) {
// Instantiate the wizard class properly.
$parameters += [
@@ -16,7 +20,7 @@ class OpenModalWizardCommand extends OpenModalDialogCommand {
'step' => NULL,
];
$form = \Drupal::service('ctools.wizard.factory')->getWizardForm($object, $parameters, TRUE);
- $title = isset($form['#title']) ? $form['#title'] : '';
+ $title = $form['#title'] ?? '';
$content = $form;
parent::__construct($title, $content, $dialog_options, $settings);
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/ConstraintConditionInterface.php b/frontend/drupal9/web/modules/contrib/ctools/src/ConstraintConditionInterface.php
index 00ec2d1f6..f21d3c386 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/ConstraintConditionInterface.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/ConstraintConditionInterface.php
@@ -2,15 +2,16 @@
namespace Drupal\ctools;
-
+/**
+ * Interface for Constraint Conditions
+ */
interface ConstraintConditionInterface {
/**
* Applies relevant constraints for this condition to the injected contexts.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
- *
- * @return null
+ * Contexts to apply.
*/
public function applyConstraints(array $contexts = []);
@@ -18,8 +19,7 @@ interface ConstraintConditionInterface {
* Removes constraints for this condition from the injected contexts.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
- *
- * @return null
+ * Contexts to remove.
*/
public function removeConstraints(array $contexts = []);
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Context/AutomaticContext.php b/frontend/drupal9/web/modules/contrib/ctools/src/Context/AutomaticContext.php
index 220222966..8dcab5141 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Context/AutomaticContext.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Context/AutomaticContext.php
@@ -17,6 +17,7 @@ class AutomaticContext extends Context {
* Returns TRUE if this context is automatic and always available.
*
* @return bool
+ * If the context is automatic or not.
*/
public function isAutomatic() {
return TRUE;
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/ContextNotFoundException.php b/frontend/drupal9/web/modules/contrib/ctools/src/ContextNotFoundException.php
index b9cf15c4a..db25f6ef9 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/ContextNotFoundException.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/ContextNotFoundException.php
@@ -2,5 +2,7 @@
namespace Drupal\ctools;
-
+/**
+ *
+ */
class ContextNotFoundException extends \Exception {}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Controller/WizardEntityFormController.php b/frontend/drupal9/web/modules/contrib/ctools/src/Controller/WizardEntityFormController.php
index 341419fb0..058c1f38a 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Controller/WizardEntityFormController.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Controller/WizardEntityFormController.php
@@ -40,7 +40,7 @@ class WizardEntityFormController extends WizardFormController {
*/
protected function getFormArgument(RouteMatchInterface $route_match) {
$form_arg = $route_match->getRouteObject()->getDefault('_entity_wizard');
- list($entity_type_id, $operation) = explode('.', $form_arg);
+ [$entity_type_id, $operation] = explode('.', $form_arg);
$definition = $this->entityTypeManager->getDefinition($entity_type_id);
$handlers = $definition->getHandlerClasses();
if (empty($handlers['wizard'][$operation])) {
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Controller/WizardFormController.php b/frontend/drupal9/web/modules/contrib/ctools/src/Controller/WizardFormController.php
index 5afc7c66f..81bf06ec4 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Controller/WizardFormController.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Controller/WizardFormController.php
@@ -24,7 +24,7 @@ class WizardFormController extends FormController {
/**
* Tempstore Factory for keeping track of values in each step of the wizard.
*
- * @var \Drupal\Core\TempStore\SharedTempStoreFactory
+ * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempstore;
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Event/BlockVariantEvent.php b/frontend/drupal9/web/modules/contrib/ctools/src/Event/BlockVariantEvent.php
index 35df32dd3..ed12c3800 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Event/BlockVariantEvent.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Event/BlockVariantEvent.php
@@ -6,6 +6,9 @@ use Drupal\Core\Block\BlockPluginInterface;
use Drupal\ctools\Plugin\BlockVariantInterface;
use Symfony\Component\EventDispatcher\Event;
+/**
+ *
+ */
class BlockVariantEvent extends Event {
/**
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Event/WizardEvent.php b/frontend/drupal9/web/modules/contrib/ctools/src/Event/WizardEvent.php
index 9e28d0d8b..9c4d46a05 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Event/WizardEvent.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Event/WizardEvent.php
@@ -20,23 +20,31 @@ class WizardEvent extends Event {
*/
protected $values;
-
+ /**
+ *
+ */
public function __construct(FormWizardInterface $wizard, $values) {
$this->wizard = $wizard;
$this->values = $values;
}
-
+ /**
+ *
+ */
public function getWizard() {
return $this->wizard;
}
-
+ /**
+ *
+ */
public function getValues() {
return $this->values;
}
-
+ /**
+ *
+ */
public function setValues($values) {
$this->values = $values;
return $this;
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/AjaxFormTrait.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/AjaxFormTrait.php
index 1cb0c17db..3b6b39aca 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/AjaxFormTrait.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/AjaxFormTrait.php
@@ -14,6 +14,7 @@ trait AjaxFormTrait {
* Gets attributes for use with an AJAX modal.
*
* @return array
+ * The array of attributes.
*/
public static function getAjaxAttributes() {
return [
@@ -29,6 +30,7 @@ trait AjaxFormTrait {
* Gets attributes for use with an add button AJAX modal.
*
* @return array
+ * The array of attributes.
*/
public static function getAjaxButtonAttributes() {
return NestedArray::mergeDeep(AjaxFormTrait::getAjaxAttributes(), [
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ConditionConfigure.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ConditionConfigure.php
index 10b8224b7..9fe7ddcf4 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ConditionConfigure.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ConditionConfigure.php
@@ -47,7 +47,14 @@ abstract class ConditionConfigure extends FormBase {
return new static($container->get('tempstore.shared'), $container->get('plugin.manager.condition'));
}
-
+ /**
+ * Constructor for Condition Configuration.
+ *
+ * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * The Tempstore Factory.
+ * @param \Drupal\Component\Plugin\PluginManagerInterface $manager
+ * The Plugin Manager.
+ */
public function __construct(SharedTempStoreFactory $tempstore, PluginManagerInterface $manager) {
$this->tempstore = $tempstore;
$this->manager = $manager;
@@ -76,7 +83,7 @@ abstract class ConditionConfigure extends FormBase {
$instance = $this->manager->createInstance($condition, []);
}
$form_state->setTemporaryValue('gathered_contexts', $this->getContexts($cached_values));
- /** @var $instance \Drupal\Core\Condition\ConditionInterface */
+ /** @var \Drupal\Core\Condition\ConditionInterface $instance */
$form = $instance->buildConfigurationForm($form, $form_state);
if (isset($id)) {
// Conditionally set this form element so that we can update or add.
@@ -104,17 +111,17 @@ abstract class ConditionConfigure extends FormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
- /** @var $instance \Drupal\Core\Condition\ConditionInterface */
+ /** @var \Drupal\Core\Condition\ConditionInterface $instance */
$instance = $form_state->getValue('instance');
$instance->submitConfigurationForm($form, $form_state);
$conditions = $this->getConditions($cached_values);
if ($instance instanceof ContextAwarePluginInterface) {
- /** @var $instance \Drupal\Core\Plugin\ContextAwarePluginInterface */
+ /** @var \Drupal\Core\Plugin\ContextAwarePluginInterface $instance */
$context_mapping = $form_state->hasValue('context_mapping') ? $form_state->getValue('context_mapping') : [];
$instance->setContextMapping($context_mapping);
}
if ($instance instanceof ConstraintConditionInterface) {
- /** @var $instance \Drupal\ctools\ConstraintConditionInterface */
+ /** @var \Drupal\ctools\ConstraintConditionInterface $instance */
$instance->applyConstraints($this->getContexts($cached_values));
}
if ($form_state->hasValue('id')) {
@@ -125,15 +132,25 @@ abstract class ConditionConfigure extends FormBase {
}
$cached_values = $this->setConditions($cached_values, $conditions);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
-
+ /**
+ * Ajax callback to save tempstore values.
+ *
+ * @param array $form
+ * The Drupal Form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The Form state.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * Ajax values from tempstore.
+ */
public function ajaxSave(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$url = Url::fromRoute($route_name, $route_parameters);
$response->addCommand(new RedirectCommand($url->toString()));
$response->addCommand(new CloseModalDialogCommand());
@@ -143,29 +160,34 @@ abstract class ConditionConfigure extends FormBase {
/**
* Document the route name and parameters for redirect after submission.
*
- * @param $cached_values
+ * @param array $cached_values
+ * Cached values to get the route info.
*
* @return array
- * In the format of
- * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name']];
+ * In the format of [
+ * 'route.name',
+ * ['machine_name' => $this->machine_name, 'step' => 'step_name']];
*/
abstract protected function getParentRouteInfo($cached_values);
/**
* Custom logic for retrieving the conditions array from cached_values.
*
- * @param $cached_values
+ * @param array $cached_values
+ * Cached values to get contexts from.
*
* @return array
+ * The conditions attached to cached values.
*/
- abstract protected function getConditions($cached_values);
+ abstract protected function getConditions(array $cached_values);
/**
* Custom logic for setting the conditions array in cached_values.
*
- * @param $cached_values
+ * @param array $cached_values
+ * Cached values that will get set.
*
- * @param $conditions
+ * @param mixed $conditions
* The conditions to set within the cached values.
*
* @return mixed
@@ -176,10 +198,12 @@ abstract class ConditionConfigure extends FormBase {
/**
* Custom logic for retrieving the contexts array from cached_values.
*
- * @param $cached_values
+ * @param array $cached_values
+ * Cached values to get contexts from.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
+ * The contexts from cache.
*/
- abstract protected function getContexts($cached_values);
+ abstract protected function getContexts(array $cached_values);
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ConditionDelete.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ConditionDelete.php
index 7ab905037..551fe3fc7 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ConditionDelete.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ConditionDelete.php
@@ -11,7 +11,9 @@ use Drupal\ctools\ConstraintConditionInterface;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
-
+/**
+ * Delete Condition Confirmation Form.
+ */
abstract class ConditionDelete extends ConfirmFormBase {
/**
@@ -46,7 +48,14 @@ abstract class ConditionDelete extends ConfirmFormBase {
return new static($container->get('tempstore.shared'), $container->get('plugin.manager.condition'));
}
-
+ /**
+ * Condition Delete Confirmation Form Constructor.
+ *
+ * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * The Tempstore Factory.
+ * @param \Drupal\Component\Plugin\PluginManagerInterface $manager
+ * The Plugin Manager.
+ */
public function __construct(SharedTempStoreFactory $tempstore, PluginManagerInterface $manager) {
$this->tempstore = $tempstore;
$this->manager = $manager;
@@ -89,7 +98,7 @@ abstract class ConditionDelete extends ConfirmFormBase {
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
$conditions = $this->getConditions($cached_values);
- /** @var $instance \Drupal\ctools\ConstraintConditionInterface */
+ /** @var \Drupal\ctools\ConstraintConditionInterface $instance */
$instance = $this->manager->createInstance($conditions[$this->id]['id'], $conditions[$this->id]);
if ($instance instanceof ConstraintConditionInterface) {
$instance->removeConstraints($this->getContexts($cached_values));
@@ -97,11 +106,21 @@ abstract class ConditionDelete extends ConfirmFormBase {
unset($conditions[$this->id]);
$cached_values = $this->setConditions($cached_values, $conditions);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
-
+ /**
+ * Gets the delete question.
+ *
+ * @param $id
+ * Condition ID.
+ * @param $cached_values
+ * Cached Context values.
+ *
+ * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+ * The confirmation delete question.
+ */
public function getQuestion($id = NULL, $cached_values = NULL) {
$condition = $this->getConditions($cached_values)[$id];
return $this->t('Are you sure you want to delete the @label condition?', [
@@ -150,7 +169,7 @@ abstract class ConditionDelete extends ConfirmFormBase {
*/
public function getCancelUrl() {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
return new Url($route_name, $route_parameters);
}
@@ -171,43 +190,47 @@ abstract class ConditionDelete extends ConfirmFormBase {
/**
* Document the route name and parameters for redirect after submission.
*
- * @param $cached_values
+ * @param array $cached_values
+ * The cached context values.
*
* @return array
* In the format of
* return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]];
*/
- abstract protected function getParentRouteInfo($cached_values);
+ abstract protected function getParentRouteInfo(array $cached_values);
/**
* Custom logic for retrieving the conditions array from cached_values.
*
- * @param $cached_values
+ * @param array $cached_values
+ * The cached context values.
*
* @return array
+ * The conditions.
*/
- abstract protected function getConditions($cached_values);
+ abstract protected function getConditions(array $cached_values);
/**
* Custom logic for setting the conditions array in cached_values.
*
- * @param $cached_values
- *
- * @param $conditions
+ * @param array $cached_values
+ * The cached context values.
+ * @param mixed $conditions
* The conditions to set within the cached values.
*
* @return mixed
* Return the $cached_values
*/
- abstract protected function setConditions($cached_values, $conditions);
+ abstract protected function setConditions(array $cached_values, mixed $conditions);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
- * @param $cached_values
+ * @param array $cached_values
+ * The cached context values.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
- abstract protected function getContexts($cached_values);
+ abstract protected function getContexts(array $cached_values);
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ContextConfigure.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ContextConfigure.php
index 14377bf5f..92cfc4ed1 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ContextConfigure.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ContextConfigure.php
@@ -16,27 +16,35 @@ use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
-
+/**
+ * Configure Context Form.
+ */
abstract class ContextConfigure extends FormBase {
/**
+ * Tempstore Factory.
+ *
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
*/
protected $tempstore;
/**
- * Object EntityTypeManager.
+ * Entity Type Manager.
*
- * @var Drupal\Core\Entity\EntityTypeManagerInterface
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
+ * The Tempstore ID.
+ *
* @var string
*/
protected $tempstore_id;
/**
+ * The form machine name.
+ *
* @var string
*/
protected $machine_name;
@@ -51,7 +59,14 @@ abstract class ContextConfigure extends FormBase {
);
}
-
+ /**
+ * Configure Context Form constructor.
+ *
+ * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * The tempstore factory.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ */
public function __construct(SharedTempStoreFactory $tempstore, EntityTypeManagerInterface $entity_type_manager) {
$this->tempstore = $tempstore;
$this->entityTypeManager = $entity_type_manager;
@@ -120,7 +135,7 @@ abstract class ContextConfigure extends FormBase {
'#default_value' => $description,
];
if (strpos($data_type, 'entity:') === 0) {
- list(, $entity_type) = explode(':', $data_type);
+ [, $entity_type] = explode(':', $data_type);
/** @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter $entity */
$entity = $edit ? $context->getContextValue() : NULL;
$form['context_value'] = [
@@ -187,7 +202,7 @@ abstract class ContextConfigure extends FormBase {
}
// We're dealing with an entity and should make sure it's loaded.
if (strpos($context_definition->getDataType(), 'entity:') === 0) {
- list(, $entity_type) = explode(':', $context_definition->getDataType());
+ [, $entity_type] = explode(':', $context_definition->getDataType());
if (is_numeric($form_state->getValue('context_value'))) {
$value = $this->entityTypeManager->getStorage($entity_type)->load($form_state->getValue('context_value'));
}
@@ -200,15 +215,25 @@ abstract class ContextConfigure extends FormBase {
$cached_values = $this->addContext($cached_values, $form_state->getValue('machine_name'), $context);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
-
+ /**
+ * Ajax Save Method.
+ *
+ * @param array $form
+ * Drupal Form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form State.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * The ajax data in the response.
+ */
public function ajaxSave(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$url = new Url($route_name, $route_parameters);
$response->addCommand(new RedirectCommand($url->toString()));
$response->addCommand(new CloseModalDialogCommand());
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ContextDelete.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ContextDelete.php
index fde4854d4..cad418dda 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ContextDelete.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ContextDelete.php
@@ -34,12 +34,19 @@ abstract class ContextDelete extends ConfirmFormBase {
*/
protected $context_id;
-
+ /**
+ * {@inheritdoc}
+ */
public static function create(ContainerInterface $container) {
return new static($container->get('tempstore.shared'));
}
-
+ /**
+ * Context Delete Constructor.
+ *
+ * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * Tempstore Service.
+ */
public function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
@@ -75,12 +82,24 @@ abstract class ContextDelete extends ConfirmFormBase {
$form_state->setRedirectUrl($this->getCancelUrl());
}
-
+ /**
+ * Get a temp storage object.
+ *
+ * @return mixed
+ * The tempstore object.
+ */
protected function getTempstore() {
return $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
}
-
+ /**
+ * Set the temp storage.
+ *
+ * @param array $cached_values
+ * Cached values to use in the tempstore.
+ *
+ * @throws \Drupal\Core\TempStore\TempStoreException
+ */
protected function setTempstore($cached_values) {
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageConditions.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageConditions.php
index b1dcdfeca..7618346a9 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageConditions.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageConditions.php
@@ -12,7 +12,9 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
-
+/**
+ *
+ */
abstract class ManageConditions extends FormBase {
/**
@@ -32,7 +34,9 @@ abstract class ManageConditions extends FormBase {
*/
protected $machine_name;
-
+ /**
+ *
+ */
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.condition'),
@@ -40,7 +44,9 @@ abstract class ManageConditions extends FormBase {
);
}
-
+ /**
+ *
+ */
public function __construct(PluginManagerInterface $manager, FormBuilderInterface $form_builder) {
$this->manager = $manager;
$this->formBuilder = $form_builder;
@@ -98,17 +104,19 @@ abstract class ManageConditions extends FormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
- list(, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions'));
+ [, $route_parameters] = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions'));
$form_state->setRedirect($this->getAddRoute($cached_values), $route_parameters);
}
-
+ /**
+ *
+ */
public function add(array &$form, FormStateInterface $form_state) {
$condition = $form_state->getValue('conditions');
$content = $this->formBuilder->getForm($this->getConditionClass(), $condition, $this->getTempstoreId(), $this->machine_name);
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$cached_values = $form_state->getTemporaryValue('wizard');
- list(, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions'));
+ [, $route_parameters] = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions'));
$route_name = $this->getAddRoute($cached_values);
$route_options = [
'query' => [
@@ -130,9 +138,9 @@ abstract class ManageConditions extends FormBase {
public function renderRows($cached_values) {
$configured_conditions = [];
foreach ($this->getConditions($cached_values) as $row => $condition) {
- /** @var $instance \Drupal\Core\Condition\ConditionInterface */
+ /** @var \Drupal\Core\Condition\ConditionInterface $instance */
$instance = $this->manager->createInstance($condition['id'], $condition);
- list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $row);
+ [$route_name, $route_parameters] = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $row);
$build = [
'#type' => 'operations',
'#links' => $this->getOperations($route_name, $route_parameters),
@@ -148,7 +156,9 @@ abstract class ManageConditions extends FormBase {
return $configured_conditions;
}
-
+ /**
+ *
+ */
protected function getOperations($route_name_base, array $route_parameters = []) {
$operations['edit'] = [
'title' => $this->t('Edit'),
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageContext.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageContext.php
index d7224bf0d..da44b2cd9 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageContext.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageContext.php
@@ -13,7 +13,9 @@ use Drupal\Core\Url;
use Drupal\ctools\TypedDataResolver;
use Symfony\Component\DependencyInjection\ContainerInterface;
-
+/**
+ * Manage Context Form.
+ */
abstract class ManageContext extends FormBase {
/**
@@ -159,23 +161,33 @@ abstract class ManageContext extends FormBase {
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getTriggeringElement()['#name'] == 'add') {
$cached_values = $form_state->getTemporaryValue('wizard');
- list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('context'));
+ [, $route_parameters] = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('context'));
$form_state->setRedirect($this->getContextAddRoute($cached_values), $route_parameters);
}
if ($form_state->getTriggeringElement()['#name'] == 'add_relationship') {
$cached_values = $form_state->getTemporaryValue('wizard');
- list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('relationships'));
+ [, $route_parameters] = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('relationships'));
$form_state->setRedirect($this->getRelationshipAddRoute($cached_values), $route_parameters);
}
}
-
+ /**
+ * Add a context.
+ *
+ * @param array $form
+ * The Drupal Form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * Form ajax repsonse.
+ */
public function addContext(array &$form, FormStateInterface $form_state) {
$context = $form_state->getValue('context');
- $content = $this->formBuilder->getForm($this->getContextClass(), $context, $this->getTempstoreId(), $this->machine_name);
- $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$cached_values = $form_state->getTemporaryValue('wizard');
- list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context);
+ $content = $this->formBuilder->getForm($this->getContextClass($cached_values), $context, $this->getTempstoreId(), $this->machine_name);
+ $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
+ [, $route_parameters] = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context);
$route_name = $this->getContextAddRoute($cached_values);
$route_options = [
'query' => [
@@ -189,13 +201,23 @@ abstract class ManageContext extends FormBase {
return $response;
}
-
+ /**
+ * Add relationship form.
+ *
+ * @param array $form
+ * The Drupal Form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * Form ajax repsonse.
+ */
public function addRelationship(array &$form, FormStateInterface $form_state) {
$relationship = $form_state->getValue('relationships');
- $content = $this->formBuilder->getForm($this->getRelationshipClass(), $relationship, $this->getTempstoreId(), $this->machine_name);
- $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$cached_values = $form_state->getTemporaryValue('wizard');
- list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $relationship);
+ $content = $this->formBuilder->getForm($this->getRelationshipClass($cached_values), $relationship, $this->getTempstoreId(), $this->machine_name);
+ $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
+ [, $route_parameters] = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $relationship);
$route_name = $this->getRelationshipAddRoute($cached_values);
$route_options = [
'query' => [
@@ -209,22 +231,34 @@ abstract class ManageContext extends FormBase {
return $response;
}
-
- protected function getAvailableRelationships($cached_values) {
+ /**
+ * Retrieve the available relationships.
+ *
+ * @param array $cached_values
+ * The cached context values.
+ *
+ * @return mixed
+ * The available relationships.
+ */
+ protected function getAvailableRelationships(array $cached_values) {
/** @var \Drupal\ctools\TypedDataResolver $resolver */
$resolver = $this->typedDataResolver;
return $resolver->getTokensForContexts($this->getContexts($cached_values));
}
/**
- * @param $cached_values
+ * Render the Rows.
+ *
+ * @param array $cached_values
+ * The cached context values.
*
* @return array
+ * The rendered rows.
*/
- protected function renderRows($cached_values) {
+ protected function renderRows(array $cached_values) {
$contexts = [];
foreach ($this->getContexts($cached_values) as $row => $context) {
- list($route_name, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $row);
+ [$route_name, $route_parameters] = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $row);
$build = [
'#type' => 'operations',
'#links' => $this->getOperations($cached_values, $row, $route_name, $route_parameters),
@@ -242,14 +276,21 @@ abstract class ManageContext extends FormBase {
}
/**
+ * Get available Operations.
+ *
* @param array $cached_values
+ * The cached context values.
* @param string $row
+ * The row operations are being fetched from.
* @param string $route_name_base
+ * The route name.
* @param array $route_parameters
+ * Parameters for the route.
*
* @return mixed
+ * The operations.
*/
- protected function getOperations($cached_values, $row, $route_name_base, array $route_parameters = []) {
+ protected function getOperations(array $cached_values, string $row, string $route_name_base, array $route_parameters = []) {
$operations = [];
if ($this->isEditableContext($cached_values, $row)) {
$operations['edit'] = [
@@ -286,9 +327,13 @@ abstract class ManageContext extends FormBase {
* The ContextConfigure class is designed to be subclassed with custom
* route information to control the modal/redirect needs of your use case.
*
+ * @param mixed $cached_values
+ * Cached Relationship Class values.
+ *
* @return string
+ * The context class.
*/
- abstract protected function getContextClass($cached_values);
+ abstract protected function getContextClass(mixed $cached_values);
/**
* Return a subclass of '\Drupal\ctools\Form\RelationshipConfigure'.
@@ -296,28 +341,41 @@ abstract class ManageContext extends FormBase {
* The RelationshipConfigure class is designed to be subclassed with custom
* route information to control the modal/redirect needs of your use case.
*
+ * @param mixed $cached_values
+ * Cached Relationship Class values.
+ *
* @return string
+ * The relationship Class.
*/
- abstract protected function getRelationshipClass($cached_values);
+ abstract protected function getRelationshipClass(mixed $cached_values);
/**
* The route to which context 'add' actions should submit.
*
+ * @param mixed $cached_values
+ * Cached Route info values.
+ *
* @return string
+ * The context add route.
*/
- abstract protected function getContextAddRoute($cached_values);
+ abstract protected function getContextAddRoute(mixed $cached_values);
/**
* The route to which relationship 'add' actions should submit.
*
+ * @param mixed $cached_values
+ * Cached Route info values.
+ *
* @return string
+ * Relationship Add Route.
*/
- abstract protected function getRelationshipAddRoute($cached_values);
+ abstract protected function getRelationshipAddRoute(mixed $cached_values);
/**
* Provide the tempstore id for your specified use case.
*
* @return string
+ * The tempstore ID.
*/
abstract protected function getTempstoreId();
@@ -325,34 +383,51 @@ abstract class ManageContext extends FormBase {
* Returns the contexts already available in the wizard.
*
* @param mixed $cached_values
+ * Cached Contexts.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
+ * The contexts.
*/
abstract protected function getContexts($cached_values);
/**
+ * Gets the Context Operations Route info.
+ *
* @param mixed $cached_values
+ * Cached Route info values.
* @param string $machine_name
+ * Relationship Machine Name.
* @param string $row
+ * Context Row.
*
* @return array
+ * The context operations.
*/
abstract protected function getContextOperationsRouteInfo($cached_values, $machine_name, $row);
/**
+ * Gets the Route info for Relationship Operations.
+ *
* @param mixed $cached_values
+ * Cached Route info values.
* @param string $machine_name
+ * Relationship Machine Name.
* @param string $row
+ * Context Row.
*
* @return array
+ * The operations allowed.
*/
abstract protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row);
/**
* @param mixed $cached_values
+ * Cached context values.
* @param string $row
+ * Context Row.
*
* @return bool
+ * If context is editable.
*/
abstract protected function isEditableContext($cached_values, $row);
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageResolverRelationships.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageResolverRelationships.php
index 713b74929..f7fb96a01 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageResolverRelationships.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ManageResolverRelationships.php
@@ -119,7 +119,7 @@ abstract class ManageResolverRelationships extends FormBase {
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getTriggeringElement()['#name'] == 'add') {
$cached_values = $form_state->getTemporaryValue('wizard');
- list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('relationships'));
+ [, $route_parameters] = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('relationships'));
$form_state->setRedirect($this->getAddRoute($cached_values), $route_parameters);
}
}
@@ -137,7 +137,7 @@ abstract class ManageResolverRelationships extends FormBase {
$content = $this->formBuilder->getForm($this->getContextClass(), $relationship, $this->getTempstoreId(), $this->machine_name);
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$cached_values = $form_state->getTemporaryValue('wizard');
- list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $relationship);
+ [, $route_parameters] = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $relationship);
$route_name = $this->getAddRoute($cached_values);
$route_options = [
'query' => [
@@ -175,7 +175,7 @@ abstract class ManageResolverRelationships extends FormBase {
protected function renderRows($cached_values) {
$contexts = [];
foreach ($this->getContexts($cached_values) as $row => $context) {
- list($route_name, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $row);
+ [$route_name, $route_parameters] = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $row);
$build = [
'#type' => 'operations',
'#links' => $this->getOperations($cached_values, $row, $route_name, $route_parameters),
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/RelationshipConfigure.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/RelationshipConfigure.php
index fcda78459..800e20b8b 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/RelationshipConfigure.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/RelationshipConfigure.php
@@ -12,25 +12,35 @@ use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
-
+/**
+ * Configure Relationship Form.
+ */
abstract class RelationshipConfigure extends FormBase {
/**
+ * Tempstore Factory.
+ *
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
*/
protected $tempstore;
/**
+ * Typed Data Resolver Service.
+ *
* @var \Drupal\ctools\TypedDataResolver
*/
protected $resolver;
/**
+ * Tempstore ID.
+ *
* @var string
*/
protected $tempstore_id;
/**
+ * Relationship Machine Name.
+ *
* @var string
*/
protected $machine_name;
@@ -42,7 +52,14 @@ abstract class RelationshipConfigure extends FormBase {
return new static($container->get('tempstore.shared'), $container->get('ctools.typed_data.resolver'));
}
-
+ /**
+ * Configure Relationship Form constructor.
+ *
+ * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * Tempstore Service.
+ * @param \Drupal\ctools\TypedDataResolver $resolver
+ * Typed Data Resolver Service.
+ */
public function __construct(SharedTempStoreFactory $tempstore, TypedDataResolver $resolver) {
$this->tempstore = $tempstore;
$this->resolver = $resolver;
@@ -100,19 +117,24 @@ abstract class RelationshipConfigure extends FormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
- list($route_name, $route_options) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_options] = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_options);
}
/**
+ * Ajax Save Method.
+ *
* @param array $form
+ * Drupal Form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form State.
*
* @return \Drupal\Core\Ajax\AjaxResponse
+ * The ajax data in the response.
*/
public function ajaxSave(array &$form, FormStateInterface $form_state) {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$response = new AjaxResponse();
$url = Url::fromRoute($route_name, $route_parameters);
$response->addCommand(new RedirectCommand($url->toString()));
@@ -124,33 +146,36 @@ abstract class RelationshipConfigure extends FormBase {
* Document the route name and parameters for redirect after submission.
*
* @param array $cached_values
+ * Cached Values get route info from.
*
* @return array In the format of
* In the format of
- * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name']];
+ * return ['route.name',
+ * ['machine_name' => $this->machine_name, 'step' => 'step_name']];
*/
- abstract protected function getParentRouteInfo($cached_values);
+ abstract protected function getParentRouteInfo(array $cached_values);
/**
* Custom logic for setting the conditions array in cached_values.
*
- * @param $cached_values
+ * @param array $cached_values
*
- * @param $contexts
+ * @param mixed $contexts
* The conditions to set within the cached values.
*
* @return mixed
* Return the $cached_values
*/
- abstract protected function setContexts($cached_values, $contexts);
+ abstract protected function setContexts(array $cached_values, mixed $contexts);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
- * @param $cached_values
+ * @param array $cached_values
+ * Cached Values contexts are fetched from.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
- abstract protected function getContexts($cached_values);
+ abstract protected function getContexts(array $cached_values);
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/RequiredContext.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/RequiredContext.php
index 336562b68..4bf723941 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/RequiredContext.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/RequiredContext.php
@@ -11,7 +11,9 @@ use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Form\FormBuilderInterface;
-
+/**
+ * Required Context Form.
+ */
abstract class RequiredContext extends FormBase {
/**
@@ -41,7 +43,14 @@ abstract class RequiredContext extends FormBase {
);
}
-
+ /**
+ * Required Context Form constructor.
+ *
+ * @param \Drupal\Component\Plugin\PluginManagerInterface $typed_data_manager
+ * The Typed Data Manager.
+ * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+ * The Form Builder.
+ */
public function __construct(PluginManagerInterface $typed_data_manager, FormBuilderInterface $form_builder) {
$this->typedDataManager = $typed_data_manager;
$this->formBuilder = $form_builder;
@@ -98,7 +107,7 @@ abstract class RequiredContext extends FormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
- list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('contexts'));
+ [$route_name, $route_parameters] = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('contexts'));
$form_state->setRedirect($route_name . '.edit', $route_parameters);
}
@@ -120,15 +129,19 @@ abstract class RequiredContext extends FormBase {
}
/**
+ * Render The contexts in the form.
+ *
* @param $cached_values
+ * Cached context values.
*
* @return array
+ * The rendered contexts.
*/
public function renderContexts($cached_values) {
$configured_contexts = [];
foreach ($this->getContexts($cached_values) as $row => $context) {
- list($plugin_id, $label, $machine_name, $description) = array_values($context);
- list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $row);
+ [$plugin_id, $label, $machine_name, $description] = array_values($context);
+ [$route_name, $route_parameters] = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $row);
$build = [
'#type' => 'operations',
'#links' => $this->getOperations($route_name, $route_parameters),
@@ -144,7 +157,17 @@ abstract class RequiredContext extends FormBase {
return $configured_contexts;
}
-
+ /**
+ * Retrieve Form Operations
+ *
+ * @param $route_name_base
+ * The base route name.
+ * @param array $route_parameters
+ * Route Parameters.
+ *
+ * @return array
+ * The available operations.
+ */
protected function getOperations($route_name_base, array $route_parameters = []) {
$operations['edit'] = [
'title' => $this->t('Edit'),
@@ -184,6 +207,7 @@ abstract class RequiredContext extends FormBase {
* information to control the modal/redirect needs of your use case.
*
* @return string
+ * The Context Class.
*/
abstract protected function getContextClass();
@@ -191,6 +215,7 @@ abstract class RequiredContext extends FormBase {
* Provide the tempstore id for your specified use case.
*
* @return string
+ * The Tempstore ID.
*/
abstract protected function getTempstoreId();
@@ -204,24 +229,28 @@ abstract class RequiredContext extends FormBase {
* this approach quite seamlessly.
*
* @param mixed $cached_values
- *
+ * The Cached Values.
* @param string $machine_name
- *
+ * The form machine name.
* @param string $row
+ * The form row to operate on.
*
* @return array
* In the format of
- * return ['route.base.name', ['machine_name' => $machine_name, 'context' => $row]];
+ * return ['route.base.name',
+ * ['machine_name' => $machine_name, 'context' => $row]];
*/
- abstract protected function getOperationsRouteInfo($cached_values, $machine_name, $row);
+ abstract protected function getOperationsRouteInfo(mixed $cached_values, string $machine_name, string $row);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
- * @param $cached_values
+ * @param array $cached_values
+ * The Cached Values.
*
* @return array
+ * The Contexts.
*/
- abstract protected function getContexts($cached_values);
+ abstract protected function getContexts(array $cached_values);
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/RequiredContextDelete.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/RequiredContextDelete.php
index 013d99c44..ce8011717 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/RequiredContextDelete.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/RequiredContextDelete.php
@@ -15,21 +15,31 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class RequiredContextDelete extends ConfirmFormBase {
/**
+ * Creates a shared temporary storage for a collection.
+ *
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
*/
protected $tempstore;
/**
+ * The temporary id storage.
+ *
* @var string
*/
+ // @codingStandardsIgnoreLine
protected $tempstore_id;
/**
+ * The machine name.
+ *
* @var string
*/
+ // @codingStandardsIgnoreLine
protected $machine_name;
/**
+ * The id.
+ *
* @var int
*/
protected $id;
@@ -42,7 +52,10 @@ abstract class RequiredContextDelete extends ConfirmFormBase {
}
/**
+ * The constructor.
+ *
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * The shared temporary storage.
*/
public function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
@@ -88,7 +101,7 @@ abstract class RequiredContextDelete extends ConfirmFormBase {
unset($contexts[$this->id]);
$cached_values = $this->setContexts($cached_values, $contexts);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
@@ -140,7 +153,7 @@ abstract class RequiredContextDelete extends ConfirmFormBase {
*/
public function getCancelUrl() {
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
return new Url($route_name, $route_parameters);
}
@@ -161,29 +174,35 @@ abstract class RequiredContextDelete extends ConfirmFormBase {
/**
* Document the route name and parameters for redirect after submission.
*
- * @param $cached_values
+ * @param mixed $cached_values
+ * The cached values.
*
* @return array
* In the format of
- * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]];
+ * return [
+ * 'route.name',
+ * ['machine_name' => $this->machine_name,'step' => 'step_name],
+ * ];
*/
abstract protected function getParentRouteInfo($cached_values);
/**
* Custom logic for retrieving the contexts array from cached_values.
*
- * @param $cached_values
+ * @param mixed $cached_values
+ * The cache values.
*
* @return array
+ * Return an array.
*/
abstract protected function getContexts($cached_values);
/**
* Custom logic for setting the contexts array in cached_values.
*
- * @param $cached_values
- *
- * @param $contexts
+ * @param mixed $cached_values
+ * The cache values.
+ * @param mixed $contexts
* The contexts to set within the cached values.
*
* @return mixed
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ResolverRelationshipConfigure.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ResolverRelationshipConfigure.php
index 594a899bf..8e953e521 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ResolverRelationshipConfigure.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ResolverRelationshipConfigure.php
@@ -11,7 +11,9 @@ use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
-
+/**
+ * Configure Relationships Resolver form.
+ */
abstract class ResolverRelationshipConfigure extends FormBase {
/**
@@ -36,7 +38,12 @@ abstract class ResolverRelationshipConfigure extends FormBase {
return new static($container->get('tempstore.shared'));
}
-
+ /**
+ * Configure Relationships Resolver form.
+ *
+ * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * Tempstore Factory.
+ */
public function __construct(SharedTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
@@ -105,7 +112,14 @@ abstract class ResolverRelationshipConfigure extends FormBase {
return $form;
}
-
+ /**
+ * Configuration Form Validator.
+ *
+ * @param array $form
+ * The Drupal Form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The Form State.
+ */
public function validateForm(array &$form, FormStateInterface $form_state) {
$machine_name = $form_state->getValue('machine_name');
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
@@ -136,15 +150,25 @@ abstract class ResolverRelationshipConfigure extends FormBase {
}
$cached_values = $this->setContexts($cached_values, $contexts);
$this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$form_state->setRedirect($route_name, $route_parameters);
}
-
+ /**
+ * Ajax Save Method.
+ *
+ * @param array $form
+ * Drupal Form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form State.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * The ajax data in the response.
+ */
public function ajaxSave(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name);
- list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values);
+ [$route_name, $route_parameters] = $this->getParentRouteInfo($cached_values);
$url = Url::fromRoute($route_name, $route_parameters);
$response->addCommand(new RedirectCommand($url->toString()));
$response->addCommand(new CloseModalDialogCommand());
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ResolverRelationshipDelete.php b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ResolverRelationshipDelete.php
index 36aec3b06..de91e0497 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Form/ResolverRelationshipDelete.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Form/ResolverRelationshipDelete.php
@@ -9,30 +9,42 @@ use Drupal\ctools\TypedDataResolver;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
-
+/**
+ * Resolver Relatinoship Delete Form.
+ */
abstract class ResolverRelationshipDelete extends ConfirmFormBase {
/**
+ * Tempstore Factory.
+ *
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
*/
protected $tempstore;
/**
+ * The resolver service.
+ *
* @var \Drupal\ctools\TypedDataResolver
*/
protected $resolver;
/**
+ * Tempstore ID.
+ *
* @var string
*/
protected $tempstore_id;
/**
+ * Machine name of the relationship.
+ *
* @var string
*/
protected $machine_name;
/**
+ * Resolver ID.
+ *
* @var string
*/
protected $id;
@@ -45,6 +57,8 @@ abstract class ResolverRelationshipDelete extends ConfirmFormBase {
}
/**
+ * Resolver Relationship Delete Form Constructor.
+ *
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
* The shared tempstore.
* @param \Drupal\ctools\TypedDataResolver $resolver
@@ -120,6 +134,7 @@ abstract class ResolverRelationshipDelete extends ConfirmFormBase {
* The current wizard cached values.
*
* @return array
+ * Actions to call.
*/
protected function actions(array $form, FormStateInterface $form_state, $cached_values) {
return [
@@ -144,7 +159,8 @@ abstract class ResolverRelationshipDelete extends ConfirmFormBase {
* The cached values.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
+ * Contexts from the cached values.
*/
- abstract public function getContexts($cached_values);
+ abstract public function getContexts(array $cached_values);
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/ParamConverter/TempstoreConverter.php b/frontend/drupal9/web/modules/contrib/ctools/src/ParamConverter/TempstoreConverter.php
index 67455f115..05205442f 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/ParamConverter/TempstoreConverter.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/ParamConverter/TempstoreConverter.php
@@ -110,7 +110,7 @@ class TempstoreConverter implements ParamConverterInterface {
$tempstore_id = !empty($definition['tempstore_id']) ? $definition['tempstore_id'] : $defaults['tempstore_id'];
$machine_name = $this->convertVariable($value, $defaults);
- list(, $parts) = explode(':', $definition['type'], 2);
+ [, $parts] = explode(':', $definition['type'], 2);
$parts = explode(':', $parts);
foreach ($parts as $key => $part) {
$parts[$key] = $this->convertVariable($part, $defaults);
@@ -144,11 +144,11 @@ class TempstoreConverter implements ParamConverterInterface {
* @return mixed
* The value of a variable in defaults.
*/
- protected function convertVariable($name, $defaults) {
+ protected function convertVariable($name, array $defaults) {
if (is_string($name) && strpos($name, '{') === 0) {
$length = strlen($name);
$name = substr($name, 1, $length - 2);
- return isset($defaults[$name]) ? $defaults[$name] : NULL;
+ return $defaults[$name] ?? NULL;
}
return $name;
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Block/EntityView.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Block/EntityView.php
index 62411afd4..6af56e2d3 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Block/EntityView.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Block/EntityView.php
@@ -109,7 +109,7 @@ class EntityView extends BlockBase implements ContextAwarePluginInterface, Conta
return $return_as_object ? $parent_access : $parent_access->isAllowed();
}
- /** @var $entity \Drupal\Core\Entity\EntityInterface */
+ /** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $this->getContextValue('entity');
return $entity->access('view', $account, $return_as_object);
}
@@ -118,7 +118,7 @@ class EntityView extends BlockBase implements ContextAwarePluginInterface, Conta
* {@inheritdoc}
*/
public function build() {
- /** @var $entity \Drupal\Core\Entity\EntityInterface */
+ /** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $this->getContextValue('entity');
$view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId());
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/BlockPluginCollection.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/BlockPluginCollection.php
index 56d51b7cf..1eadb3899 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/BlockPluginCollection.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/BlockPluginCollection.php
@@ -30,16 +30,16 @@ class BlockPluginCollection extends DefaultLazyPluginCollection {
$region_assignments = [];
foreach ($this as $block_id => $block) {
$configuration = $block->getConfiguration();
- $region = isset($configuration['region']) ? $configuration['region'] : NULL;
+ $region = $configuration['region'] ?? NULL;
$region_assignments[$region][$block_id] = $block;
}
foreach ($region_assignments as $region => $region_assignment) {
// @todo Determine the reason this needs error suppression.
@uasort($region_assignment, function (BlockPluginInterface $a, BlockPluginInterface $b) {
$a_config = $a->getConfiguration();
- $a_weight = isset($a_config['weight']) ? $a_config['weight'] : 0;
+ $a_weight = $a_config['weight'] ?? 0;
$b_config = $b->getConfiguration();
- $b_weight = isset($b_config['weight']) ? $b_config['weight'] : 0;
+ $b_weight = $b_config['weight'] ?? 0;
if ($a_weight == $b_weight) {
return strcmp($a->label(), $b->label());
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/BlockVariantTrait.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/BlockVariantTrait.php
index f368f2c75..584b9b011 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/BlockVariantTrait.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/BlockVariantTrait.php
@@ -92,7 +92,7 @@ trait BlockVariantTrait {
*/
public function getRegionAssignment($block_id) {
$configuration = $this->getBlock($block_id)->getConfiguration();
- return isset($configuration['region']) ? $configuration['region'] : NULL;
+ return $configuration['region'] ?? NULL;
}
/**
@@ -111,7 +111,7 @@ trait BlockVariantTrait {
*/
public function getRegionName($region) {
$regions = $this->getRegionNames();
- return isset($regions[$region]) ? $regions[$region] : '';
+ return $regions[$region] ?? '';
}
/**
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Condition/NodeType.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Condition/NodeType.php
index 1d28606ba..26006e422 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Condition/NodeType.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Condition/NodeType.php
@@ -5,7 +5,9 @@ namespace Drupal\ctools\Plugin\Condition;
use Drupal\node\Plugin\Condition\NodeType as CoreNodeType;
use Drupal\ctools\ConstraintConditionInterface;
-
+/**
+ *
+ */
class NodeType extends CoreNodeType implements ConstraintConditionInterface {
/**
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/EntityBundle.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/EntityBundle.php
index 94e4e96fd..f33fe7167 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/EntityBundle.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/EntityBundle.php
@@ -13,6 +13,13 @@ class EntityBundle extends EntityDeriverBase {
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
+
+ // Do not define any derivatives on Drupal 9.3+, instead, replace the core
+ // class in ctools_condition_info_alter().
+ if (\version_compare(\Drupal::VERSION, '9.3', '>')) {
+ return [];
+ }
+
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->hasKey('bundle')) {
$this->derivatives[$entity_type_id] = $base_plugin_definition;
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataEntityRelationshipDeriver.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataEntityRelationshipDeriver.php
index c0aae180e..416286201 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataEntityRelationshipDeriver.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataEntityRelationshipDeriver.php
@@ -4,7 +4,9 @@ namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Core\TypedData\DataDefinitionInterface;
-
+/**
+ *
+ */
class TypedDataEntityRelationshipDeriver extends TypedDataRelationshipDeriver {
/**
@@ -18,6 +20,12 @@ class TypedDataEntityRelationshipDeriver extends TypedDataRelationshipDeriver {
protected function generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, DataDefinitionInterface $base_definition, $property_name, DataDefinitionInterface $property_definition) {
if (method_exists($property_definition, 'getType') && $property_definition->getType() == 'entity_reference') {
parent::generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, $base_definition, $property_name, $property_definition);
+
+ // Provide the entity type.
+ $derivative_id = $data_type_id . ':' . $property_name;
+ if (isset($this->derivatives[$derivative_id])) {
+ $this->derivatives[$derivative_id]['target_entity_type'] = $property_definition->getFieldStorageDefinition()->getPropertyDefinition('entity')->getConstraint('EntityType');
+ }
}
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataLanguageRelationshipDeriver.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataLanguageRelationshipDeriver.php
index 08170be56..595fffae7 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataLanguageRelationshipDeriver.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataLanguageRelationshipDeriver.php
@@ -4,7 +4,9 @@ namespace Drupal\ctools\Plugin\Deriver;
use Drupal\Core\TypedData\DataDefinitionInterface;
-
+/**
+ *
+ */
class TypedDataLanguageRelationshipDeriver extends TypedDataRelationshipDeriver {
/**
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataPropertyDeriverBase.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataPropertyDeriverBase.php
index e4125651f..975f976be 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataPropertyDeriverBase.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataPropertyDeriverBase.php
@@ -15,7 +15,9 @@ use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\field\Entity\FieldConfig;
use Symfony\Component\DependencyInjection\ContainerInterface;
-
+/**
+ *
+ */
abstract class TypedDataPropertyDeriverBase extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataRelationshipDeriver.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataRelationshipDeriver.php
index 5c04855e8..7778b3219 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataRelationshipDeriver.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataRelationshipDeriver.php
@@ -8,7 +8,9 @@ use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\field\FieldConfigInterface;
-
+/**
+ *
+ */
class TypedDataRelationshipDeriver extends TypedDataPropertyDeriverBase implements ContainerDeriverInterface {
/**
@@ -32,24 +34,28 @@ class TypedDataRelationshipDeriver extends TypedDataPropertyDeriverBase implemen
'@property' => $property_definition->getLabel(),
'@base' => $data_type_definition['label'],
]);
- $derivative['data_type'] = $property_definition->getFieldStorageDefinition()->getPropertyDefinition($property_definition->getFieldStorageDefinition()->getMainPropertyName())->getDataType();
- $derivative['property_name'] = $property_name;
- if (strpos($base_data_type, 'entity:') === 0) {
- $context_definition = new EntityContextDefinition($base_data_type, $this->typedDataManager->createDataDefinition($base_data_type));
- }
- else {
- $context_definition = new ContextDefinition($base_data_type, $this->typedDataManager->createDataDefinition($base_data_type));
- }
- // Add the constraints of the base definition to the context definition.
- if ($base_definition->getConstraint('Bundle')) {
- $context_definition->addConstraint('Bundle', $base_definition->getConstraint('Bundle'));
- }
- $derivative['context_definitions'] = [
- 'base' => $context_definition,
- ];
- $derivative['property_name'] = $property_name;
- $this->derivatives[$base_data_type . ':' . $property_name] = $derivative;
+ $main_property = $property_definition->getFieldStorageDefinition()->getPropertyDefinition($property_definition->getFieldStorageDefinition()->getMainPropertyName());
+ if ($main_property) {
+ $derivative['data_type'] = $main_property->getDataType();
+ $derivative['property_name'] = $property_name;
+ if (strpos($base_data_type, 'entity:') === 0) {
+ $context_definition = new EntityContextDefinition($base_data_type, $this->typedDataManager->createDataDefinition($base_data_type));
+ }
+ else {
+ $context_definition = new ContextDefinition($base_data_type, $this->typedDataManager->createDataDefinition($base_data_type));
+ }
+ // Add the constraints of the base definition to the context definition.
+ if ($base_definition->getConstraint('Bundle')) {
+ $context_definition->addConstraint('Bundle', $base_definition->getConstraint('Bundle'));
+ }
+ $derivative['context_definitions'] = [
+ 'base' => $context_definition,
+ ];
+ $derivative['property_name'] = $property_name;
+
+ $this->derivatives[$base_data_type . ':' . $property_name] = $derivative;
+ }
}
// Individual fields can be on multiple bundles.
elseif ($property_definition instanceof FieldConfigInterface) {
@@ -58,7 +64,7 @@ class TypedDataRelationshipDeriver extends TypedDataPropertyDeriverBase implemen
// Update label.
/** @var \Drupal\Core\StringTranslation\TranslatableMarkup $label */
$label = $derivative['label'];
- list(,, $argument_name) = explode(':', $data_type_id);
+ [,, $argument_name] = explode(':', $data_type_id);
$arguments = $label->getArguments();
$arguments['@' . $argument_name] = $data_type_definition['label'];
$string_args = $arguments;
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataEntityRelationship.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataEntityRelationship.php
index e45892baa..55cd1d4fa 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataEntityRelationship.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataEntityRelationship.php
@@ -19,15 +19,17 @@ class TypedDataEntityRelationship extends TypedDataRelationship {
public function getRelationship() {
$plugin_definition = $this->getPluginDefinition();
- $entity_type = $this->getData($this->getContext('base'))->getDataDefinition()->getSetting('target_type');
- $context_definition = new EntityContextDefinition("entity:$entity_type", $plugin_definition['label']);
+ $context_definition = new EntityContextDefinition("entity:{$plugin_definition['target_entity_type']}", $plugin_definition['label']);
$context_value = NULL;
// If the 'base' context has a value, then get the property value to put on
// the context (otherwise, mapping hasn't occurred yet and we just want to
// return the context with the right definition and no value).
if ($this->getContext('base')->hasContextValue()) {
- $context_value = $this->getData($this->getContext('base'))->entity;
+ $data = $this->getData($this->getContext('base'));
+ if ($data) {
+ $context_value = $data->entity;
+ }
}
$context_definition->setDefaultValue($context_value);
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataRelationship.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataRelationship.php
index a117afc7d..3cc9ed0a4 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataRelationship.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataRelationship.php
@@ -47,12 +47,16 @@ class TypedDataRelationship extends RelationshipBase {
return new Context($context_definition, $context_value);
}
-
+ /**
+ *
+ */
public function getName() {
return $this->getPluginDefinition()['property_name'];
}
-
+ /**
+ *
+ */
protected function getData(ContextInterface $context) {
/** @var \Drupal\Core\TypedData\ComplexDataInterface $base */
$base = $context->getContextValue();
@@ -68,12 +72,16 @@ class TypedDataRelationship extends RelationshipBase {
return $data;
}
-
+ /**
+ *
+ */
protected function getMainPropertyName(FieldItemInterface $data) {
return $data->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
}
-
+ /**
+ *
+ */
public function getRelationshipValue() {
$property = $this->getMainPropertyName();
/** @var \Drupal\Core\TypedData\ComplexDataInterface $data */
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/VariantPluginCollection.php b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/VariantPluginCollection.php
index 77914cd1d..a56693ffa 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/VariantPluginCollection.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Plugin/VariantPluginCollection.php
@@ -23,7 +23,7 @@ class VariantPluginCollection extends DefaultLazyPluginCollection {
*/
public function sort() {
// @todo Determine the reason this needs error suppression.
- @uasort($this->instanceIDs, [$this, 'sortHelper']);
+ @uasort($this->instanceIds, [$this, 'sortHelper']);
return $this;
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Testing/EntityCreationTrait.php b/frontend/drupal9/web/modules/contrib/ctools/src/Testing/EntityCreationTrait.php
index 2f2927bdf..eeb1bed15 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Testing/EntityCreationTrait.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Testing/EntityCreationTrait.php
@@ -4,7 +4,9 @@ namespace Drupal\ctools\Testing;
use Drupal\Component\Render\FormattableMarkup;
-
+/**
+ * Trait used for common entity creation methods.
+ */
trait EntityCreationTrait {
/**
@@ -33,9 +35,11 @@ trait EntityCreationTrait {
\Drupal::service('router.builder')->rebuild();
if ($this instanceof \PHPUnit_Framework_TestCase) {
- $this->assertSame(SAVED_NEW, $status, (new FormattableMarkup('Created entity %id of type %type.', ['%id' => $entity->id(), '%type' => $entity_type]))->__toString());
+ // phpcs:ignore
+ $this->assertSame(SAVED_NEW, $status, (new FormattableMarkup('Created entity %id of type %type.', ['%id' => $entity->id(), '%type' => $entity_type]))->__toString()); //psp
}
else {
+ // phpcs:ignore
$this->assertEquals(SAVED_NEW, $status, (new FormattableMarkup('Created entity %id of type %type.', ['%id' => $entity->id(), '%type' => $entity_type]))->__toString());
}
@@ -43,7 +47,10 @@ trait EntityCreationTrait {
}
/**
- * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * Retrieves the Entity Type Manager for the Entity.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManager|\Drupal\Core\Entity\EntityTypeManagerInterface|object|null
+ * @throws \Exception
*/
protected function getEntityTypeManager() {
if (!isset($this->entityTypeManager)) {
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/TypedDataResolver.php b/frontend/drupal9/web/modules/contrib/ctools/src/TypedDataResolver.php
index d8fb9e6e0..f258202ae 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/TypedDataResolver.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/TypedDataResolver.php
@@ -14,7 +14,9 @@ use Drupal\Core\TypedData\ListDataDefinitionInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
-
+/**
+ * Typed Data Resolver Service.
+ */
class TypedDataResolver {
/**
@@ -32,6 +34,8 @@ class TypedDataResolver {
protected $translation;
/**
+ * Typed Data Resolver Service constructor.
+ *
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $manager
* The typed data manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
@@ -59,7 +63,7 @@ class TypedDataResolver {
*
* @throws \Exception
*/
- public function getContextFromProperty($property_path, ContextInterface $context) {
+ public function getContextFromProperty(string $property_path, ContextInterface $context) {
$value = NULL;
$data_definition = NULL;
if ($context->hasContextValue()) {
@@ -147,7 +151,7 @@ class TypedDataResolver {
* TypedDataResolver which will convert it to an appropriate ContextInterface
* object.
*
- * @param $token
+ * @param string $token
* A ":" delimited set of tokens representing
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* The array of available contexts.
@@ -157,13 +161,13 @@ class TypedDataResolver {
*
* @throws \Drupal\ctools\ContextNotFoundException
*/
- public function convertTokenToContext($token, $contexts) {
+ public function convertTokenToContext(string $token, array $contexts) {
// If the requested token is already a context, just return it.
if (isset($contexts[$token])) {
return $contexts[$token];
}
else {
- list($base, $property_path) = explode(':', $token, 2);
+ [$base, $property_path] = explode(':', $token, 2);
// A base must always be set. This method recursively calls itself
// setting bases for this reason.
if (!empty($contexts[$base])) {
@@ -185,7 +189,7 @@ class TypedDataResolver {
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The administrative label of $token.
*/
- public function getLabelByToken($token, $contexts) {
+ public function getLabelByToken(string $token, array $contexts) {
// @todo Optimize this by allowing to limit the desired token?
$tokens = $this->getTokensForContexts($contexts);
if (isset($tokens[$token])) {
@@ -202,7 +206,7 @@ class TypedDataResolver {
* @return array
* An array of token keys and corresponding labels.
*/
- public function getTokensForContexts($contexts) {
+ public function getTokensForContexts(array $contexts) {
$tokens = [];
foreach ($contexts as $context_id => $context) {
$data_definition = $context->getContextDefinition()->getDataDefinition();
@@ -219,6 +223,7 @@ class TypedDataResolver {
* Returns tokens for a complex data definition.
*
* @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface $complex_data_definition
+ * Complex Data Definition.
*
* @return array
* An array of token keys and corresponding labels.
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/EntityFormWizardBase.php b/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/EntityFormWizardBase.php
index 2f47cbe49..889beee5b 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/EntityFormWizardBase.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/EntityFormWizardBase.php
@@ -6,9 +6,10 @@ use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\ctools\Event\WizardEvent;
-use Drupal\Core\TempStore\SharedTempStoreFactory;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
@@ -24,7 +25,7 @@ abstract class EntityFormWizardBase extends FormWizardBase implements EntityForm
protected $entityTypeManager;
/**
- * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore
* Tempstore Factory for keeping track of values in each step of the
* wizard.
* @param \Drupal\Core\Form\FormBuilderInterface $builder
@@ -35,16 +36,18 @@ abstract class EntityFormWizardBase extends FormWizardBase implements EntityForm
* The event dispatcher.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
+ * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+ * The route match object.
* @param $tempstore_id
- * The shared temp store factory collection name.
+ * The private temp store factory collection name.
* @param null $machine_name
- * The SharedTempStore key for our current wizard values.
+ * The PrivateTempStore key for our current wizard values.
* @param null $step
* The current active step of the wizard.
*/
- public function __construct(SharedTempStoreFactory $tempstore, FormBuilderInterface $builder, ClassResolverInterface $class_resolver, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match, $tempstore_id, $machine_name = NULL, $step = NULL) {
+ public function __construct(PrivateTempStoreFactory $tempstore, FormBuilderInterface $builder, ClassResolverInterface $class_resolver, EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match, RendererInterface $renderer, $tempstore_id, EntityTypeManagerInterface $entity_type_manager, $machine_name = NULL, $step = NULL) {
$this->entityTypeManager = $entity_type_manager;
- parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $tempstore_id, $machine_name, $step);
+ parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $renderer, $tempstore_id, $machine_name, $step);
}
/**
@@ -52,11 +55,12 @@ abstract class EntityFormWizardBase extends FormWizardBase implements EntityForm
*/
public static function getParameters() {
$parameters = [
- 'tempstore' => \Drupal::service('tempstore.shared'),
+ 'tempstore' => \Drupal::service('tempstore.private'),
'builder' => \Drupal::service('form_builder'),
'class_resolver' => \Drupal::service('class_resolver'),
'event_dispatcher' => \Drupal::service('event_dispatcher'),
'entity_type_manager' => \Drupal::service('entity_type.manager'),
+ 'renderer' => \Drupal::service('renderer'),
];
// Keep the deprecated entity manager service as a parameter as well for
// BC, so that subclasses still work.
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/FormWizardBase.php b/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/FormWizardBase.php
index 187fba80b..c63d14080 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/FormWizardBase.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/FormWizardBase.php
@@ -9,11 +9,12 @@ use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Drupal\ctools\Ajax\OpenModalWizardCommand;
use Drupal\ctools\Event\WizardEvent;
-use Drupal\Core\TempStore\SharedTempStoreFactory;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -25,7 +26,7 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
/**
* Tempstore Factory for keeping track of values in each step of the wizard.
*
- * @var \Drupal\Core\TempStore\SharedTempStoreFactory
+ * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempstore;
@@ -51,14 +52,14 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
protected $dispatcher;
/**
- * The shared temp store factory collection name.
+ * The private temp store factory collection name.
*
* @var string
*/
protected $tempstore_id;
/**
- * The SharedTempStore key for our current wizard values.
+ * The PrivateTempStore key for our current wizard values.
*
* @var string|null
*/
@@ -72,7 +73,14 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
protected $step;
/**
- * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempstore
+ * Renderer.
+ *
+ * @var \Drupal\Core\Render\RendererInterface
+ */
+ protected $renderer;
+
+ /**
+ * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore
* Tempstore Factory for keeping track of values in each step of the
* wizard.
* @param \Drupal\Core\Form\FormBuilderInterface $builder
@@ -82,18 +90,19 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param $tempstore_id
- * The shared temp store factory collection name.
- * @param null $machine_name
- * The SharedTempStore key for our current wizard values.
- * @param null $step
+ * The private temp store factory collection name.
+ * @param string $machine_name
+ * The PrivateTempStore key for our current wizard values.
+ * @param string $step
* The current active step of the wizard.
*/
- public function __construct(SharedTempStoreFactory $tempstore, FormBuilderInterface $builder, ClassResolverInterface $class_resolver, EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match, $tempstore_id, $machine_name = NULL, $step = NULL) {
+ public function __construct(PrivateTempStoreFactory $tempstore, FormBuilderInterface $builder, ClassResolverInterface $class_resolver, EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match, RendererInterface $renderer, $tempstore_id, $machine_name = NULL, $step = NULL) {
$this->tempstore = $tempstore;
$this->builder = $builder;
$this->classResolver = $class_resolver;
$this->dispatcher = $event_dispatcher;
$this->routeMatch = $route_match;
+ $this->renderer = $renderer;
$this->tempstore_id = $tempstore_id;
$this->machine_name = $machine_name;
$this->step = $step;
@@ -104,10 +113,11 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
*/
public static function getParameters() {
return [
- 'tempstore' => \Drupal::service('tempstore.shared'),
+ 'tempstore' => \Drupal::service('tempstore.private'),
'builder' => \Drupal::service('form_builder'),
'class_resolver' => \Drupal::service('class_resolver'),
'event_dispatcher' => \Drupal::service('event_dispatcher'),
+ 'renderer' => \Drupal::service('renderer'),
];
}
@@ -234,7 +244,7 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
$cached_values = $this->getTempstore()->get($this->getMachineName());
}
$operation = $this->getOperation($cached_values);
- /* @var $operation \Drupal\Core\Form\FormInterface */
+ /** @var \Drupal\Core\Form\FormInterface $operation */
$operation = $this->classResolver->getInstanceFromDefinition($operation['form']);
return $operation->getFormId();
}
@@ -247,7 +257,7 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
// Get the current form operation.
$operation = $this->getOperation($cached_values);
$form = $this->customizeForm($form, $form_state);
- /* @var $formClass \Drupal\Core\Form\FormInterface */
+ /** @var \Drupal\Core\Form\FormInterface $formClass */
$formClass = $this->classResolver->getInstanceFromDefinition($operation['form']);
// Pass include any custom values for this operation.
if (!empty($operation['values'])) {
@@ -336,8 +346,7 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
'#wizard' => $this,
'#cached_values' => $form_state->getTemporaryValue('wizard'),
];
- // @todo properly inject the renderer.
- $form['#prefix'] = \Drupal::service('renderer')->render($prefix);
+ $form['#prefix'] = $this->renderer->render($prefix);
return $form;
}
@@ -364,6 +373,7 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
$after = array_slice($operations, array_search($step, $steps) + 1);
$actions = [
+ '#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Next'),
@@ -438,7 +448,9 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
return $actions;
}
-
+ /**
+ *
+ */
public function ajaxSubmit(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$response = new AjaxResponse();
@@ -447,7 +459,9 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
return $response;
}
-
+ /**
+ *
+ */
public function ajaxPrevious(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
$response = new AjaxResponse();
@@ -456,14 +470,18 @@ abstract class FormWizardBase extends FormBase implements FormWizardInterface {
return $response;
}
-
+ /**
+ *
+ */
public function ajaxFinish(array $form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new CloseModalDialogCommand());
return $response;
}
-
+ /**
+ *
+ */
public function getRouteName() {
return $this->routeMatch->getRouteName();
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/FormWizardInterface.php b/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/FormWizardInterface.php
index fad81cc4f..688eb7746 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/FormWizardInterface.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/FormWizardInterface.php
@@ -30,21 +30,21 @@ interface FormWizardInterface extends FormInterface {
public function initValues();
/**
- * The shared temp store factory collection name.
+ * The private temp store factory collection name.
*
* @return string
*/
public function getTempstoreId();
/**
- * The active SharedTempStore for this wizard.
+ * The active PrivateTempStore for this wizard.
*
- * @return \Drupal\Core\TempStore\SharedTempStore
+ * @return \Drupal\Core\TempStore\PrivateTempStore
*/
public function getTempstore();
/**
- * The SharedTempStore key for our current wizard values.
+ * The PrivateTempStore key for our current wizard values.
*
* @return null|string
*/
@@ -173,13 +173,19 @@ interface FormWizardInterface extends FormInterface {
*/
public function finish(array &$form, FormStateInterface $form_state);
-
+ /**
+ *
+ */
public function ajaxSubmit(array $form, FormStateInterface $form_state);
-
+ /**
+ *
+ */
public function ajaxPrevious(array $form, FormStateInterface $form_state);
-
+ /**
+ *
+ */
public function ajaxFinish(array $form, FormStateInterface $form_state);
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/WizardFactory.php b/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/WizardFactory.php
index 847b1ae92..60efa9588 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/WizardFactory.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/src/Wizard/WizardFactory.php
@@ -93,7 +93,7 @@ class WizardFactory implements WizardFactoryInterface {
$arguments[] = $parameter->getDefaultValue();
}
}
- /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface */
+ /** @var \Drupal\ctools\Wizard\FormWizardInterface $wizard */
$wizard = $reflection->newInstanceArgs($arguments);
return $wizard;
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_block_display_test/ctools_block_display_test.info.yml b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_block_display_test/ctools_block_display_test.info.yml
index eeff2d437..86c684cbe 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_block_display_test/ctools_block_display_test.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_block_display_test/ctools_block_display_test.info.yml
@@ -6,7 +6,7 @@ package: Testing
dependencies:
- ctools:ctools
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_block_display_test/src/Plugin/DisplayVariant/BlockDisplayVariant.php b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_block_display_test/src/Plugin/DisplayVariant/BlockDisplayVariant.php
index 2aa254b35..1de82badc 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_block_display_test/src/Plugin/DisplayVariant/BlockDisplayVariant.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_block_display_test/src/Plugin/DisplayVariant/BlockDisplayVariant.php
@@ -4,6 +4,9 @@ namespace Drupal\ctools_block_display_test\Plugin\DisplayVariant;
use Drupal\ctools\Plugin\DisplayVariant\BlockDisplayVariant as BaseBlockDisplayVariant;
+/**
+ *
+ */
class BlockDisplayVariant extends BaseBlockDisplayVariant {
/**
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.info.yml b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.info.yml
index bcff0f2f3..e38fe086d 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.info.yml
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.info.yml
@@ -4,7 +4,7 @@ description: 'Provides testing for ctools wizard'
package: Testing
# version: 3.x
-# Information added by Drupal.org packaging script on 2021-06-16
-version: '8.x-3.7'
+# Information added by Drupal.org packaging script on 2022-07-01
+version: '8.x-3.8'
project: 'ctools'
-datestamp: 1623822132
+datestamp: 1656633726
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExternalForm.php b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExternalForm.php
index d82b80c4c..0c59d9e78 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExternalForm.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExternalForm.php
@@ -4,7 +4,7 @@ namespace Drupal\ctools_wizard_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\TempStore\SharedTempStoreFactory;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -15,17 +15,17 @@ class ExampleConfigEntityExternalForm extends FormBase {
/**
* Tempstore factory.
*
- * @var \Drupal\Core\TempStore\SharedTempStoreFactory
+ * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempstore;
/**
* Constructs a new ExampleConfigEntityExternalForm.
*
- * @param \Drupal\ctools_wizard_test\Form\SharedTempStoreFactory $tempstore
- * Creates a shared temporary storage for a collection.
+ * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore
+ * Creates a private temporary storage for a collection.
*/
- public function __construct(SharedTempStoreFactory $tempstore) {
+ public function __construct(PrivateTempStoreFactory $tempstore) {
$this->tempstore = $tempstore;
}
@@ -33,7 +33,7 @@ class ExampleConfigEntityExternalForm extends FormBase {
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
- return new static($container->get('tempstore.shared'));
+ return new static($container->get('tempstore.private'));
}
/**
@@ -48,7 +48,7 @@ class ExampleConfigEntityExternalForm extends FormBase {
*/
public function buildForm(array $form, FormStateInterface $form_state, $machine_name = '') {
$cached_values = $this->tempstore->get('ctools_wizard_test.config_entity')->get($machine_name);
- /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */
+ /** @var \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity $page */
$config_entity = $cached_values['ctools_wizard_test_config_entity'];
$form['blah'] = [
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityGeneralForm.php b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityGeneralForm.php
index 8bebdcb66..6744c7b39 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityGeneralForm.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityGeneralForm.php
@@ -22,7 +22,7 @@ class ExampleConfigEntityGeneralForm extends FormBase {
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
- /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */
+ /** @var \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity $page */
$config_entity = $cached_values['ctools_wizard_test_config_entity'];
// The label and id will be added by the EntityFormWizardBase.
@@ -34,7 +34,7 @@ class ExampleConfigEntityGeneralForm extends FormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
- /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */
+ /** @var \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity $page */
$config_entity = $cached_values['ctools_wizard_test_config_entity'];
$config_entity->set('id', $form_state->getValue('id'));
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityOneForm.php b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityOneForm.php
index a10a6399b..09b6f789f 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityOneForm.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityOneForm.php
@@ -23,7 +23,7 @@ class ExampleConfigEntityOneForm extends FormBase {
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
- /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */
+ /** @var \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity $page */
$config_entity = $cached_values['ctools_wizard_test_config_entity'];
$form['one'] = [
@@ -52,7 +52,7 @@ class ExampleConfigEntityOneForm extends FormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
- /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */
+ /** @var \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity $page */
$config_entity = $cached_values['ctools_wizard_test_config_entity'];
$config_entity->set('one', $form_state->getValue('one'));
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityTwoForm.php b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityTwoForm.php
index d2d5f9654..06c48c102 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityTwoForm.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityTwoForm.php
@@ -22,7 +22,7 @@ class ExampleConfigEntityTwoForm extends FormBase {
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
- /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */
+ /** @var \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity $page */
$config_entity = $cached_values['ctools_wizard_test_config_entity'];
$form['two'] = [
@@ -38,7 +38,7 @@ class ExampleConfigEntityTwoForm extends FormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$cached_values = $form_state->getTemporaryValue('wizard');
- /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */
+ /** @var \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity $page */
$config_entity = $cached_values['ctools_wizard_test_config_entity'];
$config_entity->set('two', $form_state->getValue('two'));
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityAddWizardTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityAddWizardTest.php
index 77ab5d599..568a9365f 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityAddWizardTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityAddWizardTest.php
@@ -2,7 +2,9 @@
namespace Drupal\ctools_wizard_test\Wizard;
-
+/**
+ *
+ */
class EntityAddWizardTest extends EntityEditWizardTest {
/**
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityEditWizardTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityEditWizardTest.php
index 6a42fb0dd..8337977f9 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityEditWizardTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityEditWizardTest.php
@@ -4,7 +4,9 @@ namespace Drupal\ctools_wizard_test\Wizard;
use Drupal\ctools\Wizard\EntityFormWizardBase;
-
+/**
+ *
+ */
class EntityEditWizardTest extends EntityFormWizardBase {
/**
@@ -39,7 +41,7 @@ class EntityEditWizardTest extends EntityFormWizardBase {
* {@inheritdoc}
*/
public function getOperations($cached_values) {
- /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */
+ /** @var \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity $page */
$config_entity = $cached_values['ctools_wizard_test_config_entity'];
$steps = [
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/WizardTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/WizardTest.php
index e7df39857..fdb0cf598 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/WizardTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/WizardTest.php
@@ -5,7 +5,9 @@ namespace Drupal\ctools_wizard_test\Wizard;
use Drupal\Core\Form\FormStateInterface;
use Drupal\ctools\Wizard\FormWizardBase;
-
+/**
+ *
+ */
class WizardTest extends FormWizardBase {
/**
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Functional/CToolsWizardTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Functional/CToolsWizardTest.php
index cdd42d181..86e7ab063 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Functional/CToolsWizardTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Functional/CToolsWizardTest.php
@@ -13,7 +13,7 @@ use Drupal\Tests\BrowserTestBase;
class CToolsWizardTest extends BrowserTestBase {
use StringTranslationTrait;
- public static $modules = ['ctools', 'ctools_wizard_test'];
+ protected static $modules = ['ctools', 'ctools_wizard_test'];
/**
* {@inheritdoc}
@@ -33,23 +33,24 @@ class CToolsWizardTest extends BrowserTestBase {
$edit = [
'one' => 'test',
];
- $this->drupalPostForm('ctools/wizard', $edit, $this->t('Next'));
+ $this->drupalGet('ctools/wizard');
+ $this->submitForm($edit, $this->t('Next'));
// Redirected to the second step.
$this->assertSession()->pageTextContains('Form Two');
$this->assertSession()->pageTextContains('Dynamic value submitted: Xylophone');
// Check that $operations['two']['values'] worked.
$this->assertSession()->pageTextContains('Zebra');
// Hit previous to make sure our form value are preserved.
- $this->drupalPostForm(NULL, [], $this->t('Previous'));
+ $this->submitForm([], $this->t('Previous'));
// Check the known form values.
$this->assertSession()->fieldValueEquals('one', 'test');
$this->assertSession()->pageTextContains('Xylophone');
// Goto next step again and finish this wizard.
- $this->drupalPostForm(NULL, [], $this->t('Next'));
+ $this->submitForm([], $this->t('Next'));
$edit = [
'two' => 'Second test',
];
- $this->drupalPostForm(NULL, $edit, $this->t('Finish'));
+ $this->submitForm($edit, $this->t('Finish'));
// Check that the wizard finished properly.
$this->assertSession()->pageTextContains('Value One: test');
$this->assertSession()->pageTextContains('Value Two: Second test');
@@ -65,7 +66,8 @@ class CToolsWizardTest extends BrowserTestBase {
$edit = [
'one' => 'wrong',
];
- $this->drupalPostForm('ctools/wizard', $edit, $this->t('Next'));
+ $this->drupalGet('ctools/wizard');
+ $this->submitForm($edit, $this->t('Next'));
// We're still on the first form and the error is present.
$this->assertSession()->pageTextContains('Form One');
$this->assertSession()->pageTextContains('Cannot set the value to "wrong".');
@@ -73,13 +75,14 @@ class CToolsWizardTest extends BrowserTestBase {
$edit = [
'one' => 'magic',
];
- $this->drupalPostForm('ctools/wizard', $edit, $this->t('Next'));
+ $this->drupalGet('ctools/wizard');
+ $this->submitForm($edit, $this->t('Next'));
// Redirected to the second step.
$this->assertSession()->pageTextContains('Form Two');
$edit = [
'two' => 'Second test',
];
- $this->drupalPostForm(NULL, $edit, $this->t('Finish'));
+ $this->submitForm($edit, $this->t('Finish'));
// Check that the magic value triggered our submit callback.
$this->assertSession()->pageTextContains('Value One: Abraham');
$this->assertSession()->pageTextContains('Value Two: Second test');
@@ -101,19 +104,19 @@ class CToolsWizardTest extends BrowserTestBase {
'id' => 'test123',
'label' => 'Test Config Entity 123',
];
- $this->drupalPostForm(NULL, $edit, $this->t('Next'));
+ $this->submitForm($edit, $this->t('Next'));
// Submit the first step.
$edit = [
'one' => 'The first bit',
];
- $this->drupalPostForm(NULL, $edit, $this->t('Next'));
+ $this->submitForm($edit, $this->t('Next'));
// Submit the second step.
$edit = [
'two' => 'The second bit',
];
- $this->drupalPostForm(NULL, $edit, $this->t('Finish'));
+ $this->submitForm($edit, $this->t('Finish'));
// Now we should be looking at the list of entities.
$this->assertSession()->addressEquals('admin/structure/ctools_wizard_test_config_entity');
@@ -130,12 +133,12 @@ class CToolsWizardTest extends BrowserTestBase {
$this->assertSession()->responseContains('Value from one: The first bit');
$this->drupalGet($previous);
// Change the value for 'one'.
- $this->drupalPostForm(NULL, ['one' => 'New value'], $this->t('Next'));
+ $this->submitForm(['one' => 'New value'], $this->t('Next'));
$this->assertSession()->fieldValueEquals('two', 'The second bit');
- $this->drupalPostForm(NULL, [], $this->t('Next'));
+ $this->submitForm([], $this->t('Next'));
// Make sure we get the additional step because the entity exists.
$this->assertSession()->pageTextContains('This step only shows if the entity is already existing!');
- $this->drupalPostForm(NULL, [], $this->t('Finish'));
+ $this->submitForm([], $this->t('Finish'));
// Edit the entity again and make sure the change stuck.
$this->assertSession()->addressEquals('admin/structure/ctools_wizard_test_config_entity');
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/BlockDisplayVariantTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/BlockDisplayVariantTest.php
index 01e462665..e0f8aab9c 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/BlockDisplayVariantTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/BlockDisplayVariantTest.php
@@ -18,7 +18,7 @@ class BlockDisplayVariantTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
- public static $modules = ['ctools', 'ctools_block_display_test', 'system', 'user'];
+ protected static $modules = ['ctools', 'ctools_block_display_test', 'system', 'user'];
/**
* Tests that events are fired when manipulating a block variant.
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/Plugin/Block/EntityViewTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/Plugin/Block/EntityViewTest.php
index 4e53dd22c..9ef8d172f 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/Plugin/Block/EntityViewTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/Plugin/Block/EntityViewTest.php
@@ -2,8 +2,7 @@
namespace Drupal\Tests\ctools\Kernel\Plugin\Block;
-use Drupal\Core\Access\AccessResultForbidden;
-use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\ctools\Plugin\Block\EntityView;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\node\Traits\NodeCreationTrait;
@@ -24,7 +23,7 @@ class EntityViewTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
- public static $modules = [
+ protected static $modules = [
'block',
'ctools',
'filter',
@@ -68,8 +67,8 @@ class EntityViewTest extends KernelTestBase {
],
];
$definition = [
- 'context' => [
- 'entity' => new ContextDefinition('entity:node', NULL, TRUE, FALSE, NULL, $node),
+ 'context_definitions' => [
+ 'entity' => new EntityContextDefinition('entity:node', NULL, TRUE, FALSE, NULL, $node),
],
'provider' => 'ctools',
];
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/RelationshipManagerTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/RelationshipManagerTest.php
index 5ab67b0de..6f071aaf1 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/RelationshipManagerTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/RelationshipManagerTest.php
@@ -43,20 +43,20 @@ class RelationshipManagerTest extends RelationshipsTestBase {
'node' => new Context($context_definition, $this->entities['node1']),
];
$definitions = $this->relationshipManager->getDefinitionsForContexts($contexts);
- // $this->assertTrue(isset($definitions['typed_data_relationship:entity:node:body']));
+
$context_definition = new EntityContextDefinition('entity:node');
$contexts = [
'node' => new Context($context_definition, $this->entities['node2']),
];
$definitions = $this->relationshipManager->getDefinitionsForContexts($contexts);
- $this->assertFalse(isset($definitions['typed_data_relationship:entity:node:body']));
+ $this->assertArrayNotHasKey('typed_data_relationship:entity:node:body', $definitions);
$context_definition = new EntityContextDefinition('entity:node');
$contexts = [
'node' => new Context($context_definition, $this->entities['node3']),
];
$definitions = $this->relationshipManager->getDefinitionsForContexts($contexts);
- // $this->assertTrue(isset($definitions['typed_data_relationship:entity:node:body']));
+
}
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/RelationshipsTestBase.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/RelationshipsTestBase.php
index 5145bfba7..bbd88715b 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/RelationshipsTestBase.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/RelationshipsTestBase.php
@@ -5,7 +5,9 @@ namespace Drupal\Tests\ctools\Kernel;
use Drupal\ctools\Testing\EntityCreationTrait;
use Drupal\KernelTests\KernelTestBase;
-
+/**
+ *
+ */
abstract class RelationshipsTestBase extends KernelTestBase {
use EntityCreationTrait;
@@ -24,7 +26,7 @@ abstract class RelationshipsTestBase extends KernelTestBase {
*
* @var array
*/
- public static $modules = [
+ protected static $modules = [
'user',
'system',
'node',
@@ -84,12 +86,17 @@ abstract class RelationshipsTestBase extends KernelTestBase {
'type' => 'foo',
'uid' => $user->id(),
]);
+ $node4 = $this->createEntity('node', [
+ 'title' => 'Node 4',
+ 'type' => 'foo',
+ ])->set('uid', NULL);
$this->entities = [
'user' => $user,
'node1' => $node1,
'node2' => $node2,
'node3' => $node3,
+ 'node4' => $node4,
];
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/SerializableTempstoreTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/SerializableTempstoreTest.php
index 20ceeeabc..00090c4f3 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/SerializableTempstoreTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/SerializableTempstoreTest.php
@@ -16,7 +16,7 @@ class SerializableTempstoreTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
- public static $modules = ['ctools', 'system', 'user'];
+ protected static $modules = ['ctools', 'system', 'user'];
/**
* {@inheritdoc}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataEntityRelationshipPluginTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataEntityRelationshipPluginTest.php
index 8b117aabc..a1724eea7 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataEntityRelationshipPluginTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataEntityRelationshipPluginTest.php
@@ -41,6 +41,13 @@ class TypedDataEntityRelationshipPluginTest extends RelationshipsTestBase {
$relationship = $uid_plugin->getRelationship();
$this->assertTrue($relationship->getContextValue() instanceof User);
$this->assertSame('entity:user', $relationship->getContextDefinition()->getDataType());
+
+ /** @var \Drupal\ctools\Plugin\RelationshipInterface $uid_plugin */
+ $uid_plugin = $this->relationshipManager->createInstance('typed_data_entity_relationship:entity:node:uid');
+ $uid_plugin->setContextValue('base', $this->entities['node4']);
+ $relationship = $uid_plugin->getRelationship();
+ $this->assertFalse($relationship->hasContextValue());
+ $this->assertSame('entity:user', $relationship->getContextDefinition()->getDataType());
}
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataRelationshipPluginTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataRelationshipPluginTest.php
index 67d3149e3..31fa9b71a 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataRelationshipPluginTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataRelationshipPluginTest.php
@@ -48,54 +48,54 @@ class TypedDataRelationshipPluginTest extends RelationshipsTestBase {
$nid_plugin->setContextValue('base', $this->entities['node1']);
$relationship = $nid_plugin->getRelationship();
$this->assertTrue($relationship instanceof ContextInterface);
- $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'integer');
+ $this->assertEquals('integer', $relationship->getContextDefinition()->getDataType());
$this->assertTrue($relationship->hasContextValue());
- $this->assertTrue($relationship->getContextValue() == $this->entities['node1']->id());
+ $this->assertEquals($this->entities['node1']->id(), $relationship->getContextValue());
/** @var \Drupal\ctools\Plugin\RelationshipInterface $uuid_plugin */
$uuid_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:uuid');
$uuid_plugin->setContextValue('base', $this->entities['node1']);
$relationship = $uuid_plugin->getRelationship();
$this->assertTrue($relationship instanceof ContextInterface);
- $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'string');
+ $this->assertEquals('string', $relationship->getContextDefinition()->getDataType());
$this->assertTrue($relationship->hasContextValue());
- $this->assertTrue($relationship->getContextValue() == $this->entities['node1']->uuid());
+ $this->assertEquals($this->entities['node1']->uuid(), $relationship->getContextValue());
/** @var \Drupal\ctools\Plugin\RelationshipInterface $title_plugin */
$title_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:title');
$title_plugin->setContextValue('base', $this->entities['node1']);
$relationship = $title_plugin->getRelationship();
$this->assertTrue($relationship instanceof ContextInterface);
- $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'string');
+ $this->assertEquals('string', $relationship->getContextDefinition()->getDataType());
$this->assertTrue($relationship->hasContextValue());
- $this->assertTrue($relationship->getContextValue() == $this->entities['node1']->label());
+ $this->assertEquals($this->entities['node1']->label(), $relationship->getContextValue());
/** @var \Drupal\ctools\Plugin\RelationshipInterface $body_plugin */
$body_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:body');
$body_plugin->setContextValue('base', $this->entities['node1']);
$relationship = $body_plugin->getRelationship();
$this->assertTrue($relationship instanceof ContextInterface);
- $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'string');
+ $this->assertEquals('string', $relationship->getContextDefinition()->getDataType());
$this->assertTrue($relationship->hasContextValue());
- $this->assertTrue($relationship->getContextValue() == $this->entities['node1']->get('body')->first()->get('value')->getValue());
+ $this->assertEquals($this->entities['node1']->get('body')->first()->get('value')->getValue(), $relationship->getContextValue());
/** @var \Drupal\ctools\Plugin\RelationshipInterface $uid_plugin */
$uid_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:uid');
$uid_plugin->setContextValue('base', $this->entities['node3']);
$relationship = $uid_plugin->getRelationship();
$this->assertTrue($relationship instanceof ContextInterface);
- $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'integer');
+ $this->assertEquals('integer', $relationship->getContextDefinition()->getDataType());
$this->assertTrue($relationship->hasContextValue());
- $this->assertTrue($relationship->getContextValue() == $this->entities['node3']->getOwnerId());
+ $this->assertEquals($this->entities['node3']->getOwnerId(), $relationship->getContextValue());
/** @var \Drupal\ctools\Plugin\RelationshipInterface $mail_plugin */
$mail_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:user:mail');
$mail_plugin->setContextValue('base', $this->entities['user']);
$relationship = $mail_plugin->getRelationship();
$this->assertTrue($relationship instanceof ContextInterface);
- $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'email');
+ $this->assertEquals('email', $relationship->getContextDefinition()->getDataType());
$this->assertTrue($relationship->hasContextValue());
- $this->assertTrue($relationship->getContextValue() == $this->entities['user']->getEmail());
+ $this->assertEquals($this->entities['user']->getEmail(), $relationship->getContextValue());
}
}
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataResolverTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataResolverTest.php
index a75ea6bcf..bfa11c4d8 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataResolverTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Kernel/TypedDataResolverTest.php
@@ -22,7 +22,7 @@ class TypedDataResolverTest extends KernelTestBase {
*
* @var array
*/
- public static $modules = ['user', 'system', 'entity_test', 'ctools'];
+ protected static $modules = ['user', 'system', 'entity_test', 'ctools'];
/**
* @var \Drupal\ctools\TypedDataResolver
@@ -77,7 +77,7 @@ class TypedDataResolverTest extends KernelTestBase {
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity to test with.
- * @param $property_path
+ * @param string $property_path
* The property path to look for.
* @param $expected_data_type
* The expected data type.
@@ -85,7 +85,7 @@ class TypedDataResolverTest extends KernelTestBase {
* @return \Drupal\Core\Plugin\Context\ContextInterface
* The context with a value.
*/
- protected function assertPropertyPath(ContentEntityInterface $entity, $property_path, $expected_data_type) {
+ protected function assertPropertyPath(ContentEntityInterface $entity, string $property_path, $expected_data_type) {
$typed_data_entity = $entity->getTypedData();
if (strpos($typed_data_entity->getDataDefinition()->getDataType(), 'entity:') === 0) {
$context_definition = new EntityContextDefinition($typed_data_entity->getDataDefinition()->getDataType());
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/BlockDisplayVariantTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/BlockDisplayVariantTest.php
index 86f2513e1..d51cd9055 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/BlockDisplayVariantTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/BlockDisplayVariantTest.php
@@ -45,7 +45,9 @@ class BlockDisplayVariantTest extends UnitTestCase {
return [];
}
-
+ /**
+ *
+ */
public function getRegionNames() {
return [
'top' => 'Top',
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/BlockVariantTraitTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/BlockVariantTraitTest.php
index 8a142f100..cc76c3d56 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/BlockVariantTraitTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/BlockVariantTraitTest.php
@@ -35,6 +35,9 @@ class BlockVariantTraitTest extends UnitTestCase {
$this->assertSame($expected, $display_variant->getRegionAssignments());
}
+ /**
+ *
+ */
public function providerTestGetRegionAssignments() {
return [
[
@@ -75,7 +78,9 @@ class BlockVariantTraitTest extends UnitTestCase {
}
}
-
+/**
+ *
+ */
class TestBlockVariantTrait {
use BlockVariantTrait;
diff --git a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/VariantCollectionTraitTest.php b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/VariantCollectionTraitTest.php
index bf292e689..29b6c65c0 100644
--- a/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/VariantCollectionTraitTest.php
+++ b/frontend/drupal9/web/modules/contrib/ctools/tests/src/Unit/VariantCollectionTraitTest.php
@@ -27,7 +27,7 @@ class VariantCollectionTraitTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
- protected function setUp(): void {
+ protected function setUp() {
parent::setUp();
$container = new ContainerBuilder();
$this->manager = $this->prophesize(PluginManagerInterface::class);
@@ -119,7 +119,7 @@ class VariantCollectionTraitTest extends UnitTestCase {
* @depends testAddVariant
*/
public function testGetVariant($data) {
- list($trait_object, $uuid, $plugin) = $data;
+ [$trait_object, $uuid, $plugin] = $data;
$this->manager->createInstance()->shouldNotBeCalled();
$this->assertSame($plugin, $trait_object->getVariant($uuid));
@@ -132,7 +132,7 @@ class VariantCollectionTraitTest extends UnitTestCase {
* @depends testGetVariant
*/
public function testRemoveVariant($data) {
- list($trait_object, $uuid) = $data;
+ [$trait_object, $uuid] = $data;
$this->assertSame($trait_object, $trait_object->removeVariant($uuid));
$this->assertFalse($trait_object->getVariants()->has($uuid));
@@ -145,7 +145,7 @@ class VariantCollectionTraitTest extends UnitTestCase {
* @depends testRemoveVariant
*/
public function testGetVariantException($data) {
- list($trait_object, $uuid) = $data;
+ [$trait_object, $uuid] = $data;
// Attempt to retrieve a variant that has been removed.
$this->expectException('\Drupal\Component\Plugin\Exception\PluginNotFoundException');
$this->expectExceptionMessage("Plugin ID 'test-uuid' was not found.");
@@ -153,7 +153,9 @@ class VariantCollectionTraitTest extends UnitTestCase {
}
}
-
+/**
+ *
+ */
class TestVariantCollectionTrait {
use VariantCollectionTrait;
diff --git a/frontend/drupal9/web/modules/contrib/twig_vardumper/LICENSE.txt b/frontend/drupal9/web/modules/contrib/facets/LICENSE.txt
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/twig_vardumper/LICENSE.txt
rename to frontend/drupal9/web/modules/contrib/facets/LICENSE.txt
diff --git a/frontend/drupal9/web/modules/contrib/facets/README.txt b/frontend/drupal9/web/modules/contrib/facets/README.txt
new file mode 100644
index 000000000..53d64e6b7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/README.txt
@@ -0,0 +1,203 @@
+CONTENTS OF THIS FILE
+---------------------
+
+ * Requirements
+ * Recommended Modules
+ * Installation
+ * Configuration
+ * Features
+ * Extension modules
+ * FAQ
+ * Maintainers
+
+
+INTRODUCTION
+------------
+
+The Facets module allows site builders to easily create and manage faceted
+search interfaces.
+
+
+REQUIREMENTS
+------------
+
+No other modules required; we're supporting Drupal Core's search as a source for
+creating facets.
+
+
+RECOMMENDED MODULES
+-------------------
+
+ * Search API - https://www.drupal.org/project/search_api
+
+
+INSTALLATION
+------------
+
+ * Install as you would normally install a contributed Drupal module. Visit:
+ https://www.drupal.org/node/1897420 for further information.
+
+
+CONFIGURATION
+-------------
+
+Before adding a facet, there should be a facet source. Facet sources can be:
+- Drupal core's search.
+- A view based on a Search API index with a page display.
+- A page from the search_api_page module.
+
+After adding one of those, you can add a facet on the facets configuration page:
+/admin/config/search/facets, there's an `add facet` link, that links to:
+admin/config/search/facets/add-facet. Use that page to add the facet by
+selecting the correct facet source and field from that source.
+
+If you're using Search API views, make sure to disable views cache when using
+facets for that view.
+
+
+KNOWN ISSUES
+------------
+
+When choosing the "Hard limit" option on a search_api_db backend, be aware that
+the limitation is done internally after sorting on the number of results ("num")
+first and then sorting by the raw value of the facet (e.g. entity-id) in the
+second dimension. This can lead to edge cases when there is an equal amount of
+results on facets that are exactly on the threshold of the hard limit. In this
+case the raw facet value with the lower value is preferred:
+
+| num | value | label |
+|-----|-------|-------|
+| 3 | 4 | Bar |
+| 3 | 5 | Atom |
+| 2 | 2 | Zero |
+| 2 | 3 | Clown |
+
+"Clown" will be cut off due to its higher internal value (entity-id). For
+further details see: https://www.drupal.org/node/2834730
+
+
+FEATURES
+--------
+
+If you are the developer of a search API backend implementation and want
+to support facets with your service class, too, you'll have to support the
+"search_api_facets" feature. In short, you'll just have to return facet terms
+and counts according to the query's "search_api_facets" option, when executing
+a query.
+For the module to be able to tell that your server supports facets,
+you will also have to change your service's supportsFeature() method to
+something like the following:
+
+```
+ public function getSupportedFeatures() {
+ return ['search_api_facets'];
+ }
+```
+
+If you don't do that, there's no way for the facet source to pick up facets.
+
+The "search_api_facets" option looks as follows:
+
+```
+$query->setOption('search_api_facets', [
+ $facet_id => [
+ // The Search API field ID of the field to facet on.
+ 'field' => (string),
+ // The maximum number of filters to retrieve for the facet.
+ 'limit' => (int),
+ // The facet operator: "and" or "or".
+ 'operator' => (string),
+ // The minimum count a filter/value must have to be returned.
+ 'min_count' => (int),
+ // Whether to retrieve a facet for "missing" values.
+ 'missing' => (bool),
+ ],
+ // …
+]);
+```
+
+The structure of the returned facets array should look like this:
+
+```
+$results->setExtraData('search_api_facets', [
+ $facet_id => [
+ [
+ 'count' => (int),
+ 'filter' => (string),
+ ],
+ // …
+ ],
+ // …
+]);
+```
+
+A filter is a string with one of the following forms:
+- `"VALUE"`: Filter by the literal value VALUE (always include the quotes, not
+ only for strings).
+- `[VALUE1 VALUE2]`: Filter for a value between VALUE1 and VALUE2. Use
+ parentheses for excluding the border values and square brackets for including
+ them. An asterisk (*) can be used as a wildcard. E.g., (* 0) or [* 0) would be
+ a filter for all negative values.
+- `!`: Filter for items without a value for this field (i.e., the "missing"
+ facet).
+
+
+EXTENSION MODULES
+-----------------
+
+- https://www.drupal.org/project/entity_reference_facet_link
+ Provides a link to a facet through an entity reference field.
+- https://www.drupal.org/project/facets_prefix_suffix
+ Provides a plugin to configure a prefix/suffix per result.
+- https://www.drupal.org/project/facets_block
+ Provide the facets as a Drupal block.
+- https://www.drupal.org/project/facets_taxonomy_path_processor
+ Sets taxonomy facet items active if present in route.
+- https://www.drupal.org/project/facets_view_mode_processor
+ Provides a processor to render facet entity reference items as view modes.
+- https://www.drupal.org/project/facets_range_input
+ Provides an input range form (min and max) as a processor and widget.
+- https://www.drupal.org/project/facets_range_dropdowns
+ Provides an dropdown widget that works with the range processor.
+
+FAQ
+---
+
+Q: Why do the facets disappear after a refresh?
+A: We don't support cached views, change the view to disable caching.
+
+Q: Why doesn't chosen (or similar JavaScript dropdown replacement) not work
+with the dropdown widget?
+A: Because the dropdown we create for the widget is created through JavaScript,
+the chosen module (and others, probably) doesn't find the select element.
+Though the library can be attached to the block in custom code, we haven't
+done this in facets because we don't want to support all possible frameworks.
+See https://www.drupal.org/node/2853121 for more information.
+
+Q: Why are facets results links from another language showing in the facet
+results?
+A: Facets use the same limitations as the query object passed, so when using
+views, add a filter to the view to limit to one language.
+Otherwise, this is solved by adding a `hook_search_api_query_alter()` that
+limits the results to the current language.
+
+Q: I would like a prefix/suffix for facet result items.
+A: If you just need to show text, use
+https://www.drupal.org/project/facets_prefix_suffix.
+However, if you need to include HTML you can use
+hook_preprocess_facets_result_item().
+
+Q: Why are results shown for inaccessible content?
+A: If the "Content access" Search API processor is enabled but results still
+aren't properly access-checked, you might need to write a custom processor to do
+the access checks for you.
+This should only happen if you're not using the default node access framework
+provided by Core, though. You need to use a combination of hook_node_grants and
+hook_node_access_records instead of hook_node_access.
+
+MAINTAINERS
+-----------
+
+ * Joris Vercammen (borisson_) - https://www.drupal.org/u/borisson_
+ * Jimmy Henderickx (StryKaizer) - https://www.drupal.org/u/strykaizer
+ * Nick Veenhof (Nick_vh) - https://www.drupal.org/u/nick_vh
diff --git a/frontend/drupal9/web/modules/contrib/facets/composer.json b/frontend/drupal9/web/modules/contrib/facets/composer.json
new file mode 100644
index 000000000..377ab48c6
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/composer.json
@@ -0,0 +1,30 @@
+{
+ "name": "drupal/facets",
+ "description": "The Facet module allows site builders to easily create and manage faceted search interfaces.",
+ "type": "drupal-module",
+ "homepage": "https://www.drupal.org/project/facets",
+ "authors": [
+ {
+ "name": "See all contributors",
+ "homepage": "https://www.drupal.org/node/2348769/committers"
+ }
+ ],
+ "support": {
+ "issues": "https://www.drupal.org/project/issues/facets",
+ "irc": "irc://irc.freenode.org/drupal-search-api",
+ "source": "git://git.drupal.org/project/facets.git"
+ },
+ "license": "GPL-2.0+",
+ "require-dev": {
+ "drupal/search_api": "~1.21",
+ "drupal/jquery_ui_slider": "~1.1",
+ "drupal/jquery_ui_touch_punch": "~1.0"
+ },
+ "suggest": {
+ "drupal/jquery_ui_slider": "Required for the 'Facets Range Widget' module to work",
+ "drupal/jquery_ui_touch_punch": "Required for the 'Facets Range Widget' module to work"
+ },
+ "conflict": {
+ "drupal/search_api": "<1.14"
+ }
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.blocks.schema.yml b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.blocks.schema.yml
new file mode 100644
index 000000000..856b67155
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.blocks.schema.yml
@@ -0,0 +1,7 @@
+block.settings.facet_block:*:
+ type: block_settings
+ label: 'Facet rendered as block'
+ mapping:
+ block_id:
+ type: string
+ label: 'Block ID'
diff --git a/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.facet.schema.yml b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.facet.schema.yml
new file mode 100644
index 000000000..34d34667b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.facet.schema.yml
@@ -0,0 +1,106 @@
+facets.facet.*:
+ type: config_entity
+ label : 'Facet'
+ mapping:
+ id:
+ type: string
+ label: 'ID'
+ name:
+ type: label
+ label: Name
+ weight:
+ type: integer
+ label: 'Weight'
+ min_count:
+ type: integer
+ label: 'Minimum count'
+ url_alias:
+ type: label
+ label: 'Name of facet as used in the URL'
+ facet_source_id:
+ type: string
+ label: 'Facet source id'
+ field_identifier:
+ type: string
+ label: 'Field identifier'
+ query_operator:
+ type: string
+ label: 'Query Operator'
+ hard_limit:
+ type: integer
+ label: 'Hard limit'
+ exclude:
+ type: boolean
+ label: 'Exclude'
+ use_hierarchy:
+ type: boolean
+ label: 'Use hierarchy'
+ keep_hierarchy_parents_active:
+ type: boolean
+ label: 'Keep hierarchy parents active'
+ hierarchy:
+ type: mapping
+ label: 'Hierarchy type'
+ mapping:
+ type:
+ type: string
+ label: 'Plugin id'
+ config:
+ type: facets.facet.[%parent.type]
+ label: 'Configuration'
+ expand_hierarchy:
+ type: boolean
+ label: 'Expand hierarchy'
+ enable_parent_when_child_gets_disabled:
+ type: boolean
+ label: 'Enable parent when child gets disabled'
+ widget:
+ type: mapping
+ label: 'Facet widget'
+ mapping:
+ type:
+ type: string
+ label: 'Plugin ID'
+ config:
+ type: facet.widget.config.[%parent.type]
+ label: 'Configuration'
+ empty_behavior:
+ type: mapping
+ label: 'Empty behavior'
+ mapping:
+ behavior:
+ type: string
+ label: 'The empty behavior identifier'
+ text_format:
+ type: string
+ label: 'Text format'
+ text:
+ type: text
+ label: 'Text'
+ only_visible_when_facet_source_is_visible:
+ type: boolean
+ label: 'Show this facet only when the facet source is visible.'
+ show_only_one_result:
+ type: boolean
+ label: 'Show only one result'
+ show_title:
+ type: boolean
+ label: 'Show title'
+ processor_configs:
+ type: sequence
+ label: 'Processor settings'
+ sequence:
+ type: mapping
+ label: 'A processor'
+ mapping:
+ processor_id:
+ type: string
+ label: 'The plugin ID of the processor'
+ weights:
+ type: sequence
+ label: 'The processor''s weights for the different processing stages'
+ sequence:
+ type: integer
+ label: 'The processor''s weight for this stage'
+ settings:
+ type: plugin.plugin_configuration.facets_processor.[%parent.processor_id]
diff --git a/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.facetsource.schema.yml b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.facetsource.schema.yml
new file mode 100644
index 000000000..053610da8
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.facetsource.schema.yml
@@ -0,0 +1,30 @@
+facets.facet_source.*:
+ type: config_entity
+ label : 'Facet Source'
+ mapping:
+ id:
+ type: string
+ label: 'ID'
+ name:
+ type: label
+ label: Name'
+ filter_key:
+ type: string
+ label: 'Filter key'
+ url_processor:
+ type: string
+ label: 'Url processor'
+ breadcrumb:
+ type: mapping
+ labal: 'Breadcrumb'
+ mapping:
+ active:
+ type: boolean
+ label: 'Append active facets to breadcrumb'
+ before:
+ type: boolean
+ label: 'Show facet label before active facet'
+ group:
+ type: boolean
+ label: 'Group active items under same crumb'
+ third_party_settings: {}
diff --git a/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.processor.schema.yml b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.processor.schema.yml
new file mode 100644
index 000000000..3b66a7b1e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.processor.schema.yml
@@ -0,0 +1,176 @@
+plugin.plugin_configuration.facets_processor.*:
+ type: config_object
+
+plugin.plugin_configuration.facets_processor.count_widget_widget_order:
+ type: mapping
+ label: 'Count widget order'
+ mapping:
+ sort:
+ type: string
+ label: sort order
+
+plugin.plugin_configuration.facets_processor.display_value_widget_order:
+ type: mapping
+ label: 'Display value widget order'
+ mapping:
+ sort:
+ type: string
+ label: sort order
+
+plugin.plugin_configuration.facets_processor.translate_entity:
+ type: mapping
+ label: 'Translate entity'
+ mapping:
+ sort:
+ type: boolean
+ label: translate entity
+
+plugin.plugin_configuration.facets_processor.term_weight_widget_order:
+ type: mapping
+ label: 'Display term widget order'
+ mapping:
+ sort:
+ type: string
+ label: sort order
+
+plugin.plugin_configuration.facets_processor.exclude_specified_items:
+ type: mapping
+ label: 'Exclude specified items'
+ mapping:
+ exclude:
+ type: string
+ label: Exclude
+ regex:
+ type: boolean
+ label: Regex
+ invert:
+ type: boolean
+ label: Invert
+
+plugin.plugin_configuration.facets_processor.raw_value_widget_order:
+ type: mapping
+ label: 'Raw value widget order'
+ mapping:
+ sort:
+ type: string
+ label: sort order
+
+plugin.plugin_configuration.facets_processor.active_widget_order:
+ type: mapping
+ label: 'Active widget order'
+ mapping:
+ sort:
+ type: string
+ label: sort order
+
+plugin.plugin_configuration.facets_processor.count_widget_order:
+ type: mapping
+ label: 'Active widget order'
+ mapping:
+ sort:
+ type: string
+ label: sort order
+
+plugin.plugin_configuration.facets_processor.count_limit:
+ type: mapping
+ label: 'Count limit widget'
+ mapping:
+ minimum_items:
+ type: integer
+ label: 'Mimimum amount of items to show.'
+ maximum_items:
+ type: integer
+ label: 'Maximum amount of items to show.'
+
+plugin.plugin_configuration.facets_processor.boolean_item:
+ type: mapping
+ label: 'Boolean processor'
+ mapping:
+ on_value:
+ type: label
+ label: 'On value'
+ off_value:
+ type: label
+ label: 'Off value'
+
+plugin.plugin_configuration.facets_processor.combine_processor:
+ type: sequence
+ label: 'Combine facets processor'
+ sequence:
+ type: mapping
+ label: Mapping for a processor
+ mapping:
+ combine:
+ type: boolean
+ label: 'Combine this facet'
+ mode:
+ type: string
+ label: 'Combination mode'
+
+plugin.plugin_configuration.facets_processor.dependent_processor:
+ type: sequence
+ label: 'Dependent facet processor'
+ sequence:
+ type: mapping
+ label: Mapping for a processor
+ mapping:
+ enable:
+ type: boolean
+ label: 'Enable for this facet'
+ condition:
+ type: string
+ label: 'Type of condition'
+ values:
+ type: label
+ label: 'The value of the condition'
+ negate:
+ type: boolean
+ label: 'Should the condition be negated'
+
+plugin.plugin_configuration.facets_processor.show_siblings_processor:
+ type: mapping
+ label: 'Show siblings processor'
+ mapping:
+ show_parent_siblings:
+ type: boolean
+ label: 'Show parents'
+
+plugin.plugin_configuration.facets_processor.date_item:
+ type: mapping
+ label: 'Date item processor'
+ mapping:
+ date_display:
+ type: string
+ label: 'Date display'
+ granularity:
+ type: integer
+ label: 'Granularity'
+ date_format:
+ type: string
+ label: 'Date format'
+ hierarchy:
+ type: boolean
+ label: 'Hierarchy'
+
+plugin.plugin_configuration.facets_processor.granularity_item:
+ type: mapping
+ label: 'Granular item processor'
+ mapping:
+ granularity:
+ type: integer
+ label: 'Granularity'
+ min_value:
+ type: integer
+ label: 'Minimum value'
+ max_value:
+ type: integer
+ label: 'Maximum value'
+ include_lower:
+ type: boolean
+ label: 'Include lower bounds'
+ include_upper:
+ type: boolean
+ label: 'Include upper bounds'
+ include_edges:
+ type: boolean
+ label: 'Include first lower and last upper bound'
diff --git a/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.widgets.schema.yml b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.widgets.schema.yml
new file mode 100644
index 000000000..478a760aa
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/config/schema/facets.widgets.schema.yml
@@ -0,0 +1,59 @@
+# Default config schema that all widgets can extend.
+facet.widget.default_config:
+ type: mapping
+ label: 'Default widget configuration'
+ mapping:
+ show_numbers:
+ type: boolean
+ label: 'Show counts'
+
+# Default schema for facet widgets of unknown type.
+facet.widget.config.*:
+ type: facet.widget.default_config
+
+# Config schema for dropdown, you can find the implementation in
+# Drupal\facets\Plugin\facets\widget\DropdownWidget. Options for this widget are
+# "show counts" and "default option label".
+facet.widget.config.dropdown:
+ type: facet.widget.default_config
+ label: 'Dropdown widget configuration'
+ mapping:
+ default_option_label:
+ type: label
+ label: 'Default option label'
+
+# Config schema for links, you can find the implementation in
+# Drupal\facets\Plugin\facets\widget\LinksWidget. Options for this widget are
+# "soft limit" and "show counts".
+facet.widget.config.links:
+ type: facet.widget.default_config
+ label: 'List of links widget configuration'
+ mapping:
+ soft_limit:
+ type: integer
+ label: 'Soft limit'
+ show_reset_link:
+ type: boolean
+ label: 'Show reset link'
+ reset_text:
+ type: label
+ label: 'Reset link text'
+ hide_reset_when_no_selection:
+ type: boolean
+ label: 'Hide reset link when no facet item is selected'
+ soft_limit_settings:
+ type: mapping
+ label: 'Soft limit settings'
+ mapping:
+ show_less_label:
+ type: label
+ label: 'Show less label'
+ show_more_label:
+ type: label
+ label: 'Show more label'
+
+# Config schema for checkbox, you can find the implementation in
+# Drupal\facets\Plugin\facets\widget\CheckboxWidget. Options for this widget are
+# "soft limit" and "show counts".
+facet.widget.config.checkbox:
+ type: facet.widget.config.links
diff --git a/frontend/drupal9/web/modules/contrib/facets/css/facets.admin.css b/frontend/drupal9/web/modules/contrib/facets/css/facets.admin.css
new file mode 100644
index 000000000..5cea75960
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/css/facets.admin.css
@@ -0,0 +1,44 @@
+/**
+ * @file
+ * Administration styles for the Facets module.
+ */
+
+/*
+ * Facets overview page
+ */
+.facets-groups-list {
+ margin-bottom: 2em;
+}
+.facets-groups-list tr {
+ border-bottom: none;
+}
+
+.facets-groups-list tr.facet-source {
+ border-top: 1px solid #e6e4df;
+}
+.facets-groups-list tr.facet:last-of-type {
+ border-bottom: 1px solid #e6e4df;
+}
+
+.facets-groups-list tr.facet-source .facets-type,
+.facets-groups-list tr.facet-source .search-api-title {
+ font-weight: bold;
+}
+.facets-groups-list tr.facet .facets-summary-type,
+.facets-groups-list tr.facet .facets-type {
+ padding-left: 3em;
+}
+
+/*
+ * Facets Display page
+ */
+.facets-processor-settings-sorting {
+ margin-bottom: -7px;
+ margin-left: 20px;
+ margin-top: -16px;
+}
+
+.facets-processor-settings-facet {
+ margin-left: 20px;
+ margin-bottom: 20px;
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/css/general.css b/frontend/drupal9/web/modules/contrib/facets/css/general.css
new file mode 100644
index 000000000..8d2778cdf
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/css/general.css
@@ -0,0 +1,3 @@
+.facets-widget-dropdown label {
+ display: none;
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/css/hierarchical.css b/frontend/drupal9/web/modules/contrib/facets/css/hierarchical.css
new file mode 100644
index 000000000..f0adb1c34
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/css/hierarchical.css
@@ -0,0 +1,3 @@
+.block-facets ul ul li {
+ margin-left: 10px;
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.api.php b/frontend/drupal9/web/modules/contrib/facets/facets.api.php
new file mode 100644
index 000000000..187708547
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.api.php
@@ -0,0 +1,28 @@
+=')
+ && \Drupal::service('module_handler')->moduleExists('block_content')
+ ) {
+ // block_content_update_8600() adds some fields to Blocks that makes
+ // facets_update_8006() fail if upgraded at the same time.
+ $dependencies['facets'][8006] = [
+ 'block_content' => 8600,
+ ];
+ }
+
+ return $dependencies;
+}
+
+/**
+ * Convert facets on Search Api facet sources to use the display plugin.
+ */
+function facets_update_8001() {
+ // We changed the way we work with search api facet sources, we're now using
+ // the SearchApiDisplay plugins that search api ships with. This consolidates
+ // the external points for facets, sorts, autocomplete and others. This
+ // refactor made us a better member of the Search API family. It also makes it
+ // easier for other modules that provide a display to support facets, for
+ // example, for the search_api_page module.
+ //
+ // This only works for the 3 default plugins that we previously shipped. So
+ // only views that have a page, block, or rest display. The id will get
+ // replaced from views_page:foo to search_api:views_page__foo.
+ $old_ids = ['views_page', 'views_block', 'views_rest'];
+
+ /** @var \Drupal\facets\FacetInterface[] $entities */
+ $entities = Facet::loadMultiple();
+ foreach ($entities as $entity) {
+ $facetSourceId = $entity->getFacetSourceId();
+ foreach ($old_ids as $id) {
+ if (strpos($facetSourceId, $id) !== FALSE) {
+ $new_id = str_replace($id . ':', 'search_api:' . $id . '__', $facetSourceId);
+ $entity->setFacetSourceId($new_id);
+ $entity->save();
+ }
+ }
+ }
+
+ /** @var \Drupal\facets\FacetSourceInterface[] $facetsources */
+ $facetsources = FacetSource::loadMultiple();
+ foreach ($facetsources as $facetsource) {
+ $as_array = $facetsource->toArray();
+
+ // Replace id and name to new naming scheme.
+ foreach ($old_ids as $id) {
+ if (strpos($as_array['id'], $id) !== FALSE) {
+ $as_array['id'] = str_replace($id . '__', 'search_api__' . $id . '__', $as_array['id']);
+ $as_array['name'] = str_replace($id . ':', 'search_api:' . $id . '__', $as_array['name']);
+ }
+ }
+
+ // Create new source.
+ unset($as_array['uuid']);
+ $existing = FacetSource::load($as_array['id']);
+ if (!$existing) {
+ FacetSource::create($as_array)->save();
+
+ // Delete old facet source.
+ $facetsource->delete();
+ }
+ }
+}
+
+/**
+ * Remove 'other_facet' plugin for older versions of facets.
+ */
+function facets_update_8002() {
+ $database = \Drupal::database();
+ $query = $database
+ ->query("SELECT * FROM {config} WHERE data LIKE '%other_facet%'");
+ $results = $query->fetchAll();
+
+ foreach ($results as $result) {
+ $data = unserialize($result->data);
+ if (isset($data['visibility']['other_facet'])) {
+ unset($data['visibility']['other_facet']);
+ }
+
+ $database->update('config')
+ ->fields([
+ 'data' => serialize($data),
+ ])
+ ->condition('name', $result->name)
+ ->execute();
+ }
+}
+
+/**
+ * WARNING: Facets core search support has been moved into a separate project.
+ *
+ * If you are using this feature, you need do download the "facets_core_search"
+ * module from drupal.org."
+ */
+function facets_update_8003() {
+ \Drupal::database()->delete('key_value')
+ ->condition('collection', 'system.schema')
+ ->condition('name', 'core_search_facets')
+ ->execute();
+}
+
+/**
+ * Migrate facets with date widget to use date processor and links widget.
+ */
+function facets_update_8004() {
+ foreach (Facet::loadMultiple() as $facet) {
+ $widget = $facet->getWidget();
+ if ($widget['type'] === 'datebasic') {
+ // Set widget to use links instead.
+ $facet->setWidget('links', ['show_numbers' => $widget['config']['show_numbers']]);
+ // Migrate widget to processor settings and enable date_item processor.
+ $settings = [
+ 'date_format' => $widget['config']['date_display'],
+ 'granularity' => $widget['config']['granularity'],
+ 'date_display' => 'actual_date',
+ ];
+ if ($widget['config']['display_relative']) {
+ $settings['date_display'] = 'relative_date';
+ }
+ $facet->addProcessor([
+ 'processor_id' => 'date_item',
+ 'weights' => ['build' => 35],
+ 'settings' => $settings,
+ ]);
+ $facet->save();
+ }
+ }
+}
+
+/**
+ * Migrate facets with granular widget to use date processors + links widget.
+ */
+function facets_update_8005() {
+ foreach (Facet::loadMultiple() as $facet) {
+ $widget = $facet->getWidget();
+ if ($widget['type'] === 'numericgranular') {
+ // Set widget to use links instead.
+ $facet->setWidget('links', ['show_numbers' => $widget['config']['show_numbers']]);
+ // Migrate widget to processor settings and enable date_item processor.
+ $settings = [
+ 'granularity' => $widget['config']['granularity'],
+ ];
+ $facet->addProcessor([
+ 'processor_id' => 'granularity_item',
+ 'weights' => ['build' => 35],
+ 'settings' => $settings,
+ ]);
+ $facet->save();
+ }
+ }
+}
+
+/**
+ * Update facet blocks configuration with a block id used for AJAX support.
+ */
+function facets_update_8006() {
+ $query = \Drupal::entityQuery('block')
+ ->condition('plugin', 'facet_block', 'STARTS_WITH')
+ ->execute();
+
+ foreach ($query as $block_id) {
+ $block = Block::load($block_id);
+ $configuration = $block->get('settings');
+ $configuration['block_id'] = $block_id;
+ $block->set('settings', $configuration);
+ $block->save();
+ }
+}
+
+/**
+ * Resave facets for consistent configuration export.
+ */
+function facets_update_8007() {
+ // Moved to facets_update_8009().
+}
+
+/**
+ * Support different hierarchy plugin types.
+ */
+function facets_update_8008() {
+ $config_factory = \Drupal::configFactory();
+
+ foreach ($config_factory->listAll('facets.facet.') as $facet_config_name) {
+ $facet = $config_factory->getEditable($facet_config_name);
+ $facet->set('hierarchy', ['type' => 'taxonomy', 'config' => []]);
+ $facet->save(TRUE);
+ }
+}
+
+/**
+ * Resave facets for consistent configuration export.
+ */
+function facets_update_8009() {
+ $facets = Facet::loadMultiple();
+ foreach ($facets as $facet) {
+ $facet->save();
+ }
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.libraries.yml b/frontend/drupal9/web/modules/contrib/facets/facets.libraries.yml
new file mode 100644
index 000000000..04ce10c40
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.libraries.yml
@@ -0,0 +1,82 @@
+drupal.facets.index-active-formatters:
+ version: VERSION
+ js:
+ js/index-active-formatters.js: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+ - core/jquery.once
+
+drupal.facets.edit-facet:
+ version: VERSION
+ js:
+ js/edit-facet.js: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+
+drupal.facets.admin_css:
+ version: VERSION
+ css:
+ theme:
+ css/facets.admin.css: {}
+
+widget:
+ version: VERSION
+ js:
+ js/base-widget.js: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+ - core/jquery.once
+
+drupal.facets.link-widget:
+ version: VERSION
+ js:
+ js/link-widget.js: {}
+ dependencies:
+ - facets/widget
+
+drupal.facets.checkbox-widget:
+ version: VERSION
+ js:
+ js/checkbox-widget.js: {}
+ dependencies:
+ - facets/widget
+
+drupal.facets.hierarchical:
+ version: VERSION
+ css:
+ theme:
+ css/hierarchical.css: {}
+
+drupal.facets.general:
+ version: VERSION
+ css:
+ theme:
+ css/general.css: {}
+
+drupal.facets.dropdown-widget:
+ version: VERSION
+ js:
+ js/dropdown-widget.js: {}
+ dependencies:
+ - facets/widget
+
+soft-limit:
+ version: VERSION
+ js:
+ js/soft-limit.js: {}
+ dependencies:
+ - core/jquery
+ - core/jquery.once
+ - core/drupal
+ - core/drupalSettings
+
+drupal.facets.views-ajax:
+ js:
+ js/facets-views-ajax.js: {}
+ dependencies:
+ - facets/widget
+ - core/drupalSettings
+ - core/drupal.ajax
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.link_relation_types.yml b/frontend/drupal9/web/modules/contrib/facets/facets.link_relation_types.yml
new file mode 100644
index 000000000..484c4465f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.link_relation_types.yml
@@ -0,0 +1,5 @@
+# See core/core.link_relation_types.yml
+settings-form:
+ description: A form where a resource of this type can be edited. See customize-form.
+clone-form:
+ description: A form where a resource of this type can be duplicated. See duplicate-form.
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.links.action.yml b/frontend/drupal9/web/modules/contrib/facets/facets.links.action.yml
new file mode 100644
index 000000000..7b01b0145
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.links.action.yml
@@ -0,0 +1,5 @@
+entity.facets_facet.add_form:
+ route_name: entity.facets_facet.add_form
+ title: 'Add facet'
+ appears_on:
+ - entity.facets_facet.collection
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.links.contextual.yml b/frontend/drupal9/web/modules/contrib/facets/facets.links.contextual.yml
new file mode 100644
index 000000000..46682869a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.links.contextual.yml
@@ -0,0 +1,4 @@
+entity.facets_facet.edit_form:
+ title: 'Edit facet'
+ route_name: 'entity.facets_facet.edit_form'
+ group: facets_facet
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.links.menu.yml b/frontend/drupal9/web/modules/contrib/facets/facets.links.menu.yml
new file mode 100644
index 000000000..bc74b4e92
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.links.menu.yml
@@ -0,0 +1,6 @@
+entity.facets_facet.collection:
+ title: Facets
+ description: 'Create and configure facets and configure existing facet sources.'
+ route_name: entity.facets_facet.collection
+ weight: 30
+ parent: system.admin_config_search
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.links.task.yml b/frontend/drupal9/web/modules/contrib/facets/facets.links.task.yml
new file mode 100644
index 000000000..90d6a4b6d
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.links.task.yml
@@ -0,0 +1,9 @@
+entity.facets_facet.edit_form:
+ title: 'Edit'
+ route_name: entity.facets_facet.edit_form
+ base_route: entity.facets_facet.edit_form
+
+entity.facets_facet.settings_form:
+ title: 'Facet settings'
+ route_name: entity.facets_facet.settings_form
+ base_route: entity.facets_facet.edit_form
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.module b/frontend/drupal9/web/modules/contrib/facets/facets.module
new file mode 100644
index 000000000..bc7d27e65
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.module
@@ -0,0 +1,367 @@
+' . t('About') . '';
+ $output .= '' . t('Facets test') . '
';
+ return $output;
+
+ case 'entity.facets_facet.collection':
+ $output = '';
+ $output .= '' . t('Below is a list of facets grouped by facetsources they are associated with. A facetsource is the instance where the facet does the actual filtering, for example a View on a Search API index.') . '
';
+ $output .= '' . t('The facets weight can be changed with drag and drop within the same facet source. Although you can drag and drop a facet under any facet source, this change will not be performed on save.') . '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function facets_theme($existing, $type, $theme, $path) {
+ return [
+ 'facets_result_item' => [
+ 'variables' => [
+ 'facet' => NULL,
+ 'raw_value' => '',
+ 'value' => '',
+ 'show_count' => FALSE,
+ 'count' => NULL,
+ 'is_active' => FALSE,
+ ],
+ ],
+ 'facets_item_list' => [
+ 'variables' => [
+ 'facet' => NULL,
+ 'items' => [],
+ 'title' => '',
+ 'list_type' => 'ul',
+ 'wrapper_attributes' => [],
+ 'attributes' => [],
+ 'empty' => NULL,
+ 'context' => [],
+ ],
+ ],
+ ];
+}
+
+/**
+ * Implements hook_entity_presave().
+ *
+ * We implement this to make sure that a facet gets removed on view updates, so
+ * we don't get broken facet blocks.
+ */
+function facets_entity_presave(EntityInterface $entity) {
+ // Make sure that we only react on view entities with changed displays.
+ if ($entity instanceof View && !empty($entity->original)) {
+ if ($entity->original->get('display') != $entity->get('display')) {
+
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */
+ $facet_source_plugin_manager = \Drupal::getContainer()
+ ->get('plugin.manager.facets.facet_source');
+ $definitions = $facet_source_plugin_manager->getDefinitions();
+
+ // Setup an array of sources that are deleted.
+ $sources = [];
+ foreach ($entity->original->get('display') as $k => $display) {
+ // Check if the current display is also a facet source plugin and that
+ // is removed from the view. We use the double underscore here to make
+ // sure that we use core convention of "plugin:derived_plugin".
+ $facets_source_plugin_id = 'search_api:views_' . $display['display_plugin'] . '__' . $entity->id() . '__' . $display['id'];
+ if (array_key_exists($facets_source_plugin_id, $definitions) && !array_key_exists($k, $entity->get('display'))) {
+ $entity_id = str_replace(':', '__', $facets_source_plugin_id);
+ $source_entity = FacetSource::load($entity_id);
+ $sources[] = $facets_source_plugin_id;
+ if (!is_null($source_entity)) {
+ $source_entity->delete();
+ }
+ }
+ }
+
+ // Loop over all deleted sources and delete the facets that were linked to
+ // that source.
+ if (count($sources) > 0) {
+ /** @var \Drupal\facets\FacetManager\DefaultFacetManager $fm */
+ $fm = \Drupal::getContainer()->get('facets.manager');
+ foreach ($sources as $source) {
+ $facets = $fm->getFacetsByFacetSourceId($source);
+ foreach ($facets as $facet) {
+ $facet->delete();
+ }
+ }
+ }
+ $facet_source_plugin_manager->clearCachedDefinitions();
+ }
+ }
+
+}
+
+/**
+ * Implements hook_preprocess_block().
+ *
+ * Adds a class for the widget to the facet block to allow for more specific
+ * styling.
+ */
+function facets_preprocess_block(&$variables) {
+ if ($variables['configuration']['provider'] == 'facets') {
+ // Hide the block if it's empty.
+ if (!empty($variables['elements']['content'][0]['#attributes']['class']) && in_array('facet-hidden', $variables['elements']['content'][0]['#attributes']['class'])) {
+ // Add the Drupal class for hiding this for everyone, including screen
+ // readers. See hidden.module.css in the core system module.
+ $variables['attributes']['class'][] = 'hidden';
+ }
+ if (!empty($variables['derivative_plugin_id'])) {
+ $facet = Facet::load($variables['derivative_plugin_id']);
+ $variables['attributes']['class'][] = 'block-facet--' . Html::cleanCssIdentifier($facet->getWidget()['type']);
+ }
+ }
+}
+
+/**
+ * Implements hook_entity_predelete().
+ *
+ * We implement this hook to make sure that facet source plugins are cleared
+ * when a view is deleted. It also deletes facets that are created on those
+ * plugins.
+ */
+function facets_entity_predelete(EntityInterface $entity) {
+ if ($entity instanceof View) {
+ $facet_source_plugin_manager = \Drupal::getContainer()
+ ->get('plugin.manager.facets.facet_source');
+
+ $definitions = $facet_source_plugin_manager->getDefinitions();
+
+ if (!is_array($definitions)) {
+ return;
+ }
+
+ foreach ($definitions as $plugin_id => $definition) {
+ if (strpos($plugin_id, 'search_api:' . $entity->id() . '__') !== FALSE) {
+ try {
+ $facetManager = \Drupal::getContainer()->get('facets.manager');
+ }
+ catch (ServiceNotFoundException $e) {
+ \Drupal::logger('facets')->log(RfcLogLevel::DEBUG, 'Facet manager not found on trying to delete a view.');
+ return;
+ }
+
+ $facets = $facetManager->getFacetsByFacetSourceId($plugin_id);
+ foreach ($facets as $facet) {
+ $facet->delete();
+ }
+ }
+ }
+
+ // Clear cached plugin definitions for facet source to make sure we don't
+ // show stale data.
+ $facet_source_plugin_manager->clearCachedDefinitions();
+ }
+}
+
+/**
+ * Prepares variables for facets item list templates.
+ *
+ * Default template: facets-item-list.html.twig.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - items: An array of items to be displayed in the list. Each item can be
+ * either a string or a render array. If #type, #theme, or #markup
+ * properties are not specified for child render arrays, they will be
+ * inherited from the parent list, allowing callers to specify larger
+ * nested lists without having to explicitly specify and repeat the
+ * render properties for all nested child lists.
+ * - title: A title to be prepended to the list.
+ * - list_type: The type of list to return (e.g. "ul", "ol").
+ * - wrapper_attributes: HTML attributes to be applied to the list wrapper.
+ *
+ * @see https://www.drupal.org/node/1842756
+ */
+function facets_preprocess_facets_item_list(array &$variables) {
+ if ($variables['facet'] !== NULL && $variables['facet']->get('show_title') === TRUE) {
+ $variables['title'] = $variables['facet']->label();
+ }
+ template_preprocess_item_list($variables);
+}
+
+/**
+ * Implements hook_system_breadcrumb_alter().
+ */
+function facets_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_manager */
+ $facet_source_manager = \Drupal::service('plugin.manager.facets.facet_source');
+
+ /** @var \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager */
+ $facet_manager = \Drupal::service('facets.manager');
+
+ /** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
+ $entity_type_manager = \Drupal::service('entity_type.manager');
+
+ /** @var \Drupal\Core\Entity\EntityStorageInterface $facet_source_storage */
+ $facet_source_storage = $entity_type_manager->getStorage('facets_facet_source');
+
+ $facet_sources_definitions = $facet_source_manager->getDefinitions();
+
+ $facets_url_generator = \Drupal::service('facets.utility.url_generator');
+
+ // No facet sources found, so don't do anything.
+ if (empty($facet_sources_definitions)) {
+ return;
+ }
+
+ foreach ($facet_sources_definitions as $definition) {
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginBase $facet_source_plugin */
+ $facetsource_id = $definition['id'];
+ $facet_source_plugin = $facet_source_manager->createInstance($facetsource_id);
+
+ // If the current facet source is not being rendered, don't do anything with
+ // these facet sources.
+ if (!$facet_source_plugin->isRenderedInCurrentRequest()) {
+ continue;
+ }
+
+ $source_id = str_replace(':', '__', $facetsource_id);
+ /** @var \Drupal\facets\FacetSourceInterface $facet_source */
+ $facet_source = $facet_source_storage->load($source_id);
+
+ // If the facet source is not loaded, or the facet source doesn't have
+ // breadcrumbs enabled, don't do anything.
+ if (!($facet_source && !empty($facet_source->getBreadcrumbSettings()['active']))) {
+ continue;
+ }
+
+ // Add the required cacheability metadata.
+ $breadcrumb->addCacheContexts(['url']);
+ $breadcrumb->addCacheableDependency($facet_source);
+
+ // Process the facets if they are not already processed.
+ $facet_manager->processFacets($facetsource_id);
+ $facets = $facet_manager->getFacetsByFacetSourceId($facetsource_id);
+
+ // Sort facets by weight.
+ uasort($facets, function (FacetInterface $a, FacetInterface $b) {
+ return (int) $a->getWeight() - $b->getWeight();
+ });
+
+ /** @var \Drupal\facets\UrlProcessor\UrlProcessorPluginManager $url_processor_manager */
+ $url_processor_manager = \Drupal::service('plugin.manager.facets.url_processor');
+
+ // Get active facets and results to use them at building the crumbs.
+ $active_results = [];
+ $active_facets = [];
+ foreach ($facets as $facet) {
+ if (count($facet->getActiveItems()) > 0) {
+ // Add the facet as a cacheable dependency.
+ $breadcrumb->addCacheableDependency($facet);
+ /** @var \Drupal\facets\UrlProcessor\UrlProcessorInterface $url_processor */
+ $url_processor = $url_processor_manager->createInstance($facet_source->getUrlProcessorName(), ['facet' => $facet]);
+ $facet_manager->build($facet);
+
+ foreach ($facet->getResults() as $result) {
+ if ($result->isActive() || $result->hasActiveChildren()) {
+ // Clone the result so we can mark it as inactive to be added to the
+ // url parameters when calling buildUrls.
+ $cloned_result = clone $result;
+ $cloned_result->setActiveState(FALSE);
+ $active_results[$facet->id()][] = $cloned_result;
+ }
+ }
+ if (!empty($active_results[$facet->getUrlAlias()])) {
+ $url_processor->buildUrls($facet, $active_results[$facet->getUrlAlias()]);
+ }
+ $active_facets[$facet->id()] = $facet;
+ }
+ }
+
+ // @todo find a better way to construct the url for a crumb maybe url
+ // processor will have a function to get params for a result
+ // without all the other request parameters; with this we could implement:
+ // @see https://www.drupal.org/node/2861586
+ // @todo handle not grouped facets.
+ /** @var \Drupal\facets\Result\ResultInterface[] $facet_results */
+ foreach ($active_results as $facet_id => $facet_results) {
+ $facet_used_result[$facet_id] = [];
+ $facet_crumb_items = [];
+
+ // Because we can't get the desired display value trough a url processor
+ // method we iterate each result url and remove the facet params that
+ // haven't been used on previous crumbs.
+ foreach ($facet_results as $res) {
+ $facet_used_result[$facet_id][] = $res->getRawValue();
+ $facet_crumb_items[] = $res->getDisplayValue();
+ }
+
+ sort($facet_crumb_items);
+
+ $facet_url = $facets_url_generator->getUrl($facet_used_result, FALSE);
+ if (!empty($facet_source->getBreadcrumbSettings()['before'])) {
+ $crumb_text = $active_facets[$facet_id]->label() . ': ' . implode(', ', $facet_crumb_items);
+ }
+ else {
+ $crumb_text = implode(', ', $facet_crumb_items);
+ };
+ $link = Link::fromTextAndUrl($crumb_text, $facet_url);
+ $breadcrumb->addLink($link);
+ }
+ }
+}
+
+/**
+ * Implements hook_language_switch_links_alter().
+ */
+function facets_language_switch_links_alter(array &$links, $type, Url $url) {
+ /** @var \Drupal\facets\LanguageSwitcherLinksAlterer $alterer */
+ $alterer = \Drupal::service('facets.language_switcher_links_alterer');
+ $alterer->alter($links, $type, $url);
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function facets_form_facets_facet_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+ $facet_sources = [];
+ foreach (\Drupal::service('plugin.manager.facets.facet_source')->getDefinitions() as $facet_source_id => $definition) {
+ $facet_sources[$definition['id']] = !empty($definition['label']) ? $definition['label'] : $facet_source_id;
+ }
+
+ if (count($facet_sources) == 0) {
+ unset($form['actions']);
+ }
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function facets_theme_suggestions_facets_result_item(array $variables) {
+ $suggestions = [];
+ $facet = $variables['facet'];
+ if ($facet instanceof FacetInterface) {
+ $suggestions[] = 'facets_result_item__' . $facet->getWidget()['type'];
+ $suggestions[] = 'facets_result_item__' . $facet->getWidget()['type'] . '__' . $facet->id();
+ }
+ return $suggestions;
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.permissions.yml b/frontend/drupal9/web/modules/contrib/facets/facets.permissions.yml
new file mode 100644
index 000000000..5c6d2e4ba
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.permissions.yml
@@ -0,0 +1,3 @@
+'administer facets':
+ title: 'Administer Facets'
+ description: 'Create and configure Facets for your Search pages.'
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.plugin_type.yml b/frontend/drupal9/web/modules/contrib/facets/facets.plugin_type.yml
new file mode 100644
index 000000000..8db890128
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.plugin_type.yml
@@ -0,0 +1,24 @@
+facets_processor:
+ label: Facets processor
+ plugin_manager_service_id: plugin.manager.facets.processor
+ plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+facets_url_processor:
+ label: Facets URL processor
+ plugin_manager_service_id: plugin.manager.facets.url_processor
+ plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+facets_facet_source:
+ label: Facets source
+ plugin_manager_service_id: plugin.manager.facets.facet_source
+ plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+facets_widget:
+ label: Facets widget
+ plugin_manager_service_id: plugin.manager.facets.widget
+ plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+facets_query_type:
+ label: Facets query type
+ plugin_manager_service_id: plugin.manager.facets.query_type
+ plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.routing.yml b/frontend/drupal9/web/modules/contrib/facets/facets.routing.yml
new file mode 100644
index 000000000..9f2691171
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.routing.yml
@@ -0,0 +1,57 @@
+entity.facets_facet.collection:
+ path: '/admin/config/search/facets'
+ defaults:
+ _title: 'Facets'
+ _entity_list: 'facets_facet'
+ requirements:
+ _entity_create_access: 'facets_facet'
+
+entity.facets_facet.add_form:
+ path: '/admin/config/search/facets/add-facet'
+ defaults:
+ _entity_form: 'facets_facet.default'
+ requirements:
+ _entity_create_access: 'facets_facet'
+
+entity.facets_facet.edit_form:
+ path: '/admin/config/search/facets/{facets_facet}/edit'
+ defaults:
+ _entity_form: 'facets_facet.edit'
+ requirements:
+ _entity_access: 'facets_facet.edit'
+
+entity.facets_facet.delete_form:
+ path: '/admin/config/search/facets/{facets_facet}/delete'
+ defaults:
+ _entity_form: 'facets_facet.delete'
+ requirements:
+ _entity_access: 'facets_facet.delete'
+
+entity.facets_facet.settings_form:
+ path: '/admin/config/search/facets/{facets_facet}/settings'
+ defaults:
+ _entity_form: 'facets_facet.settings'
+ requirements:
+ _entity_access: 'facets_facet.edit'
+
+entity.facets_facet.clone_form:
+ path: '/admin/config/search/facets/{facets_facet}/clone'
+ defaults:
+ _entity_form: 'facets_facet.clone'
+ requirements:
+ _entity_access: 'facets_facet.edit'
+
+entity.facets_facet_source.edit_form:
+ path: '/admin/config/search/facets/facet-sources/{facets_facet_source}/edit'
+ defaults:
+ _controller: '\Drupal\facets\Controller\FacetSourceController::facetSourceConfigForm'
+ _title: 'Edit facet source configuration'
+ requirements:
+ _entity_create_access: 'facets_facet'
+
+facets.block.ajax:
+ path: '/facets-block-ajax'
+ defaults:
+ _controller: '\Drupal\facets\Controller\FacetBlockAjaxController::ajaxFacetBlockView'
+ requirements:
+ _access: 'TRUE'
diff --git a/frontend/drupal9/web/modules/contrib/facets/facets.services.yml b/frontend/drupal9/web/modules/contrib/facets/facets.services.yml
new file mode 100644
index 000000000..103f94a62
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/facets.services.yml
@@ -0,0 +1,48 @@
+services:
+ plugin.manager.facets.query_type:
+ class: Drupal\facets\QueryType\QueryTypePluginManager
+ parent: default_plugin_manager
+ plugin.manager.facets.widget:
+ class: Drupal\facets\Widget\WidgetPluginManager
+ parent: default_plugin_manager
+ plugin.manager.facets.facet_source:
+ class: Drupal\facets\FacetSource\FacetSourcePluginManager
+ parent: default_plugin_manager
+ plugin.manager.facets.processor:
+ class: Drupal\facets\Processor\ProcessorPluginManager
+ arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@string_translation']
+ plugin.manager.facets.url_processor:
+ class: Drupal\facets\UrlProcessor\UrlProcessorPluginManager
+ parent: default_plugin_manager
+ plugin.manager.facets.hierarchy:
+ class: Drupal\facets\Hierarchy\HierarchyPluginManager
+ parent: default_plugin_manager
+ facets.manager:
+ class: Drupal\facets\FacetManager\DefaultFacetManager
+ arguments:
+ - '@plugin.manager.facets.query_type'
+ - '@plugin.manager.facets.facet_source'
+ - '@plugin.manager.facets.processor'
+ - '@entity_type.manager'
+ facets.utility.date_handler:
+ class: Drupal\facets\Utility\FacetsDateHandler
+ arguments:
+ - '@date.formatter'
+ facets.utility.url_generator:
+ class: Drupal\facets\Utility\FacetsUrlGenerator
+ arguments:
+ - '@plugin.manager.facets.url_processor'
+ - '@entity_type.manager'
+ facets.configuration_subscriber:
+ class: Drupal\facets\EventSubscriber\ConfigurationSubscriber
+ arguments: ['@plugin.manager.block']
+ tags:
+ - { name: event_subscriber }
+ facets.search_api_subscriber:
+ class: Drupal\facets\EventSubscriber\SearchApiSubscriber
+ arguments: ['@facets.manager']
+ tags:
+ - { name: event_subscriber }
+ facets.language_switcher_links_alterer:
+ class: Drupal\facets\LanguageSwitcherLinksAlterer
+ arguments: ['@language_manager', '@cache.default', '@entity_type.manager', '@plugin.manager.facets.url_processor']
diff --git a/frontend/drupal9/web/modules/contrib/facets/js/base-widget.js b/frontend/drupal9/web/modules/contrib/facets/js/base-widget.js
new file mode 100644
index 000000000..8ca3e6c0a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/js/base-widget.js
@@ -0,0 +1,60 @@
+/**
+ * @file
+ * Provides base widget behaviours.
+ */
+
+(function ($, Drupal) {
+
+ 'use strict';
+
+ /**
+ * Handles "facets_filter" event and triggers "facets_filtering".
+ *
+ * The facets module will listend and trigger defined events on elements with
+ * class: "js-facets-widget".
+ *
+ * Events are doing following:
+ * "facets_filter" - widget should trigger this event. The facets module will
+ * handle it accordingly in case of AJAX and Non-AJAX views.
+ * "facets_filtering" - The facets module will trigger this event before
+ * filter is executed.
+ *
+ * This is an example how to trigger "facets_filter" event for your widget:
+ * $('.my-custom-widget.js-facets-widget')
+ * .once('my-custom-widget-on-change')
+ * .on('change', function () {
+ * // In this example $(this).val() will provide needed URL.
+ * $(this).trigger('facets_filter', [ $(this).val() ]);
+ * });
+ *
+ * The facets module will trigger "facets_filtering" before filter is
+ * executed. Widgets can listen on "facets_filtering" event and react before
+ * filter is executed. Most common use case is to disable widget. When you
+ * disable widget, a user will not be able to trigger new "facets_filter"
+ * event before initial filter request is finished.
+ *
+ * This is an example how to handle "facets_filtering":
+ * $('.my-custom-widget.js-facets-widget')
+ * .once('my-custom-widget-on-facets-filtering')
+ * .on('facets_filtering.my_widget_module', function () {
+ * // Let's say, that widget can be simply disabled (fe. select).
+ * $(this).prop('disabled', true);
+ * });
+ *
+ * You should namespace events for your module widgets. With namespaced events
+ * you have better control on your handlers and if it's needed, you can easier
+ * register/deregister them.
+ */
+ Drupal.behaviors.facetsFilter = {
+ attach: function (context) {
+ $('.js-facets-widget', context)
+ .once('js-facet-filter')
+ .on('facets_filter.facets', function (event, url) {
+ $('.js-facets-widget').trigger('facets_filtering');
+
+ window.location = url;
+ });
+ }
+ };
+
+})(jQuery, Drupal);
diff --git a/frontend/drupal9/web/modules/contrib/facets/js/checkbox-widget.js b/frontend/drupal9/web/modules/contrib/facets/js/checkbox-widget.js
new file mode 100644
index 000000000..54b66ef9e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/js/checkbox-widget.js
@@ -0,0 +1,104 @@
+/**
+ * @file
+ * Transforms links into checkboxes.
+ */
+
+(function ($, Drupal) {
+
+ 'use strict';
+
+ Drupal.facets = Drupal.facets || {};
+ Drupal.behaviors.facetsCheckboxWidget = {
+ attach: function (context) {
+ Drupal.facets.makeCheckboxes(context);
+ }
+ };
+
+ /**
+ * Turns all facet links into checkboxes.
+ */
+ Drupal.facets.makeCheckboxes = function (context) {
+ // Find all checkbox facet links and give them a checkbox.
+ var $checkboxWidgets = $('.js-facets-checkbox-links', context)
+ .once('facets-checkbox-transform');
+
+ if ($checkboxWidgets.length > 0) {
+ $checkboxWidgets.each(function (index, widget) {
+ var $widget = $(widget);
+ var $widgetLinks = $widget.find('.facet-item > a');
+
+ // Add correct CSS selector for the widget. The Facets JS API will
+ // register handlers on that element.
+ $widget.addClass('js-facets-widget');
+
+ // Transform links to checkboxes.
+ $widgetLinks.each(Drupal.facets.makeCheckbox);
+
+ // We have to trigger attaching of behaviours, so that Facets JS API can
+ // register handlers on checkbox widgets.
+ Drupal.attachBehaviors(this.parentNode, Drupal.settings);
+ });
+
+ }
+
+ // Set indeterminate value on parents having an active trail.
+ $('.facet-item--expanded.facet-item--active-trail > input').prop('indeterminate', true);
+ };
+
+ /**
+ * Replace a link with a checked checkbox.
+ */
+ Drupal.facets.makeCheckbox = function () {
+ var $link = $(this);
+ var active = $link.hasClass('is-active');
+ var description = $link.html();
+ var href = $link.attr('href');
+ var id = $link.data('drupal-facet-item-id');
+
+ var checkbox = $(' ')
+ .attr('id', id)
+ .data($link.data())
+ .data('facetsredir', href);
+ var label = $('' + description + ' ');
+
+ checkbox.on('change.facets', function (e) {
+ e.preventDefault();
+
+ var $widget = $(this).closest('.js-facets-widget');
+
+ Drupal.facets.disableFacet($widget);
+ $widget.trigger('facets_filter', [ href ]);
+ });
+
+ if (active) {
+ checkbox.attr('checked', true);
+ label.find('.js-facet-deactivate').remove();
+ }
+
+ $link.before(checkbox).before(label).hide();
+
+ };
+
+ /**
+ * Disable all facet checkboxes in the facet and apply a 'disabled' class.
+ *
+ * @param {object} $facet
+ * jQuery object of the facet.
+ */
+ Drupal.facets.disableFacet = function ($facet) {
+ $facet.addClass('facets-disabled');
+ $('input.facets-checkbox', $facet).click(Drupal.facets.preventDefault);
+ $('input.facets-checkbox', $facet).attr('disabled', true);
+ };
+
+ /**
+ * Event listener for easy prevention of event propagation.
+ *
+ * @param {object} e
+ * Event.
+ */
+ Drupal.facets.preventDefault = function (e) {
+ e.preventDefault();
+ };
+
+})(jQuery, Drupal);
diff --git a/frontend/drupal9/web/modules/contrib/facets/js/dropdown-widget.js b/frontend/drupal9/web/modules/contrib/facets/js/dropdown-widget.js
new file mode 100644
index 000000000..bdff7a485
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/js/dropdown-widget.js
@@ -0,0 +1,104 @@
+/**
+ * @file
+ * Transforms links into a dropdown list.
+ */
+
+(function ($) {
+
+ 'use strict';
+
+ Drupal.facets = Drupal.facets || {};
+ Drupal.behaviors.facetsDropdownWidget = {
+ attach: function (context, settings) {
+ Drupal.facets.makeDropdown(context, settings);
+ }
+ };
+
+ /**
+ * Turns all facet links into a dropdown with options for every link.
+ *
+ * @param {object} context
+ * Context.
+ * @param {object} settings
+ * Settings.
+ */
+ Drupal.facets.makeDropdown = function (context, settings) {
+ // Find all dropdown facet links and turn them into an option.
+ $('.js-facets-dropdown-links').once('facets-dropdown-transform').each(function () {
+ var $ul = $(this);
+ var $links = $ul.find('.facet-item a');
+ var $dropdown = $(' ');
+ // Preserve all attributes of the list.
+ $ul.each(function () {
+ $.each(this.attributes,function (idx, elem) {
+ $dropdown.attr(elem.name, elem.value);
+ });
+ });
+ // Remove the class which we are using for .once().
+ $dropdown.removeClass('js-facets-dropdown-links');
+
+ $dropdown.addClass('facets-dropdown');
+ $dropdown.addClass('js-facets-widget');
+ $dropdown.addClass('js-facets-dropdown');
+
+ var id = $(this).data('drupal-facet-id');
+ // Add aria-labelledby attribute to reference label.
+ $dropdown.attr('aria-labelledby', "facet_" + id + "_label");
+ var default_option_label = settings.facets.dropdown_widget[id]['facet-default-option-label'];
+
+ // Add empty text option first.
+ var $default_option = $(' ')
+ .attr('value', '')
+ .text(default_option_label);
+ $dropdown.append($default_option);
+
+ $ul.prepend('' + Drupal.checkPlain(default_option_label) + ' ');
+
+ var has_active = false;
+ $links.each(function () {
+ var $link = $(this);
+ var active = $link.hasClass('is-active');
+ var $option = $(' ')
+ .attr('value', $link.attr('href'))
+ .data($link.data());
+ if (active) {
+ has_active = true;
+ // Set empty text value to this link to unselect facet.
+ $default_option.attr('value', $link.attr('href'));
+ $ul.find('.default-option a').attr("href", $link.attr('href'));
+ $option.attr('selected', 'selected');
+ $link.find('.js-facet-deactivate').remove();
+ }
+ $option.text(function () {
+ // Add hierarchy indicator in case hierarchy is enabled.
+ var $parents = $link.parent('li.facet-item').parents('li.facet-item');
+ var prefix = '';
+ for (var i = 0; i < $parents.length; i++) {
+ prefix += '-';
+ }
+ return prefix + ' ' + $link.text().trim();
+ });
+ $dropdown.append($option);
+ });
+
+ // Go to the selected option when it's clicked.
+ $dropdown.on('change.facets', function () {
+ var anchor = $($ul).find("[data-drupal-facet-item-id='" + $(this).find(':selected').data('drupalFacetItemId') + "']");
+ var $linkElement = (anchor.length > 0) ? $(anchor) : $ul.find('.default-option a');
+ var url = $linkElement.attr('href');
+
+ $(this).trigger('facets_filter', [ url ]);
+ });
+
+ // Append empty text option.
+ if (!has_active) {
+ $default_option.attr('selected', 'selected');
+ }
+
+ // Replace links with dropdown.
+ $ul.after($dropdown).hide();
+ Drupal.attachBehaviors($dropdown.parent()[0], Drupal.settings);
+ });
+ };
+
+})(jQuery);
diff --git a/frontend/drupal9/web/modules/contrib/facets/js/edit-facet.js b/frontend/drupal9/web/modules/contrib/facets/js/edit-facet.js
new file mode 100644
index 000000000..0d7136319
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/js/edit-facet.js
@@ -0,0 +1,23 @@
+/**
+ * @file
+ * UX improvements for the facet edit form.
+ */
+
+(function ($) {
+
+ 'use strict';
+
+ Drupal.behaviors.facetsEditForm = {
+ attach: function (context, settings) {
+ $('.facet-source-field-wrapper select').change(function () {
+
+ var default_name = $(this).find('option:selected').text();
+ default_name = default_name.replace(/(\s\((?!.*\().*\))/g, '');
+ $('#edit-name').val(default_name);
+ setTimeout(function () { $('#edit-name').trigger('change'); }, 100);
+
+ });
+ }
+ };
+
+})(jQuery);
diff --git a/frontend/drupal9/web/modules/contrib/facets/js/facets-views-ajax.js b/frontend/drupal9/web/modules/contrib/facets/js/facets-views-ajax.js
new file mode 100644
index 000000000..d4dfd6c19
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/js/facets-views-ajax.js
@@ -0,0 +1,219 @@
+/**
+ * @file
+ * Facets views AJAX handling.
+ */
+
+
+(function ($, Drupal) {
+ 'use strict';
+
+ /**
+ * Keep the original beforeSend method to use it later.
+ */
+ var beforeSend = Drupal.Ajax.prototype.beforeSend;
+
+ /**
+ * Trigger views AJAX refresh on click.
+ */
+ Drupal.behaviors.facetsViewsAjax = {
+ attach: function (context, settings) {
+
+ // Loop through all facets.
+ $.each(settings.facets_views_ajax, function (facetId, facetSettings) {
+ // Get the View for the current facet.
+ var view, current_dom_id, view_path;
+ if (settings.views && settings.views.ajaxViews) {
+ $.each(settings.views.ajaxViews, function (domId, viewSettings) {
+ // Check if we have facet for this view.
+ if (facetSettings.view_id == viewSettings.view_name && facetSettings.current_display_id == viewSettings.view_display_id) {
+ view = $('.js-view-dom-id-' + viewSettings.view_dom_id);
+ current_dom_id = viewSettings.view_dom_id;
+ view_path = facetSettings.ajax_path;
+ }
+ });
+ }
+
+ if (!view || view.length != 1) {
+ return;
+ }
+
+ // Update view on summary block click.
+ if (updateFacetsSummaryBlock() && (facetId === 'facets_summary_ajax')) {
+ $('[data-drupal-facets-summary-id=' + facetSettings.facets_summary_id + ']').children('ul').children('li').once().click(function (e) {
+ e.preventDefault();
+ var facetLink = $(this).find('a');
+ updateFacetsView(facetLink.attr('href'), current_dom_id, view_path);
+ });
+ }
+ // Update view on facet item click.
+ else {
+ $('[data-drupal-facet-id=' + facetId + ']').each(function (index, facet_item) {
+ if ($(facet_item).hasClass('js-facets-widget')) {
+ $(facet_item).unbind('facets_filter.facets');
+ $(facet_item).on('facets_filter.facets', function (event, url) {
+ $('.js-facets-widget').trigger('facets_filtering');
+
+ updateFacetsView(url, current_dom_id, view_path);
+ });
+ }
+ });
+
+ }
+ });
+ }
+ };
+
+ // Helper function to update views output & Ajax facets.
+ var updateFacetsView = function (href, current_dom_id, view_path) {
+ // Refresh view.
+ var views_parameters = Drupal.Views.parseQueryString(href);
+ var views_arguments = Drupal.Views.parseViewArgs(href, 'search');
+ var views_settings = $.extend(
+ {},
+ Drupal.views.instances['views_dom_id:' + current_dom_id].settings,
+ views_arguments,
+ views_parameters
+ );
+
+ // Update View.
+ var views_ajax_settings = Drupal.views.instances['views_dom_id:' + current_dom_id].element_settings;
+ views_ajax_settings.submit = views_settings;
+ views_ajax_settings.url = view_path + '?q=' + href;
+
+ Drupal.ajax(views_ajax_settings).execute();
+
+ // Update url.
+ window.historyInitiated = true;
+ window.history.pushState(null, document.title, href);
+
+ // ToDo: Update views+facets with ajax on history back.
+ // For now we will reload the full page.
+ window.addEventListener("popstate", function (e) {
+ if (window.historyInitiated) {
+ window.location.reload();
+ }
+ });
+
+ // Refresh facets blocks.
+ updateFacetsBlocks(href);
+ }
+
+ // Helper function, updates facet blocks.
+ var updateFacetsBlocks = function (href) {
+ var settings = drupalSettings;
+ var facets_blocks = facetsBlocks();
+
+ // Remove All Range Input Form Facet Blocks from being updated.
+ if(settings.facets && settings.facets.rangeInput) {
+ $.each(settings.facets.rangeInput, function (index, value) {
+ delete facets_blocks[value.facetId];
+ });
+ }
+
+ // Update facet blocks.
+ var facet_settings = {
+ url: Drupal.url('facets-block-ajax'),
+ submit: {
+ facet_link: href,
+ facets_blocks: facets_blocks
+ }
+ };
+
+ // Update facets summary block.
+ if (updateFacetsSummaryBlock()) {
+ var facet_summary_wrapper_id = $('[data-drupal-facets-summary-id=' + settings.facets_views_ajax.facets_summary_ajax.facets_summary_id + ']').attr('id');
+ var facet_summary_block_id = '';
+ if (facet_summary_wrapper_id.indexOf('--') !== -1) {
+ facet_summary_block_id = facet_summary_wrapper_id.substring(0, facet_summary_wrapper_id.indexOf('--')).replace('block-', '');
+ }
+ else {
+ facet_summary_block_id = facet_summary_wrapper_id.replace('block-', '');
+ }
+ facet_settings.submit.update_summary_block = true;
+ facet_settings.submit.facet_summary_block_id = facet_summary_block_id;
+ facet_settings.submit.facet_summary_wrapper_id = settings.facets_views_ajax.facets_summary_ajax.facets_summary_id;
+ }
+
+ Drupal.ajax(facet_settings).execute();
+ };
+
+ // Helper function to determine if we should update the summary block.
+ // Returns true or false.
+ var updateFacetsSummaryBlock = function () {
+ var settings = drupalSettings;
+ var update_summary = false;
+
+ if (settings.facets_views_ajax.facets_summary_ajax) {
+ update_summary = true;
+ }
+
+ return update_summary;
+ };
+
+ // Helper function, return facet blocks.
+ var facetsBlocks = function () {
+ // Get all ajax facets blocks from the current page.
+ var facets_blocks = {};
+
+ $('.block-facets-ajax').each(function (index) {
+ var block_id_start = 'js-facet-block-id-';
+ var block_id = $.map($(this).attr('class').split(' '), function (v, i) {
+ if (v.indexOf(block_id_start) > -1) {
+ return v.slice(block_id_start.length, v.length);
+ }
+ }).join();
+ var block_selector = '#' + $(this).attr('id');
+ facets_blocks[block_id] = block_selector;
+ });
+
+ return facets_blocks;
+ };
+
+ /**
+ * Overrides beforeSend to trigger facetblocks update on exposed filter change.
+ *
+ * @param {XMLHttpRequest} xmlhttprequest
+ * Native Ajax object.
+ * @param {object} options
+ * jQuery.ajax options.
+ */
+ Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) {
+
+ // Update facet blocks as well.
+ // Get view from options.
+ if (typeof options.extraData !== 'undefined' && typeof options.extraData.view_name !== 'undefined') {
+ var href = window.location.href;
+ var settings = drupalSettings;
+
+ // TODO: Maybe we should limit facet block reloads by view?
+ var reload = false;
+ $.each(settings.facets_views_ajax, function (facetId, facetSettings) {
+ if (facetSettings.view_id == options.extraData.view_name && facetSettings.current_display_id == options.extraData.view_display_id) {
+ reload = true;
+ }
+ });
+
+ if (reload) {
+ href = addExposedFiltersToFacetsUrl(href, options.extraData.view_name, options.extraData.view_display_id);
+ updateFacetsBlocks(href);
+ }
+ }
+
+ // Call the original Drupal method with the right context.
+ beforeSend.apply(this, arguments);
+ }
+
+ // Helper function to add exposed form data to facets url
+ var addExposedFiltersToFacetsUrl = function (href, view_name, view_display_id) {
+ var $exposed_form = $('form#views-exposed-form-' + view_name.replace(/_/g, '-') + '-' + view_display_id.replace(/_/g, '-'));
+
+ var params = Drupal.Views.parseQueryString(href);
+
+ $.each($exposed_form.serializeArray(), function () {
+ params[this.name] = this.value;
+ });
+
+ return href.split('?')[0] + '?' + $.param(params);
+ };
+
+})(jQuery, Drupal);
diff --git a/frontend/drupal9/web/modules/contrib/facets/js/index-active-formatters.js b/frontend/drupal9/web/modules/contrib/facets/js/index-active-formatters.js
new file mode 100644
index 000000000..9a400a542
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/js/index-active-formatters.js
@@ -0,0 +1,49 @@
+/**
+ * @file
+ * Attaches show/hide functionality to checkboxes in the "Processor" tab.
+ */
+
+(function ($) {
+
+ 'use strict';
+
+ Drupal.behaviors.facetsIndexFormatter = {
+ attach: function (context, settings) {
+ $('.search-api-status-wrapper input.form-checkbox', context).each(function () {
+ var $checkbox = $(this);
+ var processor_id = $checkbox.data('id');
+
+ var $rows = $('.search-api-processor-weight--' + processor_id, context);
+ var tab = $('.search-api-processor-settings-' + processor_id, context).data('verticalTab');
+
+ // Bind a click handler to this checkbox to conditionally show and hide
+ // the processor's table row and vertical tab pane.
+ $checkbox.on('click.searchApiUpdate', function () {
+ if ($checkbox.is(':checked')) {
+ $rows.show();
+ if (tab) {
+ tab.tabShow().updateSummary();
+ }
+ }
+ else {
+ $rows.hide();
+ if (tab) {
+ tab.tabHide().updateSummary();
+ }
+ }
+ });
+
+ // Attach summary for configurable items (only for screen-readers).
+ if (tab) {
+ tab.details.drupalSetSummary(function () {
+ return $checkbox.is(':checked') ? Drupal.t('Enabled') : Drupal.t('Disabled');
+ });
+ }
+
+ // Trigger our bound click handler to update elements to initial state.
+ $checkbox.triggerHandler('click.searchApiUpdate');
+ });
+ }
+ };
+
+})(jQuery);
diff --git a/frontend/drupal9/web/modules/contrib/facets/js/link-widget.js b/frontend/drupal9/web/modules/contrib/facets/js/link-widget.js
new file mode 100644
index 000000000..a96b99c27
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/js/link-widget.js
@@ -0,0 +1,46 @@
+/**
+ * @file
+ * Facets views Link widgets handling.
+ */
+
+(function ($, Drupal) {
+ 'use strict';
+
+ /**
+ * Handle link widgets.
+ */
+ Drupal.behaviors.facetsLinkWidget = {
+ attach: function (context) {
+ var $linkFacets = $('.js-facets-links', context)
+ .once('js-facets-link-on-click');
+
+ // We are using list wrapper element for Facet JS API.
+ if ($linkFacets.length > 0) {
+ $linkFacets
+ .each(function (index, widget) {
+ var $widget = $(widget);
+ var $widgetLinks = $widget.find('.facet-item > a');
+
+ // Click on link will call Facets JS API on widget element.
+ var clickHandler = function (e) {
+ e.preventDefault();
+
+ $widget.trigger('facets_filter', [$(this).attr('href')]);
+ };
+
+ // Add correct CSS selector for the widget. The Facets JS API will
+ // register handlers on that element.
+ $widget.addClass('js-facets-widget');
+
+ // Add handler for clicks on widget links.
+ $widgetLinks.on('click', clickHandler);
+
+ // We have to trigger attaching of behaviours, so that Facets JS API can
+ // register handlers on link widgets.
+ Drupal.attachBehaviors(this.parentNode, Drupal.settings);
+ });
+ }
+ }
+ };
+
+})(jQuery, Drupal);
diff --git a/frontend/drupal9/web/modules/contrib/facets/js/soft-limit.js b/frontend/drupal9/web/modules/contrib/facets/js/soft-limit.js
new file mode 100644
index 000000000..096291f2b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/js/soft-limit.js
@@ -0,0 +1,73 @@
+/**
+ * @file
+ * Provides the soft limit functionality.
+ */
+
+(function ($) {
+
+ 'use strict';
+
+ Drupal.behaviors.facetSoftLimit = {
+ attach: function (context, settings) {
+ if (settings.facets.softLimit !== 'undefined') {
+ $.each(settings.facets.softLimit, function (facet, limit) {
+ Drupal.facets.applySoftLimit(facet, limit, settings);
+ });
+ }
+ }
+ };
+
+ Drupal.facets = Drupal.facets || {};
+
+ /**
+ * Applies the soft limit UI feature to a specific facets list.
+ *
+ * @param {string} facet
+ * The facet id.
+ * @param {string} limit
+ * The maximum amount of items to show.
+ * @param {object} settings
+ * Settings.
+ */
+ Drupal.facets.applySoftLimit = function (facet, limit, settings) {
+ var zero_based_limit = (limit - 1);
+ var facet_id = facet;
+ var facetsList = $('ul[data-drupal-facet-id="' + facet_id + '"]');
+
+ // In case of multiple instances of a facet, we need to key them.
+ if (facetsList.length > 1) {
+ facetsList.each(function (key, $value) {
+ $(this).attr('data-drupal-facet-id', facet_id + '-' + key);
+ });
+ }
+
+ // Hide facets over the limit.
+ facetsList.each(function () {
+ $(this).children('li:gt(' + zero_based_limit + ')').once('applysoftlimit').hide();
+ });
+
+ // Add "Show more" / "Show less" links.
+ facetsList.once().filter(function () {
+ return $(this).find('li').length > limit;
+ }).each(function () {
+ var facet = $(this);
+ var showLessLabel = settings.facets.softLimitSettings[facet_id].showLessLabel;
+ var showMoreLabel = settings.facets.softLimitSettings[facet_id].showMoreLabel;
+ $(' ')
+ .text(showMoreLabel).attr("aria-expanded", "false")
+ .on('click', function () {
+ if (facet.find('li:hidden').length > 0) {
+ facet.find('li:gt(' + zero_based_limit + ')').slideDown();
+ facet.find('li:lt(' + (zero_based_limit + 2) + ') input').focus();
+ $(this).addClass('open').text(showLessLabel).attr("aria-expanded", "true");
+ }
+ else {
+ facet.find('li:gt(' + zero_based_limit + ')').slideUp();
+ $(this).removeClass('open').text(showMoreLabel).attr("aria-expanded", "false");
+ }
+ return false;
+ }).insertAfter($(this));
+ });
+ };
+
+})(jQuery);
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/README.txt b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/README.txt
new file mode 100644
index 000000000..e4a9e49a0
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/README.txt
@@ -0,0 +1,34 @@
+CONTENTS OF THIS FILE
+---------------------
+ * Installation
+ * FAQ
+
+Installation
+------------
+
+ * If you want to use the sliders, you need to add the Slider pips jquery
+ plugin:
+ - create the /libraries/jquery-ui-slider-pips/dist folder.
+ - download the following files from
+ https://github.com/simeydotme/jQuery-ui-Slider-Pips/tree/v1.11.3/dist
+ - jquery-ui-slider-pips.min.js
+ - jquery-ui-slider-pips.min.css
+
+ You can find more information about this jquery plugin on
+ http://simeydotme.github.io/jQuery-ui-Slider-Pips/
+
+ Alternatively, you can install from Asset Packagist using Composer.
+ If you are using the Lightning or Drupal Commerce base distros, just run
+ `composer require "bower-asset/jquery-ui-slider-pips:^1.11"
+ If you don't have Asset Packagist configured, see
+ https://github.com/drupal-composer/drupal-project/issues/278#issuecomment-300714410
+ for instructions.
+
+
+FAQ
+---
+
+Q: Why is this in a submodule?
+A: We wanted to add a requirements message when the library was not installed,
+ to give a good experience when installing the module. We didn't want everyone
+ to have to install the library though.
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/config/schema/facets_range_widget.schema.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/config/schema/facets_range_widget.schema.yml
new file mode 100644
index 000000000..a95380b3d
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/config/schema/facets_range_widget.schema.yml
@@ -0,0 +1,53 @@
+# Config schema for slider, you can find the implementation in
+# Drupal\facets_range\Plugin\facets\widget\SliderWidget.
+facet.widget.config.slider:
+ type: facet.widget.default_config
+ label: 'List of range widget configuration'
+ mapping:
+ prefix:
+ type: label
+ label: 'Prefix'
+ suffix:
+ type: label
+ label: 'Suffix'
+ min_type:
+ type: string
+ label: 'Minimum type'
+ min_value:
+ type: float
+ label: 'Minimum value'
+ max_type:
+ type: string
+ label: 'Maximum type'
+ max_value:
+ type: float
+ label: 'Maximum value'
+ step:
+ type: float
+ label: 'Step'
+
+facet.widget.config.range_slider:
+ type: facet.widget.default_config
+ label: 'List of range widget configuration'
+ mapping:
+ prefix:
+ type: label
+ label: 'Prefix'
+ suffix:
+ type: label
+ label: 'Suffix'
+ min_type:
+ type: string
+ label: 'Minimum type'
+ min_value:
+ type: float
+ label: 'Minimum value'
+ max_type:
+ type: string
+ label: 'Maximum type'
+ max_value:
+ type: float
+ label: 'Maximum value'
+ step:
+ type: float
+ label: 'Step'
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/css/slider.css b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/css/slider.css
new file mode 100644
index 000000000..0c60f76a7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/css/slider.css
@@ -0,0 +1,9 @@
+.facet-slider {
+ margin-top: 40px;
+}
+
+.facet-slider.ui-slider-float .ui-slider-tip {
+ visibility: visible;
+ opacity: 1;
+ top: -30px;
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.info.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.info.yml
new file mode 100644
index 000000000..9bb61f9d1
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.info.yml
@@ -0,0 +1,18 @@
+name: 'Facets Range Widget'
+type: module
+description: 'Provides a range widget and solid slider.'
+core_version_requirement: ^9.2 || ^10.0
+package: Search
+dependencies:
+ - facets:facets
+ - jquery_ui_slider:jquery_ui_slider
+ - jquery_ui_touch_punch:jquery_ui_touch_punch
+test_dependencies:
+ - search_api:search_api
+ - facets:facets
+ - drupal:views
+
+# Information added by Drupal.org packaging script on 2022-04-04
+version: '2.0.2'
+project: 'facets'
+datestamp: 1649070272
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.install b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.install
new file mode 100644
index 000000000..c6a7753b9
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.install
@@ -0,0 +1,39 @@
+getPath($profile);
+ $profile_path .= $library_path;
+ $library_exists = file_exists($profile_path);
+ }
+
+ $requirements = [];
+ if (!$library_exists) {
+ $arguments = [
+ ':docs' => 'https://www.drupal.org/docs/8/theming-drupal-8/adding-stylesheets-css-and-javascript-js-to-a-drupal-8-theme#external',
+ ':readme' => 'http://cgit.drupalcode.org/facets/tree/modules/facets_range_widget/README.txt',
+ ];
+ $requirements['facets_range_widget_pips_slider'] = [
+ 'title' => t('Facets range slider'),
+ 'value' => t('The jquery ui slider pips library is not installed.'),
+ 'severity' => REQUIREMENT_ERROR,
+ 'description' => t('The range slider library is not installed, check the README.txt for more information and the documentation for information on how to install a library .', $arguments),
+ ];
+ }
+
+ return $requirements;
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.libraries.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.libraries.yml
new file mode 100644
index 000000000..d994ac800
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/facets_range_widget.libraries.yml
@@ -0,0 +1,28 @@
+jquery.ui.slider.pips:
+ remote: http://simeydotme.github.io/jQuery-ui-Slider-Pips/
+ version: v1.11.3
+ license:
+ name: MIT
+ url: https://github.com/simeydotme/jQuery-ui-Slider-Pips/blob/v1.11.3/README.md
+ gpl-compatible: true
+ js:
+ /libraries/jquery-ui-slider-pips/dist/jquery-ui-slider-pips.min.js: { minified: true, attributes: { defer: true } }
+ css:
+ component:
+ /libraries/jquery-ui-slider-pips/dist/jquery-ui-slider-pips.min.css: { minified: true }
+ dependencies:
+ - jquery_ui_slider/slider
+
+slider:
+ version: VERSION
+ js:
+ js/slider.js: { attributes: { defer: true } }
+ css:
+ component:
+ css/slider.css: {}
+ dependencies:
+ - core/drupal
+ - core/drupalSettings
+ - core/jquery.once
+ - facets_range_widget/jquery.ui.slider.pips
+ - jquery_ui_touch_punch/touch-punch
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/js/slider.js b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/js/slider.js
new file mode 100644
index 000000000..d13e08df2
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/js/slider.js
@@ -0,0 +1,48 @@
+/**
+ * @file
+ * Provides the slider functionality.
+ */
+
+(function ($) {
+
+ 'use strict';
+
+ Drupal.facets = Drupal.facets || {};
+
+ Drupal.behaviors.facet_slider = {
+ attach: function (context, settings) {
+ if (settings.facets !== 'undefined' && settings.facets.sliders !== 'undefined') {
+ $.each(settings.facets.sliders, function (facet, settings) {
+ Drupal.facets.addSlider(facet, settings);
+ });
+ }
+ }
+ };
+
+ Drupal.facets.addSlider = function (facet, settings) {
+ var defaults = {
+ stop: function (event, ui) {
+ if (settings.range) {
+ window.location.href = settings.url.replace('__range_slider_min__', ui.values[0]).replace('__range_slider_max__', ui.values[1]);
+ }
+ else {
+ window.location.href = settings.urls['f_' + ui.value];
+ }
+ }
+ };
+
+ $.extend(defaults, settings);
+
+ $('[id^="' + facet + '"][id$="' + facet + '"]').slider(defaults)
+ .slider('pips', {
+ prefix: settings.prefix,
+ suffix: settings.suffix
+ })
+ .slider('float', {
+ prefix: settings.prefix,
+ suffix: settings.suffix,
+ labels: settings.labels
+ });
+ };
+
+})(jQuery);
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/processor/RangeSliderProcessor.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/processor/RangeSliderProcessor.php
new file mode 100644
index 000000000..62cf04afb
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/processor/RangeSliderProcessor.php
@@ -0,0 +1,68 @@
+getActiveItems();
+
+ array_walk($active_items, function (&$item) {
+ if (preg_match('/\(min:((?:-)?[\d\.]+),max:((?:-)?[\d\.]+)\)/i', $item, $matches)) {
+ $item = [$matches[1], $matches[2]];
+ }
+ else {
+ $item = NULL;
+ }
+ });
+ $facet->setActiveItems($active_items);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ /** @var \Drupal\facets\Plugin\facets\processor\UrlProcessorHandler $url_processor_handler */
+ $url_processor_handler = $facet->getProcessors()['url_processor_handler'];
+ $url_processor = $url_processor_handler->getProcessor();
+ $active_filters = $url_processor->getActiveFilters();
+
+ if (isset($active_filters[''])) {
+ unset($active_filters['']);
+ }
+
+ /** @var \Drupal\facets\Result\ResultInterface[] $results */
+ foreach ($results as &$result) {
+ $new_active_filters = $active_filters;
+ unset($new_active_filters[$facet->id()]);
+ // Add one generic query filter with the min and max placeholder.
+ $new_active_filters[$facet->id()][] = '(min:__range_slider_min__,max:__range_slider_max__)';
+ $url = \Drupal::service('facets.utility.url_generator')->getUrl($new_active_filters, FALSE);
+ $result->setUrl($url);
+ }
+
+ return $results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/processor/SliderProcessor.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/processor/SliderProcessor.php
new file mode 100644
index 000000000..2d7121378
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/processor/SliderProcessor.php
@@ -0,0 +1,70 @@
+getWidgetInstance();
+ $config = $widget->getConfiguration();
+ $simple_results = [];
+
+ // Generate all the "results" between min and max, with the configured step.
+ foreach ($facet->getResults() as $result) {
+ $simple_results['f_' . (float) $result->getRawValue()] = [
+ 'value' => (float) $result->getRawValue(),
+ 'count' => (int) $result->getCount(),
+ ];
+ }
+ uasort($simple_results, function ($a, $b) {
+ return (int) $a['value'] - $b['value'];
+ });
+
+ $step = $config['step'];
+ if ($config['min_type'] == 'fixed') {
+ $min = $config['min_value'];
+ $max = $config['max_value'];
+ }
+ else {
+ $min = reset($simple_results)['value'] ?? 0;
+ $max = end($simple_results)['value'] ?? 0;
+ // If max is not divisible by step, we should add the remainder to max to
+ // make sure that we don't lose any possible values.
+ if ($max % $step !== 0) {
+ $max = $max + ($step - $max % $step);
+ }
+ }
+
+ // Creates an array of all results between min and max by the step from the
+ // configuration.
+ $new_results = [];
+ for ($i = $min; $i <= $max; $i += $step) {
+ $count = isset($simple_results['f_' . $i]) ? $simple_results['f_' . $i]['count'] : 0;
+ $new_results[] = new Result($facet, (float) $i, (float) $i, $count);
+ }
+
+ // Overwrite the current facet values with the generated results.
+ $facet->setResults($new_results);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/widget/RangeSliderWidget.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/widget/RangeSliderWidget.php
new file mode 100644
index 000000000..dc546a748
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/widget/RangeSliderWidget.php
@@ -0,0 +1,68 @@
+getResults())) {
+ return $build;
+ }
+
+ $active = $facet->getActiveItems();
+ $facet_settings = &$build['#attached']['drupalSettings']['facets']['sliders'][$facet->id()];
+
+ $facet_settings['range'] = TRUE;
+ $facet_settings['url'] = reset($facet_settings['urls']);
+
+ unset($facet_settings['value']);
+ unset($facet_settings['urls']);
+
+ $min = $facet_settings['min'];
+ $max = $facet_settings['max'];
+ $facet_settings['values'] = [
+ isset($active[0][0]) ? (float) $active[0][0] : $min,
+ isset($active[0][1]) ? (float) $active[0][1] : $max,
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isPropertyRequired($name, $type) {
+ if ($name === 'range_slider' && $type === 'processors') {
+ return TRUE;
+ }
+ if ($name === 'show_only_one_result' && $type === 'settings') {
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryType() {
+ return 'range';
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/widget/SliderWidget.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/widget/SliderWidget.php
new file mode 100644
index 000000000..216311f3e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/src/Plugin/facets/widget/SliderWidget.php
@@ -0,0 +1,179 @@
+ '',
+ 'suffix' => '',
+ 'min_type' => 'search_result',
+ 'min_value' => 0,
+ 'max_type' => 'search_result',
+ 'max_value' => 10,
+ 'step' => 1,
+ ] + parent::defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet) {
+ $build = parent::build($facet);
+
+ $results = $facet->getResults();
+ if (empty($results)) {
+ return $build;
+ }
+ ksort($results);
+
+ $show_numbers = $facet->getWidgetInstance()->getConfiguration()['show_numbers'];
+ $urls = [];
+ $labels = [];
+
+ foreach ($results as $result) {
+ $urls['f_' . $result->getRawValue()] = $result->getUrl()->toString();
+ $labels[] = $result->getDisplayValue() . ($show_numbers ? ' (' . $result->getCount() . ')' : '');
+ }
+ // The results set on the facet are sorted where the minimum is the first
+ // item and the last one is the one with the highest results, so it's safe
+ // to use min/max.
+ $min = (float) reset($results)->getRawValue();
+ $max = (float) end($results)->getRawValue();
+
+ $build['#items'] = [
+ [
+ '#type' => 'html_tag',
+ '#tag' => 'div',
+ '#attributes' => [
+ 'class' => ['facet-slider'],
+ 'id' => $facet->id(),
+ ],
+ ],
+ ];
+
+ $active = $facet->getActiveItems();
+
+ $build['#attached']['library'][] = 'facets_range_widget/slider';
+ $build['#attached']['drupalSettings']['facets']['sliders'][$facet->id()] = [
+ 'min' => $min,
+ 'max' => $max,
+ 'value' => isset($active[0]) ? (float) $active[0] : '',
+ 'urls' => $urls,
+ 'prefix' => $this->getConfiguration()['prefix'],
+ 'suffix' => $this->getConfiguration()['suffix'],
+ 'step' => $this->getConfiguration()['step'],
+ 'labels' => $labels,
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $config = $this->getConfiguration();
+ $form = parent::buildConfigurationForm($form, $form_state, $facet);
+
+ $form['prefix'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Value prefix'),
+ '#size' => 5,
+ '#default_value' => $config['prefix'],
+ ];
+
+ $form['suffix'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Value suffix'),
+ '#size' => 5,
+ '#default_value' => $config['suffix'],
+ ];
+
+ $form['min_type'] = [
+ '#type' => 'radios',
+ '#options' => [
+ 'fixed' => $this->t('Fixed value'),
+ 'search_result' => $this->t('Based on search result'),
+ ],
+ '#title' => $this->t('Minimum value type'),
+ '#default_value' => $config['min_type'],
+ ];
+
+ $form['min_value'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Minimum value'),
+ '#default_value' => $config['min_value'],
+ '#size' => 10,
+ '#states' => [
+ 'visible' => [
+ 'input[name="widget_config[min_type]"]' => ['value' => 'fixed'],
+ ],
+ ],
+ ];
+
+ $form['max_type'] = [
+ '#type' => 'radios',
+ '#options' => [
+ 'fixed' => $this->t('Fixed value'),
+ 'search_result' => $this->t('Based on search result'),
+ ],
+ '#title' => $this->t('Maximum value type'),
+ '#default_value' => $config['max_type'],
+ ];
+
+ $form['max_value'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Maximum value'),
+ '#default_value' => $config['max_value'],
+ '#size' => 5,
+ '#states' => [
+ 'visible' => [
+ 'input[name="widget_config[max_type]"]' => ['value' => 'fixed'],
+ ],
+ ],
+ ];
+
+ $form['step'] = [
+ '#type' => 'number',
+ '#step' => 0.001,
+ '#title' => $this->t('slider step'),
+ '#default_value' => $config['step'],
+ '#size' => 2,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isPropertyRequired($name, $type) {
+ if ($name === 'slider' && $type === 'processors') {
+ return TRUE;
+ }
+ if ($name === 'show_only_one_result' && $type === 'settings') {
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Functional/SliderIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Functional/SliderIntegrationTest.php
new file mode 100644
index 000000000..229e1d098
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Functional/SliderIntegrationTest.php
@@ -0,0 +1,133 @@
+drupalLogin($this->adminUser);
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals(5, $this->indexItems($this->indexId), '5 items were indexed.');
+ }
+
+ /**
+ * Tests slider widget.
+ */
+ public function testSliderWidget() {
+ $this->createIntegerField();
+ $id = 'owl';
+ $this->createFacet('Owl widget.', $id, 'field_integer');
+
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+
+ $this->assertSession()->checkboxNotChecked('edit-facet-settings-slider-status');
+
+ $this->submitForm(['widget' => 'slider'], 'Configure widget');
+ $this->submitForm(['widget' => 'slider'], 'Save');
+
+ $this->assertSession()->checkboxChecked('edit-facet-settings-slider-status');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->assertSession()->pageTextContains('Displaying 12 search results');
+
+ // Change the facet block.
+ $url = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['f[0]' => 'owl:2']]);
+ $this->drupalGet($url->setAbsolute()->toString());
+
+ // Check that the results have changed to the correct amount of results.
+ $this->assertSession()->pageTextContains('Displaying 1 search results');
+ $this->assertSession()->pageTextContains('foo bar baz 2');
+
+ // Change the facet block.
+ $url = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['f[0]' => 'owl:4']]);
+ $this->drupalGet($url->setAbsolute()->toString());
+
+ // Check that the results have changed to the correct amount of results.
+ $this->assertSession()->pageTextContains('Displaying 1 search results');
+ $this->assertSession()->pageTextContains('foo bar baz 4');
+ }
+
+ /**
+ * Create integer field.
+ */
+ protected function createIntegerField() {
+ $index = $this->getIndex();
+
+ // Create integer field.
+ $field_name = 'field_integer';
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'type' => 'integer',
+ ]);
+ $field_storage->save();
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => 'item',
+ ]);
+ $field->save();
+
+ // Create the field for search api.
+ $intfield = new Field($index, $field_name);
+ $intfield->setType('integer');
+ $intfield->setPropertyPath($field_name);
+ $intfield->setDatasourceId('entity:entity_test_mulrev_changed');
+ $intfield->setLabel('IntegerField');
+
+ // Add to field to the index.
+ $index->addField($intfield);
+ $index->save();
+ $this->indexItems($this->indexId);
+
+ // Add new entities.
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ for ($i = 1; $i < 8; $i++) {
+ $entity_test_storage->create([
+ 'name' => 'foo bar baz ' . $i,
+ 'body' => 'test ' . $i . ' test',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ 'field_integer' => (bool) $i % 2 ? $i : $i + 1,
+ ])->save();
+ }
+
+ // Index all the items.
+ $this->indexItems($this->indexId);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/processor/RangeSliderProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/processor/RangeSliderProcessorTest.php
new file mode 100644
index 000000000..92b9248e3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/processor/RangeSliderProcessorTest.php
@@ -0,0 +1,101 @@
+processor = new RangeSliderProcessor([], 'range_slider', []);
+
+ $facets_url_generator = $this->prophesize(FacetsUrlGenerator::class);
+ $facets_url_generator->getUrl(Argument::any(), Argument::any())->willReturn(new Url('test', [], ['query' => ['f' => ['animals::(min:__range_slider_min__,max:__range_slider_max__)']]]));
+ $url_generator = $this->prophesize(UrlGeneratorInterface::class);
+
+ $container = new ContainerBuilder();
+ $container->set('url_generator', $url_generator->reveal());
+ $container->set('facets.utility.url_generator', $facets_url_generator->reveal());
+ \Drupal::setContainer($container);
+ }
+
+ /**
+ * Tests the pre query method.
+ *
+ * @covers ::preQuery
+ */
+ public function testPreQuery() {
+ $facet = new Facet(['id' => 'llama'], 'facets_facet');
+ $facet->setActiveItems(['(min:2,max:10)']);
+
+ $this->processor->preQuery($facet);
+
+ $this->assertCount(2, $facet->getActiveItems()[0]);
+ $this->assertEquals([2, 10], $facet->getActiveItems()[0]);
+ }
+
+ /**
+ * Tests the build method.
+ *
+ * @covers ::build
+ */
+ public function testBuild() {
+ // Create the Url processor.
+ $queryString = $this->prophesize(QueryString::class);
+ $queryString->getFilterKey()->willReturn('f');
+ $queryString->getSeparator()->willReturn('::');
+ $queryString->getActiveFilters()->willReturn([]);
+ $urlHandler = $this->prophesize(UrlProcessorHandler::class);
+ $urlHandler->getProcessor()->willReturn($queryString->reveal());
+
+ $facet = $this->prophesize(Facet::class);
+ $facet->getProcessors()->willReturn(['url_processor_handler' => $urlHandler->reveal()]);
+ $facet->getUrlAlias()->willReturn('animals');
+ $facet->id()->willReturn('animals');
+
+ /** @var \Drupal\facets\Result\ResultInterface[] $results */
+ $results = [
+ new Result($facet->reveal(), 1, 1, 1),
+ new Result($facet->reveal(), 5, 5, 5),
+ ];
+ $results[0]->setUrl(new Url('test'));
+ $results[1]->setUrl(new Url('test'));
+
+ $new_results = $this->processor->build($facet->reveal(), $results);
+
+ $this->assertCount(2, $new_results);
+ $params = UrlHelper::buildQuery(['f' => ['animals::(min:__range_slider_min__,max:__range_slider_max__)']]);
+ $expected_route = 'route:test?' . $params;
+ $this->assertEquals($expected_route, $new_results[0]->getUrl()->toUriString());
+ $this->assertEquals($expected_route, $new_results[1]->getUrl()->toUriString());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/processor/SliderProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/processor/SliderProcessorTest.php
new file mode 100644
index 000000000..8ab01f0df
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/processor/SliderProcessorTest.php
@@ -0,0 +1,219 @@
+processor = new SliderProcessor([], 'slider_processor', []);
+ }
+
+ /**
+ * Tests the post query method.
+ *
+ * @covers ::postQuery
+ */
+ public function testPostQuery() {
+ $widgetconfig = ['min_type' => 'foo', 'step' => 1];
+ $facet = new Facet([], 'facets_facet');
+ $facet->setWidget('raw', $widgetconfig);
+ $this->configureContainer($widgetconfig);
+
+ $result_lower = new Result($facet, 5, '5', 1);
+ $result_higher = new Result($facet, 150, '150', 1);
+ $facet->setResults([$result_lower, $result_higher]);
+
+ // Process the data.
+ $startTime = microtime(TRUE);
+ $this->processor->postQuery($facet);
+ $new_results = $facet->getResults();
+ $stopTime = microtime(TRUE);
+
+ if (($stopTime - $startTime) > 1) {
+ $this->fail('Test is too slow');
+ }
+
+ $this->assertCount(146, $new_results);
+ $this->assertEquals(5, $new_results[0]->getRawValue());
+ $this->assertEquals(1, $new_results[0]->getCount());
+ $this->assertEquals(6, $new_results[1]->getRawValue());
+ $this->assertEquals(0, $new_results[1]->getCount());
+ }
+
+ /**
+ * Tests the post query method with a big dataset.
+ *
+ * @covers ::postQuery
+ */
+ public function testPostQueryBigDataSet() {
+ $widgetconfig = ['min_type' => 'foo', 'step' => 1];
+ $facet = new Facet([], 'facets_facet');
+ $facet->setWidget('raw', $widgetconfig);
+ $this->configureContainer($widgetconfig);
+
+ $original_results[] = new Result($facet, 1, 'Small', 5);
+ foreach (range(100, 100000, 10) as $k) {
+ $original_results[] = new Result($facet, $k, 'result ' . $k, 1);
+ }
+ $original_results[] = new Result($facet, 150000, 'Big', 5);
+ $facet->setResults($original_results);
+
+ // Process the data.
+ $startTime = microtime(TRUE);
+ $this->processor->postQuery($facet);
+ $new_results = $facet->getResults();
+ $stopTime = microtime(TRUE);
+
+ if (($stopTime - $startTime) > 1) {
+ $this->fail('Test is too slow');
+ }
+ $this->assertCount(150000, $new_results);
+ }
+
+ /**
+ * Tests the post query method result sorting.
+ *
+ * @covers ::postQuery
+ */
+ public function testPostQueryResultSorting() {
+ $widgetconfig = ['min_type' => 'foo', 'step' => 1];
+ $facet = new Facet([], 'facets_facet');
+ $facet->setWidget('raw', $widgetconfig);
+ $this->configureContainer($widgetconfig);
+
+ $original_results = [];
+ foreach ([10, 100, 200, 5] as $k) {
+ $original_results[] = new Result($facet, $k, 'result ' . $k, 1);
+ }
+ $facet->setResults($original_results);
+
+ // Process the data.
+ $this->processor->postQuery($facet);
+ $new_results = $facet->getResults();
+
+ $this->assertCount(196, $new_results);
+ $this->assertEquals(5, $new_results[0]->getRawValue());
+ $this->assertEquals(200, $new_results[195]->getRawValue());
+ }
+
+ /**
+ * Adds a regression test for the out of range values.
+ */
+ public function testOutOfRange() {
+ $widgetconfig = ['min_type' => 'foo', 'step' => 7];
+ $facet = new Facet([], 'facets_facet');
+ $facet->setWidget('raw', $widgetconfig);
+ $this->configureContainer($widgetconfig);
+
+ $result_lower = new Result($facet, 5, '5', 4);
+ $result_higher = new Result($facet, 15, '15', 4);
+ $facet->setResults([$result_lower, $result_higher]);
+
+ // Process the data.
+ $this->processor->postQuery($facet);
+ $new_results = $facet->getResults();
+
+ $this->assertCount(3, $new_results);
+ $this->assertEquals(5, $new_results[0]->getRawValue());
+ $this->assertEquals(12, $new_results[1]->getRawValue());
+ $this->assertEquals(19, $new_results[2]->getRawValue());
+ }
+
+ /**
+ * Tests the post query method with fixed min/max.
+ *
+ * @covers ::postQuery
+ */
+ public function testPostQueryFixedMinMax() {
+ $widgetconfig = [
+ 'min_type' => 'fixed',
+ 'min_value' => 10,
+ 'max_value' => 20,
+ 'step' => 1,
+ ];
+ $facet = new Facet([], 'facets_facet');
+ $facet->setWidget('raw', $widgetconfig);
+ $this->configureContainer($widgetconfig);
+
+ $result_lower = new Result($facet, 5, '5', 1);
+ $result_higher = new Result($facet, 150, '150', 1);
+ $facet->setResults([$result_lower, $result_higher]);
+
+ // Process the data.
+ $this->processor->postQuery($facet);
+ $new_results = $facet->getResults();
+
+ $this->assertCount(11, $new_results);
+ }
+
+ /**
+ * Tests the post query method with step > 1.
+ *
+ * @covers ::postQuery
+ */
+ public function testPostQueryStep() {
+ $widgetconfig = ['min_type' => 'foo', 'step' => 2];
+ $facet = new Facet([], 'facets_facet');
+ $facet->setWidget('raw', $widgetconfig);
+ $this->configureContainer($widgetconfig);
+
+ $result_lower = new Result($facet, 5, '5', 4);
+ $result_higher = new Result($facet, 15, '15', 4);
+ $facet->setResults([$result_lower, $result_higher]);
+
+ // Process the data.
+ $this->processor->postQuery($facet);
+ $new_results = $facet->getResults();
+
+ $this->assertCount(6, $new_results);
+ $this->assertEquals(5, $new_results[0]->getRawValue());
+ $this->assertEquals(4, $new_results[0]->getCount());
+ $this->assertEquals(7, $new_results[1]->getRawValue());
+ $this->assertEquals(0, $new_results[1]->getCount());
+ $this->assertEquals(15, $new_results[5]->getRawValue());
+ $this->assertEquals(4, $new_results[5]->getCount());
+ }
+
+ /**
+ * Configures the container.
+ *
+ * @param array $config
+ * The config for the widget.
+ */
+ protected function configureContainer(array $config = []) {
+ $widget = $this->prophesize(ArrayWidget::class);
+ $widget->getConfiguration()->willReturn($config);
+ $pluginManager = $this->prophesize(WidgetPluginManager::class);
+ $pluginManager->createInstance('raw', $config)
+ ->willReturn($widget->reveal());
+ $container = new ContainerBuilder();
+ $container->set('plugin.manager.facets.widget', $pluginManager->reveal());
+ \Drupal::setContainer($container);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/widget/RangeSliderWidgetTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/widget/RangeSliderWidgetTest.php
new file mode 100644
index 000000000..5a1dfdecc
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/widget/RangeSliderWidgetTest.php
@@ -0,0 +1,66 @@
+widget = new RangeSliderWidget([], 'range_slider_widget', []);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testGetQueryType() {
+ $result = $this->widget->getQueryType($this->queryTypes);
+ $this->assertEquals('range', $result);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+ $expected = [
+ 'show_numbers' => FALSE,
+ 'prefix' => '',
+ 'suffix' => '',
+ 'min_type' => 'search_result',
+ 'min_value' => 0,
+ 'max_type' => 'search_result',
+ 'max_value' => 10,
+ 'step' => 1,
+ ];
+ $this->assertEquals($expected, $default_config);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testIsPropertyRequired() {
+ $this->assertFalse($this->widget->isPropertyRequired('llama', 'owl'));
+ $this->assertTrue($this->widget->isPropertyRequired('range_slider', 'processors'));
+ $this->assertTrue($this->widget->isPropertyRequired('show_only_one_result', 'settings'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testBuild() {
+ $build = parent::testBuild();
+ $this->assertTrue($build['range']);
+ $this->assertEquals([3, 19999], $build['values']);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/widget/SliderWidgetTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/widget/SliderWidgetTest.php
new file mode 100644
index 000000000..a7eb42df5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_range_widget/tests/src/Unit/Plugin/widget/SliderWidgetTest.php
@@ -0,0 +1,110 @@
+widget = new SliderWidget([], 'slider_widget', []);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testGetQueryType() {
+ $result = $this->widget->getQueryType($this->queryTypes);
+ $this->assertEquals(NULL, $result);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+ $expected = [
+ 'show_numbers' => FALSE,
+ 'prefix' => '',
+ 'suffix' => '',
+ 'min_type' => 'search_result',
+ 'min_value' => 0,
+ 'max_type' => 'search_result',
+ 'max_value' => 10,
+ 'step' => 1,
+ ];
+ $this->assertEquals($expected, $default_config);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testIsPropertyRequired() {
+ $this->assertFalse($this->widget->isPropertyRequired('llama', 'owl'));
+ $this->assertTrue($this->widget->isPropertyRequired('slider', 'processors'));
+ $this->assertTrue($this->widget->isPropertyRequired('show_only_one_result', 'settings'));
+ }
+
+ /**
+ * Tests building of the widget.
+ */
+ public function testBuild() {
+ $widget = $this->prophesize(SliderWidget::class);
+ $widget->getConfiguration()->willReturn(['show_numbers' => FALSE]);
+ $pluginManager = $this->prophesize(WidgetPluginManager::class);
+ $pluginManager->createInstance('slider', [])
+ ->willReturn($widget->reveal());
+
+ $url_generator = $this->prophesize(UrlGeneratorInterface::class);
+
+ $container = new ContainerBuilder();
+ $container->set('plugin.manager.facets.widget', $pluginManager->reveal());
+ $container->set('url_generator', $url_generator->reveal());
+ \Drupal::setContainer($container);
+
+ $facet = new Facet(['id' => 'barn_owl'], 'facets_facet');
+ $originalResults = [];
+ foreach (range(3, 20000, 2) as $rv) {
+ $res = new Result($facet, $rv, 'Value: ' . $rv, ceil($rv / 2));
+ $res->setUrl(new Url('test'));
+ $originalResults[] = $res;
+ }
+
+ $this->originalResults = $originalResults;
+
+ $facet->setResults($this->originalResults);
+ $facet->setFieldIdentifier('owl');
+ $facet->setWidget('slider', []);
+
+ $startTime = microtime(TRUE);
+ $build = $this->widget->build($facet);
+ $stopTime = microtime(TRUE);
+
+ if (($stopTime - $startTime) > 1) {
+ $this->fail('Test is too slow');
+ }
+
+ $this->assertSame('array', gettype($build));
+ $build = $build['#attached']['drupalSettings']['facets']['sliders']['barn_owl'];
+ $this->assertEquals(3, $build['min']);
+ $this->assertEquals(19999, $build['max']);
+ return $build;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/facets_rest.info.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/facets_rest.info.yml
new file mode 100644
index 000000000..338700aa3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/facets_rest.info.yml
@@ -0,0 +1,18 @@
+name: 'Rest Facets'
+type: module
+description: 'Adds facets to rest views based on a Search API index.'
+core_version_requirement: ^9.2 || ^10.0
+package: Search
+dependencies:
+ - facets:facets
+ - drupal:rest
+test_dependencies:
+ - search_api:search_api
+ - facets:facets
+ - drupal:views
+ - drupal:rest
+
+# Information added by Drupal.org packaging script on 2022-04-04
+version: '2.0.2'
+project: 'facets'
+datestamp: 1649070272
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/src/Plugin/views/style/FacetsSerializer.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/src/Plugin/views/style/FacetsSerializer.php
new file mode 100644
index 000000000..a8a7a81b9
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/src/Plugin/views/style/FacetsSerializer.php
@@ -0,0 +1,135 @@
+get('serializer'),
+ $container->getParameter('serializer.formats'),
+ $container->getParameter('serializer.format_providers'),
+ $container->get('facets.manager')
+ );
+ }
+
+ /**
+ * Constructs a FacetsSerializer object.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, array $serializer_formats, array $serializer_format_providers, DefaultFacetManager $facets_manager) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer, $serializer_formats, $serializer_format_providers);
+ $this->facetsManager = $facets_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function defineOptions() {
+ $options = parent::defineOptions();
+ $options['show_facets'] = ['default' => TRUE];
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ $form['show_facets'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show facets in the output'),
+ '#default_value' => $this->options['show_facets'],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ $rows = [];
+ // If the Data Entity row plugin is used, this will be an array of entities
+ // which will pass through Serializer to one of the registered Normalizers,
+ // which will transform it to arrays/scalars. If the Data field row plugin
+ // is used, $rows will not contain objects and will pass directly to the
+ // Encoder.
+ foreach ($this->view->result as $row_index => $row) {
+ // Keep track of the current rendered row, like every style plugin has to
+ // do.
+ // @see \Drupal\views\Plugin\views\style\StylePluginBase::renderFields
+ $this->view->row_index = $row_index;
+ $rows['search_results'][] = $this->view->rowPlugin->render($row);
+ }
+ unset($this->view->row_index);
+
+ // Get the content type configured in the display or fallback to the
+ // default.
+ if ((empty($this->view->live_preview))) {
+ $content_type = $this->displayHandler->getContentType();
+ }
+ else {
+ $content_type = !empty($this->options['formats']) ? reset($this->options['formats']) : 'json';
+ }
+
+ // Processing facets.
+ $facetsource_id = "search_api:views_rest__{$this->view->id()}__{$this->view->getDisplay()->display['id']}";
+ $facets = $this->facetsManager->getFacetsByFacetSourceId($facetsource_id);
+ $this->facetsManager->updateResults($facetsource_id);
+
+ $processed_facets = [];
+ $facets_metadata = [];
+ foreach ($facets as $facet) {
+ $processed_facets[] = $this->facetsManager->build($facet);
+ $facets_metadata[$facet->id()] = [
+ 'label' => $facet->label(),
+ 'weight' => $facet->getWeight(),
+ 'field_id' => $facet->getFieldIdentifier(),
+ 'url_alias' => $facet->getUrlAlias(),
+ ];
+ }
+ uasort($facets_metadata, function ($a, $b) {
+ return (int) $a['weight'] - $b['weight'];
+ });
+
+ $rows['facets'] = array_values($processed_facets);
+ $rows['facets_metadata'] = $facets_metadata;
+
+ if (!$this->options['show_facets']) {
+ $rows = $rows['search_results'];
+ }
+ return $this->serializer->serialize($rows, $content_type, ['views_style_plugin' => $this]);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/rest_view/config/install/views.view.search_api_rest_test_view.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/rest_view/config/install/views.view.search_api_rest_test_view.yml
new file mode 100644
index 000000000..cc94a6a13
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/rest_view/config/install/views.view.search_api_rest_test_view.yml
@@ -0,0 +1,350 @@
+base_field: search_api_id
+base_table: search_api_index_database_search_index
+core: 8.x
+description: ''
+status: true
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: none
+ options: { }
+ cache:
+ type: none
+ options: { }
+ query:
+ type: search_api_query
+ options:
+ skip_access: true
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Search
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: full
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 20, 40, 60'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: '‹ previous'
+ next: 'next ›'
+ first: '« first'
+ last: 'last »'
+ quantity: 9
+ style:
+ type: default
+ row:
+ type: search_api
+ options:
+ view_modes:
+ bundle:
+ 'article': default
+ 'page': default
+ datasource:
+ 'entity:entity_test': default
+ fields:
+ search_api_id:
+ table: search_api_index_database_search_index
+ field: search_api_id
+ id: search_api_id
+ plugin_id: numeric
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Entity ID'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ set_precision: false
+ precision: 0
+ decimal: .
+ separator: ','
+ format_plural: false
+ format_plural_string: "1\x03@count"
+ prefix: ''
+ suffix: ''
+ filters:
+ search_api_fulltext:
+ id: search_api_fulltext
+ table: search_api_index_database_search_index
+ field: search_api_fulltext
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: and
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: search_api_fulltext_op
+ label: 'Fulltext search'
+ description: ''
+ use_operator: true
+ operator: search_api_fulltext_op
+ identifier: search_api_fulltext
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ min_length: 3
+ fields: { }
+ plugin_id: search_api_fulltext
+ id:
+ plugin_id: search_api_numeric
+ id: id
+ table: search_api_index_database_search_index
+ field: id
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: id_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: id_op
+ identifier: id
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ created:
+ plugin_id: search_api_date
+ id: created
+ table: search_api_index_database_search_index
+ field: created
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: created_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: created_op
+ identifier: created
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ keywords:
+ plugin_id: search_api_string
+ id: keywords
+ table: search_api_index_database_search_index
+ field: keywords
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: keywords_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: keywords_op
+ identifier: keywords
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ search_api_language:
+ plugin_id: search_api_language
+ id: search_api_language
+ table: search_api_index_database_search_index
+ field: search_api_language
+ relationship: none
+ admin_label: ''
+ operator: 'in'
+ group: 1
+ exposed: true
+ expose:
+ operator_id: language_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: language_op
+ identifier: language
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ sorts:
+ search_api_id:
+ id: search_api_id
+ table: search_api_index_database_search_index
+ field: search_api_id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ plugin_id: search_api
+ title: 'Fulltext test index'
+ header:
+ result:
+ id: result
+ table: views
+ field: result
+ relationship: none
+ group_type: group
+ admin_label: ''
+ content: 'Displaying @total search results'
+ plugin_id: result
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments:
+ search_api_datasource:
+ plugin_id: search_api
+ id: search_api_datasource
+ table: search_api_index_database_search_index
+ field: search_api_datasource
+ break_phrase: true
+ type:
+ plugin_id: search_api
+ id: type
+ table: search_api_index_database_search_index
+ field: type
+ break_phrase: false
+ not: true
+ keywords:
+ plugin_id: search_api
+ id: keywords
+ table: search_api_index_database_search_index
+ field: keywords
+ break_phrase: true
+ rest_export_1:
+ display_plugin: rest_export
+ id: rest_export_1
+ display_title: 'REST export'
+ position: 3
+ display_options:
+ display_extenders: { }
+ path: facets-rest
+ style:
+ type: facets_serializer
+ row:
+ type: data_field
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - request_format
+ tags: { }
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ path: facets-page
+label: 'Search API Test Fulltext REST search view'
+module: views
+id: search_api_rest_test_view
+tag: ''
+langcode: en
+dependencies:
+ config:
+ - search_api.index.database_search_index
+ module:
+ - search_api
+ - rest_view
+ - facets
+ - facets_rest
+ - rest
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/rest_view/rest_view.info.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/rest_view/rest_view.info.yml
new file mode 100644
index 000000000..203761820
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/rest_view/rest_view.info.yml
@@ -0,0 +1,18 @@
+name: 'Rest view'
+type: module
+description: 'Provides a Search API + rest dependency to execute tests.'
+package: 'Testing'
+hidden: true
+core_version_requirement: ^9.2 || ^10.0
+dependencies:
+ - drupal:rest
+ - drupal:views
+ - drupal:serialization
+ - search_api:search_api
+ - search_api:search_api_test_db
+ - facets:facets_rest
+
+# Information added by Drupal.org packaging script on 2022-04-04
+version: '2.0.2'
+project: 'facets'
+datestamp: 1649070272
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/src/Functional/RestIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/src/Functional/RestIntegrationTest.php
new file mode 100644
index 000000000..6f73f38be
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_rest/tests/src/Functional/RestIntegrationTest.php
@@ -0,0 +1,408 @@
+adminUser = $this->drupalCreateUser([
+ 'administer search_api',
+ 'administer facets',
+ 'access administration pages',
+ 'administer nodes',
+ 'access content overview',
+ 'administer content types',
+ 'administer blocks',
+ 'administer views',
+ ]);
+ $this->drupalLogin($this->adminUser);
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals(5, $this->indexItems($this->indexId), '5 items were indexed.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function installModulesFromClassProperty(ContainerInterface $container) {
+ // This will just set the Drupal state to include the necessary bundles for
+ // our test entity type. Otherwise, fields from those bundles won't be found
+ // and thus removed from the test index. (We can't do it in setUp(), before
+ // calling the parent method, since the container isn't set up at that
+ // point.)
+ $bundles = [
+ 'entity_test_mulrev_changed' => ['label' => 'Entity Test Bundle'],
+ 'item' => ['label' => 'item'],
+ 'article' => ['label' => 'article'],
+ ];
+ \Drupal::state()->set('entity_test_mulrev_changed.bundles', $bundles);
+
+ parent::installModulesFromClassProperty($container);
+ }
+
+ /**
+ * Tests that the facet results are correct.
+ */
+ public function testRestResults() {
+ global $base_url;
+
+ $get_options = ['query' => ['_format' => 'json']];
+
+ $result = $this->drupalGet('/facets-rest', $get_options);
+ $this->assertSession()->responseHeaderEquals('content-type', 'application/json');
+ $this->assertSession()->statusCodeEquals(200);
+ $json_decoded = json_decode($result, TRUE);
+ $this->assertArrayHasKey('search_results', $json_decoded);
+ $this->assertArrayHasKey('facets', $json_decoded);
+ $this->assertEmpty($json_decoded['facets']);
+
+ // Add a new facet to filter by content type.
+ $this->createFacet('Type', 'type', 'type', 'rest_export_1', 'views_rest__search_api_rest_test_view');
+
+ // Use the array widget.
+ $facet_edit_page = '/admin/config/search/facets/type/edit';
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->submitForm(['widget' => 'array'], 'Configure widget');
+
+ $values['widget'] = 'array';
+ $values['widget_config[show_numbers]'] = TRUE;
+ $values['facet_sorting[count_widget_order][status]'] = TRUE;
+ $values['facet_sorting[count_widget_order][settings][sort]'] = 'ASC';
+ $values['facet_sorting[display_value_widget_order][status]'] = FALSE;
+ $values['facet_sorting[active_widget_order][status]'] = FALSE;
+ $values['facet_settings[query_operator]'] = 'or';
+ $values['facet_settings[only_visible_when_facet_source_is_visible]'] = TRUE;
+ $this->submitForm($values, 'Save');
+
+ // Add a new facet to filter by keywords.
+ $this->createFacet('Keywords', 'keywords', 'keywords', 'rest_export_1', 'views_rest__search_api_rest_test_view');
+
+ // Use the array widget.
+ $facet_edit_page = '/admin/config/search/facets/keywords/edit';
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->submitForm(['widget' => 'array'], 'Configure widget');
+
+ $values['widget'] = 'array';
+ $values['widget_config[show_numbers]'] = TRUE;
+ $values['facet_sorting[count_widget_order][status]'] = TRUE;
+ $values['facet_sorting[count_widget_order][settings][sort]'] = 'ASC';
+ $values['facet_sorting[display_value_widget_order][status]'] = FALSE;
+ $values['facet_sorting[active_widget_order][status]'] = FALSE;
+ $values['facet_settings[query_operator]'] = 'or';
+ $values['facet_settings[only_visible_when_facet_source_is_visible]'] = TRUE;
+ $this->submitForm($values, 'Save');
+
+ // Get the output from the rest view and decode it into an array.
+ $result = $this->drupalGet('/facets-rest', $get_options);
+ $this->assertSession()->responseHeaderEquals('content-type', 'application/json');
+ $this->assertSession()->statusCodeEquals(200);
+ $json_decoded = json_decode($result);
+
+ $this->assertEquals(5, count($json_decoded->search_results));
+
+ // Verify the facet "Type".
+ $results = [
+ 'article' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=type%3Aarticle',
+ ],
+ 'raw_value' => 'article',
+ 'count' => 2,
+ ],
+ 'item' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=type%3Aitem',
+ ],
+ 'raw_value' => 'item',
+ 'count' => 3,
+ ],
+ ];
+
+ foreach ($json_decoded->facets[1][0]->type as $result) {
+ $value = $result->values->value;
+ $this->assertEquals($result->values->count, $results[$value]['count']);
+ $this->assertSame($results[$value]['raw_value'], $result->raw_value);
+ foreach ($results[$value]['url'] as $url_part) {
+ $this->assertNotFalse(strpos($result->url, $url_part));
+ }
+ }
+
+ // Verify the facet "Keywords".
+ $results = [
+ 'banana' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Abanana',
+ ],
+ 'raw_value' => 'banana',
+ 'count' => 1,
+ ],
+ 'strawberry' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Astrawberry',
+ ],
+ 'raw_value' => 'strawberry',
+ 'count' => 2,
+ ],
+ 'apple' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Aapple',
+ ],
+ 'raw_value' => 'apple',
+ 'count' => 2,
+ ],
+ 'orange' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Aorange',
+ ],
+ 'raw_value' => 'orange',
+ 'count' => 3,
+ ],
+ 'grape' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Agrape',
+ ],
+ 'raw_value' => 'grape',
+ 'count' => 3,
+ ],
+ ];
+
+ foreach ($json_decoded->facets[0][0]->keywords as $result) {
+ $value = $result->values->value;
+ $this->assertEquals($result->values->count, $results[$value]['count']);
+ $this->assertSame($results[$value]['raw_value'], $result->raw_value);
+ foreach ($results[$value]['url'] as $url_part) {
+ $this->assertNotFalse(strpos($result->url, $url_part));
+ }
+ }
+
+ // Filter and verify that the results are correct.
+ $json = $this->drupalGet($base_url . '/facets-rest?f%5B0%5D=type%3Aitem', $get_options);
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->responseHeaderEquals('content-type', 'application/json');
+ $json_decoded = json_decode($json);
+
+ $this->assertEquals(3, count($json_decoded->search_results));
+
+ $results = [
+ 'article' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=type%3Aarticle&f%5B1%5D=type%3Aitem',
+ ],
+ 'raw_value' => 'article',
+ 'count' => 2,
+ ],
+ 'item' => [
+ 'url' => [$base_url, '/facets-rest', '_format=json'],
+ 'raw_value' => 'item',
+ 'count' => 3,
+ ],
+ 'banana' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Abanana&f%5B1%5D=type%3Aitem',
+ ],
+ 'raw_value' => 'banana',
+ 'count' => 0,
+ ],
+ 'strawberry' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Astrawberry&f%5B1%5D=type%3Aitem',
+ ],
+ 'raw_value' => 'strawberry',
+ 'count' => 0,
+ ],
+ 'apple' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Aapple&f%5B1%5D=type%3Aitem',
+ ],
+ 'raw_value' => 'apple',
+ 'count' => 1,
+ ],
+ 'orange' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Aorange&f%5B1%5D=type%3Aitem',
+ ],
+ 'raw_value' => 'orange',
+ 'count' => 2,
+ ],
+ 'grape' => [
+ 'url' => [
+ $base_url,
+ '/facets-rest',
+ '_format=json',
+ 'f%5B0%5D=keywords%3Agrape&f%5B1%5D=type%3Aitem',
+ ],
+ 'raw_value' => 'grape',
+ 'count' => 1,
+ ],
+ ];
+
+ foreach ($json_decoded->facets[1][0]->type as $result) {
+ $value = $result->values->value;
+ $this->assertEquals($results[$value]['count'], $result->values->count);
+ $this->assertSame($results[$value]['raw_value'], $result->raw_value);
+ foreach ($results[$value]['url'] as $url_part) {
+ $this->assertStringContainsString($url_part, $result->url);
+ }
+ }
+
+ foreach ($json_decoded->facets[0][0]->keywords as $result) {
+ $value = $result->values->value;
+ $this->assertEquals($results[$value]['count'], $result->values->count);
+ $this->assertSame($results[$value]['raw_value'], $result->raw_value);
+ foreach ($results[$value]['url'] as $url_part) {
+ $this->assertStringContainsString($url_part, $result->url);
+ }
+ }
+ }
+
+ /**
+ * Tests that the system raises an error when selecting the wrong widget.
+ */
+ public function testWidgetSelection() {
+ $id = 'type';
+
+ // Add a new facet to filter by content type.
+ $this->createFacet('Type', $id, 'type', 'rest_export_1', 'views_rest__search_api_rest_test_view');
+
+ // Use the array widget.
+ $facet_edit_page = '/admin/config/search/facets/' . $id . '/edit';
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->submitForm(['widget' => 'checkbox'], 'Configure widget');
+ $this->assertSession()->pageTextContains('The Facet source is a Rest export. Please select a raw widget.');
+
+ $this->submitForm(['widget' => 'array'], 'Configure widget');
+ $this->assertSession()->pageTextNotContains('The Facet source is a Rest export. Please select a raw widget.');
+ }
+
+ /**
+ * Tests urls on the same path.
+ */
+ public function testSamePath() {
+ $get_options = ['query' => ['_format' => 'json']];
+
+ $id = 'type';
+ $this->createFacet('Type', $id . '_rest', 'type', 'rest_export_1', 'views_rest__search_api_rest_test_view', FALSE);
+ $this->createFacet('Type', $id, 'type', 'page_1', 'views_page__search_api_rest_test_view');
+
+ $values['widget'] = 'array';
+ $values['widget_config[show_numbers]'] = TRUE;
+ $values['facet_settings[url_alias]'] = 'type';
+ $values['facet_settings[only_visible_when_facet_source_is_visible]'] = TRUE;
+ $this->drupalGet('/admin/config/search/facets/type_rest/edit');
+ $this->submitForm(['widget' => 'array'], 'Configure widget');
+ $this->submitForm($values, 'Save');
+
+ $this->drupalGet('facets-page');
+ $this->clickLink('item');
+ $this->assertSession()->pageTextContains('Displaying 3 search results');
+ $pageUrl = $this->getSession()->getCurrentUrl();
+ $restUrl = str_replace('facets-page', 'facets-rest', $pageUrl);
+
+ $result = $this->drupalGet($restUrl, $get_options);
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->responseHeaderEquals('content-type', 'application/json');
+ $json_decoded = json_decode($result);
+
+ $this->assertEquals(3, count($json_decoded->search_results));
+ }
+
+ /**
+ * Tests hiding of facets from rest views.
+ */
+ public function testHideFacets() {
+ $get_options = ['query' => ['_format' => 'json']];
+
+ $id = 'type_rest';
+ $this->createFacet('Type', $id, 'type', 'rest_export_1', 'views_rest__search_api_rest_test_view', FALSE);
+
+ $facet = Facet::load($id);
+ $facet->setWidget('array', ['show_numbers' => TRUE]);
+ $facet->save();
+
+ $result = $this->drupalGet('facets-rest', $get_options);
+ $this->assertSession()->responseHeaderEquals('content-type', 'application/json');
+ $this->assertSession()->statusCodeEquals(200);
+
+ $json_decoded = json_decode($result, TRUE);
+ $this->assertArrayHasKey('facets', $json_decoded);
+ $this->assertArrayHasKey('search_results', $json_decoded);
+
+ $this->drupalGet('admin/structure/views/nojs/display/search_api_rest_test_view/rest_export_1/style_options');
+ $this->submitForm(['style_options[show_facets]' => FALSE], 'Apply');
+ $this->submitForm([], 'Save');
+
+ $result = $this->drupalGet('facets-rest', $get_options);
+ $this->assertSession()->responseHeaderEquals('content-type', 'application/json');
+ $this->assertSession()->statusCodeEquals(200);
+
+ $json_decoded = json_decode($result, TRUE);
+ $this->assertArrayNotHasKey('facets', $json_decoded);
+ $this->assertArrayNotHasKey('search_results', $json_decoded);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/config/schema/facets_searchbox_widget.schema.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/config/schema/facets_searchbox_widget.schema.yml
new file mode 100755
index 000000000..baa70b708
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/config/schema/facets_searchbox_widget.schema.yml
@@ -0,0 +1,4 @@
+facet.widget.config.searchbox_checkbox:
+ type: facet.widget.config.checkbox
+facet.widget.config.searchbox_links:
+ type: facet.widget.config.links
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/css/searchbox.css b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/css/searchbox.css
new file mode 100755
index 000000000..12ea821bf
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/css/searchbox.css
@@ -0,0 +1,17 @@
+.facets-widget-searchbox_checkbox .facets-widget-searchbox {
+ margin-bottom: 1rem;
+}
+
+.facets-widget-searchbox_links .facets-widget-searchbox {
+ margin-bottom: 1rem;
+}
+
+.facets-widget-searchbox_links .facet-item--expanded .facets-widget-searchbox,
+.facets-widget-searchbox_links .facet-item--expanded .facets-widget-searchbox-no-result {
+ display: none;
+}
+
+.facets-widget-searchbox-no-result.hide,
+.hide-if-no-result.hide {
+ display: none;
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.info.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.info.yml
new file mode 100644
index 000000000..1967ffdcd
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.info.yml
@@ -0,0 +1,16 @@
+name: 'Facets Searchbox Widget'
+type: module
+description: 'Provides a input to search and filter facet items.'
+core_version_requirement: ^9.2 || ^10.0
+package: Search
+dependencies:
+ - facets:facets
+test_dependencies:
+ - search_api:search_api
+ - facets:facets
+ - drupal:views
+
+# Information added by Drupal.org packaging script on 2022-04-04
+version: '2.0.2'
+project: 'facets'
+datestamp: 1649070272
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.libraries.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.libraries.yml
new file mode 100755
index 000000000..468f2135f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.libraries.yml
@@ -0,0 +1,11 @@
+searchbox:
+ version: VERSION
+ js:
+ js/searchbox.js: { attributes: { defer: true } }
+ css:
+ component:
+ css/searchbox.css: {}
+ dependencies:
+ - core/drupal
+ - core/drupalSettings
+ - core/jquery.once
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.module b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.module
new file mode 100755
index 000000000..3620276fa
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/facets_searchbox_widget.module
@@ -0,0 +1,84 @@
+ [
+ 'variables' => [
+ 'facet' => NULL,
+ 'items' => [],
+ 'title' => '',
+ 'list_type' => 'ul',
+ 'wrapper_attributes' => [],
+ 'attributes' => [],
+ 'empty' => NULL,
+ 'context' => [],
+ ],
+ ],
+ 'facets_item_list__searchbox_links' => [
+ 'variables' => [
+ 'facet' => NULL,
+ 'items' => [],
+ 'title' => '',
+ 'list_type' => 'ul',
+ 'wrapper_attributes' => [],
+ 'attributes' => [],
+ 'empty' => NULL,
+ 'context' => [],
+ ],
+ ],
+ ];
+}
+
+/**
+ * Prepares variables for facets summary item list templates.
+ *
+ * Default template: facets--item-list.html.twig.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - items: An array of items to be displayed in the list. Each item can be
+ * either a string or a render array. If #type, #theme, or #markup
+ * properties are not specified for child render arrays, they will be
+ * inherited from the parent list, allowing callers to specify larger
+ * nested lists without having to explicitly specify and repeat the
+ * render properties for all nested child lists.
+ * - title: A title to be prepended to the list.
+ * - list_type: The type of list to return (e.g. "ul", "ol").
+ * - wrapper_attributes: HTML attributes to be applied to the list wrapper.
+ *
+ * @see https://www.drupal.org/node/1842756
+ */
+function facets_searchbox_widget_preprocess_facets_item_list__searchbox_checkbox(array &$variables) {
+ template_preprocess_item_list($variables);
+}
+
+/**
+ * Prepares variables for facets summary item list templates.
+ *
+ * Default template: facets--item-list.html.twig.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - items: An array of items to be displayed in the list. Each item can be
+ * either a string or a render array. If #type, #theme, or #markup
+ * properties are not specified for child render arrays, they will be
+ * inherited from the parent list, allowing callers to specify larger
+ * nested lists without having to explicitly specify and repeat the
+ * render properties for all nested child lists.
+ * - title: A title to be prepended to the list.
+ * - list_type: The type of list to return (e.g. "ul", "ol").
+ * - wrapper_attributes: HTML attributes to be applied to the list wrapper.
+ *
+ * @see https://www.drupal.org/node/1842756
+ */
+function facets_searchbox_widget_preprocess_facets_item_list__searchbox_links(array &$variables) {
+ template_preprocess_item_list($variables);
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/js/searchbox.js b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/js/searchbox.js
new file mode 100644
index 000000000..2757c3394
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/js/searchbox.js
@@ -0,0 +1,103 @@
+/**
+ * @file
+ * Provides the searchbox functionality.
+ */
+
+(function ($) {
+
+ 'use strict';
+
+ Drupal.facets = Drupal.facets || {};
+
+ Drupal.behaviors.facets_searchbox = {
+ attach: function (context, settings) {
+
+ const $facetsWidgetSearchbox = $('.facets-widget-searchbox', context);
+
+ $facetsWidgetSearchbox.on("keyup", function () {
+ let $facetsWidgetSearchboxNoResult = $('.facets-widget-searchbox-no-result', context);
+ let $targetList = $(this).next('.facets-widget-searchbox-list', context);
+ let targetListId = $targetList.attr('data-drupal-facet-id');
+ let $facetsSoftLimitLink = $targetList.next('.facets-soft-limit-link', context);
+ let filter = $facetsWidgetSearchbox.val().toUpperCase();
+ let displayCount = 0;
+ let display = getDisplayBehavior.call(this);
+
+ $("[data-drupal-facet-alias='" + targetListId + "'] li").each(function () {
+ if (filter !== '') {
+ search.call(this, filter, display, $targetList);
+ } else {
+ displayCount = resetSearch.call(this, $facetsSoftLimitLink, display, displayCount);
+ }
+ });
+
+ handleNoResults(targetListId, $facetsWidgetSearchboxNoResult);
+ });
+
+ function search(filter, display, $targetList) {
+ let value = $(this).find('.facet-item__value').html();
+
+ if (value.toUpperCase().indexOf(filter) === 0) {
+ if (!$(this).hasClass('hide-if-no-result')) {
+ $(this).css('display', display);
+ }
+ $targetList.next('.facets-soft-limit-link', context).css('display', 'inline');
+ } else {
+ if (!$(this).hasClass('facet-item--expanded')) {
+ $(this).css('display', 'none');
+ } else {
+ $(this).addClass('hide-if-no-result');
+ }
+
+ $targetList.next('.facets-soft-limit-link', context).css('display', 'none');
+ }
+ }
+
+ function resetSearch($facetsSoftLimitLink, display, displayCount) {
+ if ($facetsSoftLimitLink.length === 0 || $facetsSoftLimitLink.hasClass('open')) {
+ if (!$(this).hasClass('hide-if-no-result')) {
+ $(this).css('display', display);
+ }
+ } else {
+ if (displayCount >= 5) {
+ if (!$(this).hasClass('facet-item--expanded')) {
+ $(this).css('display', 'none');
+ } else {
+ $(this).addClass('hide-if-no-result');
+ }
+ } else {
+ if (!$(this).hasClass('hide-if-no-result')) {
+ $(this).css('display', display);
+ }
+ displayCount += 1;
+ }
+ }
+ $facetsSoftLimitLink.css('display', 'inline');
+
+ return displayCount;
+ }
+
+ function getDisplayBehavior() {
+ switch ($(this).attr('data-type')) {
+ case 'checkbox':
+ return 'flex';
+
+ case 'links':
+ return 'inline';
+ }
+ }
+
+ function handleNoResults(targetListId, $facetsWidgetSearchboxNoResult) {
+ if ($("[data-drupal-facet-alias='" + targetListId + "'] li:visible:not(.hide-if-no-result)").length === 0) {
+ $facetsWidgetSearchboxNoResult.removeClass('hide');
+ $('.hide-if-no-result').addClass('hide');
+ } else {
+ $facetsWidgetSearchboxNoResult.addClass('hide');
+ $('.hide-if-no-result').removeClass('hide');
+ }
+ }
+
+ }
+ };
+
+})(jQuery);
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/src/Plugin/facets/widget/SearchboxCheckboxWidget.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/src/Plugin/facets/widget/SearchboxCheckboxWidget.php
new file mode 100755
index 000000000..c70b3d6d7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/src/Plugin/facets/widget/SearchboxCheckboxWidget.php
@@ -0,0 +1,27 @@
+
+
+ {% if facet.widget.type %}
+ {%- set attributes = attributes.addClass('item-list__' ~ facet.widget.type, 'facets-widget-searchbox-list') %}
+ {% endif %}
+ {% if items or empty %}
+ {%- if title is not empty -%}
+ {{ title }}
+ {%- endif -%}
+
+ {%- if items -%}
+ <{{ list_type }}{{ attributes }}>
+ {%- for item in items -%}
+ {{ item.value }}
+ {%- endfor -%}
+{{ list_type }}>
+ {%- else -%}
+ {{- empty -}}
+ {%- endif -%}
+ {%- endif %}
+
+{% if facet.widget.type == "dropdown" %}
+ {{ 'Facet'|t }} {{ facet.label }}
+{%- endif %}
+{{ 'No result'|t }}
+
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/templates/facets-item-list--searchbox-links.html.twig b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/templates/facets-item-list--searchbox-links.html.twig
new file mode 100755
index 000000000..7a23c11a1
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/templates/facets-item-list--searchbox-links.html.twig
@@ -0,0 +1,52 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a facets item list.
+ *
+ * Available variables:
+ * - items: A list of items. Each item contains:
+ * - attributes: HTML attributes to be applied to each list item.
+ * - value: The content of the list element.
+ * - title: The title of the list.
+ * - list_type: The tag for list element ("ul" or "ol").
+ * - wrapper_attributes: HTML attributes to be applied to the list wrapper.
+ * - attributes: HTML attributes to be applied to the list.
+ * - empty: A message to display when there are no items. Allowed value is a
+ * string or render array.
+ * - context: A list of contextual data associated with the list. May contain:
+ * - list_style: The ID of the widget plugin this facet uses.
+ * - facet: The facet for this result item.
+ * - id: the machine name for the facet.
+ * - label: The facet label.
+ *
+ * @see facets_preprocess_facets_item_list()
+ *
+ * @ingroup themeable
+ */
+#}
+
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/FunctionalJavascript/SearchboxWidgetJSTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/FunctionalJavascript/SearchboxWidgetJSTest.php
new file mode 100755
index 000000000..5811f8993
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/FunctionalJavascript/SearchboxWidgetJSTest.php
@@ -0,0 +1,114 @@
+getStorage('facets_facet');
+ $id = 'sl';
+
+ // Create and save a facet with a checkbox widget on the 'type' field.
+ $facet_storage->create([
+ 'id' => $id,
+ 'name' => strtoupper($id),
+ 'url_alias' => $id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'field_identifier' => 'type',
+ 'empty_behavior' => ['behavior' => 'none'],
+ 'weight' => 1,
+ 'widget' => [
+ 'type' => 'searchbox_links',
+ ],
+ 'processor_configs' => [
+ 'url_processor_handler' => [
+ 'processor_id' => 'url_processor_handler',
+ 'weights' => ['pre_query' => -10, 'build' => -10],
+ 'settings' => [],
+ ],
+ ],
+ ])->save();
+ $this->createBlock($id);
+
+ // Go to the views page.
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Make sure the block is shown on the page.
+ $page = $this->getSession()->getPage();
+ $block = $page->findById('block-sl-block');
+ $block->isVisible();
+
+ // Make sure the searchbox input exists.
+ $this->assertSession()->elementExists('css', '.facets-widget-searchbox');
+ // Make sure the searchbox link list exists.
+ $this->assertSession()->elementExists('css', '.facets-widget-searchbox-list');
+ }
+
+ /**
+ * Tests searchbox for checkbox widget.
+ */
+ public function testSearchboxCheckboxWidget() {
+ $facet_storage = \Drupal::entityTypeManager()->getStorage('facets_facet');
+ $id = 'sc';
+
+ // Create and save a facet with a checkbox widget on the 'type' field.
+ $facet_storage->create([
+ 'id' => $id,
+ 'name' => strtoupper($id),
+ 'url_alias' => $id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'field_identifier' => 'type',
+ 'empty_behavior' => ['behavior' => 'none'],
+ 'widget' => [
+ 'type' => 'searchbox_checkbox',
+ 'config' => [
+ 'show_numbers' => TRUE,
+ ],
+ ],
+ 'processor_configs' => [
+ 'url_processor_handler' => [
+ 'processor_id' => 'url_processor_handler',
+ 'weights' => ['pre_query' => -10, 'build' => -10],
+ 'settings' => [],
+ ],
+ ],
+ ])->save();
+ $this->createBlock($id);
+
+ // Go to the views page.
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Make sure the block is shown on the page.
+ $page = $this->getSession()->getPage();
+ $block = $page->findById('block-sc-block');
+ $this->assertTrue($block->isVisible());
+
+ // Make sure the searchbox input exists.
+ $this->assertSession()->elementExists('css', '.facets-widget-searchbox');
+ // Make sure the searchbox link list exists.
+ $this->assertSession()->elementExists('css', '.facets-widget-searchbox-list');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/Unit/Plugin/widget/SearchboxCheckboxWidgetTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/Unit/Plugin/widget/SearchboxCheckboxWidgetTest.php
new file mode 100644
index 000000000..b31dd5b3e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/Unit/Plugin/widget/SearchboxCheckboxWidgetTest.php
@@ -0,0 +1,71 @@
+widget = new CheckboxWidget(['show_numbers' => TRUE], 'checkbox_widget', []);
+ }
+
+ /**
+ * Tests widget without filters.
+ */
+ public function testNoFilterResults() {
+ $facet = $this->facet;
+ $facet->setResults($this->originalResults);
+
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $this->assertEquals(['facet-inactive', 'js-facets-checkbox-links'], $output['#attributes']['class']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertSame('array', gettype($output['#items'][$index]['#title']));
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+ $this->assertArrayHasKey('show_numbers', $default_config);
+ $this->assertArrayHasKey('soft_limit', $default_config);
+ $this->assertArrayHasKey('show_reset_link', $default_config);
+ $this->assertArrayHasKey('reset_text', $default_config);
+ $this->assertArrayHasKey('soft_limit_settings', $default_config);
+ $this->assertArrayHasKey('show_less_label', $default_config['soft_limit_settings']);
+ $this->assertArrayHasKey('show_more_label', $default_config['soft_limit_settings']);
+
+ $this->assertEquals(FALSE, $default_config['show_numbers']);
+ $this->assertEquals(0, $default_config['soft_limit']);
+ $this->assertEquals(FALSE, $default_config['show_reset_link']);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/Unit/Plugin/widget/SearchboxLinksWidgetTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/Unit/Plugin/widget/SearchboxLinksWidgetTest.php
new file mode 100644
index 000000000..357b22477
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_searchbox_widget/tests/src/Unit/Plugin/widget/SearchboxLinksWidgetTest.php
@@ -0,0 +1,284 @@
+widget = new LinksWidget([], 'links_widget', []);
+ }
+
+ /**
+ * Tests widget without filters.
+ */
+ public function testNoFilterResults() {
+ $facet = $this->facet;
+ $facet->setResults($this->originalResults);
+
+ $this->widget->setConfiguration(['show_numbers' => TRUE]);
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertSame('array', gettype($output['#items'][$index]['#title']));
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * Test widget with 2 active items.
+ */
+ public function testActiveItems() {
+ $original_results = $this->originalResults;
+ $original_results[0]->setActiveState(TRUE);
+ $original_results[3]->setActiveState(TRUE);
+
+ $facet = $this->facet;
+ $facet->setResults($original_results);
+
+ $this->widget->setConfiguration(['show_numbers' => TRUE]);
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10, TRUE),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9, TRUE),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ if ($index === 0 || $index === 3) {
+ $this->assertEquals(['is-active'], $output['#items'][$index]['#attributes']['class']);
+ }
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * Tests widget, make sure hiding and showing numbers works.
+ */
+ public function testHideNumbers() {
+ $original_results = $this->originalResults;
+ $original_results[1]->setActiveState(TRUE);
+
+ $facet = $this->facet;
+ $facet->setResults($original_results);
+
+ $this->widget->setConfiguration(['show_numbers' => FALSE]);
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10, FALSE, FALSE),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20, TRUE, FALSE),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15, FALSE, FALSE),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9, FALSE, FALSE),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ if ($index === 1) {
+ $this->assertEquals(['is-active'], $output['#items'][$index]['#attributes']['class']);
+ }
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+
+ // Enable the 'show_numbers' setting again to make sure that the switch
+ // between those settings works.
+ $this->widget->setConfiguration(['show_numbers' => TRUE]);
+
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20, TRUE),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ if ($index === 1) {
+ $this->assertEquals(['is-active'], $output['#items'][$index]['#attributes']['class']);
+ }
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * Tests for links widget with children.
+ */
+ public function testChildren() {
+ $original_results = $this->originalResults;
+
+ $facet = $this->facet;
+ $child = new Result($facet, 'snake', 'Snake', 5);
+ $original_results[1]->setActiveState(TRUE);
+ $original_results[1]->setChildren([$child]);
+
+ $facet->setResults($original_results);
+
+ $this->widget->setConfiguration(['show_numbers' => TRUE]);
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20, TRUE),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ if ($index === 1) {
+ $this->assertEquals(['is-active'], $output['#items'][$index]['#attributes']['class']);
+ $this->assertEquals(['facet-item', 'facet-item--expanded'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ else {
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+ }
+
+ /**
+ * Tests the rest link.
+ */
+ public function testResetLink() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $request = new Request();
+ $request->query->set('f', []);
+
+ $request_stack = new RequestStack();
+ $request_stack->push($request);
+
+ $this->createContainer();
+ $container = \Drupal::getContainer();
+ $container->set('request_stack', $request_stack);
+ \Drupal::setContainer($container);
+
+ // Enable the show reset link.
+ $this->widget->setConfiguration(['show_reset_link' => TRUE]);
+ $output = $this->widget->build($facet);
+
+ // Check that we now have more results.
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(5, $output['#items']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+ $this->assertArrayHasKey('show_numbers', $default_config);
+ $this->assertArrayHasKey('soft_limit', $default_config);
+ $this->assertArrayHasKey('show_reset_link', $default_config);
+ $this->assertArrayHasKey('reset_text', $default_config);
+ $this->assertArrayHasKey('soft_limit_settings', $default_config);
+ $this->assertArrayHasKey('show_less_label', $default_config['soft_limit_settings']);
+ $this->assertArrayHasKey('show_more_label', $default_config['soft_limit_settings']);
+
+ $this->assertEquals(FALSE, $default_config['show_numbers']);
+ $this->assertEquals(0, $default_config['soft_limit']);
+ $this->assertEquals(FALSE, $default_config['show_reset_link']);
+ }
+
+ /**
+ * Sets up a container.
+ */
+ protected function createContainer() {
+ $router = $this->getMockBuilder(TestRouterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $router->expects($this->any())
+ ->method('matchRequest')
+ ->willReturn([
+ '_raw_variables' => new ParameterBag([]),
+ '_route' => 'test',
+ ]);
+
+ $url_processor = $this->getMockBuilder(UrlProcessorInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $manager = $this->getMockBuilder(FacetSourcePluginManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $manager->expects($this->exactly(1))
+ ->method('createInstance')
+ ->willReturn($url_processor);
+
+ $storage = $this->createMock(EntityStorageInterface::class);
+ $em = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $em->expects($this->exactly(1))
+ ->method('getStorage')
+ ->willReturn($storage);
+
+ $container = new ContainerBuilder();
+ $container->set('router.no_access_checks', $router);
+ $container->set('entity_type.manager', $em);
+ $container->set('plugin.manager.facets.url_processor', $manager);
+ \Drupal::setContainer($container);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/config/schema/facets_summary.facets_summary.schema.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/config/schema/facets_summary.facets_summary.schema.yml
new file mode 100644
index 000000000..674ebd476
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/config/schema/facets_summary.facets_summary.schema.yml
@@ -0,0 +1,55 @@
+facets_summary.facets_summary.*:
+ type: config_entity
+ label : 'Facet'
+ mapping:
+ id:
+ type: string
+ label: 'ID'
+ name:
+ type: label
+ label: 'Name'
+ facet_source_id:
+ type: string
+ label: 'Facet source id'
+ processor_configs:
+ type: sequence
+ label: 'Processor settings'
+ sequence:
+ type: mapping
+ label: 'A processor'
+ mapping:
+ processor_id:
+ type: string
+ label: 'The plugin ID of the processor'
+ weights:
+ type: sequence
+ label: 'The processors weight for this stage'
+ sequence:
+ type: string
+ label: 'the weight'
+ settings:
+ type: plugin.plugin_configuration.facets_summary_processor.[%parent.processor_id]
+ facets:
+ type: sequence
+ label: 'Facets configuration'
+ sequence:
+ type: mapping
+ label: 'Facet'
+ mapping:
+ checked:
+ type: boolean
+ label: 'Is this facet enabled'
+ label:
+ type: string
+ label: 'Label'
+ translatable: true
+ separator:
+ type: string
+ label: 'Results separator'
+ translatable: true
+ show_count:
+ type: boolean
+ label: 'Show count on items'
+ weight:
+ type: integer
+ label: 'Facet Weight'
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/config/schema/facets_summary.processor.schema.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/config/schema/facets_summary.processor.schema.yml
new file mode 100644
index 000000000..91b6d3b28
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/config/schema/facets_summary.processor.schema.yml
@@ -0,0 +1,27 @@
+plugin.plugin_configuration.facets_summary_processor.show_summary:
+ type: config_object
+
+plugin.plugin_configuration.facets_summary_processor.show_count:
+ type: config_object
+
+plugin.plugin_configuration.facets_summary_processor.hide_when_not_rendered:
+ type: config_object
+
+plugin.plugin_configuration.facets_summary_processor.show_text_when_empty:
+ type: mapping
+ label: 'No results behavior'
+ mapping:
+ text:
+ type: text_format
+ label: 'The text to show when there is no current search'
+
+plugin.plugin_configuration.facets_summary_processor.reset_facets:
+ type: mapping
+ label: 'Reset facets link'
+ mapping:
+ link_text:
+ type: label
+ label: 'The text to show for the reset link.'
+ position:
+ type: string
+ label: 'Position of the reset link.'
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.info.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.info.yml
new file mode 100644
index 000000000..448cbde6d
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.info.yml
@@ -0,0 +1,16 @@
+name: 'Facets summary (Experimental)'
+type: module
+description: 'Exposes a Facets block summary showing the current search.'
+core_version_requirement: ^9.2 || ^10.0
+package: Search
+configure: entity.facets_facet.collection
+dependencies:
+ - facets:facets
+test_dependencies:
+ - search_api:search_api
+ - drupal:views
+
+# Information added by Drupal.org packaging script on 2022-04-04
+version: '2.0.2'
+project: 'facets'
+datestamp: 1649070272
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.install b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.install
new file mode 100644
index 000000000..acb30e59c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.install
@@ -0,0 +1,52 @@
+getFacetSourceId();
+ $old_ids = ['views_page', 'views_block', 'views_rest'];
+
+ foreach ($old_ids as $id) {
+ if (strpos($facetSourceId, $id) !== FALSE) {
+ $new_id = str_replace($id . ':', 'search_api:' . $id . '__', $facetSourceId);
+ $entity->setFacetSourceId($new_id);
+ $entity->save();
+ }
+ }
+ }
+}
+
+/**
+ * Set reset link position default value for all existing Facet summary.
+ */
+function facets_summary_update_8002(&$sandbox = NULL) {
+ \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'facets_summary', function ($facets_summary) {
+ $update = FALSE;
+
+ if ($facets_summary instanceof FacetsSummaryInterface) {
+ $processor_settings = $facets_summary->getProcessorConfigs();
+
+ if (isset($processor_settings['reset_facets'])) {
+ $processor_settings['reset_facets']['settings']['position'] = ResetFacetsProcessor::POSITION_BEFORE;
+ $facets_summary->addProcessor($processor_settings['reset_facets']);
+ $update = TRUE;
+ }
+ }
+
+ return $update;
+ });
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.action.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.action.yml
new file mode 100644
index 000000000..de2a139b3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.action.yml
@@ -0,0 +1,5 @@
+entity.facets_summary.add_form:
+ route_name: entity.facets_summary.add_form
+ title: 'Add facet summary'
+ appears_on:
+ - entity.facets_facet.collection
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.contextual.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.contextual.yml
new file mode 100644
index 000000000..83d82fbbc
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.contextual.yml
@@ -0,0 +1,4 @@
+entity.facets_summary.edit_form:
+ title: 'Edit facets summary'
+ route_name: 'entity.facets_summary.edit_form'
+ group: facets_summary
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.task.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.task.yml
new file mode 100644
index 000000000..9c06d8dd3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.links.task.yml
@@ -0,0 +1,9 @@
+entity.facets_summary.edit_form:
+ title: 'Edit'
+ route_name: entity.facets_summary.edit_form
+ base_route: entity.facets_summary.edit_form
+
+entity.facets_summary.settings_form:
+ title: 'Facet Summary settings'
+ route_name: entity.facets_summary.settings_form
+ base_route: entity.facets_summary.edit_form
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.module b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.module
new file mode 100644
index 000000000..6720862c6
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.module
@@ -0,0 +1,100 @@
+ [
+ 'variables' => [
+ 'count' => NULL,
+ ],
+ ],
+ 'facets_summary_facet' => [
+ 'render element' => 'elements',
+ 'variables' => [
+ 'label' => NULL,
+ 'separator' => '',
+ 'items' => [],
+ 'facet_id' => NULL,
+ 'facet_admin_label' => NULL,
+ ],
+ ],
+ 'facets_summary_facet_result' => [
+ 'variables' => [
+ 'label' => NULL,
+ 'show_count' => FALSE,
+ 'count' => NULL,
+ 'facet_id' => NULL,
+ ],
+ ],
+ 'facets_summary_empty' => [
+ 'variables' => [
+ 'message' => '',
+ ],
+ ],
+ 'facets_summary_item_list' => [
+ 'variables' => [
+ 'items' => [],
+ 'title' => '',
+ 'list_type' => 'ul',
+ 'wrapper_attributes' => [],
+ 'attributes' => [],
+ 'empty' => NULL,
+ 'context' => [],
+ 'facet_summary_id' => NULL,
+ ],
+ ],
+ ];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function facets_summary_theme_suggestions_facets_summary_facet(array $variables) {
+ // Add suggestions as: facets-summary-facet--{facet_id}.
+ return ['facets_summary_facet__' . $variables['facet_id']];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function facets_summary_theme_suggestions_facets_summary_facet_result(array $variables) {
+ // Add suggestions as: facets-summary-facet-result--{facet_id}.
+ return [$variables['theme_hook_original'] . '__' . $variables['facet_id']];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function facets_summary_theme_suggestions_facets_summary_item_list(array $variables) {
+ return [$variables['theme_hook_original'] . '__' . $variables['facet_summary_id']];
+}
+
+/**
+ * Prepares variables for facets summary item list templates.
+ *
+ * Default template: facets-summary-item-list.html.twig.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - items: An array of items to be displayed in the list. Each item can be
+ * either a string or a render array. If #type, #theme, or #markup
+ * properties are not specified for child render arrays, they will be
+ * inherited from the parent list, allowing callers to specify larger
+ * nested lists without having to explicitly specify and repeat the
+ * render properties for all nested child lists.
+ * - title: A title to be prepended to the list.
+ * - list_type: The type of list to return (e.g. "ul", "ol").
+ * - wrapper_attributes: HTML attributes to be applied to the list wrapper.
+ *
+ * @see https://www.drupal.org/node/1842756
+ */
+function facets_summary_preprocess_facets_summary_item_list(array &$variables) {
+ template_preprocess_item_list($variables);
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.routing.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.routing.yml
new file mode 100644
index 000000000..2ebb21e3a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.routing.yml
@@ -0,0 +1,27 @@
+entity.facets_summary.add_form:
+ path: '/admin/config/search/facets/add-facet-summary'
+ defaults:
+ _entity_form: 'facets_summary.default'
+ requirements:
+ _entity_create_access: 'facets_summary'
+
+entity.facets_summary.delete_form:
+ path: '/admin/config/search/facets/facet-summary/{facets_summary}/delete'
+ defaults:
+ _entity_form: 'facets_summary.delete'
+ requirements:
+ _entity_access: 'facets_summary.delete'
+
+entity.facets_summary.edit_form:
+ path: '/admin/config/search/facets/facet-summary/{facets_summary}/edit'
+ defaults:
+ _entity_form: 'facets_summary.edit'
+ requirements:
+ _entity_create_access: 'facets_summary'
+
+entity.facets_summary.settings_form:
+ path: '/admin/config/search/facets/facet-summary/{facets_summary}/settings'
+ defaults:
+ _entity_form: 'facets_summary.settings'
+ requirements:
+ _entity_access: 'facets_summary.edit'
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.services.yml b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.services.yml
new file mode 100644
index 000000000..ba1a6f5fb
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/facets_summary.services.yml
@@ -0,0 +1,15 @@
+services:
+ plugin.manager.facets_summary.processor:
+ class: Drupal\facets_summary\Processor\ProcessorPluginManager
+ arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@string_translation']
+ facets_summary.manager:
+ class: Drupal\facets_summary\FacetsSummaryManager\DefaultFacetsSummaryManager
+ arguments:
+ - '@plugin.manager.facets.facet_source'
+ - '@plugin.manager.facets_summary.processor'
+ - '@facets.manager'
+ facets_summary.search_api_subscriber:
+ class: Drupal\facets_summary\EventSubscriber\SearchApiSubscriber
+ arguments: ['@entity_type.manager']
+ tags:
+ - { name: event_subscriber }
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Annotation/SummaryProcessor.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Annotation/SummaryProcessor.php
new file mode 100644
index 000000000..9e36a35f6
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Annotation/SummaryProcessor.php
@@ -0,0 +1,65 @@
+name;
+ }
+
+ /**
+ * Returns the facet source identifier.
+ *
+ * @return string
+ * The id of the facet source plugin.
+ */
+ public function getFacetSourceId() {
+ return $this->facet_source_id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFacetSourceId($facet_source_id) {
+ $this->facet_source_id = $facet_source_id;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFacetSource() {
+ if (!$this->facet_source_instance && $this->facet_source_id) {
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */
+ $facet_source_plugin_manager = \Drupal::service('plugin.manager.facets.facet_source');
+ $this->facet_source_instance = $facet_source_plugin_manager->createInstance($this->facet_source_id, ['facets_summary' => $this]);
+ }
+
+ return $this->facet_source_instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFacets() {
+ return $this->facets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFacets(array $facets) {
+ return $this->facets = $facets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeFacet($facet_id) {
+ unset($this->facets[$facet_id]);
+ return $this;
+ }
+
+ /**
+ * Retrieves all processors supported by this facets summary.
+ *
+ * @return \Drupal\facets_summary\Processor\ProcessorInterface[]
+ * The loaded processors, keyed by processor ID.
+ */
+ protected function loadProcessors() {
+ if (is_array($this->processors)) {
+ return $this->processors;
+ }
+
+ /** @var \Drupal\facets\Processor\ProcessorPluginManager $processor_plugin_manager */
+ $processor_plugin_manager = \Drupal::service('plugin.manager.facets_summary.processor');
+ $processor_settings = $this->getProcessorConfigs();
+
+ foreach ($processor_plugin_manager->getDefinitions() as $name => $processor_definition) {
+ if (class_exists($processor_definition['class']) && empty($this->processors[$name])) {
+ // Create our settings for this processor.
+ $settings = empty($processor_settings[$name]['settings']) ? [] : $processor_settings[$name]['settings'];
+ $settings['facets_summary'] = $this;
+
+ /** @var \Drupal\facets_summary\Processor\ProcessorInterface $processor */
+ $processor = $processor_plugin_manager->createInstance($name, $settings);
+ $this->processors[$name] = $processor;
+ }
+ elseif (!class_exists($processor_definition['class'])) {
+ \Drupal::logger('facets_summary')
+ ->warning('Processor @id specifies a non-existing @class.', [
+ '@id' => $name,
+ '@class' => $processor_definition['class'],
+ ]);
+ }
+ }
+
+ return $this->processors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessorConfigs() {
+ return !empty($this->processor_configs) ? $this->processor_configs : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessors($only_enabled = TRUE) {
+ $processors = $this->loadProcessors();
+
+ // Filter processors by status if required. Enabled processors are those
+ // which have settings in the processor_configs.
+ if ($only_enabled) {
+ $processors_settings = $this->getProcessorConfigs();
+ $processors = array_intersect_key($processors, $processors_settings);
+ }
+ return $processors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessorsByStage($stage, $only_enabled = TRUE) {
+ $processors = $this->getProcessors($only_enabled);
+ $processor_settings = $this->getProcessorConfigs();
+ $processor_weights = [];
+
+ // Get a list of all processors for given stage.
+ foreach ($processors as $name => $processor) {
+ if ($processor->supportsStage($stage)) {
+ if (!empty($processor_settings[$name]['weights'][$stage])) {
+ $processor_weights[$name] = $processor_settings[$name]['weights'][$stage];
+ }
+ else {
+ $processor_weights[$name] = $processor->getDefaultWeight($stage);
+ }
+ }
+ }
+
+ // Sort requested processors by weight.
+ asort($processor_weights);
+
+ $return_processors = [];
+ foreach ($processor_weights as $name => $weight) {
+ $return_processors[$name] = $processors[$name];
+ }
+ return $return_processors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addProcessor(array $processor) {
+ $this->processor_configs[$processor['processor_id']] = [
+ 'processor_id' => $processor['processor_id'],
+ 'weights' => $processor['weights'],
+ 'settings' => $processor['settings'],
+ ];
+
+ // Sort the processors so we won't have unnecessary changes.
+ ksort($this->processor_configs);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeProcessor($processor_id) {
+ unset($this->processor_configs[$processor_id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ parent::calculateDependencies();
+ if ($this->getFacetSource() === NULL) {
+ return $this;
+ }
+
+ $facet_source_dependencies = $this->getFacetSource()->calculateDependencies();
+ if (!empty($facet_source_dependencies)) {
+ $this->addDependencies($facet_source_dependencies);
+ }
+
+ return $this;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/EventSubscriber/SearchApiSubscriber.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/EventSubscriber/SearchApiSubscriber.php
new file mode 100644
index 000000000..26b303678
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/EventSubscriber/SearchApiSubscriber.php
@@ -0,0 +1,74 @@
+entityTypeManager = $entityTypeManager;
+ }
+
+ /**
+ * Reacts to the query alter event.
+ *
+ * @param \Drupal\search_api\Event\QueryPreExecuteEvent $event
+ * The query alter event.
+ */
+ public function queryAlter(QueryPreExecuteEvent $event) {
+ $query = $event->getQuery();
+
+ $facet_source_id = 'search_api:' . str_replace(':', '__', $query->getSearchId());
+ $storage = $this->entityTypeManager->getStorage('facets_summary');
+ // Get all the facet summaries for the facet source.
+ $facet_summaries = $storage->loadByProperties(['facet_source_id' => $facet_source_id]);
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface $facet_summary */
+ foreach ($facet_summaries as $facet_summary) {
+ $processors = $facet_summary->getProcessors();
+ // If the count processor is enabled, results count must not be skipped.
+ if (in_array('show_count', array_keys($processors))) {
+ $query->setOption('skip result count', FALSE);
+ break;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ // Workaround to avoid a fatal error during site install from existing
+ // config.
+ // @see https://www.drupal.org/project/facets/issues/3199156
+ if (!class_exists('\Drupal\search_api\Event\SearchApiEvents', TRUE)) {
+ return [];
+ }
+
+ return [
+ SearchApiEvents::QUERY_PRE_EXECUTE => 'queryAlter',
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/FacetsSummaryBlockInterface.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/FacetsSummaryBlockInterface.php
new file mode 100644
index 000000000..37ba325ec
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/FacetsSummaryBlockInterface.php
@@ -0,0 +1,18 @@
+facetSourcePluginManager = $facet_source_manager;
+ $this->processorPluginManager = $processor_plugin_manager;
+ $this->facetManager = $facet_manager;
+ }
+
+ /**
+ * Builds a facet and returns it as a renderable array.
+ *
+ * This method delegates to the relevant plugins to render a facet, it calls
+ * out to a widget plugin to do the actual rendering when results are found.
+ * When no results are found it calls out to the correct empty result plugin
+ * to build a render array.
+ *
+ * Before doing any rendering, the processors that implement the
+ * BuildProcessorInterface enabled on this facet will run.
+ *
+ * @param \Drupal\facets_summary\FacetsSummaryInterface $facets_summary
+ * The facet we should build.
+ *
+ * @return array
+ * Facet render arrays.
+ *
+ * @throws \Drupal\facets\Exception\InvalidProcessorException
+ * Throws an exception when an invalid processor is linked to the facet.
+ */
+ public function build(FacetsSummaryInterface $facets_summary) {
+ // Let the facet_manager build the facets.
+ $facetsource_id = $facets_summary->getFacetSourceId();
+
+ /** @var \Drupal\facets\Entity\Facet[] $facets */
+ $facets = $this->facetManager->getFacetsByFacetSourceId($facetsource_id);
+ // Get the current results from the facets and let all processors that
+ // trigger on the build step do their build processing.
+ // @see \Drupal\facets\Processor\BuildProcessorInterface.
+ // @see \Drupal\facets\Processor\SortProcessorInterface.
+ $this->facetManager->updateResults($facetsource_id);
+
+ $facets_config = $facets_summary->getFacets();
+ // Exclude facets which were not selected for this summary.
+ $facets = array_filter($facets,
+ function ($item) use ($facets_config) {
+ return (isset($facets_config[$item->id()]));
+ }
+ );
+
+ foreach ($facets as $facet) {
+ // Do not build the facet in summary if facet is not rendered.
+ if (!$facet->getActiveItems()) {
+ continue;
+ }
+ // For clarity, process facets is called each build.
+ // The first facet therefor will trigger the processing. Note that
+ // processing is done only once, so repeatedly calling this method will
+ // not trigger the processing more than once.
+ $this->facetManager->build($facet);
+ }
+
+ $build = [
+ '#theme' => 'facets_summary_item_list',
+ '#facet_summary_id' => $facets_summary->id(),
+ '#attributes' => [
+ 'data-drupal-facets-summary-id' => $facets_summary->id(),
+ ],
+ ];
+
+ $results = [];
+ foreach ($facets as $facet) {
+ $show_count = $facets_config[$facet->id()]['show_count'];
+ $results = array_merge($results, $this->buildResultTree($show_count, $facet->getResults()));
+ }
+ $build['#items'] = $results;
+
+ // Allow our Facets Summary processors to alter the build array in a
+ // configured order.
+ foreach ($facets_summary->getProcessorsByStage(ProcessorInterface::STAGE_BUILD) as $processor) {
+ if (!$processor instanceof BuildProcessorInterface) {
+ throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a build definition but doesn't implement the required BuildProcessorInterface interface");
+ }
+ $build = $processor->build($facets_summary, $build, $facets);
+ }
+
+ return $build;
+ }
+
+ /**
+ * Build result tree, taking possible children into account.
+ *
+ * @param bool $show_count
+ * Show the count next to the facet.
+ * @param \Drupal\facets\Result\ResultInterface[] $results
+ * Facet results array.
+ *
+ * @return array
+ * The rendered links to the active facets.
+ */
+ protected function buildResultTree($show_count, array $results) {
+ $items = [];
+ foreach ($results as $result) {
+ if ($result->isActive()) {
+ $item = [
+ '#theme' => 'facets_result_item__summary',
+ '#value' => $result->getDisplayValue(),
+ '#show_count' => $show_count,
+ '#count' => $result->getCount(),
+ '#is_active' => TRUE,
+ '#facet' => $result->getFacet(),
+ '#raw_value' => $result->getRawValue(),
+ ];
+ $item = (new Link($item, $result->getUrl()))->toRenderable();
+ $item['#wrapper_attributes'] = [
+ 'class' => [
+ 'facet-summary-item--facet',
+ ],
+ ];
+ $items[] = $item;
+ }
+ if ($children = $result->getChildren()) {
+ $items = array_merge($items, $this->buildResultTree($show_count, $children));
+ }
+ }
+ return $items;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Form/FacetsSummaryDeleteConfirmForm.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Form/FacetsSummaryDeleteConfirmForm.php
new file mode 100644
index 000000000..4e4ce92c0
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Form/FacetsSummaryDeleteConfirmForm.php
@@ -0,0 +1,20 @@
+entityTypeManager = $entity_type_manager;
+ $this->facetSourcePluginManager = $facet_source_plugin_manager;
+ $this->facetSummaryStorage = $entity_type_manager->getStorage('facets_summary');
+ $this->facetManager = $facet_manager;
+ $this->processorPluginManager = $processor_plugin_manager;
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ /** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
+ $entity_type_manager = $container->get('entity_type.manager');
+
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */
+ $facet_source_plugin_manager = $container->get('plugin.manager.facets.facet_source');
+
+ /** @var \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager */
+ $facet_manager = $container->get('facets.manager');
+
+ /** @var \Drupal\facets_summary\Processor\ProcessorPluginManager $processor_plugin_manager */
+ $processor_plugin_manager = $container->get('plugin.manager.facets_summary.processor');
+
+ return new static($entity_type_manager, $facet_source_plugin_manager, $facet_manager, $processor_plugin_manager);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBaseFormId() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form['#attached']['library'][] = 'facets/drupal.facets.admin_css';
+
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface $facets_summary */
+ $facets_summary = $this->entity;
+
+ $form['#tree'] = TRUE;
+ $form['#attached']['library'][] = 'facets/drupal.facets.index-active-formatters';
+ $form['#title'] = $this->t('Edit %label facets summary', ['%label' => $facets_summary->label()]);
+
+ $form['facets'] = [
+ '#type' => 'table',
+ '#header' => [
+ $this->t('Enabled facets'),
+ $this->t('Label'),
+ $this->t('Separator'),
+ $this->t('Show counts'),
+ $this->t('Weight'),
+ ],
+ '#tabledrag' => [
+ [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'facets-order-weight',
+ ],
+ ],
+ '#caption' => $this->t('Select the facets to be shown in the summary block. You can reorder them.'),
+ ];
+ $facets = $facets_summary->getFacets();
+ $default_facets = array_keys($facets);
+
+ $all_facets = $this->facetManager->getFacetsByFacetSourceId($facets_summary->getFacetSourceId());
+ if (!empty($all_facets)) {
+ foreach ($all_facets as $facet) {
+ if (!in_array($facet->id(), $default_facets)) {
+ $facets[$facet->id()] = [
+ 'label' => $facet->getName(),
+ 'separator' => ', ',
+ 'show_count' => FALSE,
+ ];
+ }
+ $facets[$facet->id()]['name'] = $facet->getName();
+ }
+
+ foreach ($facets as $id => $facet) {
+ $form['facets'][$id] = [
+ 'checked' => [
+ '#type' => 'checkbox',
+ '#title' => $facet['name'],
+ '#default_value' => in_array($id, $default_facets),
+ ],
+ 'label' => [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#default_value' => $facet['label'],
+ '#size' => 25,
+ ],
+ 'separator' => [
+ '#type' => 'textfield',
+ '#title' => $this->t('Separator'),
+ '#default_value' => $facet['separator'],
+ '#size' => 8,
+ ],
+ 'show_count' => [
+ '#type' => 'checkbox',
+ '#default_value' => $facet['show_count'],
+ ],
+ 'weight' => [
+ '#type' => 'weight',
+ '#title' => $this->t('Weight for @title', ['@title' => $facet['name']]),
+ '#title_display' => 'invisible',
+ '#attributes' => ['class' => ['facets-order-weight']],
+ ],
+ '#attributes' => ['class' => ['draggable']],
+ ];
+ }
+ }
+ else {
+ $form['facets'] = ['#markup' => $this->t('No facets found.')];
+ }
+
+ // Retrieve lists of all processors, and the stages and weights they have.
+ if (!$form_state->has('processors')) {
+ $all_processors = $facets_summary->getProcessors(FALSE);
+ }
+ else {
+ $all_processors = $form_state->get('processors');
+ }
+ $enabled_processors = $facets_summary->getProcessors(TRUE);
+
+ $stages = $this->processorPluginManager->getProcessingStages();
+ $processors_by_stage = [];
+ foreach ($stages as $stage => $definition) {
+ $processors_by_stage[$stage] = $facets_summary->getProcessorsByStage($stage, FALSE);
+ }
+
+ // Add the list of all other processors with checkboxes to enable/disable
+ // them.
+ $form['facets_summary_settings'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Facets Summary settings'),
+ '#attributes' => [
+ 'class' => [
+ 'search-api-status-wrapper',
+ ],
+ ],
+ ];
+ foreach ($all_processors as $processor_id => $processor) {
+ $clean_css_id = Html::cleanCssIdentifier($processor_id);
+ $form['facets_summary_settings'][$processor_id]['status'] = [
+ '#type' => 'checkbox',
+ '#title' => (string) $processor->getPluginDefinition()['label'],
+ '#default_value' => !empty($enabled_processors[$processor_id]),
+ '#description' => $processor->getDescription(),
+ '#attributes' => [
+ 'class' => [
+ 'search-api-processor-status-' . $clean_css_id,
+ ],
+ 'data-id' => $clean_css_id,
+ ],
+ ];
+
+ $form['facets_summary_settings'][$processor_id]['settings'] = [];
+ $processor_form_state = SubformState::createForSubform($form['facets_summary_settings'][$processor_id]['settings'], $form, $form_state);
+ $processor_form = $processor->buildConfigurationForm($form, $processor_form_state, $facets_summary);
+ if ($processor_form) {
+ $form['facets_summary_settings'][$processor_id]['settings'] = [
+ '#type' => 'details',
+ '#title' => $this->t('%processor settings', ['%processor' => (string) $processor->getPluginDefinition()['label']]),
+ '#open' => TRUE,
+ '#attributes' => [
+ 'class' => [
+ 'facets-processor-settings-' . Html::cleanCssIdentifier($processor_id),
+ 'facets-processor-settings-facet',
+ 'facets-processor-settings',
+ ],
+ ],
+ '#states' => [
+ 'visible' => [
+ ':input[name="facets_summary_settings[' . $processor_id . '][status]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['facets_summary_settings'][$processor_id]['settings'] += $processor_form;
+ }
+ }
+
+ $form['weights'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Advanced settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ ];
+
+ $form['weights']['order'] = [
+ '#prefix' => '',
+ '#markup' => $this->t('Processor order'),
+ '#suffix' => ' ',
+ ];
+
+ // Order enabled processors per stage, create all the containers for the
+ // different stages.
+ foreach ($stages as $stage => $description) {
+ $form['weights'][$stage] = [
+ '#type' => 'fieldset',
+ '#title' => $description['label'],
+ '#attributes' => [
+ 'class' => [
+ 'search-api-stage-wrapper',
+ 'search-api-stage-wrapper-' . Html::cleanCssIdentifier($stage),
+ ],
+ ],
+ ];
+ $form['weights'][$stage]['order'] = [
+ '#type' => 'table',
+ ];
+ $form['weights'][$stage]['order']['#tabledrag'][] = [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'search-api-processor-weight-' . Html::cleanCssIdentifier($stage),
+ ];
+ }
+
+ $processor_settings = $facets_summary->getProcessorConfigs();
+
+ // Fill in the containers previously created with the processors that are
+ // enabled on the facet.
+ foreach ($processors_by_stage as $stage => $processors) {
+ /** @var \Drupal\facets\Processor\ProcessorInterface $processor */
+ foreach ($processors as $processor_id => $processor) {
+ $weight = isset($processor_settings[$processor_id]['weights'][$stage])
+ ? $processor_settings[$processor_id]['weights'][$stage]
+ : $processor->getDefaultWeight($stage);
+ if ($processor->isHidden()) {
+ $form['processors'][$processor_id]['weights'][$stage] = [
+ '#type' => 'value',
+ '#value' => $weight,
+ ];
+ continue;
+ }
+ $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'draggable';
+ $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'search-api-processor-weight--' . Html::cleanCssIdentifier($processor_id);
+ $form['weights'][$stage]['order'][$processor_id]['#weight'] = $weight;
+ $form['weights'][$stage]['order'][$processor_id]['label']['#plain_text'] = (string) $processor->getPluginDefinition()['label'];
+ $form['weights'][$stage]['order'][$processor_id]['weight'] = [
+ '#type' => 'weight',
+ '#title' => $this->t('Weight for processor %title', ['%title' => (string) $processor->getPluginDefinition()['label']]),
+ '#title_display' => 'invisible',
+ '#default_value' => $weight,
+ '#parents' => ['processors', $processor_id, 'weights', $stage],
+ '#attributes' => [
+ 'class' => [
+ 'search-api-processor-weight-' . Html::cleanCssIdentifier($stage),
+ ],
+ ],
+ ];
+ }
+ }
+
+ // Add vertical tabs containing the settings for the processors. Tabs for
+ // disabled processors are hidden with JS magic, but need to be included in
+ // case the processor is enabled.
+ $form['processor_settings'] = [
+ '#title' => $this->t('Processor settings'),
+ '#type' => 'vertical_tabs',
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface $facets_summary */
+ $facets_summary = $this->entity;
+
+ $values = $form_state->getValues();
+ /** @var \Drupal\facets_summary\Processor\ProcessorInterface[] $processors */
+ $processors = $facets_summary->getProcessors(FALSE);
+
+ // Iterate over all processors that have a form and are enabled.
+ foreach ($form['facets_summary_settings'] as $processor_id => $processor_form) {
+ if (!empty($values['processors'][$processor_id])) {
+
+ $processor_form_state = SubformState::createForSubform($form['facets_summary_settings'][$processor_id]['settings'], $form, $form_state);
+ $processors[$processor_id]->validateConfigurationForm($form['facets_summary_settings'][$processor_id], $processor_form_state, $facets_summary);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+
+ // Store processor settings.
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface $facets_summary */
+ $facets_summary = $this->entity;
+
+ /** @var \Drupal\facets_summary\Processor\ProcessorInterface $processor */
+ $processors = $facets_summary->getProcessors(FALSE);
+ foreach ($processors as $processor_id => $processor) {
+ $form_container_key = 'facets_summary_settings';
+ if (empty($values[$form_container_key][$processor_id]['status'])) {
+ $facets_summary->removeProcessor($processor_id);
+ continue;
+ }
+
+ $new_settings = [
+ 'processor_id' => $processor_id,
+ 'weights' => [],
+ 'settings' => [],
+ ];
+
+ if (!empty($values['processors'][$processor_id]['weights'])) {
+ $new_settings['weights'] = $values['processors'][$processor_id]['weights'];
+ }
+
+ if (isset($form[$form_container_key][$processor_id]['settings'])) {
+ $processor_form_state = SubformState::createForSubform($form[$form_container_key][$processor_id]['settings'], $form, $form_state);
+ $processor->submitConfigurationForm($form[$form_container_key][$processor_id]['settings'], $processor_form_state, $facets_summary);
+ $new_settings['settings'] = $processor->getConfiguration();
+ }
+ $facets_summary->addProcessor($new_settings);
+ }
+
+ $value = $form_state->getValue('facets') ?: [];
+ $enabled_facets = array_filter($value, function ($item) {
+ return isset($item['checked']) && $item['checked'] == 1;
+ });
+
+ $facets_summary->setFacets((array) $enabled_facets);
+ $facets_summary->save();
+
+ $this->messenger()->addMessage($this->t('Facets Summary %name has been updated.', ['%name' => $facets_summary->getName()]));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions = parent::actions($form, $form_state);
+
+ // We don't have a "delete" action here.
+ unset($actions['delete']);
+
+ return $actions;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Form/FacetsSummarySettingsForm.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Form/FacetsSummarySettingsForm.php
new file mode 100644
index 000000000..2b19def23
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Form/FacetsSummarySettingsForm.php
@@ -0,0 +1,257 @@
+entityTypeManager = $entity_type_manager;
+ $this->facetSourcePluginManager = $facet_source_plugin_manager;
+ $this->facetSummaryStorage = $entity_type_manager->getStorage('facets_summary');
+ $this->facetManager = $facet_manager;
+ $this->processorPluginManager = $processor_plugin_manager;
+ $this->blockManager = $block_manager;
+ $this->urlGenerator = $url_generator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ /** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
+ $entity_type_manager = $container->get('entity_type.manager');
+
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */
+ $facet_source_plugin_manager = $container->get('plugin.manager.facets.facet_source');
+
+ /** @var \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager */
+ $facet_manager = $container->get('facets.manager');
+
+ /** @var \Drupal\facets_summary\Processor\ProcessorPluginManager $processor_plugin_manager */
+ $processor_plugin_manager = $container->get('plugin.manager.facets_summary.processor');
+
+ /** @var \Drupal\Core\Block\BlockManager $block_manager */
+ $block_manager = $container->get('plugin.manager.block');
+
+ /** @var \Drupal\Core\Routing\UrlGeneratorInterface $url_generator */
+ $url_generator = $container->get('url_generator');
+
+ return new static($entity_type_manager, $facet_source_plugin_manager, $facet_manager, $processor_plugin_manager, $block_manager, $url_generator);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBaseFormId() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface $facets_summary */
+ $facets_summary = $this->entity;
+
+ $facet_sources = [];
+
+ // If the form is being rebuilt, rebuild the entity with the current form
+ // values.
+ if ($form_state->isRebuilding()) {
+ $this->entity = $this->buildEntity($form, $form_state);
+ }
+
+ $form = parent::form($form, $form_state);
+
+ // Set the page title according to whether we are creating or editing the
+ // facet.
+ if ($this->getEntity()->isNew()) {
+ $form['#title'] = $this->t('Add facets summary');
+ }
+ else {
+ $form['#title'] = $this->t('Facets settings for %label', [
+ '%label' => $this->getEntity()
+ ->label(),
+ ]);
+ }
+
+ foreach ($this->facetSourcePluginManager->getDefinitions() as $facet_source_id => $definition) {
+ $facet_sources[$definition['id']] = !empty($definition['label']) ? $definition['label'] : $facet_source_id;
+ }
+
+ if (count($facet_sources) == 0) {
+ $form['#markup'] = $this->t('You currently have no facet sources defined. You should start by adding a facet source before creating facets.');
+ return TRUE;
+ }
+
+ $form['facet_source_id'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Facet source'),
+ '#description' => $this->t('The source where this summary will be built from.'),
+ '#options' => $facet_sources,
+ '#default_value' => $facets_summary->getFacetSourceId(),
+ '#required' => TRUE,
+ ];
+
+ $form['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Name'),
+ '#description' => $this->t('The administrative name used for this summary.'),
+ '#default_value' => $facets_summary->label(),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $facets_summary->id(),
+ '#maxlength' => 50,
+ '#required' => TRUE,
+ '#machine_name' => [
+ 'exists' => [$this->facetSummaryStorage, 'load'],
+ 'source' => ['name'],
+ ],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm($form, $form_state);
+
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface $facets_summary */
+ $facets_summary = $this->getEntity();
+ $is_new = $facets_summary->isNew();
+ $facets_summary->save();
+
+ if ($is_new) {
+ if ($this->moduleHandler->moduleExists('block')) {
+ $message = $this->t(
+ 'Facet Summary %name has been created. Go to the Block overview page to place the new block in the desired region.',
+ [
+ '%name' => $facets_summary->getName(),
+ ':block_overview' => $this->urlGenerator->generateFromRoute('block.admin_display'),
+ ]
+ );
+ $this->messenger()->addMessage($message);
+ $form_state->setRedirect('entity.facets_summary.edit_form', ['facets_summary' => $facets_summary->id()]);
+ }
+
+ // On facet creation, enable all locked processors by default, using their
+ // default settings.
+ $stages = $this->processorPluginManager->getProcessingStages();
+ $processors_definitions = $this->processorPluginManager->getDefinitions();
+
+ foreach ($processors_definitions as $processor_id => $processor) {
+ $is_locked = isset($processor['locked']) && $processor['locked'] == TRUE;
+ $is_default_enabled = isset($processor['default_enabled']) && $processor['default_enabled'] == TRUE;
+ if ($is_locked || $is_default_enabled) {
+ $weights = [];
+ foreach ($stages as $stage_id => $stage) {
+ if (isset($processor['stages'][$stage_id])) {
+ $weights[$stage_id] = $processor['stages'][$stage_id];
+ }
+ }
+ $facets_summary->addProcessor([
+ 'processor_id' => $processor_id,
+ 'weights' => $weights,
+ 'settings' => [],
+ ]);
+ }
+ }
+ }
+ else {
+ $this->messenger()->addMessage($this->t('Facet %name has been updated.', ['%name' => $facets_summary->getName()]));
+ }
+
+ // Clear Drupal cache for blocks to reflect recent changes.
+ $this->blockManager->clearCachedDefinitions();
+ $facet_source_id = $form_state->getValue('facet_source_id');
+ list($type,) = explode(':', $facet_source_id);
+ if ($type !== 'search_api') {
+ return $facets_summary;
+ }
+
+ // Ensure that the caching of the view display is disabled, so the search
+ // correctly returns the facets.
+ $facet_source = $this->facetSourcePluginManager->createInstance($facet_source_id, ['facet' => $this->getEntity()]);
+ if (isset($facet_source) && $facet_source instanceof SearchApiFacetSourceInterface) {
+ $view = $facet_source->getViewsDisplay();
+ if ($view !== NULL) {
+ $view->display_handler->overrideOption('cache', ['type' => 'none']);
+ $view->save();
+ $this->messenger()->addMessage($this->t('Caching of view %view has been disabled.', ['%view' => $view->storage->label()]));
+ }
+ }
+
+ return $facets_summary;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php
new file mode 100644
index 000000000..e805c5ec4
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php
@@ -0,0 +1,139 @@
+facetsSummaryManager = $facets_summary_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('facets_summary.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntity() {
+ if (!isset($this->facetsSummary)) {
+ $source_id = $this->getDerivativeId();
+ if (!$this->facetsSummary = FacetsSummary::load($source_id)) {
+ $this->facetsSummary = FacetsSummary::create(['id' => $source_id]);
+ $this->facetsSummary->save();
+ }
+ }
+ return $this->facetsSummary;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ // Do not build the facet summary if the block is being previewed.
+ if ($this->getContextValue('in_preview')) {
+ return [];
+ }
+
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface $summary */
+ $facets_summary = $this->getEntity();
+
+ // Let the facet_manager build the facets.
+ $build = $this->facetsSummaryManager->build($facets_summary);
+
+ // Add contextual links only when we have results.
+ if (!empty($build)) {
+ $build['#contextual_links']['facets_summary'] = [
+ 'route_parameters' => ['facets_summary' => $facets_summary->id()],
+ ];
+ }
+
+ /** @var \Drupal\views\ViewExecutable $view */
+ if ($view = $facets_summary->getFacetSource()->getViewsDisplay()) {
+ $build['#attached']['drupalSettings']['facets_views_ajax'] = [
+ 'facets_summary_ajax' => [
+ 'facets_summary_id' => $facets_summary->id(),
+ 'view_id' => $view->id(),
+ 'current_display_id' => $view->current_display,
+ 'ajax_path' => Url::fromRoute('views.ajax')->toString(),
+ ],
+ ];
+ }
+
+ return $build;
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $source_id = $this->getDerivativeId();
+ if ($summary = FacetsSummary::load($source_id)) {
+ return [$summary->getConfigDependencyKey() => [$summary->getConfigDependencyName()]];
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPreviewFallbackString() {
+ return $this->t('Placeholder for the "@facet_summary" facet summary', ['@facet_summary' => $this->getDerivativeId()]);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlockDeriver.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlockDeriver.php
new file mode 100644
index 000000000..3848c48ae
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlockDeriver.php
@@ -0,0 +1,77 @@
+facetsSummaryStorage = $container->get('entity_type.manager')->getStorage('facets_summary');
+
+ return $deriver;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
+ $derivatives = $this->getDerivativeDefinitions($base_plugin_definition);
+ return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ $base_plugin_id = $base_plugin_definition['id'];
+ if (!isset($this->derivatives[$base_plugin_id])) {
+ $plugin_derivatives = [];
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface[] $all_facets_summaries */
+ $all_facets_summaries = $this->facetsSummaryStorage->loadMultiple();
+ foreach ($all_facets_summaries as $facets_summary) {
+ $machine_name = $facets_summary->id();
+
+ $plugin_derivatives[$machine_name] = [
+ 'id' => $base_plugin_id . PluginBase::DERIVATIVE_SEPARATOR . $machine_name,
+ 'label' => $this->t('Facet Summary: :facet_summary', [':facet_summary' => $facets_summary->getName()]),
+ 'admin_label' => $facets_summary->getName(),
+ 'description' => $this->t('Facets Summary'),
+ 'context_definitions' => [
+ 'in_preview' => new ContextDefinition('string', $this->t('In preview'), FALSE),
+ ],
+ ] + $base_plugin_definition;
+ }
+ $this->derivatives[$base_plugin_id] = $plugin_derivatives;
+ }
+ return $this->derivatives[$base_plugin_id];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php
new file mode 100644
index 000000000..2f41f67aa
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php
@@ -0,0 +1,35 @@
+getFacetSource();
+ if (!$facet_source->isRenderedInCurrentRequest()) {
+ return [];
+ }
+ return $build;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ResetFacetsProcessor.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ResetFacetsProcessor.php
new file mode 100644
index 000000000..636abecc4
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ResetFacetsProcessor.php
@@ -0,0 +1,166 @@
+getProcessorConfigs()[$this->getPluginId()];
+ $hasReset = FALSE;
+
+ // Do nothing if there are no selected facets.
+ if (empty($build['#items'])) {
+ return $build;
+ }
+
+ $request_stack = \Drupal::requestStack();
+ // Support 9.3+.
+ // @todo remove switch after 9.3 or greater is required.
+ $request = version_compare(\Drupal::VERSION, '9.3', '>=') ? $request_stack->getMainRequest() : $request_stack->getMasterRequest();
+ $query_params = $request->query->all();
+
+ // Bypass all active facets and remove them from the query parameters array.
+ foreach ($facets as $facet) {
+ $url_alias = $facet->getUrlAlias();
+ $filter_key = $facet->getFacetSourceConfig()->getFilterKey() ?: 'f';
+
+ if ($facet->getActiveItems()) {
+ // This removes query params when using the query url processor.
+ if (isset($query_params[$filter_key])) {
+ foreach ($query_params[$filter_key] as $delta => $param) {
+ if (strpos($param, $url_alias . ':') !== FALSE) {
+ unset($query_params[$filter_key][$delta]);
+ }
+ }
+
+ if (!$query_params[$filter_key]) {
+ unset($query_params[$filter_key]);
+ }
+ }
+
+ $hasReset = TRUE;
+ }
+ }
+
+ if (!$hasReset) {
+ return $build;
+ }
+
+ $path = \Drupal::service('path.current')->getPath();
+ /** @var \Drupal\path_alias\AliasManager $pathAliasManager */
+ $pathAliasManager = \Drupal::service('path_alias.manager');
+ $path = $pathAliasManager->getAliasByPath($path);
+ try {
+ $url = Url::fromUserInput($path);
+ }
+ catch (InvalidArgumentException $e) {
+ $url = Url::fromUri($path);
+ }
+ $url->setOptions(['query' => $query_params]);
+ // Check if reset link text is not set or it contains only whitespaces.
+ // Set text from settings or set default text.
+ if (empty($configuration['settings']['link_text']) || strlen(trim($configuration['settings']['link_text'])) === 0) {
+ $itemText = $this->t('Reset');
+ }
+ else {
+ $itemText = $configuration['settings']['link_text'];
+ }
+ $item = (new Link($itemText, $url))->toRenderable();
+ $item['#wrapper_attributes'] = [
+ 'class' => [
+ 'facet-summary-item--clear',
+ ],
+ ];
+
+ // Place link at necessary position.
+ if ($configuration['settings']['position'] == static::POSITION_BEFORE) {
+ array_unshift($build['#items'], $item);
+ }
+ elseif ($configuration['settings']['position'] == static::POSITION_AFTER) {
+ $build['#items'][] = $item;
+ }
+ else {
+ $build['#items'] = [
+ $item,
+ ];
+ }
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetsSummaryInterface $facets_summary) {
+ // By default, there should be no config form.
+ $config = $this->getConfiguration();
+
+ $build['link_text'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Reset facets link text'),
+ '#default_value' => $config['link_text'],
+ ];
+
+ $build['position'] = [
+ '#type' => 'select',
+ '#options' => [
+ static::POSITION_BEFORE => $this->t('Show reset link before facets links'),
+ static::POSITION_AFTER => $this->t('Show reset link after facets links'),
+ static::POSITION_REPLACE => $this->t('Show only reset link'),
+ ],
+ '#title' => $this->t('Position'),
+ '#description' => $this->t('Set position of the link to display it before, after or instead of facets links.'),
+ '#default_value' => $config['position'],
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'link_text' => '',
+ 'position' => static::POSITION_BEFORE,
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowCountProcessor.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowCountProcessor.php
new file mode 100644
index 000000000..2fc9427c3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowCountProcessor.php
@@ -0,0 +1,40 @@
+getFacetSource()->getCount();
+ $build_count = [
+ '#theme' => 'facets_summary_count',
+ '#count' => $count,
+ ];
+ array_unshift($build['#items'], $build_count);
+ return $build;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowSummaryProcessor.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowSummaryProcessor.php
new file mode 100644
index 000000000..83313a56a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowSummaryProcessor.php
@@ -0,0 +1,74 @@
+getFacets();
+
+ if (!isset($build['#items'])) {
+ return $build;
+ }
+
+ /** @var \Drupal\facets\Entity\Facet $facet */
+ foreach ($facets as $facet) {
+ if (empty($facet->getActiveItems())) {
+ continue;
+ }
+ $items = $this->getActiveDisplayValues($facet->getResults());
+ $facet_summary = [
+ '#theme' => 'facets_summary_facet',
+ '#label' => $facets_config[$facet->id()]['label'],
+ '#separator' => $facets_config[$facet->id()]['separator'],
+ '#items' => $items,
+ '#facet_id' => $facet->id(),
+ '#facet_admin_label' => $facet->getName(),
+ ];
+ array_unshift($build['#items'], $facet_summary);
+ }
+ return $build;
+ }
+
+ /**
+ * Get all active results' display values from hierarchy.
+ *
+ * @param \Drupal\facets\Result\ResultInterface[] $results
+ * The results to check for active children.
+ *
+ * @return \Drupal\facets\Result\ResultInterface[]
+ * The active results found.
+ */
+ protected function getActiveDisplayValues(array $results) {
+ $items = [];
+ foreach ($results as $result) {
+ if ($result->isActive()) {
+ $items[] = $result->getDisplayValue();
+ }
+ if ($result->hasActiveChildren()) {
+ $items = array_merge($items, $this->getActiveDisplayValues($result->getChildren()));
+ }
+ }
+ return $items;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowTextWhenEmptyProcessor.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowTextWhenEmptyProcessor.php
new file mode 100644
index 000000000..0c4f1f784
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Plugin/facets_summary/processor/ShowTextWhenEmptyProcessor.php
@@ -0,0 +1,82 @@
+getConfiguration();
+
+ $results_count = array_sum(array_map(function ($it) {
+ /** @var \Drupal\facets\FacetInterface $it */
+ return count($it->getResults());
+ }, $facets));
+
+ // No items are found, so we should return the empty summary.
+ if (!isset($build['#items']) || $results_count === 0) {
+ return [
+ '#theme' => 'facets_summary_empty',
+ '#message' => [
+ '#type' => 'processed_text',
+ '#text' => $config['text']['value'],
+ '#format' => $config['text']['format'],
+ ],
+ ];
+ }
+
+ // Return the actual items.
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetsSummaryInterface $facets_summary) {
+ // By default, there should be no config form.
+ $config = $this->getConfiguration();
+
+ $build['text'] = [
+ '#type' => 'text_format',
+ '#title' => $this->t('Empty text'),
+ '#format' => $config['text']['format'],
+ '#editor' => TRUE,
+ '#default_value' => $config['text']['value'],
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'text' => [
+ 'format' => 'plain_text',
+ 'value' => $this->t('No results found.'),
+ ],
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Processor/BuildProcessorInterface.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Processor/BuildProcessorInterface.php
new file mode 100644
index 000000000..ba81ad65f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Processor/BuildProcessorInterface.php
@@ -0,0 +1,27 @@
+setConfiguration($form_state->getValues());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsStage($stage_identifier) {
+ $plugin_definition = $this->getPluginDefinition();
+ return isset($plugin_definition['stages'][$stage_identifier]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefaultWeight($stage) {
+ $plugin_definition = $this->getPluginDefinition();
+ return isset($plugin_definition['stages'][$stage]) ? (int) $plugin_definition['stages'][$stage] : 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLocked() {
+ return !empty($this->pluginDefinition['locked']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isHidden() {
+ return !empty($this->pluginDefinition['hidden']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ $plugin_definition = $this->getPluginDefinition();
+ return isset($plugin_definition['description']) ? $plugin_definition['description'] : '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ unset($this->configuration['facets_summary']);
+ return $this->configuration + $this->defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = $configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $this->addDependency('module', $this->getPluginDefinition()['provider']);
+ return $this->dependencies;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Processor/ProcessorPluginManager.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Processor/ProcessorPluginManager.php
new file mode 100644
index 000000000..a7a034724
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/src/Processor/ProcessorPluginManager.php
@@ -0,0 +1,53 @@
+setCacheBackend($cache_backend, 'facets_summary_processors');
+ $this->setStringTranslation($translation);
+ }
+
+ /**
+ * Retrieves information about the available processing stages.
+ *
+ * These are then used by processors in their "stages" definition to specify
+ * in which stages they will run.
+ *
+ * @return array
+ * An associative array mapping stage identifiers to information about that
+ * stage. The information itself is an associative array with the following
+ * keys:
+ * - label: The translated label for this stage.
+ */
+ public function getProcessingStages() {
+ return [
+ ProcessorInterface::STAGE_BUILD => [
+ 'label' => $this->t('Build stage'),
+ ],
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-count.html.twig b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-count.html.twig
new file mode 100644
index 000000000..f87b5e3fd
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-count.html.twig
@@ -0,0 +1,12 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display source summary block total count.
+ *
+ * Available variables:
+ * - count: The total number of records retrieved.
+ */
+#}
+
+ {% trans %}1 result found{% plural count %}{{ count }} results found{% endtrans %}
+
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-empty.html.twig b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-empty.html.twig
new file mode 100644
index 000000000..34d3699d5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-empty.html.twig
@@ -0,0 +1,11 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display source summary 'no results' message.
+ *
+ * Available variables:
+ * - message: A configurable formatted message to be shown when there are no
+ * results.
+ */
+#}
+{{ message }}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-facet.html.twig b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-facet.html.twig
new file mode 100644
index 000000000..2af6932f7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-facet.html.twig
@@ -0,0 +1,20 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display source summary facet item.
+ *
+ * Accepts suggestions as: facets-source-summary-facet--{facet_id}.
+ *
+ * Available variables:
+ * - label: The label configured by the user.
+ * - separator: The separator user to concatenate facet active items.
+ * - items: An array of associative arrays, each having 2 keys: label, count.
+ * - facet_id: The facet id.
+ * - facet_admin_label: The facet administrative label.
+ */
+#}
+
+{% if items %}
+ {{ label }}: {{ items|safe_join(separator) }}
+{% endif %}
+
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-item-list.html.twig b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-item-list.html.twig
new file mode 100644
index 000000000..3887bc3a9
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/templates/facets-summary-item-list.html.twig
@@ -0,0 +1,41 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a facets summary item list.
+ *
+ * Available variables:
+ * - items: A list of items. Each item contains:
+ * - attributes: HTML attributes to be applied to each list item.
+ * - value: The content of the list element.
+ * - title: The title of the list.
+ * - list_type: The tag for list element ("ul" or "ol").
+ * - wrapper_attributes: HTML attributes to be applied to the list wrapper.
+ * - attributes: HTML attributes to be applied to the list.
+ * - empty: A message to display when there are no items. Allowed value is a
+ * string or render array.
+ * - context: A list of contextual data associated with the list. May contain:
+ * - list_style: The custom list style.
+ *
+ * @see facets_summary_preprocess_facets_summary_item_list()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if context.list_style %}
+ {%- set attributes = attributes.addClass('item-list__' ~ context.list_style) %}
+{% endif %}
+{% if items or empty %}
+ {%- if title is not empty -%}
+ {{ title }}
+ {%- endif -%}
+
+ {%- if items -%}
+ <{{ list_type }}{{ attributes }}>
+ {%- for item in items -%}
+ {{ item.value }}
+ {%- endfor -%}
+ {{ list_type }}>
+ {%- else -%}
+ {{- empty -}}
+ {%- endif -%}
+{%- endif %}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/HierarchicalFacetIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/HierarchicalFacetIntegrationTest.php
new file mode 100644
index 000000000..97957421b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/HierarchicalFacetIntegrationTest.php
@@ -0,0 +1,277 @@
+blocks = NULL;
+
+ $this->drupalLogin($this->adminUser);
+
+ // Create hierarchical terms in a new vocabulary.
+ $this->vocabulary = $this->createVocabulary();
+ $this->createHierarchialTermStructure();
+
+ // Default content that is extended with a term reference field below.
+ $this->setUpExampleStructure();
+
+ // Create a taxonomy_term_reference field on the article and item.
+ $this->fieldName = 'hierarchy_field';
+ $fieldLabel = 'Hierarchy field';
+
+ $this->createEntityReferenceField('entity_test_mulrev_changed', 'article', $this->fieldName, $fieldLabel, 'taxonomy_term');
+ $this->createEntityReferenceField('entity_test_mulrev_changed', 'item', $this->fieldName, $fieldLabel, 'taxonomy_term');
+
+ $this->insertExampleContent();
+
+ // Add fields to index.
+ $index = $this->getIndex();
+
+ // Index the taxonomy and entity reference fields.
+ $term_field = new Field($index, $this->fieldName);
+ $term_field->setType('integer');
+ $term_field->setPropertyPath($this->fieldName);
+ $term_field->setDatasourceId('entity:entity_test_mulrev_changed');
+ $term_field->setLabel($fieldLabel);
+ $index->addField($term_field);
+
+ $index->save();
+ $this->indexItems($this->indexId);
+
+ $facet_name = 'hierarchical facet';
+ $facet_id = 'hierarchical_facet';
+ $this->facetEditPage = 'admin/config/search/facets/' . $facet_id . '/edit';
+
+ $this->createFacet($facet_name, $facet_id, $this->fieldName);
+ }
+
+ /**
+ * Test the hierarchical facets functionality.
+ */
+ public function testHierarchicalFacet() {
+ // Verify that the link to the index processors settings page is available.
+ $this->drupalGet($this->facetEditPage);
+ $this->clickLink('Search api index processor configuration');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Enable hierarchical facets and translation of entity ids to its names for
+ // a better readability.
+ $this->drupalGet($this->facetEditPage);
+ $edit = [
+ 'facet_settings[use_hierarchy]' => '1',
+ 'facet_settings[translate_entity][status]' => TRUE,
+ ];
+ $this->submitForm($edit, 'Save');
+
+ $values = [
+ 'name' => 'Owl',
+ 'id' => 'owl',
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+ $this->submitForm([], 'Save');
+
+ $block = [
+ 'region' => 'footer',
+ 'id' => str_replace('_', '-', 'owl'),
+ 'weight' => 50,
+ ];
+ $block = $this->drupalPlaceBlock('facets_summary_block:owl', $block);
+
+ // Child elements should be collapsed and invisible.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->assertFacetLabel('Parent 1');
+ $this->assertFacetLabel('Parent 2');
+ $this->assertSession()->linkNotExists('Child 1');
+ $this->assertSession()->linkNotExists('Child 2');
+ $this->assertSession()->linkNotExists('Child 3');
+ $this->assertSession()->linkNotExists('Child 4');
+
+ $this->assertSession()->pageTextContains($block->label());
+
+ // Click the first parent and make sure its children are visible.
+ $this->clickLink('Parent 1');
+ $this->assertFacetBlocksAppear();
+ $this->checkFacetIsActive('Parent 1');
+ $this->assertFacetLabel('Child 1');
+ $this->assertFacetLabel('Child 2');
+ $this->assertSession()->linkNotExists('Child 3');
+ $this->assertSession()->linkNotExists('Child 4');
+
+ $this->assertSession()->pageTextContains($block->label());
+ }
+
+ /**
+ * Setup a term structure for our test.
+ */
+ protected function createHierarchialTermStructure() {
+ // Generate 2 parent terms.
+ foreach (['Parent 1', 'Parent 2'] as $name) {
+ $this->parents[$name] = Term::create([
+ 'name' => $name,
+ 'description' => '',
+ 'vid' => $this->vocabulary->id(),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ ]);
+ $this->parents[$name]->save();
+ }
+
+ // Generate 4 child terms.
+ foreach (range(1, 4) as $i) {
+ $this->terms[$i] = Term::create([
+ 'name' => sprintf('Child %d', $i),
+ 'description' => '',
+ 'vid' => $this->vocabulary->id(),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ ]);
+ $this->terms[$i]->save();
+ }
+
+ // Build up the hierarchy.
+ $this->terms[1]->parent = [$this->parents['Parent 1']->id()];
+ $this->terms[1]->save();
+
+ $this->terms[2]->parent = [$this->parents['Parent 1']->id()];
+ $this->terms[2]->save();
+
+ $this->terms[3]->parent = [$this->parents['Parent 2']->id()];
+ $this->terms[3]->save();
+
+ $this->terms[4]->parent = [$this->parents['Parent 2']->id()];
+ $this->terms[4]->save();
+ }
+
+ /**
+ * Creates several test entities with the term-reference field.
+ */
+ protected function insertExampleContent() {
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+
+ $this->entities[1] = $entity_test_storage->create([
+ 'name' => 'foo bar baz',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ $this->fieldName => [$this->parents['Parent 1']->id()],
+ ]);
+ $this->entities[1]->save();
+
+ $this->entities[2] = $entity_test_storage->create([
+ 'name' => 'foo test',
+ 'body' => 'bar test',
+ 'type' => 'item',
+ 'keywords' => ['orange', 'apple', 'grape'],
+ 'category' => 'item_category',
+ $this->fieldName => [$this->parents['Parent 2']->id()],
+ ]);
+ $this->entities[2]->save();
+
+ $this->entities[3] = $entity_test_storage->create([
+ 'name' => 'bar',
+ 'body' => 'test foobar',
+ 'type' => 'item',
+ $this->fieldName => [$this->terms[1]->id()],
+ ]);
+ $this->entities[3]->save();
+
+ $this->entities[4] = $entity_test_storage->create([
+ 'name' => 'foo baz',
+ 'body' => 'test test test',
+ 'type' => 'article',
+ 'keywords' => ['apple', 'strawberry', 'grape'],
+ 'category' => 'article_category',
+ $this->fieldName => [$this->terms[2]->id()],
+ ]);
+ $this->entities[4]->save();
+
+ $this->entities[5] = $entity_test_storage->create([
+ 'name' => 'bar baz',
+ 'body' => 'foo',
+ 'type' => 'article',
+ 'keywords' => ['orange', 'strawberry', 'grape', 'banana'],
+ 'category' => 'article_category',
+ $this->fieldName => [$this->terms[3]->id()],
+ ]);
+ $this->entities[5]->save();
+
+ $this->entities[6] = $entity_test_storage->create([
+ 'name' => 'bar baz',
+ 'body' => 'foo',
+ 'type' => 'article',
+ 'keywords' => ['orange', 'strawberry', 'grape', 'banana'],
+ 'category' => 'article_category',
+ $this->fieldName => [$this->terms[4]->id()],
+ ]);
+ $this->entities[6]->save();
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/IntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/IntegrationTest.php
new file mode 100644
index 000000000..ef7849b43
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/IntegrationTest.php
@@ -0,0 +1,645 @@
+drupalLogin($this->adminUser);
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals(5, $this->indexItems($this->indexId), '5 items were indexed.');
+
+ // Make absolutely sure the ::$blocks variable doesn't pass information
+ // along between tests.
+ $this->blocks = NULL;
+ }
+
+ /**
+ * Tests the overall functionality of the Facets summary admin UI.
+ */
+ public function testFramework() {
+ $this->drupalGet('admin/config/search/facets');
+ $this->assertSession()->pageTextNotContains('Facets Summary');
+
+ $values = [
+ 'name' => 'Owl',
+ 'id' => 'owl',
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+ $this->submitForm([], 'Save');
+
+ $this->drupalGet('admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Facets Summary');
+ $this->assertSession()->pageTextContains('Owl');
+
+ $this->drupalGet('admin/config/search/facets/facet-summary/owl/edit');
+ $this->assertSession()->pageTextContains('No facets found.');
+
+ $this->createFacet('Llama', 'llama');
+ $this->drupalGet('admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Llama');
+
+ // Go back to the facet summary and check that the facets are not checked by
+ // default and that they show up in the list here.
+ $this->drupalGet('admin/config/search/facets/facet-summary/owl/edit');
+ $this->assertSession()->pageTextNotContains('No facets found.');
+ $this->assertSession()->pageTextContains('Llama');
+ $this->assertSession()->checkboxNotChecked('edit-facets-llama-checked');
+
+ // Post the form and check that no facets are checked after saving the form.
+ $this->submitForm([], 'Save');
+ $this->assertSession()->checkboxNotChecked('edit-facets-llama-checked');
+
+ // Enable a facet and check it's status after saving.
+ $this->submitForm(['facets[llama][checked]' => TRUE], 'Save');
+ $this->assertSession()->checkboxChecked('edit-facets-llama-checked');
+
+ $this->configureShowCountProcessor();
+ $this->configureResetFacetsProcessor();
+ }
+
+ /**
+ * Tests with multiple facets.
+ *
+ * Includes a regression test for #2841357
+ */
+ public function testMultipleFacets() {
+ // Create facets.
+ $this->createFacet('Giraffe', 'giraffe', 'keywords');
+ // Clear all the caches between building the 2 facets - because things fail
+ // otherwise.
+ $this->resetAll();
+ $this->createFacet('Llama', 'llama');
+
+ // Add a summary.
+ $values = [
+ 'name' => 'Owlß',
+ 'id' => 'owl',
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+ $this->submitForm([], 'Save');
+
+ // Edit the summary and enable the giraffe's.
+ $summaries = [
+ 'facets[giraffe][checked]' => TRUE,
+ 'facets[giraffe][label]' => 'Summary giraffe',
+ ];
+ $this->drupalGet('admin/config/search/facets/facet-summary/owl/edit');
+ $this->submitForm($summaries, 'Save');
+
+ $block = [
+ 'region' => 'footer',
+ 'id' => str_replace('_', '-', 'owl'),
+ 'weight' => 50,
+ ];
+ $block = $this->drupalPlaceBlock('facets_summary_block:owl', $block);
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertSession()->pageTextContains($block->label());
+ $this->assertFacetBlocksAppear();
+
+ $this->clickLink('apple');
+ $list_items = $this->getSession()
+ ->getPage()
+ ->findById('block-' . $block->id())
+ ->findAll('css', 'li');
+ $this->assertCount(1, $list_items);
+
+ $this->clickLink('item');
+ $list_items = $this->getSession()
+ ->getPage()
+ ->findById('block-' . $block->id())
+ ->findAll('css', 'li');
+ $this->assertCount(1, $list_items);
+
+ // Edit the summary and enable the giraffe's.
+ $summaries = [
+ 'facets[giraffe][checked]' => TRUE,
+ 'facets[giraffe][label]' => 'Summary giraffe',
+ 'facets[llama][checked]' => TRUE,
+ 'facets[llama][label]' => 'Summary llama',
+ ];
+ $this->drupalGet('admin/config/search/facets/facet-summary/owl/edit');
+ $this->submitForm($summaries, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertSession()->pageTextContains($block->label());
+ $this->assertFacetBlocksAppear();
+
+ $this->clickLink('apple');
+ $list_items = $this->getSession()
+ ->getPage()
+ ->findById('block-' . $block->id())
+ ->findAll('css', 'li');
+ $this->assertCount(1, $list_items);
+
+ $this->clickLink('item');
+ $list_items = $this->getSession()
+ ->getPage()
+ ->findById('block-' . $block->id())
+ ->findAll('css', 'li');
+ $this->assertCount(2, $list_items);
+
+ $this->checkShowCountProcessor();
+ $this->checkResetFacetsProcessor();
+ }
+
+ /**
+ * Tests "Show a summary of all selected facets".
+ *
+ * Regression test for https://www.drupal.org/node/2878851.
+ */
+ public function testShowSummary() {
+ // Create facets.
+ $this->createFacet('Giraffe', 'giraffe', 'keywords');
+ // Clear all the caches between building the 2 facets - because things fail
+ // otherwise.
+ $this->resetAll();
+ $this->createFacet('Llama', 'llama');
+
+ // Add a summary.
+ $values = [
+ 'name' => 'Owlß',
+ 'id' => 'owl',
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+
+ // Edit the summary and enable the facets.
+ $summaries = [
+ 'facets[giraffe][checked]' => TRUE,
+ 'facets[giraffe][label]' => 'Summary giraffe',
+ 'facets[llama][checked]' => TRUE,
+ 'facets[llama][label]' => 'Summary llama',
+ 'facets_summary_settings[show_summary][status]' => TRUE,
+ ];
+ $this->submitForm($summaries, 'Save');
+
+ $block = [
+ 'region' => 'footer',
+ 'id' => str_replace('_', '-', 'owl'),
+ 'weight' => 50,
+ ];
+ $block = $this->drupalPlaceBlock('facets_summary_block:owl', $block);
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertText('Displaying 5 search results');
+ $this->clickLink('item');
+
+ /** @var \Behat\Mink\Element\NodeElement[] $list_items */
+ $list_items = $this->getSession()
+ ->getPage()
+ ->findById('block-' . $block->id())
+ ->findAll('css', 'li');
+ $this->assertCount(2, $list_items);
+ $this->assertEquals('Summary llama: item', $list_items[0]->getText());
+ $this->assertEquals('(-) item', $list_items[1]->getText());
+ }
+
+ /**
+ * Check that the disabling of the cache works.
+ */
+ public function testViewsCacheDisable() {
+ // Load the view, verify cache settings.
+ $view = Views::getView('search_api_test_view');
+ $view->setDisplay('page_1');
+ $current_cache = $view->display_handler->getOption('cache');
+ $this->assertEquals('none', $current_cache['type']);
+ $view->display_handler->setOption('cache', ['type' => 'tag']);
+ $view->save();
+ $current_cache = $view->display_handler->getOption('cache');
+ $this->assertEquals('tag', $current_cache['type']);
+
+ // Create a facet and check for the cache disabled message.
+ $id = "western_screech_owl";
+ $name = "Western screech owl";
+ $values = [
+ 'name' => $name,
+ 'id' => $id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+ $this->assertSession()->pageTextContains('Caching of view Search API Test Fulltext search view has been disabled.');
+
+ // Check the view's cache settings again to see if they've been updated.
+ $view = Views::getView('search_api_test_view');
+ $view->setDisplay('page_1');
+ $current_cache = $view->display_handler->getOption('cache');
+ $this->assertEquals('none', $current_cache['type']);
+ }
+
+ /**
+ * Tests counts for summaries.
+ *
+ * @see https://www.drupal.org/node/2873523
+ */
+ public function testCount() {
+ // Create facets.
+ $this->createFacet('Otter', 'otter', 'keywords');
+ // Clear all the caches between building the 2 facets - because things fail
+ // otherwise.
+ $this->resetAll();
+ $this->createFacet('Wolverine', 'wolverine');
+
+ // Make sure the numbers are shown with the facets.
+ $edit = [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => '1',
+ ];
+ $this->drupalGet('admin/config/search/facets/otter/edit');
+ $this->submitForm($edit, 'Save');
+ $this->drupalGet('admin/config/search/facets/wolverine/edit');
+ $this->submitForm($edit, 'Save');
+
+ // Add a summary.
+ $values = [
+ 'name' => 'Mustelidae',
+ 'id' => 'mustelidae',
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+
+ // Configure the summary to hide the count.
+ $summaries = [
+ 'facets[otter][checked]' => TRUE,
+ 'facets[otter][label]' => 'Summary giraffe',
+ 'facets[otter][show_count]' => FALSE,
+ 'facets[wolverine][checked]' => TRUE,
+ 'facets[wolverine][label]' => 'Summary llama',
+ 'facets[wolverine][show_count]' => FALSE,
+ ];
+ $this->submitForm($summaries, 'Save');
+
+ // Place the block.
+ $block = [
+ 'region' => 'footer',
+ 'id' => str_replace('_', '-', 'owl'),
+ 'weight' => 50,
+ ];
+ $summary_block = $this->drupalPlaceBlock('facets_summary_block:mustelidae', $block);
+
+ $this->drupalGet('search-api-test-fulltext');
+ $webAssert = $this->assertSession();
+ $webAssert->pageTextContains('Displaying 5 search results');
+ $this->assertFacetBlocksAppear();
+ $webAssert->pageTextContains($summary_block->label());
+
+ $this->assertFacetLabel('article (2)');
+ $this->assertFacetLabel('apple (2)');
+
+ $summaries = [
+ 'facets[otter][show_count]' => TRUE,
+ 'facets[wolverine][show_count]' => TRUE,
+ ];
+ $this->drupalGet('admin/config/search/facets/facet-summary/mustelidae/edit');
+ $this->submitForm($summaries, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $webAssert = $this->assertSession();
+ $webAssert->pageTextContains('Displaying 5 search results');
+ $this->assertFacetBlocksAppear();
+ $webAssert->pageTextContains($summary_block->label());
+
+ $this->assertFacetLabel('article (2)');
+ $this->assertFacetLabel('apple (2)');
+ }
+
+ /**
+ * Tests for deleting a block.
+ */
+ public function testBlockDelete() {
+ $name = 'Owl';
+ $id = 'owl';
+
+ $values = [
+ 'name' => $name,
+ 'id' => $id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+ $this->submitForm([], 'Save');
+
+ $block_settings = [
+ 'region' => 'footer',
+ 'id' => $id,
+ ];
+ $block = $this->drupalPlaceBlock('facets_summary_block:' . $id, $block_settings);
+
+ $this->drupalGet('admin/structure/block');
+ $this->assertSession()->pageTextContains($block->label());
+
+ $this->drupalGet('admin/structure/block/library/' . $this->defaultTheme);
+ $this->assertSession()->pageTextContains($name);
+
+ // Check for the warning message that additional config entities will be
+ // deleted if the facet summary is removed.
+ $this->drupalGet('admin/config/search/facets/facet-summary/' . $id . '/delete');
+ $this->assertSession()->pageTextContains('The listed configuration will be deleted.');
+ $this->assertSession()->pageTextContains($block->label());
+ $this->submitForm([], 'Delete');
+
+ $this->drupalGet('admin/structure/block/library/' . $this->defaultTheme);
+ $this->assertSession()->pageTextNotContains($name);
+ }
+
+ /**
+ * Tests configuring show_count processor.
+ */
+ protected function configureShowCountProcessor() {
+ $this->assertSession()->checkboxNotChecked('edit-facets-summary-settings-show-count-status');
+ $this->submitForm(['facets_summary_settings[show_count][status]' => TRUE], 'Save');
+ $this->assertSession()->checkboxChecked('edit-facets-summary-settings-show-count-status');
+ $this->assertSession()->pageTextContains($this->t('Facets Summary Owl has been updated.'));
+ }
+
+ /**
+ * Tests configuring reset facets processor.
+ */
+ protected function configureResetFacetsProcessor() {
+ $this->assertSession()->checkboxNotChecked('edit-facets-summary-settings-reset-facets-status');
+ $this->submitForm(['facets_summary_settings[reset_facets][status]' => TRUE], 'Save');
+ $this->assertSession()->checkboxChecked('edit-facets-summary-settings-reset-facets-status');
+ $this->assertSession()->pageTextContains($this->t('Facets Summary Owl has been updated.'));
+
+ $this->assertSession()->fieldExists('facets_summary_settings[reset_facets][settings][link_text]');
+ $this->assertSession()->fieldExists('facets_summary_settings[reset_facets][settings][position]');
+ $this->submitForm([
+ 'facets_summary_settings[reset_facets][settings][link_text]' => 'Reset facets',
+ 'facets_summary_settings[reset_facets][settings][position]' => ResetFacetsProcessor::POSITION_BEFORE,
+ ], 'Save');
+ $this->assertSession()->pageTextContains($this->t('Facets Summary Owl has been updated.'));
+ $this->assertSession()->fieldValueEquals('facets_summary_settings[reset_facets][settings][link_text]', 'Reset facets');
+ $this->assertSession()->fieldValueEquals('facets_summary_settings[reset_facets][settings][position]', ResetFacetsProcessor::POSITION_BEFORE);
+ }
+
+ /**
+ * Tests show_count processor.
+ */
+ protected function checkShowCountProcessor() {
+ // Create new facets summary.
+ FacetsSummary::create([
+ 'id' => 'show_count',
+ 'name' => 'Show count summary',
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'facets' => [
+ 'giraffe' => [
+ 'checked' => 1,
+ 'label' => 'Giraffe',
+ 'separator' => ',',
+ 'weight' => 0,
+ 'show_count' => 0,
+ ],
+ 'llama' => [
+ 'checked' => 1,
+ 'label' => 'Llama',
+ 'separator' => ',',
+ 'weight' => 0,
+ 'show_count' => 0,
+ ],
+ ],
+ 'processor_configs' => [
+ 'show_count' => [
+ 'processor_id' => 'show_count',
+ 'weights' => ['build' => -10],
+ ],
+ ],
+ ])->save();
+
+ // Clear the cache after the new facet summary entity was created.
+ $this->resetAll();
+
+ // Place a block and test show_count processor.
+ $blockConfig = [
+ 'region' => 'footer',
+ 'id' => 'show-count',
+ 'label' => 'show-count-block',
+ ];
+ $this->drupalPlaceBlock('facets_summary_block:show_count', $blockConfig);
+ $this->drupalGet('search-api-test-fulltext');
+
+ $this->assertSession()->pageTextContains('5 results found');
+
+ $this->clickLink('apple');
+ $this->assertSession()->pageTextContains('2 results found');
+
+ $this->clickLink('item');
+ $this->assertSession()->pageTextContains('1 result found');
+ }
+
+ /**
+ * Tests reset facets processor.
+ */
+ protected function checkResetFacetsProcessor() {
+ // Create new facets summary.
+ FacetsSummary::create([
+ 'id' => 'reset_facets',
+ 'name' => $this->t('Reset facets summary'),
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'facets' => [
+ 'giraffe' => [
+ 'checked' => 1,
+ 'label' => 'Giraffe',
+ 'separator' => ',',
+ 'weight' => 0,
+ 'show_count' => 0,
+ ],
+ 'llama' => [
+ 'checked' => 1,
+ 'label' => 'Llama',
+ 'separator' => ',',
+ 'weight' => 0,
+ 'show_count' => 0,
+ ],
+ ],
+ 'processor_configs' => [
+ 'reset_facets' => [
+ 'processor_id' => 'reset_facets',
+ 'weights' => ['build' => -10],
+ 'settings' => [
+ 'link_text' => 'Reset facets',
+ 'position' => ResetFacetsProcessor::POSITION_BEFORE,
+ ],
+ ],
+ ],
+ ])->save();
+
+ // Clear the cache after the new facet summary entity was created.
+ $this->resetAll();
+
+ // Place a block and test reset facets processor.
+ $blockConfig = [
+ 'label' => 'Reset block',
+ 'region' => 'footer',
+ 'id' => 'reset-facets',
+ ];
+ $this->drupalPlaceBlock('facets_summary_block:reset_facets', $blockConfig);
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->addressEquals('/search-api-test-fulltext');
+
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertSession()->pageTextNotContains('Reset facets');
+
+ $this->clickLink('apple');
+ $this->assertSession()->pageTextContains('Displaying 2 search results');
+ $this->assertSession()->pageTextContains('Reset facets');
+
+ $this->clickLink('Reset facets');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->addressEquals('/search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertSession()->pageTextNotContains('Reset facets');
+ }
+
+ /**
+ * Tests first facet doesn't have any item in for a particular filter.
+ */
+ public function testEmptyFacetLinks() {
+ // Create facets.
+ $this->createFacet('Kepler-442b', 'category', 'category');
+ // Clear all the caches between building the 2 facets - because things fail
+ // otherwise.
+ $this->createFacet('Kepler-438b', 'keywords', 'keywords');
+ $this->resetAll();
+
+ // Create a new item, make sure it doesn't have a "keywords" property at
+ // all.
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ $this->entities[] = $entity_test_storage->create([
+ 'name' => 'Test with no category',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['rotten orange'],
+ ])->save();
+
+ $this->indexItems($this->indexId);
+
+ // Add a facets summary entity.
+ $values = [
+ 'name' => 'Kepler planets',
+ 'id' => 'kepler',
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+
+ // Place the block.
+ $block = [
+ 'region' => 'footer',
+ 'id' => 'kplanets',
+ 'weight' => -10,
+ ];
+ $summary_block = $this->drupalPlaceBlock('facets_summary_block:kepler', $block);
+
+ // Enable the facets for the summary.
+ $summaries = [
+ 'facets[category][checked]' => TRUE,
+ 'facets[category][weight]' => 0,
+ 'facets[keywords][checked]' => TRUE,
+ 'facets[keywords][weight]' => 1,
+ 'facets_summary_settings[reset_facets][status]' => 1,
+ 'facets_summary_settings[reset_facets][settings][link_text]' => 'Reset',
+ 'facets_summary_settings[reset_facets][settings][position]' => ResetFacetsProcessor::POSITION_BEFORE,
+ ];
+ $this->drupalGet('admin/config/search/facets/facet-summary/kepler/edit');
+ $this->submitForm($summaries, 'Save');
+
+ // Go to the search view, and check that the summary, as well as the facets
+ // are shown on the page.
+ $this->drupalGet('search-api-test-fulltext');
+ $web_assert = $this->assertSession();
+ $web_assert->pageTextContains('Displaying 6 search results');
+ $this->assertFacetBlocksAppear();
+ $web_assert->pageTextContains($summary_block->label());
+
+ // Filter on the item type.
+ $this->clickLink('rotten orange');
+ $web_assert->pageTextContains('Test with no category');
+ }
+
+ /**
+ * Tests the reset facet link.
+ *
+ * @see https://www.drupal.org/project/facets/issues/2960137
+ */
+ public function testResetFacetLink() {
+ $this->createFacet('Brasserie d\'Orval', 'orval', 'category');
+ $this->resetAll();
+ // Add a facets summary entity.
+ $values = [
+ 'name' => 'Trappist beers',
+ 'id' => 'trappist',
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+ $this->drupalGet('admin/config/search/facets/add-facet-summary');
+ $this->submitForm($values, 'Save');
+
+ // Place the block.
+ $block = [
+ 'region' => 'footer',
+ 'id' => 'trappist',
+ 'weight' => -10,
+ ];
+ $summary_block = $this->drupalPlaceBlock('facets_summary_block:trappist', $block);
+
+ // Enable the facets for the summary.
+ $summaries = [
+ 'facets[orval][checked]' => TRUE,
+ 'facets[orval][weight]' => 0,
+ 'facets_summary_settings[reset_facets][status]' => 1,
+ 'facets_summary_settings[reset_facets][settings][link_text]' => 'Reset',
+ 'facets_summary_settings[reset_facets][settings][position]' => ResetFacetsProcessor::POSITION_BEFORE,
+ ];
+ $this->drupalGet('admin/config/search/facets/facet-summary/trappist/edit');
+ $this->submitForm($summaries, 'Save');
+
+ // Go to the search view, and check that the summary, as well as the facets
+ // are shown on the page.
+ $this->drupalGet('search-api-test-fulltext');
+ $web_assert = $this->assertSession();
+ $web_assert->pageTextContains('Displaying 5 search results');
+ $this->assertFacetBlocksAppear();
+ $web_assert->pageTextContains($summary_block->label());
+
+ $links = $this->xpath('//a[normalize-space(text())=:label]', [':label' => 'Reset']);
+ $this->assertEmpty($links);
+ $this->clickLink('article_category');
+ $links = $this->xpath('//a[normalize-space(text())=:label]', [':label' => 'Reset']);
+ $this->assertNotEmpty($links);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/Rest/FacetSummaryJsonAnonTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/Rest/FacetSummaryJsonAnonTest.php
new file mode 100644
index 000000000..890c35974
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/Rest/FacetSummaryJsonAnonTest.php
@@ -0,0 +1,26 @@
+grantPermissionsToTestedRole(['administer facets']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createEntity() {
+ $entity = FacetsSummary::create();
+ $entity->set('id', 'tapir')
+ ->set('name', 'Tapir')
+ ->set('uuid', 'tapir-uuid')
+ ->save();
+
+ return $entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedNormalizedEntity() {
+ return [
+ 'dependencies' => [],
+ 'facet_source_id' => NULL,
+ 'facets' => [],
+ 'id' => 'tapir',
+ 'langcode' => 'en',
+ 'name' => 'Tapir',
+ 'processor_configs' => [],
+ 'status' => TRUE,
+ 'uuid' => 'tapir-uuid',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedPostEntity() {
+ // @todo Update after https://www.drupal.org/node/2300677.
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/Rest/FacetSummaryXmlAnonTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/Rest/FacetSummaryXmlAnonTest.php
new file mode 100644
index 000000000..834499c04
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Functional/Rest/FacetSummaryXmlAnonTest.php
@@ -0,0 +1,28 @@
+installEntitySchema('facets_facet');
+ $this->installEntitySchema('facets_summary');
+ }
+
+ /**
+ * Tests that the "hide when not rendered" processors is last.
+ */
+ public function testHideWhenNotRenderedIsLast() {
+ /** @var \Drupal\facets_summary\Processor\ProcessorPluginManager $processor_manager */
+ $processor_manager = $this->container->get('plugin.manager.facets_summary.processor');
+ $defs = $processor_manager->getDefinitions();
+ $hide_when_not_rendered_weight = $defs['hide_when_not_rendered']['stages']['build'];
+ unset($defs['hide_when_not_rendered']);
+ foreach ($defs as $def) {
+ $this->assertLessThan($hide_when_not_rendered_weight, $def['stages']['build']);
+ }
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Kernel/SummaryEntityTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Kernel/SummaryEntityTest.php
new file mode 100644
index 000000000..94df78e09
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Kernel/SummaryEntityTest.php
@@ -0,0 +1,116 @@
+installEntitySchema('facets_facet');
+ $this->installEntitySchema('facets_summary');
+ }
+
+ /**
+ * Tests for getName.
+ *
+ * @covers ::getName
+ */
+ public function testName() {
+ $entity = new FacetsSummary(['description' => 'Owls', 'name' => 'owl'], 'facets_summary');
+ $this->assertEquals('owl', $entity->getName());
+ }
+
+ /**
+ * Tests for facet sources.
+ *
+ * @covers ::setFacetSourceId
+ * @covers ::getFacetSourceId
+ */
+ public function testFacetSourceId() {
+ $entity = new FacetsSummary(['description' => 'Owls', 'name' => 'owl'], 'facets_summary');
+ $source = $entity->setFacetSourceId('foo');
+ $this->assertInstanceOf(FacetsSummary::class, $source);
+
+ $this->assertEquals('foo', $entity->getFacetSourceId());
+ }
+
+ /**
+ * Tests facets.
+ *
+ * @covers ::setFacets
+ * @covers ::getFacets
+ * @covers ::removeFacet
+ */
+ public function testFacets() {
+ $entity = new FacetsSummary(['description' => 'Owls', 'name' => 'owl'], 'facets_summary');
+
+ $this->assertEmpty($entity->getFacets());
+
+ $facets = ['foo' => 'bar'];
+ $entity->setFacets($facets);
+ $this->assertEquals($facets, $entity->getFacets());
+
+ $entity->removeFacet('foo');
+ $this->assertEmpty($entity->getFacets());
+ }
+
+ /**
+ * Tests processor behavior.
+ *
+ * @covers ::getProcessorsByStage
+ * @covers ::getProcessors
+ * @covers ::getProcessorConfigs
+ * @covers ::addProcessor
+ * @covers ::removeProcessor
+ * @covers ::loadProcessors
+ */
+ public function testProcessor() {
+ $entity = new FacetsSummary([], 'facets_summary');
+
+ $this->assertEmpty($entity->getProcessorConfigs());
+ $this->assertEmpty($entity->getProcessors());
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_BUILD));
+
+ $id = 'hide_when_not_rendered';
+ $config = [
+ 'processor_id' => $id,
+ 'weights' => [],
+ 'settings' => [],
+ ];
+ $entity->addProcessor($config);
+ $this->assertEquals([$id => $config], $entity->getProcessorConfigs());
+
+ $this->assertNotEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_BUILD));
+ $processors = $entity->getProcessors();
+ $this->assertArrayHasKey($id, $processors);
+ $this->assertInstanceOf(HideWhenNotRenderedProcessor::class, $processors[$id]);
+
+ $entity->removeProcessor($id);
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_BUILD));
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/HideWhenNotRenderedProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/HideWhenNotRenderedProcessorTest.php
new file mode 100644
index 000000000..552b0a9d1
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/HideWhenNotRenderedProcessorTest.php
@@ -0,0 +1,107 @@
+processor = new HideWhenNotRenderedProcessor([], 'hide_when_not_rendered', []);
+ }
+
+ /**
+ * Tests the is hidden method.
+ *
+ * @covers ::isHidden
+ */
+ public function testIsHidden() {
+ $this->assertFalse($this->processor->isHidden());
+ }
+
+ /**
+ * Tests the is locked method.
+ *
+ * @covers ::isLocked
+ */
+ public function testIsLocked() {
+ $this->assertFalse($this->processor->isLocked());
+ }
+
+ /**
+ * Tests the build method, containing the actual work of the processor.
+ *
+ * @covers ::build
+ */
+ public function testBuild() {
+ $this->createContainer(TRUE);
+
+ $summary = new FacetsSummary([], 'facets_summary');
+ $summary->setFacetSourceId('foo');
+
+ $result = $this->processor->build($summary, ['foo'], []);
+ $this->assertEquals(['foo'], $result);
+ }
+
+ /**
+ * Tests the build method, containing the actual work of the processor.
+ *
+ * @covers ::build
+ */
+ public function testBuildWithNoCurrentRequest() {
+ $this->createContainer(FALSE);
+
+ $summary = new FacetsSummary([], 'facets_summary');
+ $summary->setFacetSourceId('foo');
+
+ $result = $this->processor->build($summary, ['foo'], []);
+ $this->assertEquals([], $result);
+ }
+
+ /**
+ * Rendered in current request.
+ *
+ * @param bool $renderedInCurrentRequestValue
+ * The value for rendered in current request.
+ */
+ protected function createContainer($renderedInCurrentRequestValue) {
+ $fsi = $this->getMockBuilder(FacetSourcePluginInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $fsi->method('isRenderedInCurrentRequest')
+ ->willReturn($renderedInCurrentRequestValue);
+
+ $facetSourceManager = $this->getMockBuilder(FacetSourcePluginManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $facetSourceManager->method('createInstance')
+ ->willReturn($fsi);
+
+ $container = new ContainerBuilder();
+ $container->set('plugin.manager.facets.facet_source', $facetSourceManager);
+ \Drupal::setContainer($container);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/ResetFacetsProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/ResetFacetsProcessorTest.php
new file mode 100644
index 000000000..5cf421cbd
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/ResetFacetsProcessorTest.php
@@ -0,0 +1,85 @@
+prophesize(TranslationInterface::class);
+
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $string_translation->reveal());
+ \Drupal::setContainer($container);
+
+ $this->processor = new ResetFacetsProcessor([
+ 'settings' => [
+ 'link_text' => 'Text',
+ 'position' => ResetFacetsProcessor::POSITION_BEFORE,
+ ],
+ ], 'reset_facets', []);
+ }
+
+ /**
+ * Tests the is hidden method.
+ *
+ * @covers ::isHidden
+ */
+ public function testIsHidden() {
+ $this->assertFalse($this->processor->isHidden());
+ }
+
+ /**
+ * Tests the is locked method.
+ *
+ * @covers ::isLocked
+ */
+ public function testIsLocked() {
+ $this->assertFalse($this->processor->isLocked());
+ }
+
+ /**
+ * Tests the build method.
+ *
+ * @covers ::build
+ */
+ public function testBuildWithEmptyItems() {
+ $summary = new FacetsSummary([], 'facets_summary');
+ $summary->setFacetSourceId('foo');
+ $config = [
+ 'processor_id' => 'reset_facets',
+ 'weights' => [],
+ 'settings' => [
+ 'link_text' => 'Text',
+ 'position' => ResetFacetsProcessor::POSITION_BEFORE,
+ ],
+ ];
+ $summary->addProcessor($config);
+
+ $result = $this->processor->build($summary, ['foo'], []);
+ $this->assertSame('array', gettype($result));
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/ShowTextWhenEmptyProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/ShowTextWhenEmptyProcessorTest.php
new file mode 100644
index 000000000..249c1e6c7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/modules/facets_summary/tests/src/Unit/Plugin/Processor/ShowTextWhenEmptyProcessorTest.php
@@ -0,0 +1,128 @@
+prophesize(TranslationInterface::class);
+
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $string_translation->reveal());
+ \Drupal::setContainer($container);
+
+ $this->processor = new ShowTextWhenEmptyProcessor([], 'show_text_when_empty', []);
+ }
+
+ /**
+ * Tests the is hidden method.
+ *
+ * @covers ::isHidden
+ */
+ public function testIsHidden() {
+ $this->assertFalse($this->processor->isHidden());
+ }
+
+ /**
+ * Tests the is locked method.
+ *
+ * @covers ::isLocked
+ */
+ public function testIsLocked() {
+ $this->assertFalse($this->processor->isLocked());
+ }
+
+ /**
+ * Tests the build method.
+ *
+ * @covers ::build
+ */
+ public function testBuild() {
+ $summary = new FacetsSummary([], 'facets_summary');
+ $summary->setFacetSourceId('foo');
+
+ $result = $this->processor->build($summary, ['foo'], []);
+ $this->assertSame('array', gettype($result));
+ $this->assertArrayHasKey('#theme', $result);
+ $this->assertEquals('facets_summary_empty', $result['#theme']);
+ $this->assertArrayHasKey('#message', $result);
+
+ $configuration = [
+ 'text' => [
+ 'value' => 'llama',
+ 'format' => 'html',
+ ],
+ ];
+ $this->processor->setConfiguration($configuration);
+ $result = $this->processor->build($summary, ['foo'], []);
+ $this->assertEquals('llama', $result['#message']['#text']);
+ }
+
+ /**
+ * Tests the build method.
+ *
+ * @covers ::build
+ */
+ public function testBuildWithEmptyItems() {
+ $summary = new FacetsSummary([], 'facets_summary');
+ $summary->setFacetSourceId('foo');
+
+ $build = ['#items' => []];
+ $result = $this->processor->build($summary, $build, []);
+ $this->assertSame('array', gettype($result));
+ $this->assertArrayHasKey('#theme', $result);
+ $this->assertEquals('facets_summary_empty', $result['#theme']);
+ $this->assertArrayHasKey('#message', $result);
+ $this->assertArrayHasKey('#text', $result['#message']);
+ $this->assertEquals(new TranslatableMarkup('No results found.'), (string) $result['#message']['#text']);
+ $this->assertEquals('plain_text', $result['#message']['#format']);
+ }
+
+ /**
+ * Tests build with config changes.
+ *
+ * @covers ::build
+ */
+ public function testBuildWithConfigChange() {
+ $summary = new FacetsSummary([], 'facets_summary');
+ $summary->setFacetSourceId('foo');
+
+ $build = ['#items' => []];
+ $this->processor->setConfiguration([
+ 'text' => [
+ 'value' => 'Owl',
+ 'format' => 'llama',
+ ],
+ ]);
+ $result = $this->processor->build($summary, $build, []);
+ $this->assertSame('array', gettype($result));
+ $this->assertArrayHasKey('#text', $result['#message']);
+ $this->assertEquals('Owl', (string) $result['#message']['#text']);
+ $this->assertEquals('llama', $result['#message']['#format']);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Annotation/FacetsFacetSource.php b/frontend/drupal9/web/modules/contrib/facets/src/Annotation/FacetsFacetSource.php
new file mode 100644
index 000000000..15a42daed
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Annotation/FacetsFacetSource.php
@@ -0,0 +1,53 @@
+storage = $this->entityTypeManager()->getStorage('block');
+ $this->renderer = $renderer;
+ $this->currentPath = $currentPath;
+ $this->router = $router;
+ $this->pathProcessor = $pathProcessor;
+ $this->currentRouteMatch = $currentRouteMatch;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('renderer'),
+ $container->get('path.current'),
+ $container->get('router'),
+ $container->get('path_processor_manager'),
+ $container->get('current_route_match')
+ );
+ }
+
+ /**
+ * Loads and renders the facet blocks via AJAX.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The current request object.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * The ajax response.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+ * Thrown when the view was not found.
+ */
+ public function ajaxFacetBlockView(Request $request) {
+ $response = new AjaxResponse();
+
+ // Rebuild the request and the current path, needed for facets.
+ $path = $request->request->get('facet_link');
+ $facets_blocks = $request->request->get('facets_blocks');
+
+ if (empty($path) || empty($facets_blocks)) {
+ throw new NotFoundHttpException('No facet link or facet blocks found.');
+ }
+
+ // Make sure we are not updating blocks multiple times.
+ $facets_blocks = array_unique($facets_blocks);
+
+ $new_request = Request::create($path);
+ // Support 9.3+.
+ // @todo remove after 9.3 or greater is required.
+ if (class_exists(DrupalRequestStack::class)) {
+ $request_stack = new DrupalRequestStack();
+ }
+ // Legacy request stack.
+ else {
+ $request_stack = new SymfonyRequestStack();
+ }
+ $processed = $this->pathProcessor->processInbound($path, $new_request);
+ $processed_request = Request::create($processed);
+
+ $this->currentPath->setPath($processed_request->getPathInfo());
+ $request->attributes->add($this->router->matchRequest($new_request));
+ $this->currentRouteMatch->resetRouteMatch();
+ $request_stack->push($new_request);
+
+ $container = \Drupal::getContainer();
+ $container->set('request_stack', $request_stack);
+ $active_facet = $request->request->get('active_facet');
+
+ // Build the facets blocks found for the current request and update.
+ foreach ($facets_blocks as $block_id => $block_selector) {
+ $block_entity = $this->storage->load($block_id);
+
+ if ($block_entity) {
+ // Render a block, then add it to the response as a replace command.
+ $block_view = $this->entityTypeManager
+ ->getViewBuilder('block')
+ ->view($block_entity);
+
+ $block_view = (string) $this->renderer->renderPlain($block_view);
+ $response->addCommand(new ReplaceCommand($block_selector, $block_view));
+ }
+ }
+
+ $response->addCommand(new InvokeCommand('[data-block-plugin-id="' . $active_facet . '"]', 'addClass', ['facet-active']));
+
+ // Update filter summary block.
+ $update_summary_block = $request->request->get('update_summary_block');
+ if ($update_summary_block) {
+ $facet_summary_block_id = $request->request->get('facet_summary_block_id');
+ $facet_summary_wrapper_id = $request->request->get('facet_summary_wrapper_id');
+ $facet_summary_block_id = str_replace('-', '_', $facet_summary_block_id);
+
+ if ($facet_summary_block_id) {
+ $block_entity = $this->storage->load($facet_summary_block_id);
+ $block_view = $this->entityTypeManager
+ ->getViewBuilder('block')
+ ->view($block_entity);
+ $block_view = (string) $this->renderer->renderPlain($block_view);
+
+ $response->addCommand(new ReplaceCommand('[data-drupal-facets-summary-id=' . $facet_summary_wrapper_id . ']', $block_view));
+ }
+ }
+
+ return $response;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Controller/FacetController.php b/frontend/drupal9/web/modules/contrib/facets/src/Controller/FacetController.php
new file mode 100644
index 000000000..e88cadce5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Controller/FacetController.php
@@ -0,0 +1,43 @@
+entityTypeManager()
+ ->getStorage('facets_facet')
+ ->load($facets_facet->id());
+ return $this->entityFormBuilder()->getForm($facet, 'default');
+ }
+
+ /**
+ * Returns the page title for an facets's "View" tab.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet that is displayed.
+ *
+ * @return string
+ * The page title.
+ */
+ public function pageTitle(FacetInterface $facet) {
+ return new FormattableMarkup('@title', ['@title' => $facet->label()]);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Controller/FacetSourceController.php b/frontend/drupal9/web/modules/contrib/facets/src/Controller/FacetSourceController.php
new file mode 100644
index 000000000..14ec0f0ef
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Controller/FacetSourceController.php
@@ -0,0 +1,44 @@
+entityTypeManager()->getFormObject('facets_facet_source', 'edit');
+ assert($form_object instanceof EntityForm);
+
+ $facet_source_storage = $this->entityTypeManager()->getStorage('facets_facet_source');
+ $source_id = str_replace(':', '__', $facets_facet_source);
+ $facet_source = $facet_source_storage->load($source_id);
+
+ if (!$facet_source instanceof FacetSourceInterface) {
+ $facet_source = $facet_source_storage->create([
+ 'id' => $source_id,
+ 'name' => $facets_facet_source,
+ ]);
+ $facet_source->save();
+ }
+ $form_object->setEntity($facet_source);
+
+ return $this->formBuilder()->getForm($form_object);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Entity/Facet.php b/frontend/drupal9/web/modules/contrib/facets/src/Entity/Facet.php
new file mode 100644
index 000000000..f615f35a9
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Entity/Facet.php
@@ -0,0 +1,1103 @@
+widget_plugin_manager ?: \Drupal::service('plugin.manager.facets.widget');
+ }
+
+ /**
+ * Returns the hierarchy plugin manager.
+ *
+ * @return \Drupal\facets\Hierarchy\HierarchyPluginManager
+ * The hierarchy plugin manager.
+ */
+ public function getHierarchyManager() {
+ return $this->hierarchy_manager ?: \Drupal::service('plugin.manager.facets.hierarchy');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setWidget($id, array $configuration = NULL) {
+ if ($configuration === NULL) {
+ $instance = $this->getWidgetManager()->createInstance($id);
+ // Get the default configuration for this plugin.
+ $configuration = $instance->getConfiguration();
+ }
+ $this->widget = ['type' => $id, 'config' => $configuration];
+
+ // Unset the widget instance, if exists.
+ unset($this->widgetInstance);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getWidget() {
+ return $this->widget;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getWidgetInstance() {
+ if ($this->widget === NULL) {
+ throw new InvalidProcessorException();
+ }
+
+ if (!isset($this->widgetInstance)) {
+ $definition = $this->getWidget();
+ $this->widgetInstance = $this->getWidgetManager()
+ ->createInstance($definition['type'], (array) $definition['config']);
+ }
+ return $this->widgetInstance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setHierarchy($id, array $configuration = NULL) {
+ if ($configuration === NULL) {
+ $instance = $this->getHierarchyManager()->createInstance($id);
+ // Get the default configuration for this plugin.
+ $configuration = $instance->getConfiguration();
+ }
+ $this->hierarchy = ['type' => $id, 'config' => $configuration];
+
+ // Unset the hierarchy instance, if exists.
+ unset($this->hierarchy_instance);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHierarchy() {
+ return $this->hierarchy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHierarchyInstance() {
+ if (!isset($this->hierarchy_instance)) {
+ $definition = $this->getHierarchy();
+ $this->hierarchy_instance = $this->getHierarchyManager()
+ ->createInstance($definition['type'], (array) $definition['config']);
+ }
+ return $this->hierarchy_instance;
+ }
+
+ /**
+ * Retrieves all processors supported by this facet.
+ *
+ * @return \Drupal\facets\Processor\ProcessorInterface[]
+ * The loaded processors, keyed by processor ID.
+ */
+ protected function loadProcessors() {
+ if (is_array($this->processors)) {
+ return $this->processors;
+ }
+
+ /** @var \Drupal\facets\Processor\ProcessorPluginManager $processor_plugin_manager */
+ $processor_plugin_manager = \Drupal::service('plugin.manager.facets.processor');
+ $processor_settings = $this->getProcessorConfigs();
+
+ foreach ($processor_plugin_manager->getDefinitions() as $name => $processor_definition) {
+ if (class_exists($processor_definition['class']) && empty($this->processors[$name])) {
+ // Create our settings for this processor.
+ $settings = empty($processor_settings[$name]['settings']) ? [] : $processor_settings[$name]['settings'];
+ $settings['facet'] = $this;
+
+ /** @var \Drupal\facets\Processor\ProcessorInterface $processor */
+ $processor = $processor_plugin_manager->createInstance($name, $settings);
+ $this->processors[$name] = $processor;
+ }
+ elseif (!class_exists($processor_definition['class'])) {
+ \Drupal::logger('facets')
+ ->warning('Processor @id specifies a non-existing @class.', [
+ '@id' => $name,
+ '@class' => $processor_definition['class'],
+ ]);
+ }
+ }
+
+ return $this->processors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessorConfigs() {
+ return !empty($this->processor_configs) ? $this->processor_configs : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryType() {
+ $facet_source = $this->getFacetSource();
+ if (is_null($facet_source)) {
+ throw new Exception("No facet source defined for facet.");
+ }
+
+ $query_types = $facet_source->getQueryTypesForFacet($this);
+
+ // Get the widget configured for this facet.
+ /** @var \Drupal\facets\Widget\WidgetPluginInterface $widget */
+ $widget = $this->getWidgetInstance();
+
+ // Give the widget the chance to select a preferred query type. This is
+ // needed for widget that have different query type. For example the need
+ // for a range query.
+ $widgetQueryType = $widget->getQueryType();
+
+ // Allow widgets to also specify a query type.
+ $processorQueryTypes = [];
+ foreach ($this->getProcessors() as $processor) {
+ $pqt = $processor->getQueryType();
+ if ($pqt !== NULL) {
+ $processorQueryTypes[] = $pqt;
+ }
+ }
+ $processorQueryTypes = array_flip($processorQueryTypes);
+
+ // The widget has made no decision and neither have the processors.
+ if ($widgetQueryType === NULL && count($processorQueryTypes) === 0) {
+ return $this->pickQueryType($query_types, 'string');
+ }
+ // The widget has made no decision but the processors have made 1 decision.
+ if ($widgetQueryType === NULL && count($processorQueryTypes) === 1) {
+ return $this->pickQueryType($query_types, key($processorQueryTypes));
+ }
+ // The widget has made a decision and the processors have not.
+ if ($widgetQueryType !== NULL && count($processorQueryTypes) === 0) {
+ return $this->pickQueryType($query_types, $widgetQueryType);
+ }
+ // The widget has made a decision and the processors have 1, being the same.
+ if ($widgetQueryType !== NULL && count($processorQueryTypes) === 1 && key($processorQueryTypes) === $widgetQueryType) {
+ return $this->pickQueryType($query_types, $widgetQueryType);
+ }
+
+ // Invalid choice.
+ throw new InvalidQueryTypeException("Invalid query type combination in widget / processors. Widget: {$widgetQueryType}, Processors: " . implode(', ', array_keys($processorQueryTypes)) . ".");
+ }
+
+ /**
+ * Choose the query type.
+ *
+ * @param array $allTypes
+ * An array of query type definitions.
+ * @param string $type
+ * The chose query type.
+ *
+ * @return string
+ * The class name of the chose query type.
+ *
+ * @throws \Drupal\facets\Exception\InvalidQueryTypeException
+ */
+ protected function pickQueryType(array $allTypes, $type) {
+ if (!isset($allTypes[$type])) {
+ throw new InvalidQueryTypeException("Query type {$type} doesn't exist.");
+ }
+ return $allTypes[$type];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setQueryOperator($operator = '') {
+ return $this->query_operator = $operator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryOperator() {
+ return $this->query_operator ?: 'or';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUseHierarchy($use_hierarchy) {
+ return $this->use_hierarchy = $use_hierarchy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUseHierarchy() {
+ return $this->use_hierarchy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setKeepHierarchyParentsActive($keep_hierarchy_parents_active) {
+ return $this->keep_hierarchy_parents_active = $keep_hierarchy_parents_active;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getKeepHierarchyParentsActive() {
+ return $this->keep_hierarchy_parents_active;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setExpandHierarchy($expand_hierarchy) {
+ return $this->expand_hierarchy = $expand_hierarchy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExpandHierarchy() {
+ return $this->expand_hierarchy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setEnableParentWhenChildGetsDisabled($enable_parent_when_child_gets_disabled) {
+ return $this->enable_parent_when_child_gets_disabled = $enable_parent_when_child_gets_disabled;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEnableParentWhenChildGetsDisabled() {
+ return $this->enable_parent_when_child_gets_disabled;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setHardLimit($limit) {
+ return $this->hard_limit = $limit;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHardLimit() {
+ return $this->hard_limit ?: 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataDefinition() {
+ return $this->getFacetSource()->getDataDefinition($this->field_identifier);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setExclude($exclude) {
+ return $this->exclude = $exclude;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExclude() {
+ return $this->exclude;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldAlias() {
+ // For now, create the field alias based on the field identifier.
+ $field_alias = preg_replace('/[:\/]+/', '_', $this->field_identifier);
+ return $field_alias;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setActiveItem($value) {
+ if (!in_array($value, $this->active_values)) {
+ $this->active_values[] = $value;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getActiveItems() {
+ return $this->active_values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setActiveItems(array $values) {
+ $this->active_values = $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldIdentifier() {
+ return $this->field_identifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFieldIdentifier($field_identifier) {
+ $this->field_identifier = $field_identifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUrlAlias() {
+ return $this->url_alias;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUrlAlias($url_alias) {
+ $this->url_alias = $url_alias;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFacetSourceId($facet_source_id) {
+ $this->facet_source_id = $facet_source_id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFacetSourceId() {
+ return $this->facet_source_id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFacetSource() {
+ if (is_null($this->facet_source_instance) && $this->facet_source_id) {
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */
+ $facet_source_plugin_manager = \Drupal::service('plugin.manager.facets.facet_source');
+ if (!$facet_source_plugin_manager->hasDefinition($this->facet_source_id)) {
+ return NULL;
+ }
+ $this->facet_source_instance = $facet_source_plugin_manager
+ ->createInstance($this->facet_source_id, ['facet' => $this]);
+ }
+
+ return $this->facet_source_instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getShowOnlyOneResult() {
+ return $this->show_only_one_result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setShowOnlyOneResult($show_only_one_result) {
+ $this->show_only_one_result = $show_only_one_result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFacetSourceConfig() {
+ // Return the facet source config object, if it's already set on the facet.
+ if ($this->facetSourceConfig instanceof FacetSource) {
+ return $this->facetSourceConfig;
+ }
+
+ $storage = \Drupal::entityTypeManager()->getStorage('facets_facet_source');
+ $source_id = str_replace(':', '__', $this->facet_source_id);
+
+ // Load and return the facet source config object from the storage.
+ $facet_source = $storage->load($source_id);
+ if ($facet_source instanceof FacetSource) {
+ $this->facetSourceConfig = $facet_source;
+ return $this->facetSourceConfig;
+ }
+
+ // We didn't have a facet source config entity yet for this facet source
+ // plugin, so we create it on the fly.
+ $facet_source = new FacetSource(
+ [
+ 'id' => $source_id,
+ 'name' => $this->facet_source_id,
+ 'filter_key' => 'f',
+ 'url_processor' => 'query_string',
+ ],
+ 'facets_facet_source'
+ );
+
+ return $facet_source;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResults() {
+ return $this->results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResultsKeyedByRawValue($results = NULL) {
+ if ($results === NULL) {
+ $results = $this->results;
+ }
+
+ $keyed_results = [];
+
+ foreach ($results as $result) {
+ $keyed_results[$result->getRawValue()] = $result;
+ if ($children = $result->getChildren()) {
+ $keyed_results = $keyed_results + $this->getResultsKeyedByRawValue($children);
+ }
+ }
+
+ return $keyed_results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setResults(array $results) {
+ $this->results = $results;
+ // If there are active values,
+ // set the results which are active to active.
+ if (count($this->active_values)) {
+ foreach ($this->results as $result) {
+ if (in_array($result->getRawValue(), $this->active_values)) {
+ $result->setActiveState(TRUE);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isActiveValue($value) {
+ $is_active = FALSE;
+ if (in_array($value, $this->active_values)) {
+ $is_active = TRUE;
+ }
+ return $is_active;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFacetSources($only_enabled = FALSE) {
+ if (!isset($this->facetSourcePlugins)) {
+ $this->facetSourcePlugins = [];
+
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */
+ $facet_source_plugin_manager = \Drupal::service('plugin.manager.facets.facet_source');
+
+ foreach ($facet_source_plugin_manager->getDefinitions() as $name => $facet_source_definition) {
+ if (class_exists($facet_source_definition['class']) && empty($this->facetSourcePlugins[$name])) {
+ // Create our settings for this facet source..
+ $config = isset($this->facetSourcePlugins[$name]) ? $this->facetSourcePlugins[$name] : [];
+
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
+ $facet_source = $facet_source_plugin_manager->createInstance($name, $config);
+ $this->facetSourcePlugins[$name] = $facet_source;
+ }
+ elseif (!class_exists($facet_source_definition['class'])) {
+ \Drupal::logger('facets')
+ ->warning('Facet Source @id specifies a non-existing @class.', [
+ '@id' => $name,
+ '@class' => $facet_source_definition['class'],
+ ]);
+ }
+ }
+ }
+
+ // Filter facet sources by status if required.
+ if (!$only_enabled) {
+ return $this->facetSourcePlugins;
+ }
+
+ return array_intersect_key($this->facetSourcePlugins, array_flip($this->facetSourcePlugins));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessors($only_enabled = TRUE) {
+ $processors = $this->loadProcessors();
+
+ // Filter processors by status if required. Enabled processors are those
+ // which have settings in the processor_configs.
+ if ($only_enabled) {
+ $processors_settings = $this->getProcessorConfigs();
+ $processors = array_intersect_key($processors, $processors_settings);
+ }
+
+ return $processors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessorsByStage($stage, $only_enabled = TRUE) {
+ $processors = $this->getProcessors($only_enabled);
+ $processor_settings = $this->getProcessorConfigs();
+ $processor_weights = [];
+
+ // Get a list of all processors for given stage.
+ foreach ($processors as $name => $processor) {
+ if ($processor->supportsStage($stage)) {
+ if (!empty($processor_settings[$name]['weights'][$stage])) {
+ $processor_weights[$name] = $processor_settings[$name]['weights'][$stage];
+ }
+ else {
+ $processor_weights[$name] = $processor->getDefaultWeight($stage);
+ }
+ }
+ }
+
+ // Sort requested processors by weight.
+ asort($processor_weights);
+
+ $return_processors = [];
+ foreach ($processor_weights as $name => $weight) {
+ $return_processors[$name] = $processors[$name];
+ }
+ return $return_processors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHierarchies() {
+ if (is_array($this->hierarchies)) {
+ return $this->hierarchies;
+ }
+
+ $this->hierarchies = [];
+
+ $hierarchy_plugin_manager = $this->getHierarchyManager();
+
+ foreach ($hierarchy_plugin_manager->getDefinitions() as $name => $hierarchy_definition) {
+ if (class_exists($hierarchy_definition['class']) && empty($this->hierarchies[$name])) {
+
+ /** @var \Drupal\facets\Hierarchy\HierarchyInterface $hierarchy */
+ $hierarchy = $hierarchy_plugin_manager->createInstance($name);
+ $this->hierarchies[$name] = $hierarchy;
+ }
+ elseif (!class_exists($hierarchy_definition['class'])) {
+ \Drupal::logger('facets')
+ ->warning('Hierarchy @id specifies a non-existing @class.', [
+ '@id' => $name,
+ '@class' => $hierarchy_definition['class'],
+ ]);
+ }
+ }
+
+ return $this->hierarchies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOnlyVisibleWhenFacetSourceIsVisible($only_visible_when_facet_source_is_visible) {
+ $this->only_visible_when_facet_source_is_visible = $only_visible_when_facet_source_is_visible;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOnlyVisibleWhenFacetSourceIsVisible() {
+ return $this->only_visible_when_facet_source_is_visible;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addProcessor(array $processor) {
+ $this->processor_configs[$processor['processor_id']] = [
+ 'processor_id' => $processor['processor_id'],
+ 'weights' => $processor['weights'],
+ 'settings' => $processor['settings'],
+ ];
+ // Sort the processors so we won't have unnecessary changes.
+ ksort($this->processor_configs);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeProcessor($processor_id) {
+ unset($this->processor_configs[$processor_id]);
+ unset($this->processors[$processor_id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEmptyBehavior() {
+ return $this->empty_behavior;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setEmptyBehavior(array $empty_behavior) {
+ $this->empty_behavior = $empty_behavior;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getWeight() {
+ return $this->weight;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setWeight($weight) {
+ $this->weight = $weight;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMinCount($min_count) {
+ $this->min_count = $min_count;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMinCount() {
+ return $this->min_count;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ parent::calculateDependencies();
+ $source = $this->getFacetSource();
+ if ($source === NULL) {
+ return $this;
+ }
+
+ $facet_dependencies = $source->calculateDependencies();
+ if (!empty($facet_dependencies)) {
+ $this->addDependencies($facet_dependencies);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preSave(EntityStorageInterface $storage) {
+ if (!$this->getHierarchy()) {
+ $this->setHierarchy('taxonomy');
+ }
+ parent::preSave($storage);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+ parent::postSave($storage, $update);
+ if (!$update) {
+ self::clearBlockCache();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function postDelete(EntityStorageInterface $storage, array $entities) {
+ parent::postDelete($storage, $entities);
+ self::clearBlockCache();
+ }
+
+ /**
+ * Clear the block cache.
+ *
+ * This includes resetting the shared plugin block manager as this can result
+ * in the block definition cache being rebuilt in the same request with stale
+ * static caches in the deriver.
+ */
+ protected static function clearBlockCache() {
+ $container = \Drupal::getContainer();
+
+ // If the block manager has already been loaded, we may have stale static
+ // caches in the facet deriver, so lets clear it out.
+ $container->set('plugin.manager.block', NULL);
+
+ // Now rebuild the cache to force a fresh set of data.
+ $container->get('plugin.manager.block')->clearCachedDefinitions();
+ }
+
+ /**
+ * Remove the facet lazy built data when the facet is serialized.
+ */
+ public function __sleep() {
+ unset($this->facet_source_instance);
+ unset($this->processors);
+ return parent::__sleep();
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Entity/FacetSource.php b/frontend/drupal9/web/modules/contrib/facets/src/Entity/FacetSource.php
new file mode 100644
index 000000000..b18e0e3a2
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Entity/FacetSource.php
@@ -0,0 +1,129 @@
+name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFilterKey($filter_key) {
+ $this->filter_key = $filter_key;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilterKey() {
+ return $this->filter_key;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUrlProcessor($processor_name) {
+ $this->url_processor = $processor_name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUrlProcessorName() {
+ return $this->url_processor;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBreadcrumbSettings() {
+ return $this->breadcrumb;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setBreadcrumbSettings(array $settings) {
+ $this->breadcrumb = $settings;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Event/ActiveFiltersParsed.php b/frontend/drupal9/web/modules/contrib/facets/src/Event/ActiveFiltersParsed.php
new file mode 100644
index 000000000..466d7b840
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Event/ActiveFiltersParsed.php
@@ -0,0 +1,116 @@
+facetsourceId = $facetsource_id;
+ $this->queryParameters = $queryParameters;
+ $this->activeFilters = $activeFilters;
+ $this->filterKey = $filter_key;
+ }
+
+ /**
+ * Get the facet source id.
+ *
+ * @return string
+ * The facet source id.
+ */
+ public function getFacetSourceId() {
+ return $this->facetsourceId;
+ }
+
+ /**
+ * Get the active filters.
+ *
+ * Only to be used as context, because changing this will not result in any
+ * changes to the final url.
+ *
+ * @return array
+ * The active filters.
+ */
+ public function getActiveFilters() {
+ return $this->activeFilters;
+ }
+
+ /**
+ * Set the active filters.
+ *
+ * @param array $activeFilters
+ * The active filters.
+ */
+ public function setActiveFilters(array $activeFilters) {
+ $this->activeFilters = $activeFilters;
+ }
+
+ /**
+ * Get the get parameters.
+ *
+ * @return \Symfony\Component\HttpFoundation\ParameterBag
+ * The get parameters.
+ */
+ public function getQueryParameters() {
+ return $this->queryParameters;
+ }
+
+ /**
+ * Get the facet parameter filter key.
+ *
+ * @return string
+ * The facet parameter filter key.
+ */
+ public function getFilterKey() {
+ return $this->filterKey;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Event/FacetsEvents.php b/frontend/drupal9/web/modules/contrib/facets/src/Event/FacetsEvents.php
new file mode 100644
index 000000000..e05b2fefc
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Event/FacetsEvents.php
@@ -0,0 +1,28 @@
+queryParameters = $queryParameters;
+ $this->filterParameters = $filterParameters;
+ $this->facetResult = $facetResult;
+ $this->activeFilters = $activeFilters;
+ $this->facet = $facet;
+ }
+
+ /**
+ * Get the get parameters.
+ *
+ * @return \Symfony\Component\HttpFoundation\ParameterBag
+ * The get parameters.
+ */
+ public function getQueryParameters() {
+ return $this->queryParameters;
+ }
+
+ /**
+ * Get the filter parameters.
+ *
+ * @return array
+ * The filter parameters.
+ */
+ public function getFilterParameters() {
+ return $this->filterParameters;
+ }
+
+ /**
+ * Set the filter parameters.
+ *
+ * @param array $filterParameters
+ * The filter parameters to set.
+ */
+ public function setFilterParameters(array $filterParameters) {
+ $this->filterParameters = $filterParameters;
+ }
+
+ /**
+ * Get the facet result.
+ *
+ * Only to be used as context, because changing this will not result in any
+ * changes to the final url.
+ *
+ * @return \Drupal\facets\Result\ResultInterface
+ * The facet result.
+ */
+ public function getFacetResult() {
+ return $this->facetResult;
+ }
+
+ /**
+ * Get the active filters.
+ *
+ * Only to be used as context, because changing this will not result in any
+ * changes to the final url.
+ *
+ * @return array
+ * The active filters.
+ */
+ public function getActiveFilters() {
+ return $this->activeFilters;
+ }
+
+ /**
+ * Get the facet.
+ *
+ * Only to be used as context, because changing this will not result in any
+ * changes to the final url.
+ *
+ * @return \Drupal\facets\FacetInterface
+ * The facet.
+ */
+ public function getFacet() {
+ return $this->facet;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/EventSubscriber/ConfigurationSubscriber.php b/frontend/drupal9/web/modules/contrib/facets/src/EventSubscriber/ConfigurationSubscriber.php
new file mode 100644
index 000000000..bfb2567e5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/EventSubscriber/ConfigurationSubscriber.php
@@ -0,0 +1,53 @@
+blockManager = $block_manager;
+ }
+
+ /**
+ * Reacts to a config delete event to clear the required caches.
+ *
+ * @param \Drupal\Core\Config\ConfigCrudEvent $event
+ * The config delete event.
+ */
+ public function onConfigDelete(ConfigCrudEvent $event) {
+ $config = $event->getConfig();
+ if (strpos($config->getName(), 'facets') !== FALSE) {
+ $this->blockManager->clearCachedDefinitions();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ $events[ConfigEvents::DELETE][] = ['onConfigDelete', 50];
+ return $events;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/EventSubscriber/SearchApiSubscriber.php b/frontend/drupal9/web/modules/contrib/facets/src/EventSubscriber/SearchApiSubscriber.php
new file mode 100644
index 000000000..403103b16
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/EventSubscriber/SearchApiSubscriber.php
@@ -0,0 +1,71 @@
+facetManager = $facetManager;
+ }
+
+ /**
+ * Reacts to the query alter event.
+ *
+ * @param \Drupal\search_api\Event\QueryPreExecuteEvent $event
+ * The query alter event.
+ */
+ public function queryAlter(QueryPreExecuteEvent $event) {
+ $query = $event->getQuery();
+
+ if ($query->getIndex()->getServerInstance()->supportsFeature('search_api_facets')) {
+ // It's safe to hardcode this to the search api scheme because this is in
+ // an event subscriber. If this generated source is not correct,
+ // implementing the same subscriber and directly calling
+ // $manager->alterQuery($query, $your_facetsource_id); will fix that.
+ $facet_source = 'search_api:' . str_replace(':', '__', $query->getSearchId());
+
+ // Add the active filters.
+ $this->facetManager->alterQuery($query, $facet_source);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ // Workaround to avoid a fatal error during site install from existing
+ // config.
+ // @see https://www.drupal.org/project/facets/issues/3199156
+ if (!class_exists('\Drupal\search_api\Event\SearchApiEvents', TRUE)) {
+ return [];
+ }
+
+ return [
+ SearchApiEvents::QUERY_PRE_EXECUTE => 'queryAlter',
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Exception/Exception.php b/frontend/drupal9/web/modules/contrib/facets/src/Exception/Exception.php
new file mode 100644
index 000000000..f21021667
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Exception/Exception.php
@@ -0,0 +1,8 @@
+" instead of "=".
+ *
+ * @return bool
+ * A boolean flag indicating if search should exlude selected facets
+ */
+ public function getExclude();
+
+ /**
+ * Returns the value of the use_hierarchy boolean.
+ *
+ * This will return true when the results in the facet should be rendered in
+ * a hierarchical structure.
+ *
+ * @return bool
+ * A boolean flag indicating if results should be rendered using hierarchy.
+ */
+ public function getUseHierarchy();
+
+ /**
+ * Sets the use_hierarchy.
+ *
+ * @param bool $use_hierarchy
+ * A boolean flag indicating if results should be rendered using hierarchy.
+ */
+ public function setUseHierarchy($use_hierarchy);
+
+ /**
+ * Returns the value of the keep_hierarchy_parents_active boolean.
+ *
+ * This will return true when the parent results of a hierarchical facet
+ * should be kept active when a child becomes active.
+ *
+ * @return bool
+ * A boolean flag indicating if the parent results of a hierarchical facet
+ * should be kept active when a child becomes active.
+ */
+ public function getKeepHierarchyParentsActive();
+
+ /**
+ * Sets the keep_hierarchy_parents_active.
+ *
+ * @param bool $keep_hierarchy_parents_active
+ * A boolean flag indicating if the parent results of a hierarchical facet
+ * should be kept active when a child becomes active.
+ */
+ public function setKeepHierarchyParentsActive($keep_hierarchy_parents_active);
+
+ /**
+ * Returns the value of the expand_hierarchy boolean.
+ *
+ * This will return true when the results in the facet should be expanded in
+ * a hierarchical structure, regardless of active state.
+ *
+ * @return bool
+ * Wether or not results should always be expanded using hierarchy.
+ */
+ public function getExpandHierarchy();
+
+ /**
+ * Sets the expand_hierarchy.
+ *
+ * @param bool $expand_hierarchy
+ * Wether or not results should always be expanded using hierarchy.
+ */
+ public function setExpandHierarchy($expand_hierarchy);
+
+ /**
+ * Returns the value of the enable_parent_when_child_gets_disabled boolean.
+ *
+ * This will return true when the parent item in the facet should be enabled
+ * in an hierarchical structure, when a child facet item gets disabled.
+ *
+ * @return bool
+ * Wether or not parents should be enabled when a child gets disabled.
+ */
+ public function getEnableParentWhenChildGetsDisabled();
+
+ /**
+ * Sets the enable_parent_when_child_gets_disabled.
+ *
+ * @param bool $enable_parent_when_child_gets_disabled
+ * Wether or not parents should be enabled when a child gets disabled.
+ */
+ public function setEnableParentWhenChildGetsDisabled($enable_parent_when_child_gets_disabled);
+
+ /**
+ * Sets a string representation of the Facet source plugin.
+ *
+ * This is usually the name of the Search-api view.
+ *
+ * @param string $facet_source_id
+ * The facet source id.
+ */
+ public function setFacetSourceId($facet_source_id);
+
+ /**
+ * Sets the query operator.
+ *
+ * @param string $operator
+ * The query operator being used.
+ */
+ public function setQueryOperator($operator);
+
+ /**
+ * Sets the hard limit of facet items.
+ *
+ * @param int $limit
+ * Hard limit of the facet.
+ */
+ public function setHardLimit($limit);
+
+ /**
+ * Sets the exclude.
+ *
+ * @param bool $exclude
+ * A boolean flag indicating if search should exclude selected facets.
+ */
+ public function setExclude($exclude);
+
+ /**
+ * Returns the Facet source id.
+ *
+ * @return string
+ * The id of the facet source.
+ */
+ public function getFacetSourceId();
+
+ /**
+ * Returns the plugin instance of a facet source.
+ *
+ * @return \Drupal\facets\FacetSource\FacetSourcePluginInterface|null
+ * The plugin instance for the facet source.
+ */
+ public function getFacetSource();
+
+ /**
+ * Returns the facet source configuration object.
+ *
+ * @return \Drupal\facets\FacetSourceInterface
+ * A facet source configuration object.
+ */
+ public function getFacetSourceConfig();
+
+ /**
+ * Loads the facet sources for this facet.
+ *
+ * @param bool $only_enabled
+ * Only return enabled facet sources.
+ *
+ * @return \Drupal\facets\FacetSource\FacetSourcePluginInterface[]
+ * An array of facet sources.
+ */
+ public function getFacetSources($only_enabled = TRUE);
+
+ /**
+ * Returns an array of processors with their configuration.
+ *
+ * @param bool $only_enabled
+ * Only return enabled processors.
+ *
+ * @return \Drupal\facets\Processor\ProcessorInterface[]
+ * An array of processors.
+ */
+ public function getProcessors($only_enabled = TRUE);
+
+ /**
+ * Loads this facets processors for a specific stage.
+ *
+ * @param string $stage
+ * The stage for which to return the processors. One of the
+ * \Drupal\facets\Processor\ProcessorInterface::STAGE_* constants.
+ * @param bool $only_enabled
+ * (optional) If FALSE, also include disabled processors. Otherwise, only
+ * load enabled ones.
+ *
+ * @return \Drupal\facets\Processor\ProcessorInterface[]
+ * An array of all enabled (or available, if if $only_enabled is FALSE)
+ * processors that support the given stage, ordered by the weight for that
+ * stage.
+ */
+ public function getProcessorsByStage($stage, $only_enabled = TRUE);
+
+ /**
+ * Retrieves this facets's processor configs.
+ *
+ * @return array
+ * An array of processors and their configs.
+ */
+ public function getProcessorConfigs();
+
+ /**
+ * Sets the "only visible when facet source is visible" boolean flag.
+ *
+ * @param bool $only_visible_when_facet_source_is_visible
+ * A boolean flag indicating if the facet should be hidden on a page that
+ * does not show the facet source.
+ */
+ public function setOnlyVisibleWhenFacetSourceIsVisible($only_visible_when_facet_source_is_visible);
+
+ /**
+ * Returns the "only visible when facet source is visible" boolean flag.
+ *
+ * @return bool
+ * True when the facet is only shown on a page with the facet source.
+ */
+ public function getOnlyVisibleWhenFacetSourceIsVisible();
+
+ /**
+ * Adds a processor for this facet.
+ *
+ * @param array $processor
+ * An array definition for a processor.
+ */
+ public function addProcessor(array $processor);
+
+ /**
+ * Removes a processor for this facet.
+ *
+ * @param string $processor_id
+ * The plugin id of the processor.
+ */
+ public function removeProcessor($processor_id);
+
+ /**
+ * Defines the no-results behavior.
+ *
+ * @param array $behavior
+ * The definition of the behavior.
+ */
+ public function setEmptyBehavior(array $behavior);
+
+ /**
+ * Returns the defined no-results behavior or NULL if none defined.
+ *
+ * @return array|null
+ * The behavior definition or NULL.
+ */
+ public function getEmptyBehavior();
+
+ /**
+ * Returns the weight of the facet.
+ */
+ public function getWeight();
+
+ /**
+ * Sets the weight of the facet.
+ *
+ * @param int $weight
+ * Weight of the facet.
+ */
+ public function setWeight($weight);
+
+ /**
+ * Sets the minimum count of the result to show.
+ *
+ * @param int $min_count
+ * Minimum count.
+ */
+ public function setMinCount($min_count);
+
+ /**
+ * Returns the minimum count of the result to show.
+ *
+ * @return int
+ * Minimum count.
+ */
+ public function getMinCount();
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/FacetListBuilder.php b/frontend/drupal9/web/modules/contrib/facets/src/FacetListBuilder.php
new file mode 100644
index 000000000..40e2f9c39
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/FacetListBuilder.php
@@ -0,0 +1,391 @@
+access('update') && $entity->hasLinkTemplate('edit-form')) {
+ $operations['edit'] = [
+ 'title' => $this->t('Edit'),
+ 'weight' => 10,
+ 'url' => $entity->toUrl('edit-form'),
+ ];
+ }
+ if ($entity->access('update') && $entity->hasLinkTemplate('settings-form')) {
+ $operations['settings'] = [
+ 'title' => $this->t('Facet settings'),
+ 'weight' => 20,
+ 'url' => $entity->toUrl('settings-form'),
+ ];
+ }
+ if ($entity->access('update') && $entity->hasLinkTemplate('clone-form')) {
+ $operations['clone'] = [
+ 'title' => $this->t('Clone facet'),
+ 'weight' => 90,
+ 'url' => $entity->toUrl('clone-form'),
+ ];
+ }
+ if ($entity->access('delete') && $entity->hasLinkTemplate('delete-form')) {
+ $operations['delete'] = [
+ 'title' => $this->t('Delete'),
+ 'weight' => 100,
+ 'url' => $entity->toUrl('delete-form'),
+ ];
+ }
+ }
+ elseif ($entity instanceof FacetsSummaryInterface) {
+ $operations['edit'] = [
+ 'title' => $this->t('Edit'),
+ 'weight' => 10,
+ 'url' => $entity->toUrl('edit-form'),
+ ];
+ if ($entity->access('update') && $entity->hasLinkTemplate('settings-form')) {
+ $operations['settings'] = [
+ 'title' => $this->t('Facet Summary settings'),
+ 'weight' => 20,
+ 'url' => $entity->toUrl('settings-form'),
+ ];
+ }
+ if ($entity->access('delete') && $entity->hasLinkTemplate('delete-form')) {
+ $operations['delete'] = [
+ 'title' => $this->t('Delete'),
+ 'weight' => 100,
+ 'url' => $entity->toUrl('delete-form'),
+ ];
+ }
+ }
+
+ return $operations;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildHeader() {
+ $header = [
+ 'type' => $this->t('Type'),
+ 'machine_name' => $this->t('Machine name'),
+ 'title' => [
+ 'data' => $this->t('Title'),
+ ],
+ ];
+ return $header + parent::buildHeader();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ /** @var \Drupal\facets\FacetInterface $entity */
+ $facet_configs = \Drupal::entityTypeManager()
+ ->getStorage('facets_facet')
+ ->load($entity->getConfigTarget());
+ $row = [
+ 'type' => [
+ '#theme_wrappers' => [
+ 'container' => [
+ '#attributes' => ['class' => 'facets-type'],
+ ],
+ ],
+ '#type' => 'markup',
+ '#markup' => 'Facet',
+ ],
+ 'machine_name' => ['#markup' => $entity->id()],
+ 'title' => [
+ '#type' => 'link',
+ '#title' => $facet_configs->get('name'),
+ '#suffix' => '' . $entity->getFieldAlias() . ' - ' . $entity->getWidget()['type'] . '
',
+ '#attributes' => [
+ 'class' => ['search-api-title'],
+ ],
+ ] + $entity->toUrl('edit-form')->toRenderArray(),
+ '#attributes' => [
+ 'title' => $this->t('ID: @name', ['@name' => $entity->id()]),
+ 'class' => [
+ 'facet',
+ ],
+ ],
+ ];
+ return array_merge_recursive($row, parent::buildRow($entity));
+ }
+
+ /**
+ * Builds an array of facet summary for display in the overview.
+ */
+ public function buildFacetSummaryRow(FacetsSummaryInterface $entity) {
+ $row = parent::buildRow($entity);
+ return [
+ 'type' => [
+ '#theme_wrappers' => [
+ 'container' => [
+ '#attributes' => ['class' => 'facets-summary-type'],
+ ],
+ ],
+ '#type' => 'markup',
+ '#markup' => 'Facets Summary',
+ ],
+ 'machine_name' => ['#markup' => $entity->id()],
+ 'title' => [
+ '#theme_wrappers' => [
+ 'container' => [
+ '#attributes' => ['class' => 'facets-title'],
+ ],
+ ],
+ '#type' => 'link',
+ '#title' => $entity->label(),
+ '#attributes' => [
+ 'class' => ['search-api-title'],
+ ],
+ '#wrapper_attributes' => [
+ 'colspan' => 2,
+ ],
+ ] + $entity->toUrl('edit-form')->toRenderArray(),
+ 'operations' => $row['operations'],
+ '#attributes' => [
+ 'title' => $this->t('ID: @name', ['@name' => $entity->id()]),
+ 'class' => [
+ 'facet',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Builds an array of facet sources for display in the overview.
+ */
+ public function buildFacetSourceRow(array $facet_source = []) {
+ return [
+ 'type' => [
+ '#theme_wrappers' => [
+ 'container' => [
+ '#attributes' => ['class' => 'facets-type'],
+ ],
+ ],
+ '#type' => 'markup',
+ '#markup' => 'Facet source',
+ ],
+ 'title' => [
+ '#theme_wrappers' => [
+ 'container' => [
+ '#attributes' => ['class' => 'facets-title'],
+ ],
+ ],
+ '#type' => 'markup',
+ '#markup' => $facet_source['id'],
+ '#wrapper_attributes' => [
+ 'colspan' => 3,
+ ],
+ ],
+ 'operations' => [
+ 'data' => Link::createFromRoute(
+ $this->t('Configure'),
+ 'entity.facets_facet_source.edit_form',
+ ['facets_facet_source' => $facet_source['id']]
+ )->toRenderable(),
+ ],
+ '#attributes' => [
+ 'class' => ['facet-source', 'facet-source-' . $facet_source['id']],
+ 'no_striping' => TRUE,
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $groups = $this->loadGroups();
+
+ $form['facets'] = [
+ '#type' => 'table',
+ '#header' => $this->buildHeader(),
+ '#empty' => $groups['lone_facets'] ? '' : $this->t('There are no facet sources or facets defined.'),
+ '#attributes' => [
+ 'class' => [
+ 'facets-groups-list',
+ ],
+ ],
+ ];
+
+ // When no facet sources are found, we should show a message that you can't
+ // add facets yet.
+ if (empty($groups['facet_source_groups'])) {
+ return [
+ '#markup' => $this->t(
+ 'You currently have no facet sources defined. You should start by adding a facet source before creating facets.
+ An example of a facet source is a view based on Search API or a Search API page.
+ Other modules can also implement a facet source by providing a plugin that implements the FacetSourcePluginInterface.'
+ ),
+ ];
+ }
+
+ $form['#attached']['library'][] = 'facets/drupal.facets.admin_css';
+
+ foreach ($groups['facet_source_groups'] as $facet_source_group) {
+ $subgroup_class = Html::cleanCssIdentifier('facets-weight-' . $facet_source_group['facet_source']['id']);
+ $delta = round(count($facet_source_group['facets']) / 2);
+
+ $form['facets']['#tabledrag'][] = [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'weight',
+ 'subgroup' => $subgroup_class,
+ ];
+ $form['facets'][$facet_source_group['facet_source']['id']] = $this->buildFacetSourceRow($facet_source_group['facet_source']);
+ foreach ($facet_source_group['facets'] as $facet) {
+ if ($facet instanceof FacetInterface) {
+ $form['facets'][$facet->id()] = $this->buildRow($facet);
+ $form['facets'][$facet->id()]['weight']['#attributes']['class'][] = $subgroup_class;
+ $form['facets'][$facet->id()]['weight']['#delta'] = $delta;
+ }
+ elseif ($facet instanceof FacetsSummaryInterface) {
+ $form['facets'][$facet->id()] = $this->buildFacetSummaryRow($facet);
+ }
+ }
+ }
+
+ // Output the list of facets without a facet source separately.
+ if (!empty($groups['lone_facets'])) {
+ $subgroup_class = 'facets-weight-lone-facets';
+ $form['facets']['#tabledrag'][] = [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'weight',
+ 'subgroup' => $subgroup_class,
+ ];
+ $form['facets']['lone_facets'] = [
+ 'type' => [
+ '#theme_wrappers' => [
+ 'container' => [
+ '#attributes' => ['class' => 'facets-type'],
+ ],
+ ],
+ '#type' => 'markup',
+ '#markup' => '' . $this->t('Facets not currently associated with any facet source') . ' ',
+ ],
+ '#wrapper_attributes' => [
+ 'colspan' => 4,
+ ],
+ ];
+ /** @var \Drupal\facets\FacetInterface $facet */
+ foreach ($groups['lone_facets'] as $facet) {
+ // Facets core search moved into a separate project. Show a clean
+ // message to notify users how to resolve their broken facets.
+ if (substr($facet->getFacetSourceId(), 0, 16) == 'core_node_search') {
+ $project_link = Link::fromTextAndUrl('https://www.drupal.org/project/facets_core_search', Url::fromUri('https://www.drupal.org/project/facets_core_search'))->toString();
+ \Drupal::messenger()->addError(
+ $this->t(
+ 'Core search facets has been moved to a separate project. You need to download and enable this module from @project_link to continue using your core search facets.',
+ ['@project_link' => $project_link]),
+ 'error'
+ );
+ }
+ $form['facets'][$facet->id()] = $this->buildRow($facet);
+ $form['facets'][$facet->id()]['weight']['#attributes']['class'][] = $subgroup_class;
+ }
+ }
+
+ $form['actions']['#type'] = 'actions';
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save'),
+ '#button_type' => 'primary',
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $entities = $this->storage->loadMultiple(array_keys($form_state->getValue('facets')));
+ /** @var \Drupal\block\BlockInterface[] $entities */
+ foreach ($entities as $entity_id => $entity) {
+ $entity_values = $form_state->getValue(['facets', $entity_id]);
+ $entity->setWeight($entity_values['weight']);
+ $entity->save();
+ }
+ \Drupal::messenger()->addMessage($this->t('The facets have been updated.'));
+ }
+
+ /**
+ * Loads facet sources and facets, grouped by facet sources.
+ *
+ * @return \Drupal\Core\Config\Entity\ConfigEntityInterface[][]
+ * An associative array with two keys:
+ * - facet sources: All available facet sources, each followed by all facets
+ * attached to it.
+ * - lone_facets: All facets that aren't attached to any facet source.
+ */
+ public function loadGroups() {
+ $facet_source_plugin_manager = \Drupal::service('plugin.manager.facets.facet_source');
+ $facets = $this->load();
+ $facets_summaries = [];
+ if (\Drupal::moduleHandler()->moduleExists('facets_summary')) {
+ $facets_summaries = FacetsSummary::loadMultiple();
+ }
+ $facet_sources = $facet_source_plugin_manager->getDefinitions();
+
+ $facet_source_groups = [];
+ foreach ($facet_sources as $facet_source) {
+ $facet_source_groups[$facet_source['id']] = [
+ 'facet_source' => $facet_source,
+ 'facets' => [],
+ ];
+
+ foreach ($facets as $facet) {
+ /** @var \Drupal\facets\FacetInterface $facet */
+ if ($facet->getFacetSourceId() == $facet_source['id']) {
+ $facet_source_groups[$facet_source['id']]['facets'][$facet->id()] = $facet;
+ // Remove this facet from $facet so it will finally only contain those
+ // facets not belonging to any facet_source.
+ unset($facets[$facet->id()]);
+ }
+ }
+
+ foreach ($facets_summaries as $summary) {
+ /** @var \Drupal\facets_summary\FacetsSummaryInterface $summary */
+ if ($summary->getFacetSourceId() == $facet_source['id']) {
+ $facet_source_groups[$facet_source['id']]['facets'][$summary->id()] = $summary;
+ // Remove this facet from $facet so it will finally only contain those
+ // facets not belonging to any facet_source.
+ unset($facets_summaries[$summary->id()]);
+ }
+ }
+ }
+
+ return [
+ 'facet_source_groups' => $facet_source_groups,
+ 'lone_facets' => $facets,
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/FacetManager/DefaultFacetManager.php b/frontend/drupal9/web/modules/contrib/facets/src/FacetManager/DefaultFacetManager.php
new file mode 100644
index 000000000..b3e93dfe1
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/FacetManager/DefaultFacetManager.php
@@ -0,0 +1,502 @@
+queryTypePluginManager = $query_type_plugin_manager;
+ $this->facetSourcePluginManager = $facet_source_manager;
+ $this->processorPluginManager = $processor_plugin_manager;
+ $this->facetStorage = $entity_type_manager->getStorage('facets_facet');
+ }
+
+ /**
+ * Allows the backend to add facet queries to its native query object.
+ *
+ * This method is called by the implementing module to initialize the facet
+ * display process.
+ *
+ * @param mixed $query
+ * The backend's native query object.
+ * @param string $facetsource_id
+ * The facet source ID to process.
+ */
+ public function alterQuery(&$query, $facetsource_id) {
+ /** @var \Drupal\facets\FacetInterface[] $facets */
+ $facets = $this->getFacetsByFacetSourceId($facetsource_id);
+ foreach ($facets as $facet) {
+
+ $processors = $facet->getProcessors();
+ if (isset($processors['dependent_processor'])) {
+ $conditions = $processors['dependent_processor']->getConfiguration();
+
+ $enabled_conditions = [];
+ foreach ($conditions as $facet_id => $condition) {
+ if (empty($condition['enable'])) {
+ continue;
+ }
+ $enabled_conditions[$facet_id] = $condition;
+ }
+
+ foreach ($enabled_conditions as $facet_id => $condition_settings) {
+
+ if (!isset($facets[$facet_id]) || !$processors['dependent_processor']->isConditionMet($condition_settings, $facets[$facet_id])) {
+ // The conditions are not met anymore, remove the active items.
+ $facet->setActiveItems([]);
+
+ // Remove the query parameter from other facets.
+ foreach ($facets as $other_facet) {
+ /** @var \Drupal\facets\UrlProcessor\UrlProcessorInterface $urlProcessor */
+ $urlProcessor = $other_facet->getProcessors()['url_processor_handler']->getProcessor();
+ $active_filters = $urlProcessor->getActiveFilters();
+ unset($active_filters[$facet->id()]);
+ $urlProcessor->setActiveFilters($active_filters);
+ }
+
+ // Don't convert this facet's active items into query conditions.
+ // Continue with the next facet.
+ continue(2);
+ }
+ }
+ }
+
+ /** @var \Drupal\facets\QueryType\QueryTypeInterface $query_type_plugin */
+ $query_type_plugin = $this->queryTypePluginManager->createInstance(
+ $facet->getQueryType(),
+ [
+ 'query' => $query,
+ 'facet' => $facet,
+ ]
+ );
+ $query_type_plugin->execute();
+ }
+ }
+
+ /**
+ * Returns enabled facets for the searcher associated with this FacetManager.
+ *
+ * @return \Drupal\facets\FacetInterface[]
+ * An array of enabled facets.
+ */
+ public function getEnabledFacets() {
+ return $this->facetStorage->loadMultiple();
+ }
+
+ /**
+ * Returns currently rendered facets filtered by facetsource ID.
+ *
+ * @param string $facetsource_id
+ * The facetsource ID to filter by.
+ *
+ * @return \Drupal\facets\FacetInterface[]
+ * An array of enabled facets.
+ */
+ public function getFacetsByFacetSourceId($facetsource_id) {
+ // Immediately initialize the facets.
+ $this->initFacets();
+ $facets = [];
+ foreach ($this->facets as $facet) {
+ if ($facet->getFacetSourceId() === $facetsource_id) {
+ $facets[$facet->id()] = $facet;
+ }
+ }
+ return $facets;
+ }
+
+ /**
+ * Initializes facet builds, sets the breadcrumb trail.
+ *
+ * Facets are built via FacetsFacetProcessor objects. Facets only need to be
+ * processed, or built, once the FacetsFacetManager::processed semaphore is
+ * set when this method is called ensuring that facets are built only once
+ * regardless of how many times this method is called.
+ *
+ * @param string|null $facetsource_id
+ * The facetsource if of the currently processed facet.
+ *
+ * @throws \Drupal\facets\Exception\InvalidProcessorException
+ * Thrown when one of the defined processors is invalid.
+ */
+ public function processFacets($facetsource_id = NULL) {
+ if ($facetsource_id === NULL) {
+ foreach ($this->facets as $facet) {
+ $current_facetsource_id = $facet->getFacetSourceId();
+ $this->processFacets($current_facetsource_id);
+ }
+ }
+
+ $unprocessedFacets = array_filter($this->facets, function ($item) use ($facetsource_id) {
+ return $item->getFacetSourceId() === $facetsource_id && !isset($this->processedFacets[$facetsource_id][$item->id()]);
+ });
+
+ // All facets were already processed on a previous run, so no need to do so
+ // again.
+ if (count($unprocessedFacets) === 0) {
+ return;
+ }
+
+ $this->updateResults($facetsource_id);
+
+ foreach ($unprocessedFacets as $facet) {
+ $processor_configs = $facet->getProcessorConfigs();
+ foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_POST_QUERY) as $processor) {
+ $processor_config = $processor_configs[$processor->getPluginDefinition()['id']]['settings'];
+ $processor_config['facet'] = $facet;
+ /** @var \Drupal\facets\processor\PostQueryProcessorInterface $post_query_processor */
+ $post_query_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], $processor_config);
+ if (!$post_query_processor instanceof PostQueryProcessorInterface) {
+ throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a post_query definition but doesn't implement the required PostQueryProcessor interface");
+ }
+ $post_query_processor->postQuery($facet);
+ }
+ $this->processedFacets[$facetsource_id][$facet->id()] = $facet;
+ }
+ }
+
+ /**
+ * Initializes enabled facets.
+ *
+ * In this method all pre-query processors get called and their contents are
+ * executed.
+ *
+ * @throws \Drupal\facets\Exception\InvalidProcessorException
+ * Thrown if one of the pre query processors is invalid.
+ */
+ protected function initFacets() {
+ if (count($this->facets) > 0) {
+ return;
+ }
+
+ $this->facets = $this->getEnabledFacets();
+ foreach ($this->facets as $facet) {
+ foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_PRE_QUERY) as $processor) {
+ /** @var \Drupal\facets\Processor\PreQueryProcessorInterface $pre_query_processor */
+ $pre_query_processor = $facet->getProcessors()[$processor->getPluginDefinition()['id']];
+ if (!$pre_query_processor instanceof PreQueryProcessorInterface) {
+ throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a pre_query definition but doesn't implement the required PreQueryProcessorInterface interface");
+ }
+ $pre_query_processor->preQuery($facet);
+ }
+ }
+ }
+
+ /**
+ * Builds a facet.
+ *
+ * This method delegates to the relevant plugins in Build stage, the
+ * processors that implement the BuildProcessorInterface enabled on this
+ * facet will run.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet we should build.
+ *
+ * @return \Drupal\facets\FacetInterface
+ * The built Facet.
+ *
+ * @throws \Drupal\facets\Exception\InvalidProcessorException
+ * Throws an exception when an invalid processor is linked to the facet.
+ */
+ protected function processBuild(FacetInterface $facet) {
+ if (!isset($this->builtFacets[$facet->getFacetSourceId()][$facet->id()])) {
+ // Immediately initialize the facets if they are not initiated yet.
+ $this->initFacets();
+
+ // It might be that the facet received here, is not the same as the
+ // already loaded facets in the FacetManager.
+ // For that reason, get the facet from the already loaded facets in the
+ // static cache.
+ $facet = $this->facets[$facet->id()];
+
+ // For clarity, process facets is called each build.
+ // The first facet therefor will trigger the processing. Note that
+ // processing is done only once, so repeatedly calling this method will
+ // not trigger the processing more than once.
+ $this->processFacets($facet->getFacetSourceId());
+
+ // Get the current results from the facets and let all processors that
+ // trigger on the build step do their build processing.
+ // @see \Drupal\facets\Processor\BuildProcessorInterface.
+ // @see \Drupal\facets\Processor\SortProcessorInterface.
+ $results = $facet->getResults();
+
+ foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_BUILD) as $processor) {
+ if (!$processor instanceof BuildProcessorInterface) {
+ throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a build definition but doesn't implement the required BuildProcessorInterface interface");
+ }
+ $results = $processor->build($facet, $results);
+ }
+
+ // Trigger sort stage.
+ $active_sort_processors = [];
+ foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_SORT) as $processor) {
+ $active_sort_processors[] = $processor;
+ }
+
+ // Sort the actual results if we have enabled sort processors.
+ if (!empty($active_sort_processors)) {
+ $results = $this->sortFacetResults($active_sort_processors, $results);
+ }
+
+ $facet->setResults($results);
+
+ $this->builtFacets[$facet->getFacetSourceId()][$facet->id()] = $facet;
+ }
+
+ return $this->builtFacets[$facet->getFacetSourceId()][$facet->id()];
+ }
+
+ /**
+ * Builds a facet and returns it as a renderable array.
+ *
+ * This method delegates to the relevant plugins to render a facet, it calls
+ * out to a widget plugin to do the actual rendering when results are found.
+ * When no results are found it calls out to the correct empty result plugin
+ * to build a render array.
+ *
+ * Before doing any rendering, the processors that implement the
+ * BuildProcessorInterface enabled on this facet will run.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet we should build.
+ *
+ * @return array
+ * Facet render arrays.
+ *
+ * @throws \Drupal\facets\Exception\InvalidProcessorException
+ * Throws an exception when an invalid processor is linked to the facet.
+ */
+ public function build(FacetInterface $facet) {
+ $facet = $this->processBuild($facet);
+
+ if ($facet->getOnlyVisibleWhenFacetSourceIsVisible()) {
+ // Block rendering and processing should be stopped when the facet source
+ // is not available on the page. Returning an empty array here is enough
+ // to halt all further processing.
+ $facet_source = $facet->getFacetSource();
+ if (is_null($facet_source) || !$facet_source->isRenderedInCurrentRequest()) {
+ return [];
+ }
+ }
+
+ // We include this build even if empty, it may contain attached libraries.
+ /** @var \Drupal\facets\Widget\WidgetPluginInterface $widget */
+ $widget = $facet->getWidgetInstance();
+ $build = $widget->build($facet);
+
+ // No results behavior handling. Return a custom text or false depending on
+ // settings.
+ if (empty($facet->getResults())) {
+ $empty_behavior = $facet->getEmptyBehavior();
+ if ($empty_behavior && $empty_behavior['behavior'] === 'text') {
+ return [
+ [
+ 0 => $build,
+ '#type' => 'container',
+ '#attributes' => [
+ 'data-drupal-facet-id' => $facet->id(),
+ 'class' => ['facet-empty'],
+ ],
+ 'empty_text' => [
+ // @codingStandardsIgnoreStart
+ '#markup' => $this->t($empty_behavior['text']),
+ // @codingStandardsIgnoreEnd
+ ],
+ ],
+ ];
+ }
+ else {
+ // If the facet has no results, but it is being rendered trough ajax we
+ // should render a container (that is empty). This is because the
+ // javascript needs to be able to find a div to replace with the new
+ // content.
+ return [
+ [
+ 0 => $build,
+ '#type' => 'container',
+ '#attributes' => [
+ 'data-drupal-facet-id' => $facet->id(),
+ 'class' => ['facet-empty', 'facet-hidden'],
+ ],
+ ],
+ ];
+ }
+ }
+
+ return [$build];
+ }
+
+ /**
+ * Updates all facets of a given facet source with the raw results.
+ *
+ * @param string $facetsource_id
+ * The facet source ID of the currently processed facet.
+ */
+ public function updateResults($facetsource_id) {
+ $facets = $this->getFacetsByFacetSourceId($facetsource_id);
+ if ($facets) {
+ // Clear the caches of processed results.
+ unset($this->processedFacets[$facetsource_id]);
+ unset($this->builtFacets[$facetsource_id]);
+
+ /** @var \drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source_plugin */
+ $facet_source_plugin = $this->facetSourcePluginManager->createInstance($facetsource_id);
+ $facet_source_plugin->fillFacetsWithResults($facets);
+ }
+ }
+
+ /**
+ * Returns one of the processed facets.
+ *
+ * Returns one of the processed facets, this is a facet with filled results.
+ * Keep in mind that if you want to have the facet's build processor executed,
+ * call returnBuiltFacet() instead.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet to process.
+ *
+ * @return \Drupal\facets\FacetInterface|null
+ * The updated facet if it exists, NULL otherwise.
+ */
+ public function returnProcessedFacet(FacetInterface $facet) {
+ $this->processFacets($facet->getFacetSourceId());
+ return !empty($this->facets[$facet->id()]) ? $this->facets[$facet->id()] : NULL;
+ }
+
+ /**
+ * Returns one of the built facets.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet to process.
+ *
+ * @return \Drupal\facets\FacetInterface
+ * The built facet.
+ */
+ public function returnBuiltFacet(FacetInterface $facet) {
+ return $this->processBuild($facet);
+ }
+
+ /**
+ * Sort the facet results, and recurse to children to do the same.
+ *
+ * @param \Drupal\facets\Processor\SortProcessorInterface[] $active_sort_processors
+ * An array of sort processors.
+ * @param \Drupal\facets\Result\ResultInterface[] $results
+ * An array of results.
+ *
+ * @return \Drupal\facets\Result\ResultInterface[]
+ * A sorted array of results.
+ */
+ protected function sortFacetResults(array $active_sort_processors, array $results) {
+ uasort($results, function ($a, $b) use ($active_sort_processors) {
+ $return = 0;
+ foreach ($active_sort_processors as $sort_processor) {
+ if ($return = $sort_processor->sortResults($a, $b)) {
+ if ($sort_processor->getConfiguration()['sort'] == 'DESC') {
+ $return *= -1;
+ }
+ break;
+ }
+ }
+ return $return;
+ });
+
+ // Loop over the results and see if they have any children, if they do, fire
+ // a request to this same method again with the children.
+ foreach ($results as &$result) {
+ if (!empty($result->getChildren())) {
+ $children = $this->sortFacetResults($active_sort_processors, $result->getChildren());
+ $result->setChildren($children);
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourceDeriverBase.php b/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourceDeriverBase.php
new file mode 100644
index 000000000..5bd3a2b73
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourceDeriverBase.php
@@ -0,0 +1,130 @@
+get('module_handler')->getModuleList();
+ if (!in_array('search_api', array_keys($module_list))) {
+ return;
+ }
+
+ $entity_type_manager = $container->get('entity_type.manager');
+ $deriver->setEntityTypeManager($entity_type_manager);
+
+ $translation = $container->get('string_translation');
+ $deriver->setStringTranslation($translation);
+
+ $search_api_display_plugin_manager = $container->get('plugin.manager.search_api.display');
+ $deriver->setSearchApiDisplayPluginManager($search_api_display_plugin_manager);
+
+ return $deriver;
+ }
+
+ /**
+ * Retrieves the entity manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::service('entity_type.manager');
+ }
+
+ /**
+ * Sets the entity manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
+ $derivatives = $this->getDerivativeDefinitions($base_plugin_definition);
+ return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL;
+ }
+
+ /**
+ * Sets search api's display plugin manager.
+ *
+ * @param \Drupal\search_api\Display\DisplayPluginManager $search_api_display_plugin_manager
+ * The plugin manager.
+ */
+ public function setSearchApiDisplayPluginManager(DisplayPluginManager $search_api_display_plugin_manager) {
+ $this->searchApiDisplayPluginManager = $search_api_display_plugin_manager;
+ }
+
+ /**
+ * Returns the display plugin manager.
+ *
+ * @return \Drupal\search_api\Display\DisplayPluginManager
+ * The plugin manager.
+ */
+ public function getSearchApiDisplayPluginManager() {
+ return $this->searchApiDisplayPluginManager;
+ }
+
+ /**
+ * Compares two plugin definitions according to their labels.
+ *
+ * @param array $a
+ * A plugin definition, with at least a "label" key.
+ * @param array $b
+ * Another plugin definition.
+ *
+ * @return int
+ * An integer less than, equal to, or greater than zero if the first
+ * argument is considered to be respectively less than, equal to, or greater
+ * than the second.
+ */
+ public function compareDerivatives(array $a, array $b) {
+ return strnatcasecmp($a['label'], $b['label']);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourcePluginBase.php b/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourcePluginBase.php
new file mode 100644
index 000000000..9a470f8fd
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourcePluginBase.php
@@ -0,0 +1,156 @@
+queryTypePluginManager = $query_type_plugin_manager;
+
+ if (isset($configuration['facet'])) {
+ $this->facet = $configuration['facet'];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ // Insert the plugin manager for query types.
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('plugin.manager.facets.query_type')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFields() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryTypesForFacet(FacetInterface $facet) {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRenderedInCurrentRequest() {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSearchKeys($keys) {
+ $this->keys = $keys;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchKeys() {
+ return $this->keys;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildFacet() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCount() {
+ global $pager_total_items;
+ // Exposing a global here. This is pretty ugly but the only way to get the
+ // actual results for any kind of query that was done and gets back results.
+ // @see core/includes/pager.inc for more information how this works.
+ // Rounding as some backend plugins could not give accurate information on
+ // the results found.
+ // @todo Figure out when it can not be the first one in the list.
+ return round($pager_total_items[0]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ $facet_source_id = $this->facet->getFacetSourceId();
+ $field_identifier = $form_state->getValue('facet_source_configs')[$facet_source_id]['field_identifier'];
+ $this->facet->setFieldIdentifier($field_identifier);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourcePluginInterface.php b/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourcePluginInterface.php
new file mode 100644
index 000000000..1b9710126
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/FacetSourcePluginInterface.php
@@ -0,0 +1,118 @@
+setCacheBackend($cache_backend, 'facet_source_plugins');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function processDefinition(&$definition, $plugin_id) {
+ parent::processDefinition($definition, $plugin_id);
+
+ // At the very least - we need to have an ID in the definition of the
+ // plugin.
+ if (!isset($definition['id'])) {
+ throw new PluginException(sprintf('The facet source plugin %s must define the id property.', $plugin_id));
+ }
+
+ // If we're checking the search api plugin, only try to add it if search api
+ // is enabled.
+ if ($definition['id'] === 'search_api' && !$this->moduleHandler->moduleExists('search_api')) {
+ return;
+ }
+
+ // Check that other required labels are available.
+ foreach (['display_id', 'label'] as $required_property) {
+ if (empty($definition[$required_property])) {
+ throw new PluginException(sprintf('The facet source plugin %s must define the %s property.', $plugin_id, $required_property));
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function findDefinitions() {
+ $defs = parent::findDefinitions();
+
+ // Definitions that are based on search api when search api is not enabled
+ // should not exist, so make sure we do exactly that, we do this in
+ // ::findDefinitions because this one is called before the result is saved.
+ $defs = array_filter($defs, function ($item) {
+ if ($item['id'] === 'search_api' && !$this->moduleHandler->moduleExists('search_api')) {
+ return FALSE;
+ }
+ return TRUE;
+ });
+
+ return $defs;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/SearchApiFacetSourceInterface.php b/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/SearchApiFacetSourceInterface.php
new file mode 100644
index 000000000..91937ccaa
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/FacetSource/SearchApiFacetSourceInterface.php
@@ -0,0 +1,38 @@
+facetSourcePluginManager = $facetSourcePluginManager;
+ $this->displayPluginManager = $displayPluginManager;
+ $this->facetStorage = $facetStorage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.facets.facet_source'),
+ $container->get('plugin.manager.search_api.display'),
+ $container->get('entity_type.manager')->getStorage('facets_facet')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->getEntity();
+
+ if (strpos($facet->getFacetSourceId(), 'search_api:') === FALSE) {
+ // We don't know how to clone other kinds of facets.
+ $this->messenger()->addMessage($this->t('We can only clone Search API based facets.'));
+ return [];
+ }
+
+ /** @var \Drupal\search_api\Display\DisplayInterface[] $facet_source_definitions */
+ $facet_source_definitions = $this->facetSourcePluginManager->getDefinitions();
+
+ // Get the base table from the facet source's view.
+ $facet_source_id = $facet->getFacetSourceId();
+ $search_api_display_id = $facet_source_definitions[$facet_source_id]['display_id'];
+
+ /** @var \Drupal\search_api\Display\DisplayInterface $current_display */
+ $current_display = $this->displayPluginManager
+ ->createInstance($search_api_display_id);
+ $current_index = $current_display->getIndex()->id();
+
+ // Create a list of all other search api displays that have the same index.
+ $options = [];
+ foreach ($facet_source_definitions as $source) {
+ $current_display = $this->displayPluginManager
+ ->createInstance($source['display_id']);
+ if ($current_display->getIndex()->id() !== $current_index) {
+ continue;
+ }
+
+ $options[$source['id']] = $source['label'];
+ }
+
+ $form['destination_facet_source'] = [
+ '#type' => 'radios',
+ '#title' => $this->t("Clone the facet to this facet source:"),
+ '#options' => $options,
+ '#required' => TRUE,
+ ];
+
+ $form['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Name'),
+ '#description' => $this->t('The administrative name used for this facet.'),
+ '#default_value' => $this->t('Duplicate of @label', ['@label' => $this->entity->label()]),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#maxlength' => 50,
+ '#required' => TRUE,
+ '#default_value' => '',
+ '#machine_name' => [
+ 'exists' => [$this->facetStorage, 'load'],
+ 'source' => ['name'],
+ ],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Duplicate'),
+ ];
+ return $actions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->entity->createDuplicate();
+ $facet->set('name', $form_state->getValue('name'));
+ $facet->set('id', $form_state->getValue('id'));
+ $facet->set('facet_source_id', $form_state->getValue('destination_facet_source'));
+ $facet->save();
+
+ $this->messenger()->addMessage($this->t('Facet cloned to :label', [':label' => $facet->label()]));
+
+ // Redirect the user to the view admin form.
+ $form_state->setRedirectUrl($facet->toUrl('edit-form'));
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetDeleteConfirmForm.php b/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetDeleteConfirmForm.php
new file mode 100644
index 000000000..c3b29a892
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetDeleteConfirmForm.php
@@ -0,0 +1,44 @@
+t('Are you sure you want to delete the facet %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('facets.overview');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->entity->delete();
+ \Drupal::messenger()->addMessage($this->t('The facet %name has been deleted.', ['%name' => $this->entity->label()]));
+ $form_state->setRedirect('facets.overview');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetForm.php b/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetForm.php
new file mode 100644
index 000000000..98db6d649
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetForm.php
@@ -0,0 +1,855 @@
+entityTypeManager = $entity_type_manager;
+ $this->processorPluginManager = $processor_plugin_manager;
+ $this->widgetPluginManager = $widget_plugin_manager;
+ $this->facetSourcePluginManager = $facet_source_plugin_manager;
+ $this->facetsManager = $facets_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity_type.manager'),
+ $container->get('plugin.manager.facets.processor'),
+ $container->get('plugin.manager.facets.widget'),
+ $container->get('plugin.manager.facets.facet_source'),
+ $container->get('facets.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBaseFormId() {
+ return NULL;
+ }
+
+ /**
+ * Builds the configuration forms for all selected widgets.
+ *
+ * @param array $form
+ * An associative array containing the initial structure of the plugin form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the complete form.
+ */
+ public function buildWidgetConfigForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->getEntity();
+ $widget_plugin_id = $form_state->getValue('widget') ?: $facet->getWidget()['type'];
+ $widget_config = $form_state->getValue('widget_config') ?: $facet->getWidget()['config'];
+ if (empty($widget_plugin_id)) {
+ return;
+ }
+
+ /** @var \Drupal\facets\Widget\WidgetPluginBase $widget */
+ $facet->setWidget($widget_plugin_id, $widget_config);
+ $widget = $facet->getWidgetInstance();
+
+ $arguments = ['%widget' => $widget->getPluginDefinition()['label']];
+ if (!$config_form = $widget->buildConfigurationForm([], $form_state, $facet)) {
+ $type = 'details';
+ $config_form = ['#markup' => $this->t('%widget widget needs no configuration.', $arguments)];
+ }
+ else {
+ $type = 'fieldset';
+ }
+ $form['widget_config'] = [
+ '#type' => $type,
+ '#tree' => TRUE,
+ '#title' => $this->t('%widget settings', $arguments),
+ '#attributes' => ['id' => 'facets-widget-config-form'],
+ ] + $config_form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ // Redirect to facets settings page if Field Identifier is not set.
+ if ($facets = \Drupal::routeMatch()->getParameter('facets_facet')) {
+ if ($facets->getFieldIdentifier() === NULL) {
+ $facet_settings_path = $facets->toUrl('settings-form')->toString();
+ $response = new RedirectResponse($facet_settings_path);
+ $response->send();
+ return [];
+ }
+ }
+ $form['#attached']['library'][] = 'facets/drupal.facets.admin_css';
+
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->entity;
+
+ $facet_sources = [];
+ foreach ($this->facetSourcePluginManager->getDefinitions() as $facet_source_id => $definition) {
+ $facet_sources[$definition['id']] = !empty($definition['label']) ? $definition['label'] : $facet_source_id;
+ }
+ if (isset($facet_sources[$facet->getFacetSourceId()])) {
+ $form['facet_source'] = [
+ '#type' => 'item',
+ '#title' => $this->t('Facet source'),
+ '#markup' => $facet_sources[$facet->getFacetSourceId()],
+ ];
+ }
+
+ $widget_options = [];
+ foreach ($this->widgetPluginManager->getDefinitions() as $widget_id => $definition) {
+ $widget_options[$widget_id] = !empty($definition['label']) ? $definition['label'] : $widget_id;
+ }
+
+ // Filters all the available widgets to make sure that only those that
+ // this facet applies for are enabled.
+ foreach ($widget_options as $widget_id => $label) {
+ $widget = $this->widgetPluginManager->createInstance($widget_id);
+ if (!$widget->supportsFacet($facet)) {
+ unset($widget_options[$widget_id]);
+ }
+ }
+ unset($widget_id, $label, $widget);
+
+ $widget = $facet->getWidgetInstance();
+ $form['widget'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Widget'),
+ '#description' => $this->t('The widget used for displaying this facet.'),
+ '#options' => $widget_options,
+ '#default_value' => $facet->getWidget()['type'],
+ '#required' => TRUE,
+ '#ajax' => [
+ 'trigger_as' => ['name' => 'widget_configure'],
+ 'callback' => '::buildAjaxWidgetConfigForm',
+ 'wrapper' => 'facets-widget-config-form',
+ 'method' => 'replace',
+ 'effect' => 'fade',
+ ],
+ ];
+ $form['widget_config'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'id' => 'facets-widget-config-form',
+ ],
+ '#tree' => TRUE,
+ ];
+ $form['widget_configure_button'] = [
+ '#type' => 'submit',
+ '#name' => 'widget_configure',
+ '#value' => $this->t('Configure widget'),
+ '#limit_validation_errors' => [['widget']],
+ '#submit' => ['::submitAjaxWidgetConfigForm'],
+ '#ajax' => [
+ 'callback' => '::buildAjaxWidgetConfigForm',
+ 'wrapper' => 'facets-widget-config-form',
+ ],
+ '#attributes' => ['class' => ['js-hide']],
+ ];
+ $this->buildWidgetConfigForm($form, $form_state);
+
+ // Retrieve lists of all processors, and the stages and weights they have.
+ if (!$form_state->has('processors')) {
+ $all_processors = $facet->getProcessors(FALSE);
+ $sort_processors = function (ProcessorInterface $a, ProcessorInterface $b) {
+ return strnatcasecmp((string) $a->getPluginDefinition()['label'], (string) $b->getPluginDefinition()['label']);
+ };
+ uasort($all_processors, $sort_processors);
+ }
+ else {
+ $all_processors = $form_state->get('processors');
+ }
+ $enabled_processors = $facet->getProcessors(TRUE);
+
+ // Filters all the available processors to make sure that only those that
+ // this facet applies for are enabled.
+ foreach ($all_processors as $processor_id => $processor) {
+ if (!$processor->supportsFacet($facet)) {
+ unset($all_processors[$processor_id]);
+ }
+ }
+ unset($processor_id, $processor);
+
+ $stages = $this->processorPluginManager->getProcessingStages();
+ $processors_by_stage = [];
+ foreach ($stages as $stage => $definition) {
+ foreach ($facet->getProcessorsByStage($stage, FALSE) as $processor_id => $processor) {
+ if ($processor->supportsFacet($facet)) {
+ $processors_by_stage[$stage][$processor_id] = $processor;
+ }
+ }
+ unset($processor_id, $processor);
+ }
+
+ $form['#tree'] = TRUE;
+ $form['#attached']['library'][] = 'facets/drupal.facets.index-active-formatters';
+ $form['#title'] = $this->t('Edit %label facet', ['%label' => $facet->label()]);
+
+ // Add the list of all other processors with checkboxes to enable/disable
+ // them.
+ $form['facet_settings'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Facet settings'),
+ '#attributes' => [
+ 'class' => [
+ 'search-api-status-wrapper',
+ ],
+ ],
+ ];
+
+ foreach ($all_processors as $processor_id => $processor) {
+ if (!($processor instanceof SortProcessorInterface) && !($processor instanceof UrlProcessorInterface)) {
+
+ $default_value = $processor->isLocked() || $widget->isPropertyRequired($processor_id, 'processors') || !empty($enabled_processors[$processor_id]);
+ $clean_css_id = Html::cleanCssIdentifier($processor_id);
+ $form['facet_settings'][$processor_id]['status'] = [
+ '#type' => 'checkbox',
+ '#title' => (string) $processor->getPluginDefinition()['label'],
+ '#default_value' => $default_value,
+ '#description' => $processor->getDescription(),
+ '#attributes' => [
+ 'class' => [
+ 'search-api-processor-status-' . $clean_css_id,
+ ],
+ 'data-id' => $clean_css_id,
+ ],
+ '#disabled' => $processor->isLocked() || $widget->isPropertyRequired($processor_id, 'processors'),
+ '#access' => !$processor->isHidden(),
+ ];
+
+ $form['facet_settings'][$processor_id]['settings'] = [];
+ $processor_form_state = SubformState::createForSubform($form['facet_settings'][$processor_id]['settings'], $form, $form_state);
+ $processor_form = $processor->buildConfigurationForm($form, $processor_form_state, $facet);
+ if ($processor_form) {
+ $form['facet_settings'][$processor_id]['settings'] = [
+ '#type' => 'details',
+ '#title' => $this->t('%processor settings', ['%processor' => (string) $processor->getPluginDefinition()['label']]),
+ '#open' => TRUE,
+ '#attributes' => [
+ 'class' => [
+ 'facets-processor-settings-' . Html::cleanCssIdentifier($processor_id),
+ 'facets-processor-settings-facet',
+ 'facets-processor-settings',
+ ],
+ ],
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[' . $processor_id . '][status]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['facet_settings'][$processor_id]['settings'] += $processor_form;
+ }
+ }
+ }
+ // Add the list of widget sort processors with checkboxes to enable/disable
+ // them.
+ $form['facet_sorting'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Facet sorting'),
+ '#attributes' => [
+ 'class' => [
+ 'search-api-status-wrapper',
+ ],
+ ],
+ ];
+ foreach ($all_processors as $processor_id => $processor) {
+ if ($processor instanceof SortProcessorInterface) {
+ $default_value = $processor->isLocked() || $widget->isPropertyRequired($processor_id, 'processors') || !empty($enabled_processors[$processor_id]);
+ $clean_css_id = Html::cleanCssIdentifier($processor_id);
+ $form['facet_sorting'][$processor_id]['status'] = [
+ '#type' => 'checkbox',
+ '#title' => (string) $processor->getPluginDefinition()['label'],
+ '#default_value' => $default_value,
+ '#description' => $processor->getDescription(),
+ '#attributes' => [
+ 'class' => [
+ 'search-api-processor-status-' . $clean_css_id,
+ ],
+ 'data-id' => $clean_css_id,
+ ],
+ '#disabled' => $processor->isLocked(),
+ '#access' => !$processor->isHidden(),
+ ];
+
+ $form['facet_sorting'][$processor_id]['settings'] = [];
+ $processor_form_state = SubformState::createForSubform($form['facet_sorting'][$processor_id]['settings'], $form, $form_state);
+ $processor_form = $processor->buildConfigurationForm($form, $processor_form_state, $facet);
+ if ($processor_form) {
+ $form['facet_sorting'][$processor_id]['settings'] = [
+ '#type' => 'container',
+ '#open' => TRUE,
+ '#attributes' => [
+ 'class' => [
+ 'facets-processor-settings-' . Html::cleanCssIdentifier($processor_id),
+ 'facets-processor-settings-sorting',
+ 'facets-processor-settings',
+ ],
+ ],
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_sorting[' . $processor_id . '][status]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['facet_sorting'][$processor_id]['settings'] += $processor_form;
+ }
+ }
+ }
+
+ $form['facet_settings']['only_visible_when_facet_source_is_visible'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Hide facet when facet source is not rendered'),
+ '#description' => $this->t('Only display the facet if the facet source is rendered. If you want to display the facets on other pages too, you need to uncheck this setting.'),
+ '#default_value' => $widget->isPropertyRequired('only_visible_when_facet_source_is_visible', 'settings') ?: $facet->getOnlyVisibleWhenFacetSourceIsVisible(),
+ '#disabled' => $widget->isPropertyRequired('only_visible_when_facet_source_is_visible', 'settings') ?: 0,
+ ];
+
+ $form['facet_settings']['show_only_one_result'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Ensure that only one result can be displayed'),
+ '#description' => $this->t('Check this to ensure that only one result at a time can be selected for this facet.'),
+ '#default_value' => $widget->isPropertyRequired('show_only_one_result', 'settings') ?: $facet->getShowOnlyOneResult(),
+ '#disabled' => $widget->isPropertyRequired('show_only_one_result', 'settings') ?: 0,
+ ];
+
+ $form['facet_settings']['url_alias'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('URL alias'),
+ '#description' => $this->t('The alias appears in the URL to identify this facet. It cannot be blank. Allowed are only letters, digits and the following characters: dot ("."), hyphen ("-"), underscore ("_"), and tilde ("~").'),
+ '#default_value' => $facet->getUrlAlias(),
+ '#maxlength' => 50,
+ '#required' => TRUE,
+ ];
+ $form['facet_settings']['show_title'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show title of facet'),
+ '#description' => $this->t('Show the title of the facet through a Twig template'),
+ '#default_value' => $facet->get('show_title'),
+ ];
+
+ $empty_behavior_config = $facet->getEmptyBehavior();
+ $form['facet_settings']['empty_behavior'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Empty facet behavior'),
+ '#default_value' => $empty_behavior_config['behavior'] ?: 'none',
+ '#options' => [
+ 'none' => $this->t('Do not display facet'),
+ 'text' => $this->t('Display text'),
+ ],
+ '#description' => $this->t('Take this action if a facet has no items.'),
+ '#required' => TRUE,
+ ];
+ $form['facet_settings']['empty_behavior_container'] = [
+ '#type' => 'container',
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[empty_behavior]"]' => ['value' => 'text'],
+ ],
+ ],
+ ];
+ $form['facet_settings']['empty_behavior_container']['empty_behavior_text'] = [
+ '#type' => 'text_format',
+ '#title' => $this->t('Empty text'),
+ '#format' => isset($empty_behavior_config['text_format']) ? $empty_behavior_config['text_format'] : 'plain_text',
+ '#editor' => TRUE,
+ '#default_value' => isset($empty_behavior_config['text_format']) ? $empty_behavior_config['text'] : '',
+ ];
+
+ $form['facet_settings']['query_operator'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Operator'),
+ '#options' => ['or' => $this->t('OR'), 'and' => $this->t('AND')],
+ '#description' => $this->t('AND filters are exclusive and narrow the result set. OR filters are inclusive and widen the result set.'),
+ '#default_value' => $facet->getQueryOperator(),
+ ];
+
+ $hard_limit_options = [3, 5, 10, 15, 20, 30, 40, 50, 75, 100, 250, 500];
+ $form['facet_settings']['hard_limit'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Hard limit'),
+ '#default_value' => $facet->getHardLimit(),
+ '#options' => [0 => $this->t('No limit')] + array_combine($hard_limit_options, $hard_limit_options),
+ '#description' => $this->t('Display no more than this number of facet items.'),
+ ];
+ if (!$facet->getFacetSource() instanceof SearchApiDisplay) {
+ $form['facet_settings']['hard_limit']['#disabled'] = TRUE;
+ $form['facet_settings']['hard_limit']['#description'] .= ' ';
+ $form['facet_settings']['hard_limit']['#description'] .= $this->t('This setting only works with Search API based facets.');
+ }
+
+ $form['facet_settings']['exclude'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Exclude'),
+ '#description' => $this->t('Exclude the selected facets from the search result instead of restricting it to them.'),
+ '#default_value' => $facet->getExclude(),
+ ];
+
+ $form['facet_settings']['use_hierarchy'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Use hierarchy'),
+ '#default_value' => $facet->getUseHierarchy(),
+ ];
+ if (!$facet->getFacetSource() instanceof SearchApiDisplay) {
+ $form['facet_settings']['use_hierarchy']['#disabled'] = TRUE;
+ $form['facet_settings']['use_hierarchy']['#description'] = $this->t('This setting only works with Search API based facets.');
+ }
+ else {
+ $processor_url = Url::fromRoute('entity.search_api_index.processors', [
+ 'search_api_index' => $facet->getFacetSource()->getIndex()->id(),
+ ]);
+ $description = $this->t('Renders the items using hierarchy. Make sure to enable the hierarchy processor on the Search api index processor configuration for this to work as expected. If disabled all items will be flattened.', [
+ ':processor-url' => $processor_url->toString(),
+ ]);
+ $form['facet_settings']['use_hierarchy']['#description'] = $description;
+
+ $hierarchy = $facet->getHierarchy();
+ $options = array_map(function (HierarchyPluginBase $plugin) {
+ return $plugin->getPluginDefinition()['label'];
+ }, $facet->getHierarchies());
+ $form['facet_settings']['hierarchy'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Hierarchy type'),
+ '#options' => $options,
+ '#default_value' => $hierarchy ? $hierarchy['type'] : '',
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[use_hierarchy]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ }
+
+ $form['facet_settings']['keep_hierarchy_parents_active'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Keep hierarchy parents active'),
+ '#description' => $this->t('Keep the parents active when selecting a child.'),
+ '#default_value' => $facet->getKeepHierarchyParentsActive(),
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[use_hierarchy]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['facet_settings']['expand_hierarchy'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Always expand hierarchy'),
+ '#description' => $this->t('Render entire tree, regardless of whether the parents are active or not.'),
+ '#default_value' => $facet->getExpandHierarchy(),
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[use_hierarchy]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['facet_settings']['enable_parent_when_child_gets_disabled'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Enable parent when child gets disabled'),
+ '#description' => $this->t('Uncheck this if you want to allow de-activating an entire hierarchical trail by clicking an active child.'),
+ '#default_value' => $facet->getEnableParentWhenChildGetsDisabled(),
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[use_hierarchy]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['facet_settings']['min_count'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Minimum count'),
+ '#default_value' => $facet->getMinCount(),
+ '#description' => $this->t('Only display the results if there is this minimum amount of results.'),
+ '#maxlength' => 4,
+ '#required' => TRUE,
+ ];
+ if (!$facet->getFacetSource() instanceof SearchApiDisplay) {
+ $form['facet_settings']['min_count']['#disabled'] = TRUE;
+ $form['facet_settings']['min_count']['#description'] .= ' ';
+ $form['facet_settings']['min_count']['#description'] .= $this->t('This setting only works with Search API based facets.');
+ }
+
+ $form['facet_settings']['weight'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Weight'),
+ '#default_value' => $facet->getWeight(),
+ '#description' => $this->t('This weight is used to determine the order of the facets in the URL if pretty paths are used.'),
+ '#maxlength' => 4,
+ '#required' => TRUE,
+ ];
+
+ $form['weights'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Advanced settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ ];
+
+ $form['weights']['order'] = [
+ '#markup' => $this->t('Processor order'),
+ '#prefix' => '',
+ '#suffix' => ' ',
+ ];
+
+ // Order enabled processors per stage, create all the containers for the
+ // different stages.
+ foreach ($stages as $stage => $description) {
+ $form['weights'][$stage] = [
+ '#type' => 'fieldset',
+ '#title' => $description['label'],
+ '#attributes' => [
+ 'class' => [
+ 'search-api-stage-wrapper',
+ 'search-api-stage-wrapper-' . Html::cleanCssIdentifier($stage),
+ ],
+ ],
+ ];
+ $form['weights'][$stage]['order'] = [
+ '#type' => 'table',
+ ];
+ $form['weights'][$stage]['order']['#tabledrag'][] = [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'search-api-processor-weight-' . Html::cleanCssIdentifier($stage),
+ ];
+ }
+
+ $processor_settings = $facet->getProcessorConfigs();
+
+ // Fill in the containers previously created with the processors that are
+ // enabled on the facet.
+ foreach ($processors_by_stage as $stage => $processors) {
+ /** @var \Drupal\facets\Processor\ProcessorInterface $processor */
+ foreach ($processors as $processor_id => $processor) {
+ $weight = isset($processor_settings[$processor_id]['weights'][$stage])
+ ? $processor_settings[$processor_id]['weights'][$stage]
+ : $processor->getDefaultWeight($stage);
+ if ($processor->isHidden()) {
+ $form['processors'][$processor_id]['weights'][$stage] = [
+ '#type' => 'value',
+ '#value' => $weight,
+ ];
+ continue;
+ }
+ $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'draggable';
+ $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'search-api-processor-weight--' . Html::cleanCssIdentifier($processor_id);
+ $form['weights'][$stage]['order'][$processor_id]['#weight'] = $weight;
+ $form['weights'][$stage]['order'][$processor_id]['label']['#plain_text'] = (string) $processor->getPluginDefinition()['label'];
+ $form['weights'][$stage]['order'][$processor_id]['weight'] = [
+ '#type' => 'weight',
+ '#title' => $this->t('Weight for processor %title', ['%title' => (string) $processor->getPluginDefinition()['label']]),
+ '#title_display' => 'invisible',
+ '#default_value' => $weight,
+ '#parents' => ['processors', $processor_id, 'weights', $stage],
+ '#attributes' => [
+ 'class' => [
+ 'search-api-processor-weight-' . Html::cleanCssIdentifier($stage),
+ ],
+ ],
+ ];
+ }
+ }
+
+ // Add vertical tabs containing the settings for the processors. Tabs for
+ // disabled processors are hidden with JS magic, but need to be included in
+ // case the processor is enabled.
+ $form['processor_settings'] = [
+ '#title' => $this->t('Processor settings'),
+ '#type' => 'vertical_tabs',
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->entity;
+
+ $values = $form_state->getValues();
+ /** @var \Drupal\facets\Processor\ProcessorInterface[] $processors */
+ $processors = $facet->getProcessors(FALSE);
+
+ // Iterate over all processors that have a form and are enabled.
+ foreach ($form['facet_settings'] as $processor_id => $processor_form) {
+ if (!empty($values['processors'][$processor_id])) {
+
+ $processor_form_state = SubformState::createForSubform($form['facet_settings'][$processor_id]['settings'], $form, $form_state);
+ $processors[$processor_id]->validateConfigurationForm($form['facet_settings'][$processor_id], $processor_form_state, $facet);
+ }
+ }
+ // Iterate over all sorting processors that have a form and are enabled.
+ foreach ($form['facet_sorting'] as $processor_id => $processor_form) {
+ if (!empty($values['processors'][$processor_id])) {
+
+ $processor_form_state = SubformState::createForSubform($form['facet_sorting'][$processor_id]['settings'], $form, $form_state);
+ $processors[$processor_id]->validateConfigurationForm($form['facet_sorting'][$processor_id], $processor_form_state, $facet);
+ }
+ }
+
+ // Only widgets that return an array can work with rest facet sources, so if
+ // the user has selected another widget, we should point them to their
+ // misconfiguration.
+ if ($facet_source = $facet->getFacetSource()) {
+ if ($facet_source instanceof SearchApiFacetSourceInterface) {
+ if ($facet_source->getDisplay() instanceof ViewsRest) {
+ if (strpos($values['widget'], 'array') === FALSE) {
+ $form_state->setErrorByName('widget', $this->t('The Facet source is a Rest export. Please select a raw widget.'));
+ }
+ }
+ }
+ }
+
+ // Validate url alias.
+ $url_alias = $form_state->getValue(['facet_settings', 'url_alias']);
+ if ($url_alias == 'page') {
+ $form_state->setErrorByName('url_alias', $this->t('This URL alias is not allowed.'));
+ }
+ elseif (preg_match('/[^a-zA-Z0-9_~\.\-]/', $url_alias)) {
+ $form_state->setErrorByName('url_alias', $this->t('The URL alias contains characters that are not allowed.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+
+ // Store processor settings.
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->entity;
+
+ /** @var \Drupal\facets\Processor\ProcessorInterface $processor */
+ $processors = $facet->getProcessors(FALSE);
+ foreach ($processors as $processor_id => $processor) {
+ $form_container_key = $processor instanceof SortProcessorInterface ? 'facet_sorting' : 'facet_settings';
+ if (empty($values[$form_container_key][$processor_id]['status'])) {
+ $facet->removeProcessor($processor_id);
+ continue;
+ }
+ $new_settings = [
+ 'processor_id' => $processor_id,
+ 'weights' => [],
+ 'settings' => [],
+ ];
+ if (!empty($values['processors'][$processor_id]['weights'])) {
+ $new_settings['weights'] = $values['processors'][$processor_id]['weights'];
+ }
+ if (isset($form[$form_container_key][$processor_id]['settings'])) {
+ $processor_form_state = SubformState::createForSubform($form[$form_container_key][$processor_id]['settings'], $form, $form_state);
+ $processor->submitConfigurationForm($form[$form_container_key][$processor_id]['settings'], $processor_form_state, $facet);
+ $new_settings['settings'] = $processor->getConfiguration();
+ }
+ $facet->addProcessor($new_settings);
+ }
+
+ $facet->setWidget($form_state->getValue('widget'), $form_state->getValue('widget_config'));
+ $facet->setUrlAlias($form_state->getValue(['facet_settings', 'url_alias']));
+ $facet->setWeight((int) $form_state->getValue(['facet_settings', 'weight']));
+ $facet->setMinCount((int) $form_state->getValue(
+ [
+ 'facet_settings',
+ 'min_count',
+ ]
+ ));
+ $facet->setOnlyVisibleWhenFacetSourceIsVisible($form_state->getValue(
+ [
+ 'facet_settings',
+ 'only_visible_when_facet_source_is_visible',
+ ]
+ ));
+ $facet->setShowOnlyOneResult($form_state->getValue(
+ [
+ 'facet_settings',
+ 'show_only_one_result',
+ ]
+ ));
+
+ $empty_behavior_config = [];
+ $empty_behavior = $form_state->getValue(['facet_settings', 'empty_behavior']);
+ $empty_behavior_config['behavior'] = $empty_behavior;
+ if ($empty_behavior == 'text') {
+ $empty_behavior_config['text_format'] = $form_state->getValue([
+ 'facet_settings',
+ 'empty_behavior_container',
+ 'empty_behavior_text',
+ 'format',
+ ]);
+ $empty_behavior_config['text'] = $form_state->getValue([
+ 'facet_settings',
+ 'empty_behavior_container',
+ 'empty_behavior_text',
+ 'value',
+ ]);
+ }
+ $facet->setEmptyBehavior($empty_behavior_config);
+
+ $facet->setQueryOperator($form_state->getValue(
+ [
+ 'facet_settings',
+ 'query_operator',
+ ]
+ ));
+
+ $facet->setHardLimit($form_state->getValue(['facet_settings', 'hard_limit']));
+
+ $facet->setExclude($form_state->getValue(['facet_settings', 'exclude']));
+ $facet->setUseHierarchy($form_state->getValue(
+ [
+ 'facet_settings',
+ 'use_hierarchy',
+ ]
+ ));
+ $facet->setKeepHierarchyParentsActive($form_state->getValue(
+ [
+ 'facet_settings',
+ 'keep_hierarchy_parents_active',
+ ]
+ ));
+ $hierarchy_id = $form_state->getValue(['facet_settings', 'hierarchy']);
+ $facet->setHierarchy($hierarchy_id, $form_state->getValue(
+ [
+ 'facet_settings',
+ $hierarchy_id,
+ ]
+ ));
+ $facet->setExpandHierarchy($form_state->getValue(
+ [
+ 'facet_settings',
+ 'expand_hierarchy',
+ ]
+ ));
+ $facet->setEnableParentWhenChildGetsDisabled($form_state->getValue(
+ [
+ 'facet_settings',
+ 'enable_parent_when_child_gets_disabled',
+ ]
+ ));
+ $facet->set('show_title', $form_state->getValue(
+ [
+ 'facet_settings',
+ 'show_title',
+ ],
+ FALSE
+ ));
+
+ $facet->save();
+
+ $already_enabled_facets_on_same_source = $this->facetsManager->getFacetsByFacetSourceId($facet->getFacetSourceId());
+ foreach ($already_enabled_facets_on_same_source as $other) {
+ if ($other->getUrlAlias() === $facet->getUrlAlias() && $other->id() !== $facet->id()) {
+ $this->messenger()->addWarning($this->t('This alias is already in use for another facet defined on the same source.'));
+ }
+ }
+
+ $this->messenger()->addMessage($this->t('Facet %name has been updated.', ['%name' => $facet->getName()]));
+ }
+
+ /**
+ * Handles form submissions for the widget subform.
+ */
+ public function submitAjaxWidgetConfigForm($form, FormStateInterface $form_state) {
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Handles changes to the selected widgets.
+ */
+ public function buildAjaxWidgetConfigForm(array $form, FormStateInterface $form_state) {
+ return $form['widget_config'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions = parent::actions($form, $form_state);
+
+ // We don't have a "delete" action here.
+ unset($actions['delete']);
+
+ return $actions;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetSettingsForm.php b/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetSettingsForm.php
new file mode 100644
index 000000000..300014737
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetSettingsForm.php
@@ -0,0 +1,317 @@
+facetSourcePluginManager = $facet_source_plugin_manager;
+ $this->processorPluginManager = $processor_plugin_manager;
+ $this->moduleHandler = $module_handler;
+ $this->urlGenerator = $url_generator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.facets.facet_source'),
+ $container->get('plugin.manager.facets.processor'),
+ $container->get('module_handler'),
+ $container->get('url_generator')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ // If the form is being rebuilt, rebuild the entity with the current form
+ // values.
+ if ($form_state->isRebuilding()) {
+ $this->entity = $this->buildEntity($form, $form_state);
+ }
+
+ $form = parent::form($form, $form_state);
+
+ // Set the page title according to whether we are creating or editing the
+ // facet.
+ if ($this->getEntity()->isNew()) {
+ $form['#title'] = $this->t('Add facet');
+ }
+ else {
+ $form['#title'] = $this->t('Facet settings for %label facet', ['%label' => $this->getEntity()->label()]);
+ }
+
+ $this->buildEntityForm($form, $form_state, $this->getEntity());
+
+ $form['#attached']['library'][] = 'facets/drupal.facets.edit-facet';
+
+ return $form;
+ }
+
+ /**
+ * Builds the form for editing and creating a facet.
+ *
+ * @param array $form
+ * The form array for the complete form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facets facet entity that is being created or edited.
+ */
+ public function buildEntityForm(array &$form, FormStateInterface $form_state, FacetInterface $facet) {
+
+ $facet_sources = [];
+ foreach ($this->facetSourcePluginManager->getDefinitions() as $facet_source_id => $definition) {
+ $facet_sources[$definition['id']] = !empty($definition['label']) ? $definition['label'] : $facet_source_id;
+ }
+
+ if (count($facet_sources) == 0) {
+ $form['#markup'] = $this->t('You currently have no facet sources defined. You should start by adding a facet source before creating facets.');
+ return;
+ }
+
+ $form['facet_source_id'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Facet source'),
+ '#description' => $this->t('The source where this facet can find its fields.'),
+ '#options' => $facet_sources,
+ '#default_value' => $facet->getFacetSourceId(),
+ '#required' => TRUE,
+ '#ajax' => [
+ 'trigger_as' => ['name' => 'facet_source_configure'],
+ 'callback' => '::buildAjaxFacetSourceConfigForm',
+ 'wrapper' => 'facets-facet-sources-config-form',
+ 'method' => 'replace',
+ 'effect' => 'fade',
+ ],
+ ];
+ $form['facet_source_configs'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'id' => 'facets-facet-sources-config-form',
+ ],
+ '#tree' => TRUE,
+ ];
+ $form['facet_source_configure_button'] = [
+ '#type' => 'submit',
+ '#name' => 'facet_source_configure',
+ '#value' => $this->t('Configure facet source'),
+ '#limit_validation_errors' => [['facet_source_id']],
+ '#submit' => ['::submitAjaxFacetSourceConfigForm'],
+ '#ajax' => [
+ 'callback' => '::buildAjaxFacetSourceConfigForm',
+ 'wrapper' => 'facets-facet-sources-config-form',
+ ],
+ '#attributes' => ['class' => ['js-hide']],
+ ];
+ $this->buildFacetSourceConfigForm($form, $form_state);
+
+ $form['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Name'),
+ '#description' => $this->t('The administrative name used for this facet.'),
+ '#default_value' => $facet->label(),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $facet->id(),
+ '#maxlength' => 50,
+ '#required' => TRUE,
+ '#machine_name' => [
+ 'exists' => [Facet::class, 'load'],
+ 'source' => ['name'],
+ ],
+ '#disabled' => !$facet->isNew(),
+ ];
+ }
+
+ /**
+ * Handles form submissions for the facet source subform.
+ */
+ public function submitAjaxFacetSourceConfigForm($form, FormStateInterface $form_state) {
+ $form_state->setValue('id', NULL);
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Handles changes to the selected facet sources.
+ */
+ public function buildAjaxFacetSourceConfigForm(array $form, FormStateInterface $form_state) {
+ return $form['facet_source_configs'];
+ }
+
+ /**
+ * Builds the configuration forms for all possible facet sources.
+ *
+ * @param array $form
+ * An associative array containing the initial structure of the plugin form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the complete form.
+ */
+ public function buildFacetSourceConfigForm(array &$form, FormStateInterface $form_state) {
+ $facet_source_id = $this->getEntity()->getFacetSourceId();
+
+ if (!is_null($facet_source_id) && $facet_source_id !== '') {
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
+ $facet_source = $this->facetSourcePluginManager->createInstance($facet_source_id, ['facet' => $this->getEntity()]);
+
+ if ($config_form = $facet_source->buildConfigurationForm([], $form_state)) {
+ $form['facet_source_configs'][$facet_source_id]['#type'] = 'container';
+ $form['facet_source_configs'][$facet_source_id]['#attributes'] = ['class' => ['facet-source-field-wrapper']];
+ $form['facet_source_configs'][$facet_source_id]['#title'] = $this->t('%plugin settings', ['%plugin' => $facet_source->getPluginDefinition()['label']]);
+ $form['facet_source_configs'][$facet_source_id] += $config_form;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+
+ $facet_source_id = $form_state->getValue('facet_source_id');
+ if (!is_null($facet_source_id) && $facet_source_id !== '') {
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
+ $facet_source = $this->facetSourcePluginManager->createInstance($facet_source_id, ['facet' => $this->getEntity()]);
+ $facet_source->validateConfigurationForm($form, $form_state);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm($form, $form_state);
+
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->getEntity();
+ $is_new = $facet->isNew();
+ if ($is_new) {
+ // On facet creation, enable all locked processors by default, using their
+ // default settings.
+ $stages = $this->processorPluginManager->getProcessingStages();
+ $processors_definitions = $this->processorPluginManager->getDefinitions();
+
+ foreach ($processors_definitions as $processor_id => $processor) {
+ $is_locked = isset($processor['locked']) && $processor['locked'] == TRUE;
+ $is_default_enabled = isset($processor['default_enabled']) && $processor['default_enabled'] == TRUE;
+ if ($is_locked || $is_default_enabled) {
+ $weights = [];
+ foreach ($stages as $stage_id => $stage) {
+ if (isset($processor['stages'][$stage_id])) {
+ $weights[$stage_id] = $processor['stages'][$stage_id];
+ }
+ }
+ $facet->addProcessor([
+ 'processor_id' => $processor_id,
+ 'weights' => $weights,
+ 'settings' => [],
+ ]);
+ }
+ }
+
+ // Set a default widget for new facets.
+ $facet->setWidget('links');
+ $facet->setUrlAlias($form_state->getValue('id'));
+ $facet->setWeight(0);
+
+ // Set default empty behaviour.
+ $facet->setEmptyBehavior(['behavior' => 'none']);
+ $facet->setOnlyVisibleWhenFacetSourceIsVisible(TRUE);
+ }
+
+ $facet_source_id = $form_state->getValue('facet_source_id');
+ if (!is_null($facet_source_id) && $facet_source_id !== '') {
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
+ $facet_source = $this->facetSourcePluginManager->createInstance($facet_source_id, ['facet' => $this->getEntity()]);
+ $facet_source->submitConfigurationForm($form, $form_state);
+ }
+ $facet->save();
+
+ if ($is_new) {
+ if ($this->moduleHandler->moduleExists('block')) {
+ $message = $this->t(
+ 'Facet %name has been created. Go to the Block overview page to place the new block in the desired region.',
+ [
+ '%name' => $facet->getName(),
+ ':block_overview' => $this->urlGenerator->generateFromRoute('block.admin_display'),
+ ]
+ );
+ $this->messenger()->addMessage($message);
+ $form_state->setRedirect('entity.facets_facet.edit_form', ['facets_facet' => $facet->id()]);
+ }
+ }
+ else {
+ $this->messenger()->addMessage($this->t('Facet %name has been updated.', ['%name' => $facet->getName()]));
+ }
+
+ list($type,) = explode(':', $facet_source_id);
+ if ($type !== 'search_api') {
+ return $facet;
+ }
+
+ // Ensure that the caching of the view display is disabled, so the search
+ // correctly returns the facets.
+ if (isset($facet_source) && $facet_source instanceof SearchApiFacetSourceInterface) {
+ $view = $facet_source->getViewsDisplay();
+ if ($view !== NULL) {
+ if ($view->display_handler instanceof Block) {
+ $facet->setOnlyVisibleWhenFacetSourceIsVisible(FALSE);
+ }
+ $view->display_handler->overrideOption('cache', ['type' => 'none']);
+ $view->save();
+ $this->messenger()->addMessage($this->t('Caching of view %view has been disabled.', ['%view' => $view->storage->label()]));
+ }
+ }
+
+ return $facet;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetSourceEditForm.php b/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetSourceEditForm.php
new file mode 100644
index 000000000..93fa4cda5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Form/FacetSourceEditForm.php
@@ -0,0 +1,142 @@
+get('entity_type.manager'),
+ $container->get('plugin.manager.facets.url_processor'),
+ $container->get('module_handler')
+ );
+ }
+
+ /**
+ * Constructs a FacetSourceEditForm.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\facets\UrlProcessor\UrlProcessorPluginManager $url_processor_plugin_manager
+ * The url processor plugin manager.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+ * Drupal's module handler.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, UrlProcessorPluginManager $url_processor_plugin_manager, ModuleHandlerInterface $moduleHandler) {
+ $this->urlProcessorPluginManager = $url_processor_plugin_manager;
+ $this->setEntityTypeManager($entity_type_manager);
+ $this->setModuleHandler($moduleHandler);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'facet_source_edit_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+
+ /** @var \Drupal\facets\FacetSourceInterface $facet_source */
+ $facet_source = $this->getEntity();
+
+ $form['#tree'] = TRUE;
+ $form['filter_key'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Filter key'),
+ '#size' => 20,
+ '#maxlength' => 255,
+ '#default_value' => $facet_source->getFilterKey(),
+ '#description' => $this->t(
+ 'The key used in the url to identify the facet source.
+ When using multiple facet sources you should make sure each facet source has a different filter key.'
+ ),
+ ];
+
+ $url_processors = [];
+ $url_processors_description = [];
+ foreach ($this->urlProcessorPluginManager->getDefinitions() as $definition) {
+ $url_processors[$definition['id']] = $definition['label'];
+ $url_processors_description[] = $definition['description'];
+ }
+ $form['url_processor'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('URL Processor'),
+ '#options' => $url_processors,
+ '#default_value' => $facet_source->getUrlProcessorName(),
+ '#description' => $this->t(
+ 'The URL Processor defines the url structure used for this facet source.') . ' - ' . implode(' - ', $url_processors_description),
+ ];
+
+ $breadcrumb_settings = $facet_source->getBreadcrumbSettings();
+ $form['breadcrumb'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Breadcrumb'),
+ ];
+ $form['breadcrumb']['active'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Append active facets to breadcrumb'),
+ '#default_value' => isset($breadcrumb_settings['active']) ? $breadcrumb_settings['active'] : FALSE,
+ ];
+ $form['breadcrumb']['before'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show facet label before active facet'),
+ '#default_value' => isset($breadcrumb_settings['before']) ? $breadcrumb_settings['before'] : TRUE,
+ '#states' => [
+ 'visible' => [
+ ':input[name="breadcrumb[active]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['breadcrumb']['group'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Group active items under same crumb (not implemented yet - now always grouping)'),
+ '#default_value' => isset($breadcrumb_settings['group']) ? $breadcrumb_settings['group'] : FALSE,
+ '#states' => [
+ 'visible' => [
+ ':input[name="breadcrumb[active]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ // The parent's form build method will add a save button.
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm($form, $form_state);
+ $facet_source = $this->getEntity();
+ $this->messenger()->addMessage($this->t('Facet source %name has been saved.', ['%name' => $facet_source->label()]));
+ $form_state->setRedirect('entity.facets_facet.collection');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Hierarchy/HierarchyInterface.php b/frontend/drupal9/web/modules/contrib/facets/src/Hierarchy/HierarchyInterface.php
new file mode 100644
index 000000000..95fb866fb
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Hierarchy/HierarchyInterface.php
@@ -0,0 +1,60 @@
+get('request_stack');
+ // Support 9.3+.
+ // @todo remove switch after 9.3 or greater is required.
+ $request = version_compare(\Drupal::VERSION, '9.3', '>=') ? $request_stack->getMainRequest() : $request_stack->getMasterRequest();
+
+ return new static($configuration, $plugin_id, $plugin_definition, $request);
+ }
+
+ /**
+ * Provide a default implementation for backward compatibility.
+ *
+ * {@inheritdoc}
+ */
+ public function getSiblingIds(array $ids, array $activeIds = [], bool $parentSiblings = TRUE) {
+ return [];
+ }
+
+ /**
+ * Set the default values for the configuration form.
+ *
+ * @param array $form
+ * The configuration form.
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet entity.
+ */
+ protected function setConfigurationFormDefaultValues(array &$form, FacetInterface $facet) {
+ if ($this->getPluginId() === $facet->getHierarchy()['type']) {
+ foreach ($form as $key => $form_item) {
+ if (isset($facet->getHierarchy()['config'][$key])) {
+ $form[$key]['#default_value'] = $facet->getHierarchy()['config'][$key];
+ }
+ elseif (isset($this->defaultConfiguration()[$key])) {
+ $form[$key]['#default_value'] = $this->defaultConfiguration()[$key];
+ }
+ }
+ }
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Hierarchy/HierarchyPluginManager.php b/frontend/drupal9/web/modules/contrib/facets/src/Hierarchy/HierarchyPluginManager.php
new file mode 100644
index 000000000..005c1464b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Hierarchy/HierarchyPluginManager.php
@@ -0,0 +1,30 @@
+setCacheBackend($cache_backend, 'facets_hierarchy');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/LanguageSwitcherLinksAlterer.php b/frontend/drupal9/web/modules/contrib/facets/src/LanguageSwitcherLinksAlterer.php
new file mode 100644
index 000000000..690c74434
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/LanguageSwitcherLinksAlterer.php
@@ -0,0 +1,188 @@
+languageManager = $languageManager;
+ $this->cacheBackend = $cacheBackend;
+ $this->entityTypeManager = $entityTypeManager;
+ $this->urlProcessorManager = $urlProcessorManager;
+ }
+
+ /**
+ * Alters the language switcher links.
+ *
+ * @param array $links
+ * The links.
+ * @param string $type
+ * The language type.
+ * @param \Drupal\Core\Url $url
+ * The URL the switch links will be relative to.
+ *
+ * @see facets_language_switch_links_alter()
+ */
+ public function alter(array &$links, string $type, Url $url) {
+ if (!$this->data) {
+ $this->initializeData();
+ }
+
+ $current_language = $this->languageManager->getCurrentLanguage();
+
+ foreach ($links as &$link) {
+ if (empty($link['language']) || !($link['language'] instanceof LanguageInterface) || $link['language']->getId() === $current_language->getId()) {
+ continue;
+ }
+
+ foreach ($this->data as $facet_info) {
+ $filter_key = $facet_info['filter_key'];
+ $separator = $facet_info['separator'];
+ $url_aliases = $facet_info['url_aliases'];
+ $original_language = $url_aliases['original'];
+
+ if (!isset($link['query'][$filter_key]) || !is_array($link['query'][$filter_key])) {
+ continue;
+ }
+
+ $untranslated_alias = $url_aliases[$this->languageManager->getCurrentLanguage()->getId()];
+ $translated_alias = $url_aliases[$link['language']->getId()];
+
+ // If we don't have a translated alias, that means we're trying to
+ // create a link to the original language.
+ if ($translated_alias === NULL) {
+ $translated_alias = $url_aliases[$original_language];
+ }
+ // If we don't have an untranslated alias, we're trying to create a link
+ // from the original language.
+ if ($untranslated_alias === NULL) {
+ $untranslated_alias = $url_aliases[$original_language];
+ }
+
+ foreach ($link['query'][$filter_key] as &$filters) {
+ $filters = preg_replace(
+ '/(' . $untranslated_alias . ")$separator/",
+ $translated_alias . $separator,
+ $filters
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Initializes the data needed for altering the language switcher links.
+ *
+ * It runs through all the facets on the site and all the languages and
+ * creates a cache of the URL aliases for all the languages.
+ */
+ protected function initializeData() {
+ $cache = $this->cacheBackend->get('facets_language_switcher_links');
+ if ($cache) {
+ $this->data = $cache->data;
+ return;
+ }
+
+ $data = [];
+
+ /** @var \Drupal\facets\FacetInterface[] $facets */
+ $facets = $this->entityTypeManager->getStorage('facets_facet')->loadMultipleOverrideFree();
+
+ $cache_tags = [];
+ foreach ($facets as $facet) {
+ $cache_tags = Cache::mergeTags($cache_tags, $facet->getCacheTags());
+
+ /** @var \Drupal\facets\UrlProcessor\UrlProcessorInterface $urlProcessor */
+ $id = $facet->getFacetSourceConfig()->getUrlProcessorName();
+ $url_processor = $this->urlProcessorManager->createInstance($id, ['facet' => $facet]);
+
+ if (!isset($data[$facet->id()])) {
+ $data[$facet->id()] = [
+ 'separator' => $url_processor->getSeparator(),
+ 'filter_key' => $facet->getFacetSourceConfig()->getFilterKey(),
+ 'url_aliases' => [
+ 'original' => $facet->language()->getId(),
+ $facet->language()->getId() => $facet->getUrlAlias(),
+ ],
+ ];
+ }
+
+ foreach ($this->languageManager->getLanguages() as $language) {
+ if ($language->getId() === $facet->language()->getId()) {
+ // Skip the original facet language as it's covered above.
+ continue;
+ }
+
+ $config_name = 'facets.facet.' . $facet->id();
+ $translated_alias = $this->languageManager->getLanguageConfigOverride($language->getId(), $config_name)->get('url_alias');
+ $data[$facet->id()]['url_aliases'][$language->getId()] = $translated_alias;
+ }
+ }
+
+ $this->data = $data;
+ $this->cacheBackend->set('facets_language_switcher_links', $data, Cache::PERMANENT, $cache_tags);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/Block/FacetBlock.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/Block/FacetBlock.php
new file mode 100644
index 000000000..ac3b07655
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/Block/FacetBlock.php
@@ -0,0 +1,188 @@
+facetManager = $facet_manager;
+ $this->facetStorage = $facet_storage;
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('facets.manager'),
+ $container->get('entity_type.manager')->getStorage('facets_facet')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->facetStorage->load($this->getDerivativeId());
+
+ // No need to build the facet if it does not need to be visible.
+ if ($facet->getOnlyVisibleWhenFacetSourceIsVisible() &&
+ (!$facet->getFacetSource() || !$facet->getFacetSource()->isRenderedInCurrentRequest())) {
+ return [];
+ }
+
+ // Do not build the facet if the block is being previewed.
+ if ($this->getContextValue('in_preview')) {
+ return [];
+ }
+
+ // Let the facet_manager build the facets.
+ $build = $this->facetManager->build($facet);
+
+ if (!empty($build)) {
+ // Add extra elements from facet source, for example, ajax scripts.
+ // @see Drupal\facets\Plugin\facets\facet_source\SearchApiDisplay
+ /** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
+ $facet_source = $facet->getFacetSource();
+ $build += $facet_source->buildFacet();
+
+ // Add contextual links only when we have results.
+ $build['#contextual_links']['facets_facet'] = [
+ 'route_parameters' => ['facets_facet' => $facet->id()],
+ ];
+
+ if (!empty($build[0]['#attributes']['class']) && in_array('facet-active', $build[0]['#attributes']['class'], TRUE)) {
+ $build['#attributes']['class'][] = 'facet-active';
+ }
+ else {
+ $build['#attributes']['class'][] = 'facet-inactive';
+ }
+
+ // Add classes needed for ajax.
+ if (!empty($build['#use_ajax'])) {
+ $build['#attributes']['class'][] = 'block-facets-ajax';
+ // The configuration block id isn't always set in the configuration.
+ if (isset($this->configuration['block_id'])) {
+ $build['#attributes']['class'][] = 'js-facet-block-id-' . $this->configuration['block_id'];
+ }
+ else {
+ $build['#attributes']['class'][] = 'js-facet-block-id-' . $this->pluginId;
+ }
+ }
+ }
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheMaxAge() {
+ // A facet block cannot be cached, because it must always match the current
+ // search results, and Search API gets those search results from a data
+ // source that can be external to Drupal. Therefore it is impossible to
+ // guarantee that the search results are in sync with the data managed by
+ // Drupal. Consequently, it is not possible to cache the search results at
+ // all. If the search results cannot be cached, then neither can the facets,
+ // because they must always match.
+ // Fortunately, facet blocks are rendered using a lazy builder (like all
+ // blocks in Drupal), which means their rendering can be deferred (unlike
+ // the search results, which are the main content of the page, and deferring
+ // their rendering would mean sending an empty page to the user). This means
+ // that facet blocks can be rendered and sent *after* the initial page was
+ // loaded, by installing the BigPipe (big_pipe) module.
+ //
+ // When BigPipe is enabled, the search results will appear first, and then
+ // each facet block will appear one-by-one, in DOM order.
+ // See https://www.drupal.org/project/big_pipe.
+ //
+ // In a future version of Facet API, this could be refined, but due to the
+ // reliance on external data sources, it will be very difficult if not
+ // impossible to improve this significantly.
+ //
+ // Note: when using Drupal core's Search module instead of the contributed
+ // Search API module, the above limitations do not apply, but for now it is
+ // not considered worth the effort to optimize this just for Drupal core's
+ // Search.
+ return 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $this->facetStorage->load($this->getDerivativeId());
+
+ return ['config' => [$facet->getConfigDependencyName()]];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockSubmit($form, FormStateInterface $form_state) {
+ // Checks for a valid form id. Panelizer does not generate one.
+ if (isset($form['id']['#value'])) {
+ // Save block id to configuration, we do this for loading the original
+ // block with ajax.
+ $block_id = $form['id']['#value'];
+ $this->configuration['block_id'] = $block_id;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPreviewFallbackString() {
+ return $this->t('Placeholder for the "@facet" facet', ['@facet' => $this->getDerivativeId()]);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/Block/FacetBlockDeriver.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/Block/FacetBlockDeriver.php
new file mode 100644
index 000000000..5c5205444
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/Block/FacetBlockDeriver.php
@@ -0,0 +1,81 @@
+facetStorage = $container->get('entity_type.manager')->getStorage('facets_facet');
+
+ return $deriver;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
+ $derivatives = $this->getDerivativeDefinitions($base_plugin_definition);
+ return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ $base_plugin_id = $base_plugin_definition['id'];
+
+ if (!isset($this->derivatives[$base_plugin_id])) {
+ $plugin_derivatives = [];
+
+ /** @var \Drupal\facets\FacetInterface[] $all_facets */
+ $all_facets = $this->facetStorage->loadMultiple();
+
+ foreach ($all_facets as $facet) {
+ $machine_name = $facet->id();
+
+ $plugin_derivatives[$machine_name] = [
+ 'id' => $base_plugin_id . PluginBase::DERIVATIVE_SEPARATOR . $machine_name,
+ 'label' => $this->t('Facet: :facet', [':facet' => $facet->getName()]),
+ 'admin_label' => $facet->getName(),
+ 'description' => $this->t('Facet'),
+ 'context_definitions' => [
+ 'in_preview' => new ContextDefinition('string', $this->t('In preview'), FALSE),
+ ],
+ ] + $base_plugin_definition;
+ }
+
+ $this->derivatives[$base_plugin_id] = $plugin_derivatives;
+ }
+ return $this->derivatives[$base_plugin_id];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiBaseFacetSource.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiBaseFacetSource.php
new file mode 100644
index 000000000..4ed09b282
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiBaseFacetSource.php
@@ -0,0 +1,210 @@
+pluginDefinition = $plugin_definition;
+ $this->pluginId = $plugin_id;
+ $this->configuration = $configuration;
+ $this->searchApiQueryHelper = $search_results_cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('plugin.manager.facets.query_type'),
+ $container->get('search_api.query_helper')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndex() {
+ @trigger_error('Relying on $this->index is deprecated in facets:8.x-1.5 and will be removed from facets:8.x-2.0. Instead, all subclasses should implement ::getIndex() themselves, and the blanket implementation will be removed from SearchApiBaseFacetSource. See https://www.drupal.org/node/3154173', E_USER_DEPRECATED);
+ return $this->index;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDisplay() {
+ return $this->getPluginDefinition()['display_id'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getViewsDisplay() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+
+ $form['field_identifier'] = [
+ '#type' => 'select',
+ '#options' => $this->getFields(),
+ '#title' => $this->t('Field'),
+ '#description' => $this->t('The field from the selected facet source which contains the data to build a facet for. The field types supported are boolean , date , decimal , integer and string .'),
+ '#required' => TRUE,
+ '#default_value' => $this->facet->getFieldIdentifier(),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFields() {
+ $indexed_fields = [];
+ $fields = $this->getIndex()->getFields();
+ // Get the Search API Server.
+ $server = $this->getIndex()->getServerInstance();
+ // Get the Search API Backend.
+ $backend = $server->getBackend();
+ foreach ($fields as $field) {
+ $query_types = $this->getQueryTypesForDataType($backend, $field->getDataTypePlugin()->getPluginId());
+ if (!empty($query_types)) {
+ $indexed_fields[$field->getFieldIdentifier()] = $field->getLabel() . ' (' . $field->getPropertyPath() . ')';
+ }
+ }
+ return $indexed_fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryTypesForFacet(FacetInterface $facet) {
+ // Get our Facets Field Identifier, which is equal to the Search API Field
+ // identifier.
+ $field_id = $facet->getFieldIdentifier();
+ // Get the Search API Server.
+ $server = $this->getIndex()->getServerInstance();
+ // Get the Search API Backend.
+ $backend = $server->getBackend();
+
+ $fields = $this->getIndex()->getFields();
+ if (isset($fields[$field_id])) {
+ return $this->getQueryTypesForDataType($backend, $fields[$field_id]->getType());
+ }
+
+ throw new InvalidQueryTypeException("No available query types were found for facet {$facet->getName()}");
+ }
+
+ /**
+ * Retrieves the query types for a specified data type.
+ *
+ * Backend plugins can use this method to override the default query types
+ * provided by the Search API with backend-specific ones that better use
+ * features of that backend.
+ *
+ * @param \Drupal\search_api\Backend\BackendInterface $backend
+ * The backend that we want to get the query types for.
+ * @param string $data_type_plugin_id
+ * The identifier of the data type.
+ *
+ * @return string[]
+ * An associative array with the plugin IDs of allowed query types, keyed by
+ * the generic name of the query_type.
+ *
+ * @see hook_facets_search_api_query_type_mapping_alter()
+ */
+ public function getQueryTypesForDataType(BackendInterface $backend, $data_type_plugin_id) {
+ $query_types = [];
+ $query_types['string'] = 'search_api_string';
+
+ // Add additional query types for specific data types.
+ switch ($data_type_plugin_id) {
+ case 'date':
+ $query_types['date'] = 'search_api_date';
+ break;
+
+ case 'decimal':
+ case 'integer':
+ $query_types['numeric'] = 'search_api_granular';
+ $query_types['range'] = 'search_api_range';
+ break;
+
+ }
+
+ // Find out if the backend implemented the Interface to retrieve specific
+ // query types for the supported data_types.
+ if ($backend instanceof FacetsQueryTypeMappingInterface) {
+ // If the input arrays have the same string keys, then the later value
+ // for that key will overwrite the previous one. If, however, the arrays
+ // contain numeric keys, the later value will not overwrite the original
+ // value, but will be appended.
+ $query_types = array_merge($query_types, $backend->getQueryTypesForDataType($data_type_plugin_id));
+ }
+ // Add it to a variable so we can pass it by reference. Alter hook complains
+ // due to the property of the backend object is not passable by reference.
+ $backend_plugin_id = $backend->getPluginId();
+
+ // Let modules alter this mapping.
+ \Drupal::moduleHandler()->alter('facets_search_api_query_type_mapping', $backend_plugin_id, $query_types);
+
+ return $query_types;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiDisplay.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiDisplay.php
new file mode 100644
index 000000000..57911d525
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiDisplay.php
@@ -0,0 +1,421 @@
+searchApiQueryHelper = $search_results_cache;
+ $this->displayPluginManager = $display_plugin_manager;
+ $this->moduleHandler = $moduleHandler;
+ $this->request = clone $request;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ // If the Search API module is not enabled, we should just return an empty
+ // object. This allows us to have this class in the module without having a
+ // dependency on the Search API module.
+ if (!$container->get('module_handler')->moduleExists('search_api')) {
+ return new \stdClass();
+ }
+
+ $request_stack = $container->get('request_stack');
+
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('plugin.manager.facets.query_type'),
+ $container->get('search_api.query_helper'),
+ $container->get('plugin.manager.search_api.display'),
+ // Support 9.3+.
+ // @todo remove switch after 9.3 or greater is required.
+ version_compare(\Drupal::VERSION, '9.3', '>=') ? $request_stack->getMainRequest() : $request_stack->getMasterRequest(),
+ $container->get('module_handler')
+ );
+ }
+
+ /**
+ * Retrieves the Search API index for this facet source.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * The search index.
+ */
+ public function getIndex() {
+ if ($this->index === NULL) {
+ $this->index = $this->getDisplay()->getIndex();
+ }
+
+ return $this->index;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPath() {
+ if ($this->isRenderedInCurrentRequest()) {
+ return \Drupal::service('path.current')->getPath();
+ }
+ return $this->getDisplay()->getPath();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fillFacetsWithResults(array $facets) {
+ $search_id = $this->getDisplay()->getPluginId();
+
+ // Check if the results for this search id are already populated in the
+ // query helper. This is usually the case for views displays that are
+ // rendered on the same page, such as views_page.
+ $results = $this->searchApiQueryHelper->getResults($search_id);
+
+ // If there are no results, we can check the Search API Display plugin has
+ // configuration for views. If that configuration exists, we can execute
+ // that view and try to use it's results.
+ $display_definition = $this->getDisplay()->getPluginDefinition();
+ if ($results === NULL && isset($display_definition['view_id'])) {
+ $view = Views::getView($display_definition['view_id']);
+ $view->setDisplay($display_definition['view_display']);
+ $view->execute();
+ $results = $this->searchApiQueryHelper->getResults($search_id);
+ }
+
+ if (!$results instanceof ResultSetInterface) {
+ return;
+ }
+
+ // Get our facet data.
+ $facet_results = $results->getExtraData('search_api_facets');
+
+ // If no data is found in the 'search_api_facets' extra data, we can stop
+ // execution here.
+ if ($facet_results === []) {
+ return;
+ }
+
+ // Loop over each facet and execute the build method from the given
+ // query type.
+ foreach ($facets as $facet) {
+ $configuration = [
+ 'query' => $results->getQuery(),
+ 'facet' => $facet,
+ 'results' => isset($facet_results[$facet->getFieldIdentifier()]) ? $facet_results[$facet->getFieldIdentifier()] : [],
+ ];
+
+ // Get the Facet Specific Query Type so we can process the results
+ // using the build() function of the query type.
+ $query_type = $this->queryTypePluginManager->createInstance($facet->getQueryType(), $configuration);
+ $query_type->build();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRenderedInCurrentRequest() {
+ return $this->getDisplay()->isRenderedInCurrentRequest();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form['field_identifier'] = [
+ '#type' => 'select',
+ '#options' => $this->getFields(),
+ '#title' => $this->t('Field'),
+ '#description' => $this->t('The field from the selected facet source which contains the data to build a facet for. The field types supported are boolean , date , decimal , integer and string .'),
+ '#required' => TRUE,
+ '#default_value' => $this->facet->getFieldIdentifier(),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFields() {
+ $indexed_fields = [];
+ $index = $this->getIndex();
+
+ $fields = $index->getFields();
+ $server = $index->getServerInstance();
+ $backend = $server->getBackend();
+
+ foreach ($fields as $field) {
+ $data_type_plugin_id = $field->getDataTypePlugin()->getPluginId();
+ $query_types = $this->getQueryTypesForDataType($backend, $data_type_plugin_id);
+ if (!empty($query_types)) {
+ $indexed_fields[$field->getFieldIdentifier()] = $field->getLabel() . ' (' . $field->getPropertyPath() . ')';
+ }
+ }
+
+ return $indexed_fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryTypesForFacet(FacetInterface $facet) {
+ // Get our Facets Field Identifier, which is equal to the Search API Field
+ // identifier.
+ $field_id = $facet->getFieldIdentifier();
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getIndex();
+ // Get the Search API Server.
+ $server = $index->getServerInstance();
+ // Get the Search API Backend.
+ $backend = $server->getBackend();
+
+ $fields = &drupal_static(__METHOD__, []);
+
+ if (!isset($fields[$index->id()])) {
+ $fields[$index->id()] = $index->getFields();
+ }
+
+ foreach ($fields[$index->id()] as $field) {
+ if ($field->getFieldIdentifier() == $field_id) {
+ return $this->getQueryTypesForDataType($backend, $field->getType());
+ }
+ }
+
+ throw new InvalidQueryTypeException("No available query types were found for facet {$facet->getName()}");
+ }
+
+ /**
+ * Retrieves the query types for a specified data type.
+ *
+ * Backend plugins can use this method to override the default query types
+ * provided by the Search API with backend-specific ones that better use
+ * features of that backend.
+ *
+ * @param \Drupal\search_api\Backend\BackendInterface $backend
+ * The backend that we want to get the query types for.
+ * @param string $data_type_plugin_id
+ * The identifier of the data type.
+ *
+ * @return string[]
+ * An associative array with the plugin IDs of allowed query types, keyed by
+ * the generic name of the query_type.
+ *
+ * @see hook_facets_search_api_query_type_mapping_alter()
+ */
+ protected function getQueryTypesForDataType(BackendInterface $backend, $data_type_plugin_id) {
+ $query_types = [];
+ $query_types['string'] = 'search_api_string';
+
+ // Add additional query types for specific data types.
+ switch ($data_type_plugin_id) {
+ case 'date':
+ $query_types['date'] = 'search_api_date';
+ $query_types['range'] = 'search_api_range';
+ break;
+
+ case 'decimal':
+ case 'integer':
+ $query_types['numeric'] = 'search_api_granular';
+ $query_types['range'] = 'search_api_range';
+ break;
+
+ }
+
+ // Find out if the backend implemented the Interface to retrieve specific
+ // query types for the supported data_types.
+ if ($backend instanceof FacetsQueryTypeMappingInterface) {
+ $mapping = [
+ $data_type_plugin_id => &$query_types,
+ ];
+ $backend->alterFacetQueryTypeMapping($mapping);
+ }
+ // Add it to a variable so we can pass it by reference. Alter hook complains
+ // due to the property of the backend object is not passable by reference.
+ $backend_plugin_id = $backend->getPluginId();
+
+ // Let modules alter this mapping.
+ \Drupal::moduleHandler()
+ ->alter('facets_search_api_query_type_mapping', $backend_plugin_id, $query_types);
+
+ return $query_types;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $display = $this->getDisplay();
+ if ($display instanceof DependentPluginInterface) {
+ return $display->calculateDependencies();
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDisplay() {
+ return $this->displayPluginManager
+ ->createInstance($this->pluginDefinition['display_id']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getViewsDisplay() {
+ if (!$this->moduleHandler->moduleExists('views')) {
+ return NULL;
+ }
+
+ $search_api_display_definition = $this->getDisplay()->getPluginDefinition();
+ if (empty($search_api_display_definition['view_id'])) {
+ return NULL;
+ }
+
+ $view_id = $search_api_display_definition['view_id'];
+ $view_display = $search_api_display_definition['view_display'];
+
+ $view = Views::getView($view_id);
+ $view->setDisplay($view_display);
+ return $view;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataDefinition($field_name) {
+ $field = $this->getIndex()->getField($field_name);
+ if ($field) {
+ return $field->getDataDefinition();
+ }
+ throw new Exception("Field with name {$field_name} does not have a definition");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildFacet() {
+ $build = parent::buildFacet();
+ $view = $this->getViewsDisplay();
+ if ($view === NULL) {
+ return $build;
+ }
+
+ // Add JS for Views with Ajax Enabled.
+ if ($view->display_handler->ajaxEnabled()) {
+ $js_settings = [
+ 'view_id' => $view->id(),
+ 'current_display_id' => $view->current_display,
+ 'view_base_path' => ltrim($view->getPath() ?? '', '/'),
+ 'ajax_path' => Url::fromRoute('views.ajax')->toString(),
+ ];
+ $build['#attached']['library'][] = 'facets/drupal.facets.views-ajax';
+ $build['#attached']['drupalSettings']['facets_views_ajax'] = [
+ $this->facet->id() => $js_settings,
+ ];
+ $build['#use_ajax'] = TRUE;
+ }
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCount() {
+ $search_id = $this->getDisplay()->getPluginId();
+ if ($search_id && !empty($search_id)) {
+ if ($this->searchApiQueryHelper->getResults($search_id) !== NULL) {
+ return $this->searchApiQueryHelper->getResults($search_id)
+ ->getResultCount();
+ }
+ }
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiDisplayDeriver.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiDisplayDeriver.php
new file mode 100644
index 000000000..d9cc5f8b9
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/facet_source/SearchApiDisplayDeriver.php
@@ -0,0 +1,67 @@
+getSearchApiDisplayPluginManager();
+ foreach ($display_plugin_manager->getDefinitions() as $display_id => $display_definition) {
+ // If 'index' is not set on the plugin, we can't load the index.
+ if (!isset($display_definition['index'])) {
+ continue;
+ }
+
+ /** @var \Drupal\search_api\Display\DisplayInterface $display */
+ $display = $display_plugin_manager->createInstance($display_id);
+
+ $index = $display->getIndex();
+
+ // If we can't reliably load the index, we should just cancel trying to
+ // create a derivative for this display.
+ if (!$index instanceof IndexInterface) {
+ continue;
+ }
+
+ // Get the server linked to the index.
+ $server = $index->getServerInstance();
+
+ // If facets are not supported by the server, don't actually add this to
+ // the list of plugins.
+ if (empty($server) || !$server->supportsFeature('search_api_facets')) {
+ continue;
+ }
+
+ $machine_name = str_replace(':', '__', $display->getPluginId());
+ $plugin_derivatives[$machine_name] = [
+ 'id' => $base_plugin_id . PluginBase::DERIVATIVE_SEPARATOR . $machine_name,
+ 'display_id' => $display_id,
+ 'label' => $display->label(),
+ 'description' => $display->getDescription(),
+ ] + $base_plugin_definition;
+ }
+
+ uasort($plugin_derivatives, [$this, 'compareDerivatives']);
+
+ $this->derivatives[$base_plugin_id] = $plugin_derivatives;
+ return $this->derivatives[$base_plugin_id];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/hierarchy/DateItems.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/hierarchy/DateItems.php
new file mode 100644
index 000000000..7af541065
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/hierarchy/DateItems.php
@@ -0,0 +1,67 @@
+parents[$id] = $matches[1];
+ $this->children[$matches[1]][] = $id;
+ return [$matches[1]];
+ }
+
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNestedChildIds($id) {
+ $nested_children = [];
+ if (isset($this->children[$id])) {
+ foreach ($this->children[$id] as $child) {
+ $nested_children[] = $child;
+ $nested_children = array_merge($nested_children, $this->getNestedChildIds($child));
+ }
+ }
+
+ return $nested_children;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChildIds(array $ids) {
+ return array_intersect_key($this->children, array_flip($ids));
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/hierarchy/Taxonomy.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/hierarchy/Taxonomy.php
new file mode 100644
index 000000000..129c0e0ff
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/hierarchy/Taxonomy.php
@@ -0,0 +1,200 @@
+entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * Returns the term storage.
+ *
+ * @return \Drupal\taxonomy\TermStorageInterface
+ * The term storage.
+ */
+ public function getTermStorage() {
+ if (!isset($this->termStorage)) {
+ $this->termStorage = $this->entityTypeManager->getStorage('taxonomy_term');
+ }
+ return $this->termStorage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParentIds($id) {
+ $current_tid = $id;
+ while ($parent = $this->taxonomyGetParent($current_tid)) {
+ $current_tid = $parent;
+ $parents[$id][] = $parent;
+ }
+ return isset($parents[$id]) ? $parents[$id] : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNestedChildIds($id) {
+ if (isset($this->nestedChildren[$id])) {
+ return $this->nestedChildren[$id];
+ }
+
+ $children = $this->getTermStorage()->loadChildren($id);
+ $children = array_filter(array_values(array_map(function ($it) {
+ return $it->id();
+ }, $children)));
+
+ $subchilds = [];
+ foreach ($children as $child) {
+ $subchilds = array_merge($subchilds, $this->getNestedChildIds($child));
+ }
+ return $this->nestedChildren[$id] = array_merge($children, $subchilds);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChildIds(array $ids) {
+ $parents = [];
+ foreach ($ids as $id) {
+ $terms = $this->getTermStorage()->loadChildren($id);
+ $parents[$id] = array_filter(array_values(array_map(function ($it) {
+ return $it->id();
+ }, $terms)));
+ }
+ $parents = array_filter($parents);
+ return $parents;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSiblingIds(array $ids, array $activeIds = [], bool $parentSiblings = TRUE) {
+ if (empty($ids)) {
+ return [];
+ }
+
+ $parentIds = [];
+ $topLevelTerms = [];
+
+ foreach ($ids as $id) {
+ if (!$activeIds || in_array($id, $activeIds)) {
+ $currentParentIds = $this->getParentIds($id);
+ if (!$currentParentIds) {
+ if (!$topLevelTerms) {
+ /** @var \Drupal\taxonomy\Entity\Term $term */
+ $term = $this->getTermStorage()->load($id);
+ $topLevelTerms = array_map(function ($term) {
+ return $term->tid;
+ }, $this->getTermStorage()->loadTree($term->bundle(), 0, 1));
+ }
+ }
+ else {
+ $parentIds[] = $currentParentIds;
+ }
+ }
+ }
+
+ $parentIds = array_unique(array_merge([], ...$parentIds));
+ $childIds = array_merge([], ...$this->getChildIds($parentIds));
+
+ return array_diff(
+ array_merge(
+ $childIds,
+ $topLevelTerms,
+ (!$topLevelTerms && $parentSiblings) ? $this->getSiblingIds($ids, $parentIds) : []
+ ),
+ $ids
+ );
+ }
+
+ /**
+ * Returns the parent tid for a given tid, or false if no parent exists.
+ *
+ * @param int $tid
+ * A taxonomy term id.
+ *
+ * @return int|false
+ * Returns FALSE if no parent is found, else parent tid.
+ */
+ protected function taxonomyGetParent($tid) {
+ if (isset($this->termParents[$tid])) {
+ return $this->termParents[$tid];
+ }
+
+ $parents = $this->getTermStorage()->loadParents($tid);
+ if (empty($parents)) {
+ return FALSE;
+ }
+ return $this->termParents[$tid] = reset($parents)->id();
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ActiveWidgetOrderProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ActiveWidgetOrderProcessor.php
new file mode 100644
index 000000000..149e18bea
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ActiveWidgetOrderProcessor.php
@@ -0,0 +1,41 @@
+isActive() == $b->isActive()) {
+ return 0;
+ }
+ return ($a->isActive()) ? -1 : 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return ['sort' => 'DESC'];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/BooleanItemProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/BooleanItemProcessor.php
new file mode 100644
index 000000000..998fa4e3c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/BooleanItemProcessor.php
@@ -0,0 +1,126 @@
+getConfiguration();
+
+ /** @var \Drupal\facets\Result\Result $result */
+ foreach ($results as $key => $result) {
+ $value = '';
+ if ($result->getRawValue() == 0) {
+ $value = $config['off_value'];
+ }
+ elseif ($result->getRawValue() == 1) {
+ $value = $config['on_value'];
+ }
+ if ($value == '') {
+ unset($results[$key]);
+ }
+ else {
+ $result->setDisplayValue($value);
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $config = $this->getConfiguration();
+
+ $build['on_value'] = [
+ '#title' => $this->t('On value'),
+ '#type' => 'textfield',
+ '#default_value' => $config['on_value'],
+ '#description' => $this->t('Use this label instead of 0 for the On or True value. Leave empty to hide this item.'),
+ '#states' => [
+ 'required' => ['input[name="facet_settings[boolean_item][settings][off_value]"' => ['empty' => TRUE]],
+ ],
+ ];
+
+ $build['off_value'] = [
+ '#title' => $this->t('Off value'),
+ '#type' => 'textfield',
+ '#default_value' => $config['off_value'],
+ '#description' => $this->t('Use this label instead of 1 for the Off or False value. Leave empty to hide this item.'),
+ '#states' => [
+ 'required' => ['input[name="facet_settings[boolean_item][settings][on_value]"' => ['empty' => TRUE]],
+ ],
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'on_value' => 'On',
+ 'off_value' => 'Off',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFacet(FacetInterface $facet) {
+ $field_identifier = $facet->getFieldIdentifier();
+ $facet_source = $facet->getFacetSource();
+ if ($facet_source instanceof SearchApiFacetSourceInterface) {
+ $field = $facet_source->getIndex()->getField($field_identifier);
+ if ($field->getType() == "boolean") {
+ return TRUE;
+ }
+ }
+
+ $data_definition = $facet->getDataDefinition();
+ if ($data_definition->getDataType() == "boolean") {
+ return TRUE;
+ }
+ if (!($data_definition instanceof ComplexDataDefinitionInterface)) {
+ return FALSE;
+ }
+
+ $property_definitions = $data_definition->getPropertyDefinitions();
+ foreach ($property_definitions as $definition) {
+ if ($definition->getDataType() == "boolean") {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CombineFacetProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CombineFacetProcessor.php
new file mode 100644
index 000000000..40603ebf4
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CombineFacetProcessor.php
@@ -0,0 +1,170 @@
+facetsManager = $facets_manager;
+ $this->facetStorage = $entity_type_manager->getStorage('facets_facet');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('facets.manager'),
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $current_facet) {
+ $build = [];
+
+ $config = $this->getConfiguration();
+
+ // Loop over all defined blocks and filter them by provider, this builds an
+ // array of blocks that are provided by the facets module.
+ /** @var \Drupal\facets\Entity\Facet[] $facets */
+ $facets = $this->facetStorage->loadMultiple();
+ foreach ($facets as $facet) {
+ if ($facet->id() === $current_facet->id()) {
+ continue;
+ }
+
+ $build[$facet->id()]['label'] = [
+ '#title' => $facet->getName() . ' (' . $facet->getFacetSourceId() . ')',
+ '#type' => 'label',
+ ];
+
+ $build[$facet->id()]['combine'] = [
+ '#title' => $this->t('Combine'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($config[$facet->id()]['combine']),
+ ];
+
+ $build[$facet->id()]['mode'] = [
+ '#title' => $this->t('Mode'),
+ '#type' => 'radios',
+ '#options' => [
+ 'union' => $this->t("Add that facet's results to this facet's results (union)."),
+ 'diff' => $this->t("Only keep this facet's results that are not present in that facet's results (diff)."),
+ 'intersect' => $this->t('Only keep results that occur in both facets (intersect).'),
+ ],
+ '#default_value' => empty($config[$facet->id()]['mode']) ? NULL : $config[$facet->id()]['mode'],
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][combine]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ }
+
+ return parent::buildConfigurationForm($form, $form_state, $current_facet) + $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ $conditions = $this->getConfiguration();
+ $enabled_combinations = [];
+
+ foreach ($conditions as $facet_id => $condition) {
+ if (empty($condition['combine'])) {
+ continue;
+ }
+ $enabled_combinations[$facet_id] = $condition;
+ }
+
+ // Return as early as possible when there are no settings for allowed
+ // facets.
+ if (empty($enabled_combinations)) {
+ return $results;
+ }
+
+ $keyed_results = $facet->getResultsKeyedByRawValue($results);
+
+ foreach ($enabled_combinations as $facet_id => $settings) {
+ /** @var \Drupal\facets\Entity\Facet $current_facet */
+ $current_facet = $this->facetStorage->load($facet_id);
+ $current_facet = $this->facetsManager->returnBuiltFacet($current_facet);
+
+ switch ($settings['mode']) {
+ case 'union':
+ $results = $keyed_results + $current_facet->getResultsKeyedByRawValue();
+ break;
+
+ case 'diff':
+ $results = array_diff_key($keyed_results, $current_facet->getResultsKeyedByRawValue());
+ break;
+
+ case 'intersect':
+ $results = array_intersect_key($keyed_results, $current_facet->getResultsKeyedByRawValue());
+ break;
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CountLimitProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CountLimitProcessor.php
new file mode 100644
index 000000000..2290c8076
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CountLimitProcessor.php
@@ -0,0 +1,90 @@
+getConfiguration();
+
+ $min_count = $config['minimum_items'];
+ $max_count = $config['maximum_items'];
+ /** @var \Drupal\facets\Result\Result $result */
+ foreach ($results as $id => $result) {
+ if (($min_count && $result->getCount() < $min_count) ||
+ ($max_count && $result->getCount() > $max_count)) {
+ unset($results[$id]);
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $config = $this->getConfiguration();
+
+ $build['minimum_items'] = [
+ '#title' => $this->t('Minimum items'),
+ '#type' => 'number',
+ '#min' => 1,
+ '#default_value' => $config['minimum_items'],
+ '#description' => $this->t('Hide block if the facet contains less than this number of results.'),
+ ];
+
+ $max_default_value = $config['maximum_items'];
+ $build['maximum_items'] = [
+ '#title' => $this->t('Maximum items'),
+ '#type' => 'number',
+ '#min' => 1,
+ '#default_value' => $max_default_value ? $max_default_value : '',
+ '#description' => $this->t('Hide block if the facet contains more than this number of results.'),
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $values = $form_state->getValues();
+ if (!empty($values['maximum_items']) && !empty($values['minimum_items']) && $values['maximum_items'] <= $values['minimum_items']) {
+ $form_state->setErrorByName('maximum_items', $this->t('If both minimum and maximum item count are specified, the maximum item count should be higher than the minimum item count.'));
+ }
+ return parent::validateConfigurationForm($form, $form_state, $facet);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'minimum_items' => 1,
+ 'maximum_items' => 0,
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CountWidgetOrderProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CountWidgetOrderProcessor.php
new file mode 100644
index 000000000..85257ae2c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/CountWidgetOrderProcessor.php
@@ -0,0 +1,41 @@
+getCount() == $b->getCount()) {
+ return 0;
+ }
+ return ($a->getCount() < $b->getCount()) ? -1 : 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return ['sort' => 'DESC'];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DateItemProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DateItemProcessor.php
new file mode 100644
index 000000000..0922275ee
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DateItemProcessor.php
@@ -0,0 +1,111 @@
+ $this->t('Year'),
+ SearchApiDate::FACETAPI_DATE_MONTH => $this->t('Month'),
+ SearchApiDate::FACETAPI_DATE_DAY => $this->t('Day'),
+ SearchApiDate::FACETAPI_DATE_HOUR => $this->t('Hour'),
+ SearchApiDate::FACETAPI_DATE_MINUTE => $this->t('Minute'),
+ SearchApiDate::FACETAPI_DATE_SECOND => $this->t('Second', [], ['context' => 'timeperiod']),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $this->getConfiguration();
+
+ $build['date_display'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Date display'),
+ '#default_value' => $this->getConfiguration()['date_display'],
+ '#options' => [
+ 'actual_date' => $this->t('Actual date with granularity'),
+ 'relative_date' => $this->t('Relative date'),
+ ],
+ ];
+
+ $build['granularity'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Granularity'),
+ '#default_value' => $this->getConfiguration()['granularity'],
+ '#options' => $this->granularityOptions(),
+ ];
+
+ $build['hierarchy'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Hierarchy'),
+ '#default_value' => $this->getConfiguration()['hierarchy'],
+ '#description' => $this->t('Create a hierarchical facet instead of a flat list. It is important to also activate "use hierarchy" and to select "date item hierarchy" as hierarchy type.'),
+ ];
+
+ $build['date_format'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Date format'),
+ '#default_value' => $this->getConfiguration()['date_format'],
+ '#description' => $this->t('Override default date format used for the displayed filter format. See the PHP manual for available options.'),
+ '#states' => [
+ 'visible' => [':input[name="facet_settings[date_item][settings][date_display]"]' => ['value' => 'actual_date']],
+ ],
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryType() {
+ return 'date';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'date_display' => 'actual_date',
+ 'granularity' => SearchApiDate::FACETAPI_DATE_MONTH,
+ 'hierarchy' => FALSE,
+ 'date_format' => '',
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DependentFacetProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DependentFacetProcessor.php
new file mode 100644
index 000000000..7fea4a74a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DependentFacetProcessor.php
@@ -0,0 +1,221 @@
+facetsManager = $facets_manager;
+ $this->facetStorage = $entity_type_manager->getStorage('facets_facet');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('facets.manager'),
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $current_facet) {
+ $build = [];
+
+ $config = $this->getConfiguration();
+
+ // Loop over all defined blocks and filter them by provider, this builds an
+ // array of blocks that are provided by the facets module.
+ /** @var \Drupal\facets\Entity\Facet[] $facets */
+ $facets = $this->facetStorage->loadMultiple();
+ foreach ($facets as $facet) {
+ if ($facet->getFacetSourceId() !== $current_facet->getFacetSourceId()) {
+ continue;
+ }
+
+ if ($facet->id() === $current_facet->id()) {
+ continue;
+ }
+
+ $build[$facet->id()]['label'] = [
+ '#title' => $facet->getName(),
+ '#type' => 'label',
+ ];
+
+ $build[$facet->id()]['enable'] = [
+ '#title' => $this->t('Enable condition'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($config[$facet->id()]['enable']),
+ ];
+
+ $build[$facet->id()]['condition'] = [
+ '#title' => $this->t('Condition mode'),
+ '#type' => 'radios',
+ '#options' => [
+ 'presence' => $this->t('Check whether the facet is present.'),
+ 'not_empty' => $this->t('Check whether the facet is selected / not empty.'),
+ 'values' => $this->t('Check whether the facet is set to specific values.'),
+ ],
+ '#default_value' => empty($config[$facet->id()]['condition']) ? NULL : $config[$facet->id()]['condition'],
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][enable]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $build[$facet->id()]['values'] = [
+ '#title' => $this->t('Values'),
+ '#type' => 'textfield',
+ '#default_value' => empty($config[$facet->id()]['values']) ? '' : $config[$facet->id()]['values'],
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][enable]"]' => ['checked' => TRUE],
+ ':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][condition]"]' => ['value' => 'values'],
+ ],
+ ],
+ ];
+
+ $build[$facet->id()]['negate'] = [
+ '#title' => $this->t('Negate condition'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($config[$facet->id()]['negate']),
+ '#states' => [
+ 'visible' => [
+ ':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][enable]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ }
+
+ return parent::buildConfigurationForm($form, $form_state, $current_facet) + $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ $conditions = $this->getConfiguration();
+
+ foreach ($conditions as $facet_id => $condition) {
+ if (empty($condition['enable'])) {
+ continue;
+ }
+ $enabled_conditions[$facet_id] = $condition;
+ }
+
+ // Return as early as possible when there are no settings for allowed
+ // facets.
+ if (empty($enabled_conditions)) {
+ return $results;
+ }
+
+ foreach ($enabled_conditions as $facet_id => $condition_settings) {
+
+ /** @var \Drupal\facets\Entity\Facet $current_facet */
+ $current_facet = $this->facetStorage->load($facet_id);
+ $current_facet = $this->facetsManager->returnBuiltFacet($current_facet);
+
+ if (!$this->isConditionMet($condition_settings, $current_facet)) {
+ return [];
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Check if the condition for a given facet is met.
+ *
+ * @param array $condition_settings
+ * The condition settings for the facet to check.
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet to check.
+ *
+ * @return bool
+ * TRUE if the condition is met.
+ */
+ public function isConditionMet(array $condition_settings, FacetInterface $facet) {
+ $return = TRUE;
+
+ if ($condition_settings['condition'] === 'not_empty') {
+ $return = !empty($facet->getActiveItems());
+ }
+
+ if ($condition_settings['condition'] === 'values') {
+ $return = FALSE;
+
+ $values = explode(',', $condition_settings['values']);
+ foreach ($facet->getActiveItems() as $value) {
+ if (in_array($value, $values)) {
+ $return = TRUE;
+ break;
+ }
+ }
+ }
+
+ if (!empty($condition_settings['negate'])) {
+ $return = !$return;
+ }
+
+ return $return;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DisplayValueWidgetOrderProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DisplayValueWidgetOrderProcessor.php
new file mode 100644
index 000000000..81dee8e3f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/DisplayValueWidgetOrderProcessor.php
@@ -0,0 +1,82 @@
+transliteration = $transliteration;
+ }
+
+ /**
+ * Creates an instance of the plugin.
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('transliteration')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sortResults(Result $a, Result $b) {
+ // Get the transliterate values only once.
+ if (!isset($a->transliterateDisplayValue)) {
+ $a->transliterateDisplayValue = $this->transliteration->removeDiacritics($a->getDisplayValue());
+ }
+ if (!isset($b->transliterateDisplayValue)) {
+ $b->transliterateDisplayValue = $this->transliteration->removeDiacritics($b->getDisplayValue());
+ }
+
+ // Return the sort value.
+ if ($a->transliterateDisplayValue == $b->transliterateDisplayValue) {
+ return 0;
+ }
+ return strnatcasecmp($a->transliterateDisplayValue, $b->transliterateDisplayValue);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ExcludeSpecifiedItemsProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ExcludeSpecifiedItemsProcessor.php
new file mode 100644
index 000000000..ff88c5bd2
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ExcludeSpecifiedItemsProcessor.php
@@ -0,0 +1,103 @@
+getConfiguration();
+
+ /** @var \Drupal\facets\Result\ResultInterface $result */
+ $exclude_item = $config['exclude'];
+ foreach ($results as $id => $result) {
+ $is_excluded = FALSE;
+ if ($config['regex']) {
+ $matcher = '/' . trim(str_replace('/', '\\/', $exclude_item)) . '/';
+ if (preg_match($matcher, $result->getRawValue()) || preg_match($matcher, $result->getDisplayValue())) {
+ $is_excluded = TRUE;
+ }
+ }
+ else {
+ $exclude_items = explode(',', $exclude_item);
+ foreach ($exclude_items as $item) {
+ $item = trim($item);
+ if ($result->getRawValue() == $item || $result->getDisplayValue() == $item) {
+ $is_excluded = TRUE;
+ }
+ }
+ }
+
+ // Invert the is_excluded result when the invert setting is active.
+ if ($config['invert']) {
+ $is_excluded = !$is_excluded;
+ }
+
+ // Filter by the excluded results.
+ if ($is_excluded) {
+ unset($results[$id]);
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $config = $this->getConfiguration();
+
+ $build['exclude'] = [
+ '#title' => $this->t('Exclude items'),
+ '#type' => 'textarea',
+ '#default_value' => $config['exclude'],
+ '#description' => $this->t("Comma separated list of titles or values that should be excluded, matching either an item's title or value."),
+ ];
+ $build['regex'] = [
+ '#title' => $this->t('Regular expressions used'),
+ '#type' => 'checkbox',
+ '#default_value' => $config['regex'],
+ '#description' => $this->t('Interpret each exclude list item as a regular expression pattern.(Slashes are escaped automatically, patterns using a comma can be wrapped in "double quotes", and if such a pattern uses double quotes itself, just make them double-double-quotes ("")) .'),
+ ];
+ $build['invert'] = [
+ '#title' => $this->t('Invert - only list matched items'),
+ '#type' => 'checkbox',
+ '#default_value' => $config['invert'],
+ '#description' => $this->t('Instead of excluding items based on the pattern specified above, only matching items will be displayed.'),
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'exclude' => '',
+ 'regex' => 0,
+ 'invert' => 0,
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/GranularItemProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/GranularItemProcessor.php
new file mode 100644
index 000000000..c5589edf5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/GranularItemProcessor.php
@@ -0,0 +1,111 @@
+getRawValue();
+ if (is_numeric($value)) {
+ $result->setDisplayValue(((int) $value) . ' - ' . ((int) $value + $this->getConfiguration()['granularity']));
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'granularity' => 1,
+ 'min_value' => '',
+ 'max_value' => '',
+ 'include_lower' => TRUE,
+ 'include_upper' => FALSE,
+ 'include_edges' => TRUE,
+ ] + parent::defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $configuration = $this->getConfiguration();
+
+ $build['granularity'] = [
+ '#type' => 'number',
+ '#attributes' => ['min' => 1, 'step' => 1],
+ '#title' => $this->t('Granularity'),
+ '#default_value' => $configuration['granularity'],
+ '#description' => $this->t('The numeric size of the steps to group the result facets in.'),
+ ];
+
+ $build['min_value'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Minimum value'),
+ '#default_value' => $configuration['min_value'],
+ '#description' => $this->t('If the minimum value is left empty it will be calculated by the search result'),
+ '#size' => 10,
+ ];
+
+ $build['max_value'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Maximum value'),
+ '#default_value' => $configuration['max_value'],
+ '#description' => $this->t('If the maximum value is left empty it will be calculated by the search result'),
+ '#size' => 10,
+ ];
+
+ $build['include_lower'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Include lower bounds'),
+ '#default_value' => $configuration['include_lower'],
+ ];
+
+ $build['include_upper'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Include upper bounds'),
+ '#default_value' => $configuration['include_upper'],
+ ];
+
+ $build['include_edges'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Include first lower and last upper bound'),
+ '#default_value' => $configuration['include_edges'],
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryType() {
+ return 'numeric';
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideActiveItemsProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideActiveItemsProcessor.php
new file mode 100644
index 000000000..d26e94c33
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideActiveItemsProcessor.php
@@ -0,0 +1,38 @@
+ $result) {
+ if ($result->isActive()) {
+ unset($results[$id]);
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideInactiveSiblingsProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideInactiveSiblingsProcessor.php
new file mode 100644
index 000000000..644767ab9
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideInactiveSiblingsProcessor.php
@@ -0,0 +1,61 @@
+getResults();
+ $active_items = $facet->getActiveItems();
+
+ if ($facet->getUseHierarchy()) {
+ $hierarchy = $facet->getHierarchyInstance();
+
+ if (!$facet->getKeepHierarchyParentsActive()) {
+ $parents_of_active_items = [];
+ foreach ($active_items as $active_item) {
+ $parents_of_active_items = array_merge($parents_of_active_items, $hierarchy->getParentIds($active_item));
+ }
+ $active_items = array_unique(array_merge($active_items, $parents_of_active_items));
+ }
+
+ $siblings = $hierarchy->getSiblingIds($active_items);
+ $siblings_and_their_childs = array_merge($siblings, array_merge(...array_values($hierarchy->getChildIds($siblings))));
+
+ foreach ($facet_results as $id => $result) {
+ if (in_array($result->getRawValue(), $siblings_and_their_childs, FALSE) && !$result->isActive()) {
+ unset($results[$id]);
+ }
+ }
+ }
+ elseif ($active_items) {
+ foreach ($facet_results as $id => $result) {
+ if (!$result->isActive()) {
+ unset($results[$id]);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideNonNarrowingResultProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideNonNarrowingResultProcessor.php
new file mode 100644
index 000000000..d8d817a95
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideNonNarrowingResultProcessor.php
@@ -0,0 +1,45 @@
+getResults();
+ $result_count = 0;
+ foreach ($facet_results as $result) {
+ if ($result->isActive()) {
+ $result_count += $result->getCount();
+ }
+ }
+
+ /** @var \Drupal\facets\Result\ResultInterface $result */
+ foreach ($results as $id => $result) {
+ if ((($result->getCount() == $result_count) || ($result->getCount() == 0)) && !$result->isActive() && !$result->hasActiveChildren()) {
+ unset($results[$id]);
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideOnlyOneItemProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideOnlyOneItemProcessor.php
new file mode 100644
index 000000000..375ed46e8
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HideOnlyOneItemProcessor.php
@@ -0,0 +1,37 @@
+isActive() ? $results : [];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HierarchyProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HierarchyProcessor.php
new file mode 100644
index 000000000..6350d1188
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/HierarchyProcessor.php
@@ -0,0 +1,97 @@
+getUseHierarchy()) {
+ $keyed_results = [];
+ foreach ($results as $result) {
+ $keyed_results[$result->getRawValue()] = $result;
+ }
+
+ $parent_groups = $facet->getHierarchyInstance()->getChildIds(array_keys($keyed_results));
+ $keyed_results = $this->buildHierarchicalTree($keyed_results, $parent_groups);
+
+ // Remove children from primary level.
+ foreach (array_unique($this->childIds) as $child_id) {
+ unset($keyed_results[$child_id]);
+ }
+
+ $results = array_values($keyed_results);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Builds a hierarchical structure for results.
+ *
+ * When given an array of results and an array which defines the hierarchical
+ * structure, this will build the results structure and set all childs.
+ *
+ * @param \Drupal\facets\Result\ResultInterface[] $keyed_results
+ * An array of results keyed by id.
+ * @param array $parent_groups
+ * An array of 'child id arrays' keyed by their parent id.
+ *
+ * @return \Drupal\facets\Result\ResultInterface[]
+ * An array of results structured hierarchically.
+ */
+ protected function buildHierarchicalTree(array $keyed_results, array $parent_groups): array {
+ foreach ($keyed_results as &$result) {
+ $current_id = $result->getRawValue();
+ if (isset($parent_groups[$current_id]) && $parent_groups[$current_id]) {
+ $child_ids = $parent_groups[$current_id];
+ $child_keyed_results = [];
+ foreach ($child_ids as $child_id) {
+ if (isset($keyed_results[$child_id])) {
+ $child_keyed_results[$child_id] = $keyed_results[$child_id];
+ }
+ else {
+ // Children could already be built by Facets Summary manager, if
+ // they are, just loading them will suffice.
+ $children = $keyed_results[$current_id]->getChildren();
+ if (!empty($children[$child_id])) {
+ $child_keyed_results[$child_id] = $children[$child_id];
+ }
+ }
+ }
+ $result->setChildren($child_keyed_results);
+ $this->childIds = array_merge($this->childIds, $child_ids);
+ }
+ }
+
+ return $keyed_results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ListItemProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ListItemProcessor.php
new file mode 100644
index 000000000..2afc99a45
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ListItemProcessor.php
@@ -0,0 +1,192 @@
+List (integer)) or List (text) ) or a bundle field. Keep in mind that transformations on the source of this field (such as transliteration or ignore characters) may break this functionality."),
+ * stages = {
+ * "build" = 5
+ * }
+ * )
+ */
+class ListItemProcessor extends ProcessorPluginBase implements BuildProcessorInterface, ContainerFactoryPluginInterface {
+
+ /**
+ * The config manager.
+ *
+ * @var \Drupal\Core\Config\ConfigManagerInterface
+ */
+ protected $configManager;
+
+ /**
+ * The entity field manager.
+ *
+ * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+ */
+ protected $entityFieldManager;
+
+ /**
+ * The entity_type bundle info service.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+ */
+ protected $entityTypeBundleInfo;
+
+ /**
+ * Constructs a Drupal\Component\Plugin\PluginBase object.
+ *
+ * @param array $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin_id for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
+ * The config manager.
+ * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+ * The entity field manager.
+ * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+ * The entity bundle info service.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigManagerInterface $config_manager, EntityFieldManagerInterface $entity_field_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+ $this->configManager = $config_manager;
+ $this->entityFieldManager = $entity_field_manager;
+ $this->entityTypeBundleInfo = $entity_type_bundle_info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('config.manager'),
+ $container->get('entity_field.manager'),
+ $container->get('entity_type.bundle.info')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ $field_identifier = $facet->getFieldIdentifier();
+ $entity = 'node';
+ $field = FALSE;
+ $allowed_values = [];
+
+ // Support multiple entities when using Search API.
+ if ($facet->getFacetSource() instanceof SearchApiDisplay) {
+ /** @var \Drupal\search_api\Entity\Index $index */
+ $index = $facet->getFacetSource()->getIndex();
+ /** @var \Drupal\search_api\Item\Field $field */
+ $field = $index->getField($field_identifier);
+
+ if (!$field->getDatasourceId()) {
+ throw new InvalidProcessorException("This field has no datasource, there is no valid use for this processor with this facet");
+ }
+ $entity = $field->getDatasource()->getEntityTypeId();
+ }
+ // If it's an entity base field, we find it in the field definitions.
+ // We don't have access to the bundle via SearchApiFacetSourceInterface, so
+ // we check the entity's base fields only.
+ $base_fields = $this->entityFieldManager->getFieldDefinitions($entity, '');
+
+ // This only works for configurable fields.
+ $config_entity_name = sprintf('field.storage.%s.%s', $entity, $field_identifier);
+ if (isset($base_fields[$field_identifier])) {
+ $field = $base_fields[$field_identifier];
+ }
+ elseif ($this->configManager->loadConfigEntityByName($config_entity_name) !== NULL) {
+ $field = $this->configManager->loadConfigEntityByName($config_entity_name);
+ }
+ // Fields defined in code don't can't be loaded from storage so check the
+ // fields property path and see if its part of the base fields.
+ elseif ($field->getDataDefinition() instanceof FieldItemDataDefinition) {
+ $fieldDefinition = $field->getDataDefinition()
+ ->getFieldDefinition();
+ $referenced_entity_name = sprintf(
+ 'field.storage.%s.%s',
+ $fieldDefinition->getTargetEntityTypeId(),
+ $fieldDefinition->getName()
+ );
+ if ($fieldDefinition instanceof BaseFieldDefinition) {
+ if (isset($base_fields[$field->getPropertyPath()])) {
+ $field = $base_fields[$field->getPropertyPath()];
+ }
+
+ }
+ // Diggs down to get the referenced field the entity reference is based
+ // on.
+ elseif ($this->configManager->loadConfigEntityByName($referenced_entity_name) !== NULL) {
+ $field = $this->configManager
+ ->loadConfigEntityByName($referenced_entity_name);
+ }
+ }
+ if ($field instanceof FieldStorageDefinitionInterface) {
+ if ($field->getName() !== 'type') {
+ $allowed_values = options_allowed_values($field);
+ if (!empty($allowed_values)) {
+ return $this->overWriteDisplayValues($results, $allowed_values);
+ }
+ }
+ }
+ // If no values are found for the current field, try to see if this is a
+ // bundle field.
+ $list_bundles = $this->entityTypeBundleInfo->getBundleInfo($entity);
+ if (!empty($list_bundles)) {
+ foreach ($list_bundles as $key => $bundle) {
+ $allowed_values[$key] = $bundle['label'];
+ }
+ return $this->overWriteDisplayValues($results, $allowed_values);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Overwrite the display value of the result with a new text.
+ *
+ * @param \Drupal\facets\Result\ResultInterface[] $results
+ * An array of results to work on.
+ * @param array $replacements
+ * An array of values that contain possible replacements for the orignal
+ * values.
+ *
+ * @return \Drupal\facets\Result\ResultInterface[]
+ * The changed results.
+ */
+ protected function overWriteDisplayValues(array $results, array $replacements) {
+ /** @var \Drupal\facets\Result\ResultInterface $a */
+ foreach ($results as &$a) {
+ if (isset($replacements[$a->getRawValue()])) {
+ $a->setDisplayValue($replacements[$a->getRawValue()]);
+ }
+ }
+ return $results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/RawValueWidgetOrderProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/RawValueWidgetOrderProcessor.php
new file mode 100644
index 000000000..c8e45e879
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/RawValueWidgetOrderProcessor.php
@@ -0,0 +1,30 @@
+getRawValue(), $b->getRawValue());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ShowOnlyDeepestLevelItemsProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ShowOnlyDeepestLevelItemsProcessor.php
new file mode 100644
index 000000000..1b51e28fa
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ShowOnlyDeepestLevelItemsProcessor.php
@@ -0,0 +1,36 @@
+ $result) {
+ if (!empty($result->getChildren())) {
+ unset($results[$id]);
+ }
+ }
+ return $results;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ShowSiblingsProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ShowSiblingsProcessor.php
new file mode 100644
index 000000000..49d0295bf
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/ShowSiblingsProcessor.php
@@ -0,0 +1,66 @@
+getUseHierarchy()) {
+ $rawValues = array_map(function ($result) {
+ return $result->getRawValue();
+ }, $results);
+ foreach ($facet->getHierarchyInstance()->getSiblingIds($rawValues, $facet->getActiveItems(), $this->getConfiguration()['show_parent_siblings']) as $siblingId) {
+ $results[] = new Result($facet, $siblingId, $siblingId, 0);
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'show_parent_siblings' => TRUE,
+ ] + parent::defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $configuration = $this->getConfiguration();
+
+ $build['show_parent_siblings'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show parent siblings'),
+ '#description' => $this->t('If selected the siblings of all (inactive) parents of an active item will be added be shown. Otherwise only the siblings of active items will be shown. (See "Keep hierarchy parents active".)'),
+ '#default_value' => $configuration['show_parent_siblings'],
+ ];
+
+ return $build;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TermWeightWidgetOrderProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TermWeightWidgetOrderProcessor.php
new file mode 100644
index 000000000..29938931c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TermWeightWidgetOrderProcessor.php
@@ -0,0 +1,129 @@
+entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sortResults(Result $a, Result $b) {
+ // Get the term weight once.
+ if (!isset($a->termWeight) || !isset($b->termWeight)) {
+ $ids = [];
+ if (!isset($a->termWeight)) {
+ $a_raw = $a->getRawValue();
+ $ids[] = $a_raw;
+ }
+ if (!isset($b->termWeight)) {
+ $b_raw = $b->getRawValue();
+ $ids[] = $b_raw;
+ }
+ $entities = $this->entityTypeManager
+ ->getStorage('taxonomy_term')
+ ->loadMultiple($ids);
+ if (!isset($a->termWeight)) {
+ if (empty($entities[$a_raw])) {
+ return 0;
+ }
+ $a->termWeight = $entities[$a_raw]->getWeight();
+ }
+ if (!isset($b->termWeight)) {
+ if (empty($entities[$b_raw])) {
+ return 0;
+ }
+ $b->termWeight = $entities[$b_raw]->getWeight();
+ }
+ }
+
+ // Return the sort value.
+ if ($a->termWeight === $b->termWeight) {
+ return 0;
+ }
+ return ($a->termWeight < $b->termWeight) ? -1 : 1;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFacet(FacetInterface $facet) {
+ $data_definition = $facet->getDataDefinition();
+ if ($data_definition->getDataType() === 'entity_reference') {
+ return TRUE;
+ }
+ if (!($data_definition instanceof ComplexDataDefinitionInterface)) {
+ return FALSE;
+ }
+
+ $data_definition = $facet->getDataDefinition();
+ $property_definitions = $data_definition->getPropertyDefinitions();
+ foreach ($property_definitions as $definition) {
+ if ($definition instanceof DataReferenceDefinitionInterface
+ && $definition->getDataType() === 'entity_reference'
+ && $definition->getConstraint('EntityType') === 'taxonomy_term'
+ ) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TranslateEntityAggregatedFieldProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TranslateEntityAggregatedFieldProcessor.php
new file mode 100644
index 000000000..efc4c188c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TranslateEntityAggregatedFieldProcessor.php
@@ -0,0 +1,234 @@
+languageManager = $language_manager;
+ $this->entityTypeManager = $entity_type_manager;
+ $this->configManager = $config_manager;
+ $this->entityFieldManager = $entity_field_manager;
+ $this->entityTypeBundleInfo = $entity_type_bundle_info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('language_manager'),
+ $container->get('entity_type.manager'),
+ $container->get('config.manager'),
+ $container->get('entity_field.manager'),
+ $container->get('entity_type.bundle.info')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ $field_identifier = $facet->getFieldIdentifier();
+ $entity_type_ids = [];
+ $allowed_values = [];
+ $language_interface = $this->languageManager->getCurrentLanguage();
+
+ // Support multiple entities when using Search API.
+ if ($facet->getFacetSource() instanceof SearchApiDisplay) {
+ /** @var \Drupal\search_api\Entity\Index $index */
+ $index = $facet->getFacetSource()->getIndex();
+ /** @var \Drupal\search_api\Item\Field $field */
+ $field = $index->getField($field_identifier);
+
+ foreach ($field->getConfiguration()['fields'] as $field_configuration) {
+ $parts = explode(':', $field_configuration);
+ if ($parts[0] !== 'entity') {
+ throw new \InvalidArgumentException('Data type must be in the form of "entity:ENTITY_TYPE/FIELD_NAME."');
+ }
+ $parts = explode('/', $parts[1]);
+ $entity_type_id = $parts[0];
+ $field = $parts[1];
+ $entity_type_ids[] = $entity_type_id;
+
+ $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+ $field_storage = $definition_update_manager->getFieldStorageDefinition($field, $entity_type_id);
+ if ($field_storage && $field_storage->getType() === 'entity_reference') {
+ /** @var \Drupal\facets\Result\ResultInterface $result */
+ $ids = [];
+ foreach ($results as $delta => $result) {
+ $ids[$delta] = $result->getRawValue();
+ }
+
+ if ($field_storage instanceof FieldStorageDefinitionInterface) {
+ if ($field !== 'type') {
+ // Load all indexed entities of this type.
+ $entities = $this->entityTypeManager
+ ->getStorage($field_storage->getSettings()['target_type'])
+ ->loadMultiple($ids);
+
+ // Loop over all results.
+ foreach ($results as $i => $result) {
+ if (!isset($entities[$ids[$i]])) {
+ continue;
+ }
+
+ /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
+ $entity = $entities[$ids[$i]];
+ // Check for a translation of the entity and load that
+ // instead if one's found.
+ if ($entity instanceof TranslatableInterface && $entity->hasTranslation($language_interface->getId())) {
+ $entity = $entity->getTranslation($language_interface->getId());
+ }
+
+ // Overwrite the result's display value.
+ $results[$i]->setDisplayValue($entity->label());
+ }
+ }
+ }
+ }
+ }
+ // If no values are found for the current field, try to see if this is a
+ // bundle field.
+ foreach ($entity_type_ids as $entity) {
+ $list_bundles = $this->entityTypeBundleInfo->getBundleInfo($entity);
+ if (!empty($list_bundles)) {
+ foreach ($list_bundles as $key => $bundle) {
+ $allowed_values[$key] = $bundle['label'];
+ }
+ $this->overWriteDisplayValues($results, $allowed_values);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Overwrite the display value of the result with a new text.
+ *
+ * @param \Drupal\facets\Result\ResultInterface[] $results
+ * An array of results to work on.
+ * @param array $replacements
+ * An array of values that contain possible replacements for the original
+ * values.
+ *
+ * @return \Drupal\facets\Result\ResultInterface[]
+ * The changed results.
+ */
+ protected function overWriteDisplayValues(array $results, array $replacements) {
+ /** @var \Drupal\facets\Result\ResultInterface $a */
+ foreach ($results as &$a) {
+ if (isset($replacements[$a->getRawValue()])) {
+ $a->setDisplayValue($replacements[$a->getRawValue()]);
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFacet(FacetInterface $facet) {
+ $facet_source = $facet->getFacetSource();
+
+ if ($facet_source instanceof SearchApiFacetSourceInterface) {
+ /** @var \Drupal\search_api\Item\Field $field */
+ $field_identifier = $facet->getFieldIdentifier();
+ $field = $facet_source->getIndex()->getField($field_identifier);
+
+ if ($field->getPropertyPath() === 'aggregated_field') {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TranslateEntityProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TranslateEntityProcessor.php
new file mode 100644
index 000000000..dff609fa5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/TranslateEntityProcessor.php
@@ -0,0 +1,161 @@
+languageManager = $language_manager;
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('language_manager'),
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ $language_interface = $this->languageManager->getCurrentLanguage();
+
+ /** @var \Drupal\Core\TypedData\DataDefinitionInterface $data_definition */
+ $data_definition = $facet->getDataDefinition();
+
+ $property = NULL;
+ foreach ($data_definition->getPropertyDefinitions() as $k => $definition) {
+ if ($definition instanceof DataReferenceDefinitionInterface) {
+ $property = $k;
+ break;
+ }
+ }
+
+ if ($property === NULL) {
+ throw new InvalidProcessorException("Field doesn't have an entity definition, so this processor doesn't work.");
+ }
+
+ $entity_type = $data_definition
+ ->getPropertyDefinition($property)
+ ->getTargetDefinition()
+ ->getEntityTypeId();
+
+ /** @var \Drupal\facets\Result\ResultInterface $result */
+ $ids = [];
+ foreach ($results as $delta => $result) {
+ $ids[$delta] = $result->getRawValue();
+ }
+
+ // Load all indexed entities of this type.
+ $entities = $this->entityTypeManager
+ ->getStorage($entity_type)
+ ->loadMultiple($ids);
+
+ // Loop over all results.
+ foreach ($results as $i => $result) {
+ if (!isset($entities[$ids[$i]])) {
+ unset($results[$i]);
+ continue;
+ }
+
+ /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
+ $entity = $entities[$ids[$i]];
+
+ // Check for a translation of the entity and load that instead if one's
+ // found.
+ if ($entity instanceof TranslatableInterface && $entity->hasTranslation($language_interface->getId())) {
+ $entity = $entity->getTranslation($language_interface->getId());
+ }
+
+ // Overwrite the result's display value.
+ $results[$i]->setDisplayValue($entity->label());
+ }
+
+ // Return the results with the new display values.
+ return $results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFacet(FacetInterface $facet) {
+ $data_definition = $facet->getDataDefinition();
+ if ($data_definition->getDataType() === 'entity_reference') {
+ return TRUE;
+ }
+ if (!($data_definition instanceof ComplexDataDefinitionInterface)) {
+ return FALSE;
+ }
+
+ $property_definitions = $data_definition->getPropertyDefinitions();
+ foreach ($property_definitions as $definition) {
+ if ($definition instanceof DataReferenceDefinitionInterface) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/UidToUserNameCallbackProcessor.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/UidToUserNameCallbackProcessor.php
new file mode 100644
index 000000000..254037dab
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/UidToUserNameCallbackProcessor.php
@@ -0,0 +1,71 @@
+getRawValue())) !== NULL) {
+ $result->setDisplayValue($user->getDisplayName());
+ $usernames[] = $result;
+ }
+ }
+
+ return $usernames;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFacet(FacetInterface $facet) {
+ $data_definition = $facet->getDataDefinition();
+ if ($data_definition->getDataType() === 'entity_reference' &&
+ $data_definition->getTargetDefinition()->getConstraint('EntityType') === "user") {
+ return TRUE;
+ }
+
+ if (!($data_definition instanceof ComplexDataDefinitionInterface)) {
+ return FALSE;
+ }
+
+ $property_definitions = $data_definition->getPropertyDefinitions();
+ foreach ($property_definitions as $definition) {
+ if (
+ $definition instanceof DataReferenceDefinitionInterface &&
+ $definition->getDataType() === 'entity_reference' &&
+ $definition->getTargetDefinition()->getConstraint('EntityType') === "user"
+ ) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/UrlProcessorHandler.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/UrlProcessorHandler.php
new file mode 100644
index 000000000..92f865566
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/processor/UrlProcessorHandler.php
@@ -0,0 +1,85 @@
+processor;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+ if (!isset($configuration['facet']) || !$configuration['facet'] instanceof FacetInterface) {
+ throw new InvalidProcessorException("The UrlProcessorHandler doesn't have the required 'facet' in the configuration array.");
+ }
+
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $configuration['facet'];
+
+ /** @var \Drupal\facets\FacetSourceInterface $fs */
+ $fs = $facet->getFacetSourceConfig();
+
+ $url_processor_name = $fs->getUrlProcessorName();
+
+ $manager = \Drupal::getContainer()->get('plugin.manager.facets.url_processor');
+ $this->processor = $manager->createInstance($url_processor_name, ['facet' => $facet]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet, array $results) {
+ return $this->processor->buildUrls($facet, $results);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preQuery(FacetInterface $facet) {
+ $this->processor->setActiveItems($facet);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiDate.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiDate.php
new file mode 100644
index 000000000..22afa127a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiDate.php
@@ -0,0 +1,485 @@
+getProcessors();
+ $dateProcessorConfig = $processors['date_item']->getConfiguration();
+
+ $configuration = $this->getConfiguration();
+ $configuration['granularity'] = $dateProcessorConfig['granularity'];
+ $configuration['hierarchy'] = $dateProcessorConfig['hierarchy'];
+ $configuration['date_display'] = $dateProcessorConfig['date_display'];
+ $configuration['date_format'] = $dateProcessorConfig['date_format'];
+ $this->setConfiguration($configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateRange($value) {
+ $counts = count_chars($value, 1);
+ $granularity = self::FACETAPI_DATE_YEAR - ($counts[ord('-')] ?? 0) - ($counts[ord('T')] ?? 0) - ($counts[ord(':')] ?? 0);
+
+ if ($this->getDateDisplay() === 'relative_date') {
+ return $this->calculateRangeRelative($value, $granularity);
+ }
+
+ return $this->calculateRangeAbsolute($value, $granularity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ $granularity = $this->getGranularity();
+
+ if (!$this->getHierarchy() || $granularity === self::FACETAPI_DATE_YEAR) {
+ return parent::build();
+ }
+
+ $configuration = $this->getConfiguration();
+ $facet_results = [];
+
+ while ($this->getGranularity() <= self::FACETAPI_DATE_YEAR) {
+ parent::build();
+ $facet_results += $this->facet->getResults();
+ $configuration['granularity'] = $this->getGranularity() + 1;
+ $this->setConfiguration($configuration);
+ }
+
+ $configuration['granularity'] = $granularity;
+ $this->setConfiguration($configuration);
+
+ $this->facet->setResults($facet_results);
+ return $this->facet;
+ }
+
+ /**
+ * Returns a start and end date based on a unix timestamp.
+ *
+ * This method returns a start and end date with an absolute interval, based
+ * on the granularity set in the widget.
+ *
+ * @param int $value
+ * Unix timestamp.
+ * @param int $granularity
+ * The grnaularity.
+ *
+ * @return array
+ * An array with a start and end date as unix timestamps.
+ *
+ * @throws \Exception
+ * Thrown when creating a date fails.
+ */
+ protected function calculateRangeAbsolute($value, $granularity) {
+ $dateTime = new DrupalDateTime();
+
+ switch ($granularity) {
+ case static::FACETAPI_DATE_YEAR:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . '-01-01T00:00:00');
+ $stopDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . '-12-31T23:59:59');
+ break;
+
+ case static::FACETAPI_DATE_MONTH:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . '-01T00:00:00');
+ $stopDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . '-' . $startDate->format('t') . 'T23:59:59');
+ break;
+
+ case static::FACETAPI_DATE_DAY:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . 'T00:00:00');
+ $stopDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . 'T23:59:59');
+ break;
+
+ case static::FACETAPI_DATE_HOUR:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . ':00:00');
+ $stopDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . ':59:59');
+ break;
+
+ case static::FACETAPI_DATE_MINUTE:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . ':00');
+ $stopDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . ':59');
+ break;
+
+ default:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value);
+ $stopDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value);
+ break;
+ }
+
+ return [
+ 'start' => $startDate->format('U'),
+ 'stop' => $stopDate->format('U'),
+ ];
+ }
+
+ /**
+ * Returns a start and end date based on a unix timestamp.
+ *
+ * This method returns a start and end date with an relative interval, based
+ * on the granularity set in the widget.
+ *
+ * @param int $value
+ * Unix timestamp.
+ * @param int $granularity
+ * The granularity.
+ *
+ * @return array
+ * An array with a start and end date as unix timestamps.
+ *
+ * @throws \Exception
+ * Thrown when creating a date fails.
+ */
+ protected function calculateRangeRelative($value, int $granularity) {
+ $dateTime = new DrupalDateTime();
+
+ switch ($granularity) {
+ case static::FACETAPI_DATE_YEAR:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . '-01T00:00:00');
+ $stopDate = clone $startDate;
+ $stopDate->add(new \DateInterval('P1Y'));
+ $stopDate->sub(new \DateInterval('PT1S'));
+ break;
+
+ case static::FACETAPI_DATE_MONTH:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . 'T00:00:00');
+ $stopDate = clone $startDate;
+ $stopDate->add(new \DateInterval('P1M'));
+ $stopDate->sub(new \DateInterval('PT1S'));
+ break;
+
+ case static::FACETAPI_DATE_DAY:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . ':00:00');
+ $stopDate = clone $startDate;
+ $stopDate->add(new \DateInterval('P1D'));
+ $stopDate->sub(new \DateInterval('PT1S'));
+ break;
+
+ case static::FACETAPI_DATE_HOUR:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value . ':00');
+ $stopDate = clone $startDate;
+ $stopDate->add(new \DateInterval('PT1H'));
+ $stopDate->sub(new \DateInterval('PT1S'));
+ break;
+
+ case static::FACETAPI_DATE_MINUTE:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value);
+ $stopDate = clone $startDate;
+ $stopDate->add(new \DateInterval('PT1M'));
+ $stopDate->sub(new \DateInterval('PT1S'));
+ break;
+
+ default:
+ $startDate = $dateTime::createFromFormat('Y-m-d\TH:i:s', $value);
+ $stopDate = clone $startDate;
+ break;
+ }
+
+ return [
+ 'start' => $startDate->format('U'),
+ 'stop' => $stopDate->format('U'),
+ ];
+ }
+
+ /**
+ * Calculates the result of the filter.
+ *
+ * @param int $value
+ * A unix timestamp.
+ *
+ * @return array
+ * An array with a start and end date as unix timestamps.
+ */
+ public function calculateResultFilter($value) {
+ if ($this->getDateDisplay() === 'relative_date') {
+ return $this->calculateResultFilterRelative($value);
+ }
+ else {
+ return $this->calculateResultFilterAbsolute($value);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateResultFilterAbsolute($value) {
+ $date = new DrupalDateTime();
+ $date->setTimestamp($value);
+ $date_format = $this->getDateFormat();
+
+ switch ($this->getGranularity()) {
+ case static::FACETAPI_DATE_YEAR:
+ $format = 'Y';
+ $raw = $date->format('Y');
+ break;
+
+ case static::FACETAPI_DATE_MONTH:
+ $format = 'F Y';
+ $raw = $date->format('Y-m');
+ break;
+
+ case static::FACETAPI_DATE_DAY:
+ $format = 'd F Y';
+ $raw = $date->format('Y-m-d');
+ break;
+
+ case static::FACETAPI_DATE_HOUR:
+ $format = 'd/m/Y H\h';
+ $raw = $date->format('Y-m-d\TH');
+ break;
+
+ case static::FACETAPI_DATE_MINUTE:
+ $format = 'd/m/Y H:i';
+ $raw = $date->format('Y-m-d\TH:i');
+ break;
+
+ default:
+ $format = 'd/m/Y H:i:s';
+ $raw = $date->format('Y-m-d\TH:i:s');
+ break;
+ }
+
+ $format = $date_format ? $date_format : $format;
+ return [
+ 'display' => $date->format($format),
+ 'raw' => $raw,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateResultFilterRelative($value) {
+ $date = new DrupalDateTime();
+ $date->setTimestamp($value);
+ $now = new DrupalDateTime();
+ $now->setTimestamp(\Drupal::time()->getRequestTime());
+ $interval = $date->diff($now);
+ $future = $date > $now;
+
+ switch ($this->getGranularity()) {
+ case static::FACETAPI_DATE_YEAR:
+ $rounded = new \DateInterval('P' . $interval->y . 'Y');
+ if ($future) {
+ $display = $interval->y ? $this->formatPlural($interval->y, '1 year hence', '@count years hence') : $this->t('In the next year');
+ $now->add($rounded);
+ }
+ else {
+ $display = $interval->y ? $this->formatPlural($interval->y, '1 year ago', '@count years ago') : $this->t('In the last year');
+ $now->sub($rounded);
+ $now->sub(new \DateInterval('P1Y'));
+ }
+ $raw = $now->format('Y-m');
+ break;
+
+ case static::FACETAPI_DATE_MONTH:
+ $rounded = new \DateInterval('P' . $interval->y . 'Y' . $interval->m . 'M');
+ $display = $interval->y ? $this->formatPlural($interval->y, '1 year', '@count years') . ' ' : '';
+ if ($future) {
+ $display .= $interval->m ?
+ $this->formatPlural($interval->m, '1 month hence', '@count months hence') :
+ (empty($display) ? $this->t('In the next month') : $this->t('0 months hence'));
+ $now->add($rounded);
+ }
+ else {
+ $display .= $interval->m ?
+ $this->formatPlural($interval->m, '1 month ago', '@count months ago') :
+ (empty($display) ? $this->t('In the last month') : $this->t('0 months ago'));
+ $now->sub($rounded);
+ $now->sub(new \DateInterval('P1M'));
+ }
+ $raw = $now->format('Y-m-d');
+ break;
+
+ case static::FACETAPI_DATE_DAY:
+ $rounded = new \DateInterval('P' . $interval->y . 'Y' . $interval->m . 'M' . $interval->d . 'D');
+ $display = $interval->y ? $this->formatPlural($interval->y, '1 year', '@count years') . ' ' : '';
+ $display .= $interval->m ? $this->formatPlural($interval->m, '1 month', '@count months') . ' ' : '';
+ if ($future) {
+ $display .= $interval->d ?
+ $this->formatPlural($interval->d, '1 day hence', '@count days hence') :
+ (empty($display) ? $this->t('In the next day') : $this->t('0 days hence'));
+ $now->add($rounded);
+ }
+ else {
+ $display .= $interval->d ?
+ $this->formatPlural($interval->d, '1 day ago', '@count days ago') :
+ (empty($display) ? $this->t('In the last day') : $this->t('0 days ago'));
+ $now->sub($rounded);
+ $now->sub(new \DateInterval('P1D'));
+ }
+ $raw = $now->format('Y-m-d\TH');
+ break;
+
+ case static::FACETAPI_DATE_HOUR:
+ $rounded = new \DateInterval('P' . $interval->y . 'Y' . $interval->m . 'M' . $interval->d . 'DT' . $interval->h . 'H');
+ $display = $interval->y ? $this->formatPlural($interval->y, '1 year', '@count years') . ' ' : '';
+ $display .= $interval->m ? $this->formatPlural($interval->m, '1 month', '@count months') . ' ' : '';
+ $display .= $interval->d ? $this->formatPlural($interval->d, '1 day', '@count days') . ' ' : '';
+ if ($future) {
+ $display .= $interval->h ?
+ $this->formatPlural($interval->h, '1 hour hence', '@count hours hence') :
+ (empty($display) ? $this->t('In the next hour') : $this->t('0 hours hence'));
+ $now->add($rounded);
+ }
+ else {
+ $display .= $interval->h ?
+ $this->formatPlural($interval->h, '1 hour ago', '@count hours ago') :
+ (empty($display) ? $this->t('In the last hour') : $this->t('0 hours ago'));
+ $now->sub($rounded);
+ $now->sub(new \DateInterval('PT1H'));
+ }
+ $raw = $now->format('Y-m-d\TH:i');
+ break;
+
+ case static::FACETAPI_DATE_MINUTE:
+ $rounded = new \DateInterval('P' . $interval->y . 'Y' . $interval->m . 'M' . $interval->d . 'DT' . $interval->h . 'H' . $interval->i);
+ $display = $interval->y ? $this->formatPlural($interval->y, '1 year', '@count years') . ' ' : '';
+ $display .= $interval->m ? $this->formatPlural($interval->m, '1 month', '@count months') . ' ' : '';
+ $display .= $interval->d ? $this->formatPlural($interval->d, '1 day', '@count days') . ' ' : '';
+ $display .= $interval->h ? $this->formatPlural($interval->h, '1 hour', '@count hours') . ' ' : '';
+ if ($future) {
+ $display .= $interval->i ?
+ $this->formatPlural($interval->i, '1 minute hence', '@count minutes hence') :
+ (empty($display) ? $this->t('In the next minute') : $this->t('0 minutes hence'));
+ $now->add($rounded);
+ }
+ else {
+ $display .= $interval->i ?
+ $this->formatPlural($interval->i, '1 minute ago', '@count minutes ago') :
+ (empty($display) ? $this->t('In the last minute') : $this->t('0 minutes ago'));
+ $now->sub($rounded);
+ $now->sub(new \DateInterval('PT1M'));
+ }
+ $raw = $date->format('Y-m-d\TH:i:s');
+ break;
+
+ default:
+ $rounded = new \DateInterval('P' . $interval->y . 'Y' . $interval->m . 'M' . $interval->d . 'DT' . $interval->h . 'H' . $interval->i . $interval->s . 'S');
+ $display = $interval->y ? $this->formatPlural($interval->y, '1 year', '@count years') . ' ' : '';
+ $display .= $interval->m ? $this->formatPlural($interval->m, '1 month', '@count months') . ' ' : '';
+ $display .= $interval->d ? $this->formatPlural($interval->d, '1 day', '@count days') . ' ' : '';
+ $display .= $interval->h ? $this->formatPlural($interval->h, '1 hour', '@count hours') . ' ' : '';
+ $display .= $interval->i ? $this->formatPlural($interval->i, '1 minute', '@count minutes') . ' ' : '';
+ if ($future) {
+ $display .= $interval->s ?
+ $this->formatPlural($interval->s, '1 second hence', '@count seconds hence') :
+ (empty($display) ? $this->t('In the next second') : $this->t('0 secondss hence'));
+ $now->add($rounded);
+ }
+ else {
+ $display .= $interval->s ?
+ $this->formatPlural($interval->s, '1 second ago', '@count seconds ago') :
+ (empty($display) ? $this->t('In the last second') : $this->t('0 seconds ago'));
+ $now->sub($rounded);
+ $now->sub(new \DateInterval('PT1S'));
+ }
+ $raw = $date->format('Y-m-d\TH:i:s');
+ break;
+ }
+
+ return [
+ 'display' => $display,
+ 'raw' => $raw,
+ ];
+ }
+
+ /**
+ * Retrieve configuration: Granularity to use.
+ *
+ * Default behaviour an integer for the steps that the facet works in.
+ *
+ * @return int
+ * The granularity for this config.
+ */
+ protected function getGranularity() {
+ return $this->getConfiguration()['granularity'];
+ }
+
+ /**
+ * Retrieve configuration: Hierarchy.
+ *
+ * @return bool
+ * The hierarchy for this config.
+ */
+ protected function getHierarchy() {
+ return $this->getConfiguration()['hierarchy'];
+ }
+
+ /**
+ * Retrieve configuration: Date Display type.
+ *
+ * @return string
+ * Returns the display mode..
+ */
+ protected function getDateDisplay() {
+ return $this->getConfiguration()['date_display'];
+ }
+
+ /**
+ * Retrieve configuration: Date display format.
+ *
+ * @return string
+ * Returns the format.
+ */
+ protected function getDateFormat() {
+ return $this->getConfiguration()['date_format'];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiGranular.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiGranular.php
new file mode 100644
index 000000000..be77f898e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiGranular.php
@@ -0,0 +1,128 @@
+ $value,
+ 'stop' => (int) $value + $this->getGranularity(),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ // If there were no results or no query object, we can't do anything.
+ if (empty($this->results)) {
+ return $this->facet;
+ }
+
+ $supportedFeatures = array_flip($this->query
+ ->getIndex()
+ ->getServerInstance()
+ ->getBackend()
+ ->getSupportedFeatures());
+
+ // Range grouping is supported.
+ if (isset($supportedFeatures['search_api_granular'])) {
+ $query_operator = $this->facet->getQueryOperator();
+ $facet_results = [];
+ foreach ($this->results as $result) {
+ if ($result['count'] || $query_operator == 'or') {
+ $result_filter = trim($result['filter'], '"');
+ $facet_results[] = new Result($this->facet, $result_filter, $result_filter, $result['count']);
+ }
+ }
+ $this->facet->setResults($facet_results);
+
+ return $this->facet;
+ }
+
+ return parent::build();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateResultFilter($value) {
+ assert($this->getGranularity() > 0);
+
+ $min_value = (int) $this->getMinValue();
+ $max_value = $this->getMaxValue();
+ $granularity = $this->getGranularity();
+
+ if ($value < $min_value || (!empty($max_value) && $value > ($max_value + $granularity - 1))) {
+ return FALSE;
+ }
+
+ return [
+ 'display' => $value - fmod($value - $min_value, $this->getGranularity()),
+ 'raw' => $value - fmod($value - $min_value, $this->getGranularity()),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getFacetOptions() {
+ return $this->facet->getProcessors()['granularity_item']->getConfiguration()
+ + parent::getFacetOptions();
+ }
+
+ /**
+ * Looks at the configuration for this facet to determine the granularity.
+ *
+ * Default behaviour an integer for the steps that the facet works in.
+ *
+ * @return int
+ * If not an integer the inheriting class needs to deal with calculations.
+ */
+ protected function getGranularity() {
+ return $this->facet->getProcessors()['granularity_item']->getConfiguration()['granularity'];
+ }
+
+ /**
+ * Looks at the configuration for this facet to determine the min value.
+ *
+ * Default behaviour an integer for the minimum value of the facets.
+ *
+ * @return mixed
+ * It can be a number or an empty value.
+ */
+ protected function getMinValue() {
+ return $this->facet->getProcessors()['granularity_item']->getConfiguration()['min_value'];
+ }
+
+ /**
+ * Looks at the configuration for this facet to determine the max value.
+ *
+ * Default behaviour an integer for the maximum value of the facets.
+ *
+ * @return mixed
+ * It can be a number or an empty value.
+ */
+ protected function getMaxValue() {
+ return $this->facet->getProcessors()['granularity_item']->getConfiguration()['max_value'];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiRange.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiRange.php
new file mode 100644
index 000000000..e3863b6c8
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiRange.php
@@ -0,0 +1,76 @@
+query;
+
+ // Only alter the query when there's an actual query object to alter.
+ if (!empty($query)) {
+ $operator = $this->facet->getQueryOperator();
+ $field_identifier = $this->facet->getFieldIdentifier();
+ $exclude = $this->facet->getExclude();
+
+ if ($query->getProcessingLevel() === QueryInterface::PROCESSING_FULL) {
+ // Set the options for the actual query.
+ $options = &$query->getOptions();
+ $options['search_api_facets'][$field_identifier] = $this->getFacetOptions();
+ }
+
+ // Add the filter to the query if there are active values.
+ $active_items = $this->facet->getActiveItems();
+
+ if (count($active_items)) {
+ $filter = $query->createConditionGroup($operator, ['facet:' . $field_identifier]);
+ foreach ($active_items as $value) {
+ $filter->addCondition($field_identifier, $value, $exclude ? 'NOT BETWEEN' : 'BETWEEN');
+ }
+ $query->addConditionGroup($filter);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ $query_operator = $this->facet->getQueryOperator();
+
+ if (!empty($this->results)) {
+ $facet_results = [];
+ foreach ($this->results as $result) {
+ if ($result['count'] || $query_operator == 'or') {
+ $count = $result['count'];
+ while (is_array($result['filter'])) {
+ $result['filter'] = current($result['filter']);
+ }
+ $result_filter = trim($result['filter'], '"');
+ $result = new Result($this->facet, $result_filter, $result_filter, $count);
+ $facet_results[] = $result;
+ }
+ }
+ $this->facet->setResults($facet_results);
+ }
+ return $this->facet;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiString.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiString.php
new file mode 100644
index 000000000..edbf66cf4
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/query_type/SearchApiString.php
@@ -0,0 +1,85 @@
+query;
+
+ // Only alter the query when there's an actual query object to alter.
+ if (!empty($query)) {
+ $operator = $this->facet->getQueryOperator();
+ $field_identifier = $this->facet->getFieldIdentifier();
+ $exclude = $this->facet->getExclude();
+
+ if ($query->getProcessingLevel() === QueryInterface::PROCESSING_FULL) {
+ // Set the options for the actual query.
+ $options = &$query->getOptions();
+ $options['search_api_facets'][$field_identifier] = $this->getFacetOptions();
+ }
+
+ // Add the filter to the query if there are active values.
+ $active_items = $this->facet->getActiveItems();
+
+ if (count($active_items)) {
+ $filter = $query->createConditionGroup($operator, ['facet:' . $field_identifier]);
+ foreach ($active_items as $value) {
+ $filter->addCondition($this->facet->getFieldIdentifier(), $value, $exclude ? '<>' : '=');
+ }
+ $query->addConditionGroup($filter);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ $query_operator = $this->facet->getQueryOperator();
+
+ if (!empty($this->results)) {
+ $facet_results = [];
+ foreach ($this->results as $result) {
+ if ($result['count'] || $query_operator == 'or') {
+ $result_filter = $result['filter'];
+ if ($result_filter[0] === '"') {
+ $result_filter = substr($result_filter, 1);
+ }
+ if ($result_filter[strlen($result_filter) - 1] === '"') {
+ $result_filter = substr($result_filter, 0, -1);
+ }
+ $count = $result['count'];
+ $result = new Result($this->facet, $result_filter, $result_filter, $count);
+ $facet_results[] = $result;
+ }
+ }
+ $this->facet->setResults($facet_results);
+ }
+
+ return $this->facet;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/url_processor/QueryString.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/url_processor/QueryString.php
new file mode 100644
index 000000000..bedb7445f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/url_processor/QueryString.php
@@ -0,0 +1,420 @@
+eventDispatcher = $eventDispatcher;
+ $this->initializeActiveFilters();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('request_stack')->getCurrentRequest(),
+ $container->get('entity_type.manager'),
+ $container->get('event_dispatcher')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildUrls(FacetInterface $facet, array $results) {
+ // No results are found for this facet, so don't try to create urls.
+ if (empty($results)) {
+ return [];
+ }
+
+ // First get the current list of get parameters.
+ $get_params = $this->request->query;
+
+ // When adding/removing a filter the number of pages may have changed,
+ // possibly resulting in an invalid page parameter.
+ if ($get_params->has('page')) {
+ $current_page = $get_params->get('page');
+ $get_params->remove('page');
+ }
+
+ // Set the url alias from the facet object.
+ $this->urlAlias = $facet->getUrlAlias();
+
+ $facet_source_path = $facet->getFacetSource()->getPath();
+ $request = $this->getRequestByFacetSourcePath($facet_source_path);
+ $requestUrl = $this->getUrlForRequest($facet_source_path, $request);
+
+ $original_filter_params = [];
+ foreach ($this->getActiveFilters() as $facet_id => $values) {
+ $values = array_filter($values, static function ($it) {
+ return $it !== NULL;
+ });
+ foreach ($values as $value) {
+ $original_filter_params[] = $this->getUrlAliasByFacetId($facet_id, $facet->getFacetSourceId()) . ":" . $value;
+ }
+ }
+
+ /** @var \Drupal\facets\Result\ResultInterface[] $results */
+ foreach ($results as &$result) {
+ // Reset the URL for each result.
+ $url = clone $requestUrl;
+
+ // Sets the url for children.
+ if ($children = $result->getChildren()) {
+ $this->buildUrls($facet, $children);
+ }
+
+ if ($result->getRawValue() === NULL) {
+ $filter_string = NULL;
+ }
+ else {
+ $filter_string = $this->urlAlias . $this->getSeparator() . $result->getRawValue();
+ }
+ $result_get_params = clone $get_params;
+
+ $filter_params = $original_filter_params;
+
+ // If the value is active, remove the filter string from the parameters.
+ if ($result->isActive()) {
+ foreach ($filter_params as $key => $filter_param) {
+ if ($filter_param == $filter_string) {
+ unset($filter_params[$key]);
+ }
+ }
+ if ($facet->getUseHierarchy()) {
+ $id = $result->getRawValue();
+
+ // Disable child filters.
+ foreach ($facet->getHierarchyInstance()->getNestedChildIds($id) as $child_id) {
+ $filter_params = array_diff($filter_params, [$this->urlAlias . $this->getSeparator() . $child_id]);
+ }
+ if ($facet->getEnableParentWhenChildGetsDisabled()) {
+ // Enable parent id again if exists.
+ $parent_ids = $facet->getHierarchyInstance()->getParentIds($id);
+ if (isset($parent_ids[0]) && $parent_ids[0]) {
+ // Get the parents children.
+ $child_ids = $facet->getHierarchyInstance()->getNestedChildIds($parent_ids[0]);
+
+ // Check if there are active siblings.
+ $active_sibling = FALSE;
+ if ($child_ids) {
+ foreach ($results as $result2) {
+ if ($result2->isActive() && $result2->getRawValue() != $id && in_array($result2->getRawValue(), $child_ids)) {
+ $active_sibling = TRUE;
+ continue;
+ }
+ }
+ }
+ if (!$active_sibling) {
+ $filter_params[] = $this->urlAlias . $this->getSeparator() . $parent_ids[0];
+ }
+ }
+ }
+ }
+
+ }
+ // If the value is not active, add the filter string.
+ else {
+ if ($filter_string !== NULL) {
+ $filter_params[] = $filter_string;
+ }
+
+ $parents_and_child_ids = [];
+ if ($facet->getUseHierarchy()) {
+ $parent_ids = $facet->getHierarchyInstance()->getParentIds($result->getRawValue());
+ $child_ids = $facet->getHierarchyInstance()->getNestedChildIds($result->getRawValue());
+ $parents_and_child_ids = array_merge($parent_ids, $child_ids);
+
+ if (!$facet->getKeepHierarchyParentsActive()) {
+ // If hierarchy is active, unset parent trail and every child when
+ // building the enable-link to ensure those are not enabled anymore.
+ foreach ($parents_and_child_ids as $id) {
+ $filter_params = array_diff($filter_params, [$this->urlAlias . $this->getSeparator() . $id]);
+ }
+ }
+ }
+
+ // Exclude currently active results from the filter params if we are in
+ // the show_only_one_result mode.
+ if ($facet->getShowOnlyOneResult()) {
+ foreach ($results as $result2) {
+ if ($result2->isActive()) {
+ $id = $result2->getRawValue();
+ if (!in_array($id, $parents_and_child_ids)) {
+ $active_filter_string = $this->urlAlias . $this->getSeparator() . $id;
+ foreach ($filter_params as $key2 => $filter_param2) {
+ if ($filter_param2 == $active_filter_string) {
+ unset($filter_params[$key2]);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Allow other modules to alter the result url built.
+ $event = new QueryStringCreated($result_get_params, $filter_params, $result, $this->activeFilters, $facet);
+ $this->eventDispatcher->dispatch($event);
+ $filter_params = $event->getFilterParameters();
+
+ asort($filter_params, \SORT_NATURAL);
+ $result_get_params->set($this->filterKey, array_values($filter_params));
+
+ if ($result_get_params->all() !== [$this->filterKey => []]) {
+ $new_url_params = $result_get_params->all();
+
+ if (empty($new_url_params[$this->filterKey])) {
+ unset($new_url_params[$this->filterKey]);
+ }
+
+ // Facet links should be page-less.
+ // See https://www.drupal.org/node/2898189.
+ unset($new_url_params['page']);
+
+ // Remove core wrapper format (e.g. render-as-ajax-response) paremeters.
+ unset($new_url_params[MainContentViewSubscriber::WRAPPER_FORMAT]);
+
+ // Set the new url parameters.
+ $url->setOption('query', $new_url_params);
+ }
+
+ $result->setUrl($url);
+ }
+
+ // Restore page parameter again. See https://www.drupal.org/node/2726455.
+ if (isset($current_page)) {
+ $get_params->set('page', $current_page);
+ }
+ return $results;
+ }
+
+ /**
+ * Gets a request object based on the facet source path.
+ *
+ * If the facet's source has a path, we construct a request object based on
+ * that path, as it may be different than the current request's. This method
+ * statically caches the request object based on the facet source path so that
+ * subsequent calls to this processer do not recreate the same request object.
+ *
+ * @param string $facet_source_path
+ * The facet source path.
+ *
+ * @return \Symfony\Component\HttpFoundation\Request
+ * The request.
+ */
+ protected function getRequestByFacetSourcePath($facet_source_path) {
+ $requestsByPath = &drupal_static(__CLASS__ . __FUNCTION__, []);
+ if (!$facet_source_path) {
+ return $this->request;
+ }
+
+ if (array_key_exists($facet_source_path, $requestsByPath)) {
+ return $requestsByPath[$facet_source_path];
+ }
+
+ $request = Request::create($facet_source_path);
+ $request->attributes->set('_format', $this->request->get('_format'));
+ $requestsByPath[$facet_source_path] = $request;
+ return $request;
+ }
+
+ /**
+ * Gets the URL object for a request.
+ *
+ * This method statically caches the URL object for a request based on the
+ * facet source path. This reduces subsequent calls to the processor from
+ * having to regenerate the URL object.
+ *
+ * @param string $facet_source_path
+ * The facet source path.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The request.
+ *
+ * @return \Drupal\Core\Url
+ * The URL.
+ */
+ protected function getUrlForRequest($facet_source_path, Request $request) {
+ /** @var \Drupal\Core\Url[] $requestUrlsByPath */
+ $requestUrlsByPath = &drupal_static(__CLASS__ . __FUNCTION__, []);
+
+ if (array_key_exists($facet_source_path, $requestUrlsByPath)) {
+ return $requestUrlsByPath[$facet_source_path];
+ }
+
+ // Try to grab any route params from the original request.
+ // In case of request path not having a matching route, Url generator will
+ // fail with.
+ try {
+ $requestUrl = Url::createFromRequest($request);
+ }
+ catch (ResourceNotFoundException $e) {
+ // Bypass exception if no path available.
+ // Should be unreachable in default FacetSource implementations,
+ // but you never know.
+ if (!$facet_source_path) {
+ throw $e;
+ }
+
+ $requestUrl = Url::fromUserInput($facet_source_path, [
+ 'query' => [
+ '_format' => $this->request->get('_format'),
+ ],
+ ]);
+ }
+
+ $requestUrl->setOption('attributes', ['rel' => 'nofollow']);
+ $requestUrlsByPath[$facet_source_path] = $requestUrl;
+ return $requestUrl;
+ }
+
+ /**
+ * Initializes the active filters from the request query.
+ *
+ * Get all the filters that are active by checking the request query and store
+ * them in activeFilters which is an array where key is the facet id and value
+ * is an array of raw values.
+ */
+ protected function initializeActiveFilters() {
+ $url_parameters = $this->request->query;
+
+ // Get the active facet parameters.
+ $active_params = $url_parameters->get($this->filterKey, [], TRUE);
+ $facet_source_id = $this->configuration['facet']->getFacetSourceId();
+
+ // When an invalid parameter is passed in the url, we can't do anything.
+ if (!is_array($active_params)) {
+ return;
+ }
+
+ $active_filters = [];
+ // Explode the active params on the separator.
+ foreach ($active_params as $param) {
+ $explosion = explode($this->getSeparator(), $param);
+ $url_alias = array_shift($explosion);
+ if ($facet_id = $this->getFacetIdByUrlAlias($url_alias, $facet_source_id)) {
+ $value = '';
+ while (count($explosion) > 0) {
+ $value .= array_shift($explosion);
+ if (count($explosion) > 0) {
+ $value .= $this->getSeparator();
+ }
+ }
+ if (!isset($active_filters[$facet_id])) {
+ $active_filters[$facet_id] = [$value];
+ }
+ else {
+ $active_filters[$facet_id][] = $value;
+ }
+ }
+ }
+
+ // Allow other modules to alter the parsed active filters.
+ $event = new ActiveFiltersParsed($facet_source_id, $active_filters, $url_parameters, $this->filterKey);
+ $this->eventDispatcher->dispatch($event);
+ $this->activeFilters = $event->getActiveFilters();
+ }
+
+ /**
+ * Gets the facet id from the url alias & facet source id.
+ *
+ * @param string $url_alias
+ * The url alias.
+ * @param string $facet_source_id
+ * The facet source id.
+ *
+ * @return bool|string
+ * Either the facet id, or FALSE if that can't be loaded.
+ */
+ protected function getFacetIdByUrlAlias($url_alias, $facet_source_id) {
+ $mapping = &drupal_static(__FUNCTION__);
+ if (!isset($mapping[$facet_source_id][$url_alias])) {
+ $storage = $this->entityTypeManager->getStorage('facets_facet');
+ $facet = current($storage->loadByProperties(
+ [
+ 'url_alias' => $url_alias,
+ 'facet_source_id' => $facet_source_id,
+ ]
+ ));
+ if (!$facet) {
+ return NULL;
+ }
+ $mapping[$facet_source_id][$url_alias] = $facet->id();
+ }
+ return $mapping[$facet_source_id][$url_alias];
+ }
+
+ /**
+ * Gets the url alias from the facet id & facet source id.
+ *
+ * @param string $facet_id
+ * The facet id.
+ * @param string $facet_source_id
+ * The facet source id.
+ *
+ * @return bool|string
+ * Either the url alias, or FALSE if that can't be loaded.
+ */
+ protected function getUrlAliasByFacetId($facet_id, $facet_source_id) {
+ $mapping = &drupal_static(__FUNCTION__);
+ if (!isset($mapping[$facet_source_id][$facet_id])) {
+ $storage = $this->entityTypeManager->getStorage('facets_facet');
+ $facet = current($storage->loadByProperties(
+ [
+ 'id' => $facet_id,
+ 'facet_source_id' => $facet_source_id,
+ ]
+ ));
+ if (!$facet) {
+ return FALSE;
+ }
+ $mapping[$facet_source_id][$facet_id] = $facet->getUrlAlias();
+ }
+ return $mapping[$facet_source_id][$facet_id];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/ArrayWidget.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/ArrayWidget.php
new file mode 100644
index 000000000..a276f8dd3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/ArrayWidget.php
@@ -0,0 +1,103 @@
+getResults();
+
+ $configuration = $facet->getWidget();
+ $this->showNumbers = !empty($configuration['show_numbers']);
+
+ return [
+ $facet->getFieldIdentifier() => $this->buildOneLevel($results),
+ ];
+ }
+
+ /**
+ * Builds one level from results.
+ *
+ * @param \Drupal\facets\Result\ResultInterface[] $results
+ * A list of results.
+ *
+ * @return array
+ * Generated build.
+ */
+ protected function buildOneLevel(array $results): array {
+ $items = [];
+
+ foreach ($results as $result) {
+ if (is_null($result->getUrl())) {
+ $items[] = $this->generateValues($result);
+ }
+ else {
+ $item = $this->prepare($result);
+ if ($children = $result->getChildren()) {
+ // @todo This is a useless nesting.
+ $item['children'][] = $this->buildOneLevel($children);
+ }
+ $items[] = $item;
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * Prepares the URL and values for the facet.
+ *
+ * @param \Drupal\facets\Result\ResultInterface $result
+ * A result item.
+ *
+ * @return array
+ * The results.
+ */
+ protected function prepare(ResultInterface $result) {
+ return [
+ 'url' => $result->getUrl()->setAbsolute()->toString(),
+ 'raw_value' => $result->getRawValue(),
+ 'values' => $this->generateValues($result),
+ ];
+ }
+
+ /**
+ * Generates the value and the url.
+ *
+ * @param \Drupal\facets\Result\ResultInterface $result
+ * The result to extract the values.
+ *
+ * @return array
+ * The values.
+ */
+ protected function generateValues(ResultInterface $result) {
+ $values['value'] = $result->getDisplayValue();
+
+ if ($this->getConfiguration()['show_numbers'] && $result->getCount() !== FALSE) {
+ $values['count'] = $result->getCount();
+ }
+
+ if ($result->isActive()) {
+ $values['active'] = 'true';
+ }
+
+ return $values;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/CheckboxWidget.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/CheckboxWidget.php
new file mode 100644
index 000000000..207c1f19b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/CheckboxWidget.php
@@ -0,0 +1,24 @@
+ 'Choose',
+ ] + parent::defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet) {
+ $build = parent::build($facet);
+ $build['#attributes']['class'][] = 'js-facets-dropdown-links';
+ $build['#attached']['drupalSettings']['facets']['dropdown_widget'][$facet->id()]['facet-default-option-label'] = $this->getConfiguration()['default_option_label'];
+ $build['#attached']['library'][] = 'facets/drupal.facets.dropdown-widget';
+ $build['#attached']['library'][] = 'facets/drupal.facets.general';
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $config = $this->getConfiguration();
+
+ $message = $this->t('To achieve the standard behavior of a dropdown, you need to enable the facet setting below "Ensure that only one result can be displayed" .');
+ $form['warning'] = [
+ '#markup' => '' . $message . '
',
+ ];
+
+ $form += parent::buildConfigurationForm($form, $form_state, $facet);
+
+ $form['default_option_label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Default option label'),
+ '#default_value' => $config['default_option_label'],
+ ];
+
+ return $form;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/LinksWidget.php b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/LinksWidget.php
new file mode 100644
index 000000000..54fbfa9c6
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Plugin/facets/widget/LinksWidget.php
@@ -0,0 +1,196 @@
+ 0,
+ 'soft_limit_settings' => [
+ 'show_less_label' => 'Show less',
+ 'show_more_label' => 'Show more',
+ ],
+ 'show_reset_link' => FALSE,
+ 'hide_reset_when_no_selection' => FALSE,
+ 'reset_text' => $this->t('Show all'),
+ ] + parent::defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet) {
+ $build = parent::build($facet);
+ $this->appendWidgetLibrary($build);
+ $soft_limit = (int) $this->getConfiguration()['soft_limit'];
+ if ($soft_limit !== 0) {
+ $show_less_label = $this->getConfiguration()['soft_limit_settings']['show_less_label'];
+ $show_more_label = $this->getConfiguration()['soft_limit_settings']['show_more_label'];
+ $build['#attached']['library'][] = 'facets/soft-limit';
+ $build['#attached']['drupalSettings']['facets']['softLimit'][$facet->id()] = $soft_limit;
+ $build['#attached']['drupalSettings']['facets']['softLimitSettings'][$facet->id()]['showLessLabel'] = $show_less_label;
+ $build['#attached']['drupalSettings']['facets']['softLimitSettings'][$facet->id()]['showMoreLabel'] = $show_more_label;
+ }
+ if ($facet->getUseHierarchy()) {
+ $build['#attached']['library'][] = 'facets/drupal.facets.hierarchical';
+ }
+
+ $results = $facet->getResults();
+ if ($this->getConfiguration()['show_reset_link'] && count($results) > 0 && (!$this->getConfiguration()['hide_reset_when_no_selection'] || $facet->getActiveItems())) {
+ // Add reset link.
+ $max_items = array_sum(array_map(function ($item) {
+ return $item->getCount();
+ }, $results));
+
+ $urlProcessorManager = \Drupal::service('plugin.manager.facets.url_processor');
+ $url_processor = $urlProcessorManager->createInstance($facet->getFacetSourceConfig()->getUrlProcessorName(), ['facet' => $facet]);
+ $active_filters = $url_processor->getActiveFilters();
+
+ unset($active_filters[$facet->id()]);
+
+ // Only if there are still active filters, use url generator.
+ if ($active_filters) {
+ $url = \Drupal::service('facets.utility.url_generator')
+ ->getUrl($active_filters, FALSE);
+ }
+ else {
+ $request = \Drupal::request();
+ $url = Url::createFromRequest($request);
+ $params = $request->query->all();
+ unset($params[$url_processor->getFilterKey()]);
+ if (\array_key_exists('page', $params)) {
+ // Go back to the first page on reset.
+ unset($params['page']);
+ }
+ $url->setRouteParameter('facets_query', '');
+ $url->setOption('query', $params);
+ }
+
+ $result_item = new Result($facet, 'reset_all', $this->getConfiguration()['reset_text'], $max_items);
+ $result_item->setActiveState(FALSE);
+ $result_item->setUrl($url);
+
+ // Check if any other facet is in use.
+ $none_active = TRUE;
+ foreach ($results as $result) {
+ if ($result->isActive() || $result->hasActiveChildren()) {
+ $none_active = FALSE;
+ break;
+ }
+ }
+
+ // Add an is-active class when no other facet is in use.
+ if ($none_active) {
+ $result_item->setActiveState(TRUE);
+ }
+
+ // Build item.
+ $item = $this->buildListItems($facet, $result_item);
+
+ // Add a class for the reset link wrapper.
+ $item['#wrapper_attributes']['class'][] = 'facets-reset';
+
+ // Put reset facet link on first place.
+ array_unshift($build['#items'], $item);
+ }
+
+ return $build;
+ }
+
+ /**
+ * Appends widget library and relevant information for it to build array.
+ *
+ * @param array $build
+ * Reference to build array.
+ */
+ protected function appendWidgetLibrary(array &$build) {
+ $build['#attached']['library'][] = 'facets/drupal.facets.link-widget';
+ $build['#attributes']['class'][] = 'js-facets-links';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $form = parent::buildConfigurationForm($form, $form_state, $facet);
+
+ $options = [50, 40, 30, 20, 15, 10, 5, 3];
+ $form['soft_limit'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Soft limit'),
+ '#default_value' => $this->getConfiguration()['soft_limit'],
+ '#options' => [0 => $this->t('No limit')] + array_combine($options, $options),
+ '#description' => $this->t('Limit the number of displayed facets via JavaScript.'),
+ ];
+ $form['soft_limit_settings'] = [
+ '#type' => 'container',
+ '#title' => $this->t('Soft limit settings'),
+ '#states' => [
+ 'invisible' => [':input[name="widget_config[soft_limit]"]' => ['value' => 0]],
+ ],
+ ];
+ $form['soft_limit_settings']['show_less_label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Show less label'),
+ '#description' => $this->t('This text will be used for "Show less" link.'),
+ '#default_value' => $this->getConfiguration()['soft_limit_settings']['show_less_label'],
+ '#states' => [
+ 'optional' => [':input[name="widget_config[soft_limit]"]' => ['value' => 0]],
+ ],
+ ];
+ $form['soft_limit_settings']['show_more_label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Show more label'),
+ '#description' => $this->t('This text will be used for "Show more" link.'),
+ '#default_value' => $this->getConfiguration()['soft_limit_settings']['show_more_label'],
+ '#states' => [
+ 'optional' => [':input[name="widget_config[soft_limit]"]' => ['value' => 0]],
+ ],
+ ];
+
+ $form['show_reset_link'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show reset link'),
+ '#default_value' => $this->getConfiguration()['show_reset_link'],
+ ];
+ $form['reset_text'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Reset text'),
+ '#default_value' => $this->getConfiguration()['reset_text'],
+ '#states' => [
+ 'visible' => [
+ ':input[name="widget_config[show_reset_link]"]' => ['checked' => TRUE],
+ ],
+ 'required' => [
+ ':input[name="widget_config[show_reset_link]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['hide_reset_when_no_selection'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Hide reset link when no facet item is selected'),
+ '#default_value' => $this->getConfiguration()['hide_reset_when_no_selection'],
+ ];
+ return $form;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Processor/BuildProcessorInterface.php b/frontend/drupal9/web/modules/contrib/facets/src/Processor/BuildProcessorInterface.php
new file mode 100644
index 000000000..fab3d39d4
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Processor/BuildProcessorInterface.php
@@ -0,0 +1,25 @@
+setConfiguration($form_state->getValues());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsStage($stage_identifier) {
+ $plugin_definition = $this->getPluginDefinition();
+ return isset($plugin_definition['stages'][$stage_identifier]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefaultWeight($stage) {
+ $plugin_definition = $this->getPluginDefinition();
+ return isset($plugin_definition['stages'][$stage]) ? (int) $plugin_definition['stages'][$stage] : 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLocked() {
+ return !empty($this->pluginDefinition['locked']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isHidden() {
+ return !empty($this->pluginDefinition['hidden']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ $plugin_definition = $this->getPluginDefinition();
+ return isset($plugin_definition['description']) ? $plugin_definition['description'] : '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ unset($this->configuration['facet']);
+ return $this->configuration + $this->defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = $configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $this->addDependency('module', $this->getPluginDefinition()['provider']);
+ return $this->dependencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFacet(FacetInterface $facet) {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryType() {
+ return NULL;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Processor/ProcessorPluginManager.php b/frontend/drupal9/web/modules/contrib/facets/src/Processor/ProcessorPluginManager.php
new file mode 100644
index 000000000..edc626983
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Processor/ProcessorPluginManager.php
@@ -0,0 +1,62 @@
+setCacheBackend($cache_backend, 'facets_processors');
+ $this->setStringTranslation($translation);
+ }
+
+ /**
+ * Retrieves information about the available processing stages.
+ *
+ * These are then used by processors in their "stages" definition to specify
+ * in which stages they will run.
+ *
+ * @return array
+ * An associative array mapping stage identifiers to information about that
+ * stage. The information itself is an associative array with the following
+ * keys:
+ * - label: The translated label for this stage.
+ */
+ public function getProcessingStages() {
+ return [
+ ProcessorInterface::STAGE_PRE_QUERY => [
+ 'label' => $this->t('Pre query stage'),
+ ],
+ ProcessorInterface::STAGE_POST_QUERY => [
+ 'label' => $this->t('Post query stage'),
+ ],
+ ProcessorInterface::STAGE_BUILD => [
+ 'label' => $this->t('Build stage'),
+ ],
+ ProcessorInterface::STAGE_SORT => [
+ 'label' => $this->t('Sort stage'),
+ ],
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Processor/SortProcessorInterface.php b/frontend/drupal9/web/modules/contrib/facets/src/Processor/SortProcessorInterface.php
new file mode 100644
index 000000000..113108cb7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Processor/SortProcessorInterface.php
@@ -0,0 +1,25 @@
+getProcessors();
+ $config = isset($processors[$this->getPluginId()]) ? $processors[$this->getPluginId()] : NULL;
+
+ $build['sort'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Sort order'),
+ '#options' => [
+ 'ASC' => $this->t('Ascending'),
+ 'DESC' => $this->t('Descending'),
+ ],
+ '#default_value' => !is_null($config) ? $config->getConfiguration()['sort'] : $this->defaultConfiguration()['sort'],
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return ['sort' => 'ASC'];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypeInterface.php b/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypeInterface.php
new file mode 100644
index 000000000..746e1f52a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypeInterface.php
@@ -0,0 +1,20 @@
+query = $this->configuration['query'];
+ $this->facet = $this->configuration['facet'];
+ $this->results = !empty($this->configuration['results']) ? $this->configuration['results'] : [];
+ }
+
+ /**
+ * The backend native query object.
+ *
+ * @var \Drupal\search_api\Query\Query
+ */
+ protected $query;
+
+ /**
+ * The facet that needs the query type.
+ *
+ * @var \Drupal\facets\FacetInterface
+ */
+ protected $facet;
+
+ /**
+ * The results for the facet.
+ *
+ * @var array[]
+ */
+ protected $results;
+
+ /**
+ * The injected link generator.
+ *
+ * @var \Drupal\Core\Utility\LinkGeneratorInterface
+ */
+ protected $linkGenerator;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ return $this->configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = $configuration + $this->defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $this->addDependency('module', $this->getPluginDefinition()['provider']);
+ return $this->dependencies;
+ }
+
+ /**
+ * Builds facet options that will be send to the backend.
+ *
+ * @return array
+ * An array of default options for the facet.
+ */
+ protected function getFacetOptions() {
+ return [
+ 'field' => $this->facet->getFieldIdentifier(),
+ 'limit' => $this->facet->getHardLimit(),
+ 'operator' => $this->facet->getQueryOperator(),
+ 'min_count' => $this->facet->getMinCount(),
+ 'missing' => FALSE,
+ 'query_type' => $this->getPluginId(),
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypePluginManager.php b/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypePluginManager.php
new file mode 100644
index 000000000..6bdf7f2c9
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypePluginManager.php
@@ -0,0 +1,23 @@
+setCacheBackend($cache_backend, 'facet_query_type_plugins');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypeRangeBase.php b/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypeRangeBase.php
new file mode 100644
index 000000000..dd5e3cf6d
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/QueryType/QueryTypeRangeBase.php
@@ -0,0 +1,105 @@
+query;
+
+ // Alter the query here.
+ if (!empty($query)) {
+ $options = &$query->getOptions();
+
+ $operator = $this->facet->getQueryOperator();
+ $field_identifier = $this->facet->getFieldIdentifier();
+ $exclude = $this->facet->getExclude();
+ $options['search_api_facets'][$field_identifier] = $this->getFacetOptions();
+
+ // Add the filter to the query if there are active values.
+ $active_items = $this->facet->getActiveItems();
+ $filter = $query->createConditionGroup($operator, ['facet:' . $field_identifier]);
+ if (count($active_items)) {
+ foreach ($active_items as $value) {
+ $range = $this->calculateRange($value);
+
+ $conjunction = $exclude ? 'OR' : 'AND';
+ $item_filter = $query->createConditionGroup($conjunction, ['facet:' . $field_identifier]);
+ $item_filter->addCondition($this->facet->getFieldIdentifier(), $range['start'], $exclude ? '<' : '>=');
+ $item_filter->addCondition($this->facet->getFieldIdentifier(), $range['stop'], $exclude ? '>' : '<=');
+
+ $filter->addConditionGroup($item_filter);
+ }
+ $query->addConditionGroup($filter);
+ }
+ }
+ }
+
+ /**
+ * Calculate the range for a given facet filter value.
+ *
+ * Used when adding active items in self::execute() to $this->query to include
+ * the range conditions for the value.
+ *
+ * @param string $value
+ * The raw value for the facet filter.
+ *
+ * @return array
+ * Keyed with 'start' and 'stop' values.
+ */
+ abstract public function calculateRange($value);
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ // If there were no results or no query object, we can't do anything.
+ if (empty($this->results)) {
+ return $this->facet;
+ }
+
+ $query_operator = $this->facet->getQueryOperator();
+ $facet_results = [];
+ foreach ($this->results as $result) {
+ // Go through the results and add facet results grouped by filters
+ // defined by self::calculateResultFilter().
+ if ($result['count'] || $query_operator == 'or') {
+ $count = $result['count'];
+ if ($result_filter = $this->calculateResultFilter(trim($result['filter'], '"'))) {
+ if (isset($facet_results[$result_filter['raw']])) {
+ $facet_results[$result_filter['raw']]->setCount(
+ $facet_results[$result_filter['raw']]->getCount() + $count
+ );
+ }
+ else {
+ $facet_results[$result_filter['raw']] = new Result($this->facet, $result_filter['raw'], $result_filter['display'], $count);
+ }
+ }
+ }
+ }
+
+ $this->facet->setResults($facet_results);
+ return $this->facet;
+ }
+
+ /**
+ * Calculate the grouped facet filter for a given value.
+ *
+ * @param string $value
+ * The raw value for the facet before grouping.
+ *
+ * @return array
+ * Keyed by 'display' value to be shown to the user, and 'raw' to be used
+ * for the url.
+ */
+ abstract public function calculateResultFilter($value);
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Result/Result.php b/frontend/drupal9/web/modules/contrib/facets/src/Result/Result.php
new file mode 100644
index 000000000..434606ea7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Result/Result.php
@@ -0,0 +1,180 @@
+facet = $facet;
+ $this->rawValue = $raw_value;
+ $this->displayValue = $display_value;
+ $this->count = (int) $count;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDisplayValue() {
+ return $this->displayValue;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRawValue() {
+ return $this->rawValue;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCount() {
+ return $this->count;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCount($count) {
+ $this->count = (int) $count;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUrl() {
+ return $this->url;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUrl(Url $url) {
+ $this->url = $url;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setActiveState($active) {
+ $this->active = $active;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isActive() {
+ return $this->active;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDisplayValue($display_value) {
+ $this->displayValue = $display_value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setChildren(array $children) {
+ $this->children = $children;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChildren() {
+ return $this->children;
+ }
+
+ /**
+ * Returns true if the value has active children(selected).
+ *
+ * @return bool
+ * A boolean indicating the active state of children.
+ */
+ public function hasActiveChildren() {
+ foreach ($this->getChildren() as $child) {
+ if ($child->isActive() || $child->hasActiveChildren()) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFacet() {
+ return $this->facet;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Result/ResultInterface.php b/frontend/drupal9/web/modules/contrib/facets/src/Result/ResultInterface.php
new file mode 100644
index 000000000..a68fe979e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Result/ResultInterface.php
@@ -0,0 +1,116 @@
+filterKey;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSeparator() {
+ return $this->separator;
+ }
+
+ /**
+ * Constructs a new instance of the class.
+ *
+ * @param array $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin_id for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * A request object for the current request.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The Entity Type Manager.
+ *
+ * @throws \Drupal\facets\Exception\InvalidProcessorException
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->request = clone $request;
+ $this->entityTypeManager = $entity_type_manager;
+
+ if (!isset($configuration['facet'])) {
+ throw new InvalidProcessorException("The url processor doesn't have the required 'facet' in the configuration array.");
+ }
+
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = $configuration['facet'];
+
+ /** @var \Drupal\facets\FacetSourceInterface $facet_source_config */
+ $facet_source_config = $facet->getFacetSourceConfig();
+
+ $this->filterKey = $facet_source_config->getFilterKey() ?: 'f';
+
+ // Set the separator to the predefined colon char but override if passed
+ // along as part of the plugin configuration.
+ $this->separator = ':';
+ if (isset($configuration['separator'])) {
+ $this->separator = $configuration['separator'];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ $request_stack = $container->get('request_stack');
+
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ // Support 9.3+.
+ // @todo remove switch after 9.3 or greater is required.
+ version_compare(\Drupal::VERSION, '9.3', '>=') ? $request_stack->getMainRequest() : $request_stack->getMasterRequest(),
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getActiveFilters() {
+ return $this->activeFilters;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setActiveFilters(array $active_filters) {
+ $this->activeFilters = $active_filters;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setActiveItems(FacetInterface $facet) {
+ // Get the filter key of the facet.
+ if (isset($this->activeFilters[$facet->id()])) {
+ foreach ($this->activeFilters[$facet->id()] as $value) {
+ $facet->setActiveItem(trim($value, '"'));
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRequest(): Request {
+ return $this->request;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/UrlProcessor/UrlProcessorPluginManager.php b/frontend/drupal9/web/modules/contrib/facets/src/UrlProcessor/UrlProcessorPluginManager.php
new file mode 100644
index 000000000..68b23efe8
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/UrlProcessor/UrlProcessorPluginManager.php
@@ -0,0 +1,32 @@
+alterInfo('facets_url_processors_info');
+ $this->setCacheBackend($cache_backend, 'facets_url_processors');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Utility/FacetsDateHandler.php b/frontend/drupal9/web/modules/contrib/facets/src/Utility/FacetsDateHandler.php
new file mode 100644
index 000000000..8ba55429f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Utility/FacetsDateHandler.php
@@ -0,0 +1,426 @@
+dateFormatter = $date_formatter;
+ }
+
+ /**
+ * Converts dates from Unix timestamps into ISO 8601 format.
+ *
+ * @param int $timestamp
+ * An integer containing the Unix timestamp being converted.
+ * @param string $gap
+ * A string containing the gap, see FACETS_DATE_* constants for valid
+ * values. Defaults to FACETS_DATE_SECOND.
+ *
+ * @return string
+ * A string containing the date in ISO 8601 format.
+ */
+ public function isoDate($timestamp, $gap = 'SECOND') {
+ switch ($gap) {
+ case static::FACETS_DATE_SECOND:
+ $format = static::FACETS_DATE_ISO8601;
+ break;
+
+ case static::FACETS_DATE_MINUTE:
+ $format = 'Y-m-d\TH:i:00\Z';
+ break;
+
+ case static::FACETS_DATE_HOUR:
+ $format = 'Y-m-d\TH:00:00\Z';
+ break;
+
+ case static::FACETS_DATE_DAY:
+ $format = 'Y-m-d\T00:00:00\Z';
+ break;
+
+ case static::FACETS_DATE_MONTH:
+ $format = 'Y-m-01\T00:00:00\Z';
+ break;
+
+ case static::FACETS_DATE_YEAR:
+ $format = 'Y-01-01\T00:00:00\Z';
+ break;
+
+ default:
+ $format = static::FACETS_DATE_ISO8601;
+ break;
+ }
+ return gmdate($format, $timestamp);
+ }
+
+ /**
+ * Return a date gap one increment smaller than the one passed.
+ *
+ * @param string $gap
+ * A string containing the gap, see FACETS_DATE_* constants for valid
+ * values.
+ * @param string $min_gap
+ * A string containing the minimum gap that can be returned, defaults to
+ * FACETS_DATE_SECOND. This is useful for defining the smallest increment
+ * that can be used in a date drilldown.
+ *
+ * @return string
+ * A string containing the smaller date gap, NULL if there is no smaller
+ * gap. See FACETS_DATE_* constants for valid values.
+ */
+ public function getNextDateGap($gap, $min_gap = self::FACETS_DATE_SECOND) {
+ // Array of numbers used to determine whether the next gap is smaller than
+ // the minimum gap allowed in the drilldown.
+ $gap_numbers = [
+ static::FACETS_DATE_YEAR => 6,
+ static::FACETS_DATE_MONTH => 5,
+ static::FACETS_DATE_DAY => 4,
+ static::FACETS_DATE_HOUR => 3,
+ static::FACETS_DATE_MINUTE => 2,
+ static::FACETS_DATE_SECOND => 1,
+ ];
+
+ // Gets gap numbers for both the gap and minimum gap, checks if the next gap
+ // is within the limit set by the $min_gap parameter.
+ $gap_num = isset($gap_numbers[$gap]) ? $gap_numbers[$gap] : 6;
+ $min_num = isset($gap_numbers[$min_gap]) ? $gap_numbers[$min_gap] : 1;
+ return ($gap_num > $min_num) ? array_search($gap_num - 1, $gap_numbers) : $min_gap;
+ }
+
+ /**
+ * Determines the best search gap to use for an arbitrary date range.
+ *
+ * Generally, we use the maximum gap that fits between the start and end date.
+ * If they are more than a year apart, 1 year; if they are more than a month
+ * apart, 1 month; etc.
+ *
+ * This function uses Unix timestamps for its computation and so is not useful
+ * for dates outside that range.
+ *
+ * @param int $start_time
+ * A string containing the start date as an ISO date string.
+ * @param int $end_time
+ * A string containing the end date as an ISO date string.
+ * @param string|null $min_gap
+ * (Optional) The minimum gap that should be returned.
+ *
+ * @return string
+ * A string containing the gap, see FACETS_DATE_* constants for valid
+ * values. Returns FALSE of either of the dates cannot be converted to a
+ * timestamp.
+ */
+ public function getTimestampGap($start_time, $end_time, $min_gap = NULL) {
+ $time_diff = $end_time - $start_time;
+ switch (TRUE) {
+ case ($time_diff >= 31536000):
+ $gap = static::FACETS_DATE_YEAR;
+ break;
+
+ case ($time_diff >= 86400 * gmdate('t', $start_time)):
+ $gap = static::FACETS_DATE_MONTH;
+ break;
+
+ case ($time_diff >= 86400):
+ $gap = static::FACETS_DATE_DAY;
+ break;
+
+ case ($time_diff >= 3600):
+ $gap = static::FACETS_DATE_HOUR;
+ break;
+
+ case ($time_diff >= 60):
+ $gap = static::FACETS_DATE_MINUTE;
+ break;
+
+ default:
+ $gap = static::FACETS_DATE_SECOND;
+ break;
+ }
+
+ // Return the calculated gap if a minimum gap was not passed of the
+ // calculated gap is a larger interval than the minimum gap.
+ if (is_null($min_gap) || $this->gapCompare($gap, $min_gap) >= 0) {
+ return $gap;
+ }
+ else {
+ return $min_gap;
+ }
+ }
+
+ /**
+ * Converts ISO date strings to Unix timestamps.
+ *
+ * Passes values to the FACETS_get_timestamp_gap() function to calculate the
+ * gap.
+ *
+ * @param string $start_date
+ * A string containing the start date as an ISO date string.
+ * @param string $end_date
+ * A string containing the end date as an ISO date string.
+ * @param string|null $min_gap
+ * (Optional) The minimum gap that should be returned.
+ *
+ * @return string
+ * A string containing the gap, see FACETS_DATE_* constants for valid
+ * values. Returns FALSE of either of the dates cannot be converted to a
+ * timestamp.
+ *
+ * @see FACETS_get_timestamp_gap()
+ */
+ public function getDateGap($start_date, $end_date, $min_gap = NULL) {
+ $range = [strtotime($start_date), strtotime($end_date)];
+ if (!in_array(FALSE, $range, TRUE)) {
+ return $this->getTimestampGap($range[0], $range[1], $min_gap);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Returns a formatted date based on the passed timestamp and gap.
+ *
+ * This function assumes that gaps less than one day will be displayed in a
+ * search context in which a larger containing gap including a day is already
+ * displayed. So, HOUR, MINUTE, and SECOND gaps only display time information,
+ * without date.
+ *
+ * @param int $timestamp
+ * An integer containing the Unix timestamp.
+ * @param string $gap
+ * A string containing the gap, see FACETS_DATE_* constants for valid
+ * values, defaults to YEAR.
+ *
+ * @return string
+ * A gap-appropriate display date used in the facet link.
+ */
+ public function formatTimestamp($timestamp, $gap = self::FACETS_DATE_YEAR) {
+ switch ($gap) {
+ case static::FACETS_DATE_MONTH:
+ return $this->dateFormatter->format($timestamp, 'custom', 'F Y', 'UTC');
+
+ case static::FACETS_DATE_DAY:
+ return $this->dateFormatter->format($timestamp, 'custom', 'F j, Y', 'UTC');
+
+ case static::FACETS_DATE_HOUR:
+ return $this->dateFormatter->format($timestamp, 'custom', 'g A', 'UTC');
+
+ case static::FACETS_DATE_MINUTE:
+ return $this->dateFormatter->format($timestamp, 'custom', 'g:i A', 'UTC');
+
+ case static::FACETS_DATE_SECOND:
+ return $this->dateFormatter->format($timestamp, 'custom', 'g:i:s A', 'UTC');
+
+ default:
+ return $this->dateFormatter->format($timestamp, 'custom', 'Y', 'UTC');
+ }
+ }
+
+ /**
+ * Returns a formatted date based on the passed ISO date string and gap.
+ *
+ * @param string $date
+ * A string containing the date as an ISO date string.
+ * @param int $gap
+ * An integer containing the gap, see FACETS_DATE_* constants for valid
+ * values, defaults to YEAR.
+ * @param string $callback
+ * The formatting callback, defaults to "FACETS_format_timestamp". This is
+ * a string that can be called as a valid callback.
+ *
+ * @return string
+ * A gap-appropriate display date used in the facet link.
+ *
+ * @see FACETS_format_timestamp()
+ */
+ public function formatDate($date, $gap = self::FACETS_DATE_YEAR, $callback = 'facets_format_timestamp') {
+ $timestamp = strtotime($date);
+ return $callback($timestamp, $gap);
+ }
+
+ /**
+ * Returns the next increment from the given ISO date and gap.
+ *
+ * This function is useful for getting the upper limit of a date range from
+ * the given start date.
+ *
+ * @param string $date
+ * A string containing the date as an ISO date string.
+ * @param string $gap
+ * A string containing the gap, see FACETS_DATE_* constants for valid
+ * values, defaults to YEAR.
+ *
+ * @return string
+ * A string containing the date, FALSE if the passed date could not be
+ * parsed.
+ */
+ public function getNextDateIncrement($date, $gap) {
+ if (preg_match(static::FACETS_REGEX_DATE, $date, $match)) {
+
+ // Increments the timestamp.
+ switch ($gap) {
+ case static::FACETS_DATE_MONTH:
+ $match[2] += 1;
+ break;
+
+ case static::FACETS_DATE_DAY:
+ $match[3] += 1;
+ break;
+
+ case static::FACETS_DATE_HOUR:
+ $match[4] += 1;
+ break;
+
+ case static::FACETS_DATE_MINUTE:
+ $match[5] += 1;
+ break;
+
+ case static::FACETS_DATE_SECOND:
+ $match[6] += 1;
+ break;
+
+ default:
+ $match[1] += 1;
+ break;
+
+ }
+
+ // Gets the next increment.
+ return $this->isoDate(
+ gmmktime($match[4], $match[5], $match[6], $match[2], $match[3], $match[1])
+ );
+ }
+ return FALSE;
+ }
+
+ /**
+ * Compares two timestamp gaps.
+ *
+ * @param int $gap1
+ * An integer containing the gap, see FACETS_DATE_* constants for valid
+ * values.
+ * @param int $gap2
+ * An integer containing the gap, see FACETS_DATE_* constants for valid
+ * values.
+ *
+ * @return int
+ * Returns -1 if gap1 is less than gap2, 1 if gap1 is greater than gap2, and
+ * 0 if they are equal.
+ */
+ public function gapCompare($gap1, $gap2) {
+ $gap_numbers = [
+ static::FACETS_DATE_YEAR => 6,
+ static::FACETS_DATE_MONTH => 5,
+ static::FACETS_DATE_DAY => 4,
+ static::FACETS_DATE_HOUR => 3,
+ static::FACETS_DATE_MINUTE => 2,
+ static::FACETS_DATE_SECOND => 1,
+ ];
+
+ $gap1_num = isset($gap_numbers[$gap1]) ? $gap_numbers[$gap1] : 6;
+ $gap2_num = isset($gap_numbers[$gap2]) ? $gap_numbers[$gap2] : 6;
+
+ if ($gap1_num == $gap2_num) {
+ return 0;
+ }
+ else {
+ return ($gap1_num < $gap2_num) ? -1 : 1;
+ }
+ }
+
+ /**
+ * Extracts "start" and "end" dates from an active item.
+ *
+ * @param string $item
+ * The active item to extract the dates.
+ *
+ * @return mixed
+ * Returns FALSE if no item found and an array with the dates if the dates
+ * were extracted as expected.
+ */
+ public function extractActiveItems($item) {
+ $active_item = [];
+ if (preg_match(static::FACETS_REGEX_DATE_RANGE, $item, $matches)) {
+
+ $active_item['start'] = [
+ 'timestamp' => strtotime($matches[1]),
+ 'iso' => $matches[1],
+ ];
+
+ $active_item['end'] = [
+ 'timestamp' => strtotime($matches[8]),
+ 'iso' => $matches[8],
+ ];
+
+ return $active_item;
+ }
+ return FALSE;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Utility/FacetsUrlGenerator.php b/frontend/drupal9/web/modules/contrib/facets/src/Utility/FacetsUrlGenerator.php
new file mode 100644
index 000000000..3a28181fa
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Utility/FacetsUrlGenerator.php
@@ -0,0 +1,112 @@
+urlProcessorPluginManager = $urlProcessorPluginManager;
+ $this->facetStorage = $entityTypeManager->getStorage('facets_facet');
+ }
+
+ /**
+ * Returns an URL object for a facet path.
+ *
+ * Example implementations:
+ * @code
+ * // Example to generate URL for 1 facet with 1 value.
+ * \Drupal::service('facets.utility.url_generator')->getUrl(['tags' => [7]]);
+ * // Example with multiple active filters.
+ * $active_filters = ['tags' => [5, 7], 'color' => ['blue']];
+ * \Drupal::service('facets.utility.url_generator')->getUrl($active_filters);
+ * @endcode
+ *
+ * @param array $active_filters
+ * An array containing the active filters with key being the facet id and
+ * value being an array of raw values.
+ * @param bool $keep_active
+ * TRUE if the currently active facets should be included to the URL or
+ * FALSE if they should be discarded. Defaults to TRUE.
+ *
+ * @return \Drupal\Core\Url|null
+ * A Url object for the given facet/value combination or null if no Result
+ * was returned by the UrlProcessor.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function getUrl(array $active_filters, $keep_active = TRUE) {
+ // We use the first defined facet to load the url processor. As all facets
+ // should be from the same facet source, this is fine.
+ // This is because we don't support generating a url over multiple facet
+ // sources.
+ if (empty($active_filters)) {
+ throw new \InvalidArgumentException("The active filters passed in are invalid. They should look like: ['facet_id' => ['value1', 'value2']]");
+ }
+
+ $facet_id = key($active_filters);
+ if (!is_array($active_filters[$facet_id])) {
+ throw new \InvalidArgumentException("The active filters passed in are invalid. They should look like: [$facet_id => ['value1', 'value2']]");
+ }
+
+ $facet = $this->facetStorage->load($facet_id);
+ if ($facet === NULL) {
+ throw new \InvalidArgumentException("The Facet $facet_id could not be loaded.");
+ }
+
+ // We need one raw value to build a Result. If we have the raw value in the
+ // already active filters, it will be removed in the final result. So
+ // instead we copy the value into a variable and unset it from the list.
+ $raw_value = $active_filters[$facet_id][0];
+ unset($active_filters[$facet_id][0]);
+
+ /** @var \Drupal\facets\UrlProcessor\UrlProcessorInterface $url_processor */
+ $url_processor = $this
+ ->urlProcessorPluginManager
+ ->createInstance($facet->getFacetSourceConfig()
+ ->getUrlProcessorName(), ['facet' => $facet]);
+
+ if ($keep_active) {
+ $active_filters = array_merge_recursive($active_filters, $url_processor->getActiveFilters());
+ }
+ $url_processor->setActiveFilters($active_filters);
+
+ // Use the url processor to create a result and return that item's url.
+ $results = [new Result($facet, $raw_value, '', 0)];
+ $processed_results = $url_processor->buildUrls($facet, $results);
+ $result = reset($processed_results);
+ if ($result) {
+ return $result->getUrl();
+ }
+ return NULL;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Widget/WidgetPluginBase.php b/frontend/drupal9/web/modules/contrib/facets/src/Widget/WidgetPluginBase.php
new file mode 100644
index 000000000..2cbfb1f67
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Widget/WidgetPluginBase.php
@@ -0,0 +1,269 @@
+setConfiguration($configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(FacetInterface $facet) {
+ $this->facet = $facet;
+
+ $items = array_map(function (Result $result) use ($facet) {
+ if (empty($result->getUrl())) {
+ return $this->buildResultItem($result);
+ }
+ else {
+ // When the facet is being build in an AJAX request, and the facetsource
+ // is a block, we need to update the url to use the current request url.
+ if ($result->getUrl()->isRouted() && $result->getUrl()->getRouteName() === 'facets.block.ajax') {
+ $request = \Drupal::request();
+ $url_object = \Drupal::service('path.validator')
+ ->getUrlIfValid($request->getPathInfo());
+ if ($url_object) {
+ $url = $result->getUrl();
+ $options = $url->getOptions();
+ $route_params = $url_object->getRouteParameters();
+ $route_name = $url_object->getRouteName();
+ $result->setUrl(new Url($route_name, $route_params, $options));
+ }
+ }
+
+ return $this->buildListItems($facet, $result);
+ }
+ }, $facet->getResults());
+
+ $widget = $facet->getWidget();
+
+ return [
+ '#theme' => $this->getFacetItemListThemeHook($facet),
+ '#facet' => $facet,
+ '#items' => $items,
+ '#attributes' => [
+ 'data-drupal-facet-id' => $facet->id(),
+ 'data-drupal-facet-alias' => $facet->getUrlAlias(),
+ 'class' => [$facet->getActiveItems() ? 'facet-active' : 'facet-inactive'],
+ ],
+ '#context' => !empty($widget['type']) ? ['list_style' => $widget['type']] : [],
+ '#cache' => [
+ 'contexts' => [
+ 'url.path',
+ 'url.query_args',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Provides a full array of possible theme functions to try for a given hook.
+ *
+ * This allows the following template suggestions:
+ * - facets-item-list--WIDGET_TYPE--FACET_ID
+ * - facets-item-list--WIDGET_TYPE
+ * - facets-item-list.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet whose output is being generated.
+ *
+ * @return string
+ * A theme hook name with suggestions, suitable for the #theme property.
+ */
+ protected function getFacetItemListThemeHook(FacetInterface $facet) {
+ $type = $facet->getWidget()['type'] ?? 'std';
+ return 'facets_item_list__' . $type . '__' . $facet->id();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return ['show_numbers' => FALSE];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = NestedArray::mergeDeep(
+ $this->defaultConfiguration(),
+ $configuration
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ return $this->configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryType() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
+ $form['show_numbers'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show the amount of results'),
+ '#default_value' => $this->getConfiguration()['show_numbers'],
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ return [];
+ }
+
+ /**
+ * Builds a renderable array of result items.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet we need to build.
+ * @param \Drupal\facets\Result\ResultInterface $result
+ * A result item.
+ *
+ * @return array
+ * A renderable array of the result.
+ */
+ protected function buildListItems(FacetInterface $facet, ResultInterface $result) {
+ $classes = ['facet-item'];
+ $items = $this->prepareLink($result);
+
+ $children = $result->getChildren();
+ // Check if we need to expand this result.
+ if ($children && ($this->facet->getExpandHierarchy() || $result->isActive() || $result->hasActiveChildren())) {
+
+ $child_items = [];
+ $classes[] = 'facet-item--expanded';
+ foreach ($children as $child) {
+ $child_items[] = $this->buildListItems($facet, $child);
+ }
+
+ $items['children'] = [
+ '#theme' => $this->getFacetItemListThemeHook($facet),
+ '#items' => $child_items,
+ ];
+
+ if ($result->hasActiveChildren()) {
+ $classes[] = 'facet-item--active-trail';
+ }
+
+ }
+ else {
+ if ($children) {
+ $classes[] = 'facet-item--collapsed';
+ }
+ }
+
+ if ($result->isActive()) {
+ $items['#attributes']['class'][] = 'is-active';
+ }
+
+ $items['#wrapper_attributes'] = ['class' => $classes];
+ $items['#attributes']['data-drupal-facet-item-id'] = Html::getClass($this->facet->getUrlAlias() . '-' . strtr($result->getRawValue(), ' \'\"', '---'));
+ $items['#attributes']['data-drupal-facet-item-value'] = $result->getRawValue();
+ $items['#attributes']['data-drupal-facet-item-count'] = $result->getCount();
+ return $items;
+ }
+
+ /**
+ * Returns the text or link for an item.
+ *
+ * @param \Drupal\facets\Result\ResultInterface $result
+ * A result item.
+ *
+ * @return array
+ * The item as a render array.
+ */
+ protected function prepareLink(ResultInterface $result) {
+ $item = $this->buildResultItem($result);
+
+ if (!is_null($result->getUrl())) {
+ $item = (new Link($item, $result->getUrl()))->toRenderable();
+ }
+
+ return $item;
+ }
+
+ /**
+ * Builds a facet result item.
+ *
+ * @param \Drupal\facets\Result\ResultInterface $result
+ * The result item.
+ *
+ * @return array
+ * The facet result item as a render array.
+ */
+ protected function buildResultItem(ResultInterface $result) {
+ $count = $result->getCount();
+ return [
+ '#theme' => 'facets_result_item',
+ '#is_active' => $result->isActive(),
+ '#value' => $result->getDisplayValue(),
+ '#show_count' => $this->getConfiguration()['show_numbers'] && ($count !== NULL),
+ '#count' => $count,
+ '#facet' => $result->getFacet(),
+ '#raw_value' => $result->getRawValue(),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isPropertyRequired($name, $type) {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFacet(FacetInterface $facet) {
+ return TRUE;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/src/Widget/WidgetPluginInterface.php b/frontend/drupal9/web/modules/contrib/facets/src/Widget/WidgetPluginInterface.php
new file mode 100644
index 000000000..2b4c788ea
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/src/Widget/WidgetPluginInterface.php
@@ -0,0 +1,83 @@
+alterInfo('widget_plugin_info');
+ $this->setCacheBackend($cache_backend, 'facet_widget_plugins');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/templates/facets-item-list.html.twig b/frontend/drupal9/web/modules/contrib/facets/templates/facets-item-list.html.twig
new file mode 100644
index 000000000..7fc9e444a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/templates/facets-item-list.html.twig
@@ -0,0 +1,50 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a facets item list.
+ *
+ * Available variables:
+ * - items: A list of items. Each item contains:
+ * - attributes: HTML attributes to be applied to each list item.
+ * - value: The content of the list element.
+ * - title: The title of the list.
+ * - list_type: The tag for list element ("ul" or "ol").
+ * - wrapper_attributes: HTML attributes to be applied to the list wrapper.
+ * - attributes: HTML attributes to be applied to the list.
+ * - empty: A message to display when there are no items. Allowed value is a
+ * string or render array.
+ * - context: A list of contextual data associated with the list. May contain:
+ * - list_style: The ID of the widget plugin this facet uses.
+ * - facet: The facet for this result item.
+ * - id: the machine name for the facet.
+ * - label: The facet label.
+ *
+ * @see facets_preprocess_facets_item_list()
+ *
+ * @ingroup themeable
+ */
+#}
+
+ {% if facet.widget.type %}
+ {%- set attributes = attributes.addClass('item-list__' ~ facet.widget.type) %}
+ {% endif %}
+ {% if items or empty %}
+ {%- if title is not empty -%}
+
{{ title }}
+ {%- endif -%}
+
+ {%- if items -%}
+ <{{ list_type }}{{ attributes }}>
+ {%- for item in items -%}
+ {{ item.value }}
+ {%- endfor -%}
+ {{ list_type }}>
+ {%- else -%}
+ {{- empty -}}
+ {%- endif -%}
+ {%- endif %}
+
+{% if facet.widget.type == "dropdown" %}
+ {{ 'Facet'|t }} {{ facet.label }}
+{%- endif %}
+
diff --git a/frontend/drupal9/web/modules/contrib/facets/templates/facets-result-item.html.twig b/frontend/drupal9/web/modules/contrib/facets/templates/facets-result-item.html.twig
new file mode 100644
index 000000000..1fb7d3de2
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/templates/facets-result-item.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @file
+ * Default theme implementation of a facet result item.
+ *
+ * Available variables:
+ * - value: The item value.
+ * - raw_value: The raw item value.
+ * - show_count: If this facet provides count.
+ * - count: The amount of results.
+ * - is_active: The item is active.
+ * - facet: The facet for this result item.
+ * - id: the machine name for the facet.
+ * - label: The facet label.
+ *
+ * @ingroup themeable
+ */
+#}
+{% if is_active %}
+ (-)
+{% endif %}
+{{ value }}
+{% if show_count %}
+ ({{ count }})
+{% endif %}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/config/schema/facets_custom_widget.schema.yml b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/config/schema/facets_custom_widget.schema.yml
new file mode 100644
index 000000000..61fa27b38
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/config/schema/facets_custom_widget.schema.yml
@@ -0,0 +1,10 @@
+plugin.plugin_configuration.facets_processor.invalid_qt:
+ type: config_object
+
+plugin.plugin_configuration.facets_processor.test_pre_query:
+ type: mapping
+ label: Configuration
+ mapping:
+ test_value:
+ type: label
+ label: The value used for testing
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/facets_custom_widget.info.yml b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/facets_custom_widget.info.yml
new file mode 100644
index 000000000..866ad784a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/facets_custom_widget.info.yml
@@ -0,0 +1,11 @@
+name: 'Facets custom widget'
+type: module
+description: 'Facets custom widget'
+package: 'Testing'
+hidden: true
+core_version_requirement: ^9.2 || ^10.0
+
+# Information added by Drupal.org packaging script on 2022-04-04
+version: '2.0.2'
+project: 'facets'
+datestamp: 1649070272
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/processor/InvalidQT.php b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/processor/InvalidQT.php
new file mode 100644
index 000000000..55fcd481a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/processor/InvalidQT.php
@@ -0,0 +1,37 @@
+ [
+ '#type' => 'textfield',
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQueryType() {
+ return 'string';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preQuery(FacetInterface $facet) {
+ \Drupal::messenger()->addMessage($this->getConfiguration()['test_value']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFacet(FacetInterface $facet) {
+ return \Drupal::state()->get('facets_test_supports_facet', TRUE);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/widget/CustomWidget.php b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/widget/CustomWidget.php
new file mode 100644
index 000000000..9b7186ff6
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/widget/CustomWidget.php
@@ -0,0 +1,40 @@
+get('facets_test_supports_facet', TRUE);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/widget/WidgetDateQT.php b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/widget/WidgetDateQT.php
new file mode 100644
index 000000000..c9e50a4e4
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/facets_custom_widget/src/Plugin/facets/widget/WidgetDateQT.php
@@ -0,0 +1,25 @@
+ 'queryStringCreated',
+ ];
+ }
+
+ /**
+ * Event handler for the query string created event.
+ *
+ * @param \Drupal\facets\Event\QueryStringCreated $event
+ * The query string created event.
+ */
+ public function queryStringCreated(QueryStringCreated $event) {
+ $event->getQueryParameters()->add(['test' => 'fun']);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/facets_query_processor.info.yml b/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/facets_query_processor.info.yml
new file mode 100644
index 000000000..901b23d36
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/facets_query_processor.info.yml
@@ -0,0 +1,11 @@
+name: 'Facets query processor'
+type: module
+description: 'Facets query processor'
+package: 'Testing'
+hidden: true
+core_version_requirement: ^9.2 || ^10.0
+
+# Information added by Drupal.org packaging script on 2022-04-04
+version: '2.0.2'
+project: 'facets'
+datestamp: 1649070272
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/src/Plugin/Block/DisplayGeneratedLinkBlock.php b/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/src/Plugin/Block/DisplayGeneratedLinkBlock.php
new file mode 100644
index 000000000..a12d18f43
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/src/Plugin/Block/DisplayGeneratedLinkBlock.php
@@ -0,0 +1,79 @@
+urlGeneratorService = $facets_url_generator;
+ $this->state = $state;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('facets.utility.url_generator'),
+ $container->get('state')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ $url = $this->urlGeneratorService->getUrl(['owl' => ['item']], $this->state->get('facets_url_generator_keep_active', FALSE));
+ $link = new Link('Link to owl item', $url);
+
+ return $link->toRenderable() + ['#cache' => ['max-age' => 0]];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/src/Plugin/facets/url_processor/DummyQuery.php b/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/src/Plugin/facets/url_processor/DummyQuery.php
new file mode 100644
index 000000000..9bb268ba7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/facets_query_processor/src/Plugin/facets/url_processor/DummyQuery.php
@@ -0,0 +1,30 @@
+drupalLogin($this->adminUser);
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+
+ foreach ([7 => 'Owl', 8 => 'Robin', 9 => 'Hawk'] as $i => $value) {
+ $this->users[$i] = User::create([
+ 'uid' => $i,
+ 'name' => "User $value",
+ ]);
+ $this->users[$i]->save();
+
+ $this->entities[$i] = EntityTestMulRevChanged::create([
+ 'id' => $i,
+ 'user_id' => $i,
+ 'name' => "Test entity $value name",
+ 'body' => "Test entity $value body",
+ ]);
+ $this->entities[$i]->save();
+ }
+
+ $plugin_creation_helper = \Drupal::getContainer()->get('search_api.plugin_helper');
+ $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load($this->indexId);
+
+ // Add the user as a datasource.
+ $index->addDatasource($plugin_creation_helper->createDatasourcePlugin($index, 'entity:user'));
+
+ // Create the aggregated field property.
+ $property = AggregatedFieldProperty::create('string');
+
+ // Add and configure the aggregated field.
+ $field = $fields_helper->createFieldFromProperty($index, $property, NULL, 'aggregated_field', 'aggregated_field', 'string');
+ $field->setLabel('Aggregated field');
+ $field->setConfiguration([
+ 'type' => 'union',
+ 'fields' => [
+ Utility::createCombinedId('entity:entity_test_mulrev_changed', 'name'),
+ Utility::createCombinedId('entity:user', 'name'),
+ ],
+ ]);
+ $index->addField($field);
+ $index->save();
+
+ // Index all items, users and content.
+ $this->assertEquals(16, $this->indexItems($this->indexId));
+ }
+
+ /**
+ * Tests aggregated fields.
+ *
+ * @see https://www.drupal.org/node/2917323
+ */
+ public function testAggregatedField() {
+ $facet_id = 'test_agg';
+
+ // Go to the Add facet page and make sure that returns a 200.
+ $facet_add_page = '/admin/config/search/facets/add-facet';
+ $this->drupalGet($facet_add_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ $form_values = [
+ 'name' => 'Test agg',
+ 'id' => $facet_id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'facet_source_configs[search_api:views_page__search_api_test_view__page_1][field_identifier]' => 'aggregated_field',
+ ];
+
+ // Try filling out the form, and configure it to use the aggregated field.
+ $this->submitForm(['facet_source_id' => 'search_api:views_page__search_api_test_view__page_1'], 'Configure facet source');
+ $this->submitForm($form_values, 'Save');
+
+ // Check that nothing breaks.
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/BlockTestTrait.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/BlockTestTrait.php
new file mode 100644
index 000000000..748b8e2bd
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/BlockTestTrait.php
@@ -0,0 +1,98 @@
+ $id,
+ 'name' => $name,
+ 'weight' => 0,
+ 'use_hierarchy' => FALSE,
+ 'hierarchy' => ['type' => 'taxonomy', 'config' => []],
+ ]);
+ $facet->setFacetSourceId($facet_source);
+ $facet->setFieldIdentifier($field);
+ $facet->setUrlAlias($id);
+ $facet->setWidget('links', ['show_numbers' => TRUE]);
+ $facet->addProcessor([
+ 'processor_id' => 'url_processor_handler',
+ 'weights' => ['pre_query' => -10, 'build' => -10],
+ 'settings' => [],
+ ]);
+ $facet->setEmptyBehavior(['behavior' => 'none']);
+ $facet->setOnlyVisibleWhenFacetSourceIsVisible(TRUE);
+ $facet->save();
+
+ if ($allowBlockCreation) {
+ $this->blocks[$id] = $this->createBlock($id);
+ }
+ }
+
+ /**
+ * Creates a facet block by id.
+ *
+ * @param string $id
+ * The id of the block.
+ *
+ * @return \Drupal\block\Entity\Block
+ * The block entity.
+ */
+ protected function createBlock($id) {
+ $block = [
+ 'region' => 'footer',
+ 'id' => str_replace('_', '-', $id),
+ ];
+ return $this->drupalPlaceBlock('facet_block:' . $id, $block);
+ }
+
+ /**
+ * Deletes a facet block by id.
+ *
+ * @param string $id
+ * The id of the block.
+ */
+ protected function deleteBlock($id) {
+ // Delete a facet block through the UI, the text for the success message has
+ // changed in Drupal::VERSION 9.3.
+ $orig_success_message = 'The block ' . $this->blocks[$id]->label() . ' has been removed' . (\Drupal::VERSION >= 9.3 ? ' from the Footer region' : '') . '.';
+
+ $this->drupalGet('admin/structure/block/manage/' . $this->blocks[$id]->id(), ['query' => ['destination' => 'admin']]);
+ $this->clickLink('Remove block');
+ $this->submitForm([], 'Remove');
+ $this->assertSession()->pageTextContains($orig_success_message);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/BreadcrumbIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/BreadcrumbIntegrationTest.php
new file mode 100644
index 000000000..7aa9b48b0
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/BreadcrumbIntegrationTest.php
@@ -0,0 +1,176 @@
+drupalLogin($this->adminUser);
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals($this->indexItems($this->indexId), 5, '5 items were indexed.');
+
+ $block = [
+ 'region' => 'footer',
+ 'label' => 'Breadcrumbs',
+ 'provider' => 'system',
+ ];
+ $this->drupalPlaceBlock('system_breadcrumb_block', $block);
+ $this->resetAll();
+ }
+
+ /**
+ * Tests Breadcrumb integration with grouping.
+ */
+ public function testGroupingIntegration() {
+ $this->editFacetConfig();
+ $id = 'keywords';
+ $this->createFacet('Keywords', $id, 'keywords');
+ $this->resetAll();
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+
+ $id = 'type';
+ $this->createFacet('Type', $id);
+ $this->resetAll();
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->submitForm(['facet_settings[weight]' => '1'], 'Save');
+
+ // Test with a default filter key.
+ $this->editFacetConfig(['filter_key' => 'f']);
+ $this->breadcrumbTest();
+
+ // Test with an empty filter key.
+ $this->editFacetConfig(['filter_key' => '']);
+ $this->breadcrumbTest();
+
+ // Test with a specific filter key.
+ $this->editFacetConfig(['filter_key' => 'my_filter_key']);
+ $this->breadcrumbTest();
+ }
+
+ /**
+ * Tests Breadcrumb integration without grouping.
+ */
+ public function testNonGroupingIntegration() {
+ $this->markTestSkipped('Not yet implemented.');
+ }
+
+ /**
+ * Tests enabling + disabling the breadcrumb label prefix.
+ */
+ public function testBreadcrumbLabel() {
+ $id = 'type';
+ $this->createFacet('Type', $id);
+ $this->resetAll();
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->submitForm(['facet_settings[weight]' => '1'], 'Save');
+ $this->editFacetConfig(['breadcrumb[before]' => FALSE]);
+
+ $initial_query = ['search_api_fulltext' => 'foo'];
+ $this->drupalGet('search-api-test-fulltext', ['query' => $initial_query]);
+
+ $this->clickLink('item');
+ $breadcrumb = $this->getSession()->getPage()->find('css', '.breadcrumb');
+ $this->assertFalse(strpos($breadcrumb->getText(), 'Type'));
+ $breadcrumb->findLink('item');
+
+ $this->editFacetConfig(['breadcrumb[before]' => TRUE]);
+
+ $initial_query = ['search_api_fulltext' => 'foo'];
+ $this->drupalGet('search-api-test-fulltext', ['query' => $initial_query]);
+ $this->clickLink('item');
+ $breadcrumb = $this->getSession()->getPage()->find('css', '.breadcrumb');
+ $this->assertNotFalse(strpos($breadcrumb->getText(), 'Type'));
+ }
+
+ /**
+ * Edit the facet configuration with the given values.
+ *
+ * @param array $config
+ * The new configuration for the facet.
+ */
+ protected function editFacetConfig(array $config = []) {
+ $this->drupalGet('admin/config/search/facets');
+ $this->clickLink('Configure', 1);
+ $default_config = [
+ 'filter_key' => 'f',
+ 'url_processor' => 'query_string',
+ 'breadcrumb[active]' => TRUE,
+ 'breadcrumb[group]' => TRUE,
+ ];
+ $edit = array_merge($default_config, $config);
+ $this->submitForm($edit, 'Save');
+ }
+
+ /**
+ * Tests Breadcrumb with the given config.
+ */
+ protected function breadcrumbTest() {
+ // Breadcrumb should show Keywords: orange > Type: article, item.
+ $initial_query = ['search_api_fulltext' => 'foo', 'test_param' => 1];
+ $this->drupalGet('search-api-test-fulltext', ['query' => $initial_query]);
+
+ $this->clickLink('item');
+ $this->assertSession()->linkExists('Type: item');
+
+ $this->clickLink('article');
+ $this->assertSession()->linkExists('Type: article, item');
+
+ $this->clickLink('orange');
+ $this->assertSession()->linkExists('Keywords: orange');
+ $this->assertSession()->linkExists('Type: article, item');
+
+ $this->clickLink('Type: article, item');
+
+ $this->assertSession()->linkExists('Keywords: orange');
+ $this->assertSession()->linkExists('Type: article, item');
+ $this->checkFacetIsActive('orange');
+ $this->checkFacetIsActive('item');
+ $this->checkFacetIsActive('article');
+
+ $this->clickLink('Keywords: orange');
+ $this->assertSession()->linkExists('Keywords: orange');
+ $this->assertSession()->linkNotExists('Type: article, item');
+ $this->checkFacetIsActive('orange');
+ $this->checkFacetIsNotActive('item');
+ $this->checkFacetIsNotActive('article');
+
+ // Check that the current url still has the initial parameters.
+ $curr_url = UrlHelper::parse($this->getUrl());
+ foreach ($initial_query as $key => $value) {
+ $this->assertArrayHasKey($key, $curr_url['query']);
+ $this->assertEquals($value, $curr_url['query'][$key]);
+ }
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/ExampleContentTrait.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/ExampleContentTrait.php
new file mode 100644
index 000000000..27bca933f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/ExampleContentTrait.php
@@ -0,0 +1,96 @@
+count()
+ ->execute();
+
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ $this->entities[1] = $entity_test_storage->create([
+ 'name' => 'foo bar baz',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ ]);
+ $this->entities[1]->save();
+ $this->entities[2] = $entity_test_storage->create([
+ 'name' => 'foo test',
+ 'body' => 'bar test',
+ 'type' => 'item',
+ 'keywords' => ['orange', 'apple', 'grape'],
+ 'category' => 'item_category',
+ ]);
+ $this->entities[2]->save();
+ $this->entities[3] = $entity_test_storage->create([
+ 'name' => 'bar',
+ 'body' => 'test foobar',
+ 'type' => 'item',
+ ]);
+ $this->entities[3]->save();
+ $this->entities[4] = $entity_test_storage->create([
+ 'name' => 'foo baz',
+ 'body' => 'test test test',
+ 'type' => 'article',
+ 'keywords' => ['apple', 'strawberry', 'grape'],
+ 'category' => 'article_category',
+ ]);
+ $this->entities[4]->save();
+ $this->entities[5] = $entity_test_storage->create([
+ 'name' => 'bar baz',
+ 'body' => 'foo',
+ 'type' => 'article',
+ 'keywords' => ['orange', 'strawberry', 'grape', 'banana'],
+ 'category' => 'article_category',
+ ]);
+ $this->entities[5]->save();
+ $count = \Drupal::entityQuery('entity_test_mulrev_changed')
+ ->count()
+ ->execute() - $count;
+ $this->assertEquals($count, 5, "$count items inserted.");
+ }
+
+ /**
+ * Indexes all (unindexed) items on the specified index.
+ *
+ * @param string $index_id
+ * The ID of the index on which items should be indexed.
+ *
+ * @return int
+ * The number of successfully indexed items.
+ */
+ protected function indexItems($index_id) {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load($index_id);
+ return $index->indexItems();
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetSourceTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetSourceTest.php
new file mode 100644
index 000000000..8ef6053c7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetSourceTest.php
@@ -0,0 +1,111 @@
+drupalLogin($this->adminUser);
+
+ // Go to the overview and click the first configure link.
+ $this->drupalGet('admin/config/search/facets');
+ $this->assertSession()->linkExists('Configure');
+ $this->clickLink('Configure');
+ }
+
+ /**
+ * Tests the facet source editing.
+ */
+ public function testEditFilterKey() {
+ // Change the filter key.
+ $edit = [
+ 'filter_key' => 'fq',
+ ];
+ $this->assertSession()->fieldExists('filter_key');
+ $this->assertSession()->fieldExists('url_processor');
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->assertSession()->addressEquals('admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Facet source search_api:views_block__search_api_test_view__block_1 has been saved.');
+ $this->clickLink('Configure');
+
+ // Test that saving worked filter_key has the new value.
+ $this->assertSession()->fieldExists('filter_key');
+ $this->assertSession()->fieldExists('url_processor');
+ $this->assertSession()->responseContains('fq');
+ }
+
+ /**
+ * Tests editing the url processor.
+ */
+ public function testEditUrlProcessor() {
+ // Change the url processor.
+ $edit = [
+ 'url_processor' => 'dummy_query',
+ ];
+ $this->assertSession()->fieldExists('filter_key');
+ $this->assertSession()->fieldExists('url_processor');
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->assertSession()->addressEquals('admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Facet source search_api:views_block__search_api_test_view__block_1 has been saved.');
+ $this->clickLink('Configure');
+
+ // Test that saving worked and that the url processor has the new value.
+ $this->assertSession()->fieldExists('filter_key');
+ $this->assertSession()->fieldExists('url_processor');
+ /** @var \Behat\Mink\Element\NodeElement[] $elements */
+ $elements = $this->xpath('//input[@id=:id]', [':id' => 'edit-url-processor-dummy-query']);
+ $this->assertEquals('dummy_query', $elements[0]->getValue());
+ }
+
+ /**
+ * Tests editing the breadcrumb settings.
+ */
+ public function testEditBreadcrumbSettings() {
+ $this->assertSession()->fieldExists('breadcrumb[active]');
+ $this->assertSession()->fieldExists('breadcrumb[group]');
+ $this->assertSession()->checkboxNotChecked('breadcrumb[group]');
+ $this->assertSession()->checkboxNotChecked('breadcrumb[active]');
+ // Change the breadcrumb settings.
+ $edit = [
+ 'breadcrumb[active]' => TRUE,
+ 'breadcrumb[group]' => TRUE,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->assertSession()->addressEquals('admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Facet source search_api:views_block__search_api_test_view__block_1 has been saved.');
+ $this->clickLink('Configure');
+
+ // Test that saving worked and that the url processor has the new value.
+ $this->assertSession()->checkboxChecked('breadcrumb[group]');
+ $this->assertSession()->checkboxChecked('breadcrumb[active]');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetsTestBase.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetsTestBase.php
new file mode 100644
index 000000000..d88875393
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetsTestBase.php
@@ -0,0 +1,183 @@
+adminUser = $this->drupalCreateUser([
+ 'administer search_api',
+ 'administer facets',
+ 'access administration pages',
+ 'administer nodes',
+ 'access content overview',
+ 'administer content types',
+ 'administer blocks',
+ ]);
+
+ $this->unauthorizedUser = $this->drupalCreateUser(['access administration pages']);
+ $this->anonymousUser = $this->drupalCreateUser();
+ }
+
+ /**
+ * Creates or deletes a server.
+ *
+ * @param string $name
+ * (optional) The name of the server.
+ * @param string $id
+ * (optional) The ID of the server.
+ * @param string $backend_id
+ * (optional) The ID of the backend to set for the server.
+ * @param array $backend_config
+ * (optional) The backend configuration to set for the server.
+ * @param bool $reset
+ * (optional) If TRUE, delete the server instead of creating it. (Only the
+ * server's ID is required in that case).
+ *
+ * @return \Drupal\search_api\ServerInterface
+ * A search server.
+ */
+ public function getTestServer($name = 'WebTest server', $id = 'webtest_server', $backend_id = 'search_api_db', array $backend_config = [], $reset = FALSE) {
+ if ($reset) {
+ $server = Server::load($id);
+ if ($server) {
+ $server->delete();
+ }
+ }
+ else {
+ $server = Server::create([
+ 'id' => $id,
+ 'name' => $name,
+ 'description' => $name,
+ 'backend' => $backend_id,
+ 'backend_config' => $backend_config,
+ ]);
+ $server->save();
+ }
+
+ return $server;
+ }
+
+ /**
+ * Creates or deletes an index.
+ *
+ * @param string $name
+ * (optional) The name of the index.
+ * @param string $id
+ * (optional) The ID of the index.
+ * @param string $server_id
+ * (optional) The server to which the index should be attached.
+ * @param string $datasource_id
+ * (optional) The ID of a datasource to set for this index.
+ * @param bool $reset
+ * (optional) If TRUE, delete the index instead of creating it. (Only the
+ * index's ID is required in that case).
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * A search index.
+ */
+ public function getTestIndex($name = 'WebTest Index', $id = 'webtest_index', $server_id = 'webtest_server', $datasource_id = 'entity:node', $reset = FALSE) {
+ if ($reset) {
+ $index = Index::load($id);
+ if ($index) {
+ $index->delete();
+ }
+ }
+ else {
+ $index = Index::create([
+ 'id' => $id,
+ 'name' => $name,
+ 'description' => $name,
+ 'server' => $server_id,
+ 'datasources' => [$datasource_id],
+ ]);
+ $index->save();
+ $this->indexId = $index->id();
+ }
+
+ return $index;
+ }
+
+ /**
+ * Retrieves the search index used by this test.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * The search index.
+ */
+ protected function getIndex() {
+ return Index::load($this->indexId);
+ }
+
+ /**
+ * Clears the test index.
+ */
+ protected function clearIndex() {
+ $this->getIndex()->clear();
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetsUrlGeneratorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetsUrlGeneratorTest.php
new file mode 100644
index 000000000..3b1ac62c7
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/FacetsUrlGeneratorTest.php
@@ -0,0 +1,128 @@
+urlGenerator = \Drupal::service('facets.utility.url_generator');
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals(5, $this->indexItems($this->indexId), '5 items were indexed.');
+ }
+
+ /**
+ * Create url.
+ */
+ public function testCreateUrl() {
+ /** @var \Drupal\facets\FacetInterface $entity */
+ $entity = Facet::create([
+ 'id' => 'test_facet',
+ 'name' => 'Test facet',
+ ]);
+ $entity->setWidget('links');
+ $entity->setEmptyBehavior(['behavior' => 'none']);
+ $entity->setUrlAlias('owl');
+ $entity->setFacetSourceId('search_api:views_page__search_api_test_view__page_1');
+ $entity->save();
+
+ $url = $this->urlGenerator->getUrl(['test_facet' => ['fuzzy']]);
+
+ $this->assertEquals('route:view.search_api_test_view.page_1;arg_0&arg_1&arg_2?f%5B0%5D=owl%3Afuzzy', $url->toUriString());
+
+ // Setup search page URL with contextual parameters as current request and
+ // path.
+ $path = '/search-api-test-fulltext/entity:entity_test_mulrev_changed/entity_test_mulrev_changed';
+ $request = Request::create($path);
+ $result = \Drupal::service('router.no_access_checks')->matchRequest($request);
+ $request->attributes->add($result);
+ \Drupal::requestStack()->push($request);
+ \Drupal::service('path.current')->setPath($path);
+ $url = $this->urlGenerator->getUrl(['test_facet' => ['fuzzy']]);
+
+ $this->assertEquals('route:view.search_api_test_view.page_1;arg_0=entity%3Aentity_test_mulrev_changed&arg_1=entity_test_mulrev_changed&arg_2?f%5B0%5D=owl%3Afuzzy', $url->toUriString());
+ \Drupal::requestStack()->pop();
+ }
+
+ /**
+ * Create url with already set facet.
+ */
+ public function testWithAlreadySetFacet() {
+ $this->drupalPlaceBlock('display_generated_link');
+ $this->createFacet('Owl', 'owl');
+ $this->createFacet('Llama', 'llama', 'keywords');
+
+ $facet = Facet::load('owl');
+ $facet->setUrlAlias('donkey');
+ $facet->save();
+
+ $url = $this->urlGenerator->getUrl(['owl' => ['foo']]);
+ $this->assertEquals('route:view.search_api_test_view.page_1;arg_0&arg_1&arg_2?f%5B0%5D=donkey%3Afoo', $url->toUriString());
+
+ // This won't work without it being in the request, so we need to do this
+ // from a block. We first click the link, check that the "orange" facet is
+ // active as expected and that the output from the custom block is shown.
+ // Then we click the item from the custom block and check that the orange is
+ // no longer active, but item is.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('orange');
+ $this->checkFacetIsActive('orange');
+ $this->checkFacetIsNotActive('item');
+ $this->assertSession()->pageTextContains('Link to owl item');
+ $this->clickLink('Link to owl item');
+ $this->checkFacetIsActive('item');
+ $this->checkFacetIsNotActive('orange');
+
+ // This won't work without it being in the request, so we need to do this
+ // from a block. We first click the link, check that the "orange" facet is
+ // active as expected and that the output from the custom block is shown.
+ // Then we click the item from the custom block and check that the orange is
+ // still active, but item is.
+ \Drupal::state()->get('facets_url_generator_keep_active', TRUE);
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('orange');
+ $this->checkFacetIsActive('orange');
+ $this->checkFacetIsNotActive('item');
+ $this->assertSession()->pageTextContains('Link to owl item');
+ $this->clickLink('Link to owl item');
+ $this->checkFacetIsActive('item');
+ $this->checkFacetIsActive('orange');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/HierarchicalFacetIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/HierarchicalFacetIntegrationTest.php
new file mode 100644
index 000000000..a3052cd39
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/HierarchicalFacetIntegrationTest.php
@@ -0,0 +1,492 @@
+drupalLogin($this->adminUser);
+
+ // Create hierarchical terms in a new vocabulary.
+ $this->vocabulary = $this->createVocabulary();
+ $this->createHierarchialTermStructure();
+
+ // Default content that is extended with a term reference field below.
+ $this->setUpExampleStructure();
+
+ // Create a taxonomy_term_reference field on the article and item.
+ $this->fieldName = 'tax_ref_field';
+ $fieldLabel = 'Taxonomy reference field';
+
+ $this->createEntityReferenceField('entity_test_mulrev_changed', 'article', $this->fieldName, $fieldLabel, 'taxonomy_term');
+ $this->createEntityReferenceField('entity_test_mulrev_changed', 'item', $this->fieldName, $fieldLabel, 'taxonomy_term');
+
+ $this->insertExampleContent();
+
+ // Add fields to index.
+ $index = $this->getIndex();
+
+ // Index the taxonomy and entity reference fields.
+ $term_field = new Field($index, $this->fieldName);
+ $term_field->setType('integer');
+ $term_field->setPropertyPath($this->fieldName);
+ $term_field->setDatasourceId('entity:entity_test_mulrev_changed');
+ $term_field->setLabel($fieldLabel);
+ $index->addField($term_field);
+
+ $index->save();
+ $this->indexItems($this->indexId);
+
+ $facet_name = 'hierarchical facet';
+ $facet_id = 'hierarchical_facet';
+ $this->facetEditPage = 'admin/config/search/facets/' . $facet_id . '/edit';
+
+ $this->createFacet($facet_name, $facet_id, $this->fieldName);
+
+ // Make absolutely sure the ::$blocks variable doesn't pass information
+ // along between tests.
+ $this->blocks = NULL;
+ }
+
+ /**
+ * Test the hierarchical facets functionality.
+ */
+ public function testHierarchicalFacet() {
+ $this->verifyUseHierarchyOption();
+ $this->verifyKeepHierarchyParentsActiveOption();
+ $this->verifyExpandHierarchyOption();
+ $this->verifyEnableParentWhenChildGetsDisabledOption();
+ }
+
+ /**
+ * Verify the backend option "Use hierarchy" is working.
+ */
+ protected function verifyUseHierarchyOption() {
+ // Verify that the link to the index processors settings page is available.
+ $this->drupalGet($this->facetEditPage);
+ $this->clickLink('Search api index processor configuration');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Enable hierarchical facets and translation of entity ids to its names for
+ // a better readability.
+ $this->drupalGet($this->facetEditPage);
+ $edit = [
+ 'facet_settings[use_hierarchy]' => '1',
+ 'facet_settings[translate_entity][status]' => TRUE,
+ ];
+ $this->submitForm($edit, 'Save');
+
+ // Child elements should be collapsed and invisible.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('Parent 1');
+ $this->assertFacetLabel('Parent 2');
+ $this->assertSession()->linkNotExists('Child 1');
+ $this->assertSession()->linkNotExists('Child 2');
+ $this->assertSession()->linkNotExists('Child 3');
+ $this->assertSession()->linkNotExists('Child 4');
+
+ // Click the first parent and make sure its children are visible.
+ $this->clickLink('Parent 1');
+ $this->checkFacetIsActive('Parent 1');
+ $this->assertFacetLabel('Child 1');
+ $this->assertFacetLabel('Child 2');
+ $this->assertSession()->linkNotExists('Child 3');
+ $this->assertSession()->linkNotExists('Child 4');
+ }
+
+ /**
+ * Verify the "Keep parents active" option is working.
+ */
+ protected function verifyKeepHierarchyParentsActiveOption() {
+ // Expand the hierarchy and verify that all items are visible initially.
+ $this->drupalGet($this->facetEditPage);
+ $edit = [
+ 'facet_settings[expand_hierarchy]' => FALSE,
+ 'facet_settings[keep_hierarchy_parents_active]' => '1',
+ 'facet_settings[use_hierarchy]' => '1',
+ 'facet_settings[translate_entity][status]' => '1',
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Click the first parent and make sure its children are visible.
+ $this->clickLink('Parent 1');
+ $this->checkFacetIsActive('Parent 1');
+ $this->checkFacetIsNotActive('Child 1');
+ $this->checkFacetIsNotActive('Child 2');
+ $this->assertFacetLabel('Child 1');
+ $this->assertFacetLabel('Child 2');
+ $this->assertSession()->linkNotExists('Child 3');
+ $this->assertSession()->linkNotExists('Child 4');
+
+ // Click the first child and make sure its parent is still active.
+ $this->clickLink('Child 1');
+ $this->checkFacetIsActive('Parent 1');
+ $this->checkFacetIsActive('Child 1');
+ $this->checkFacetIsNotActive('Child 2');
+ $this->assertFacetLabel('Child 1');
+ $this->assertFacetLabel('Child 2');
+ $this->assertSession()->linkNotExists('Child 3');
+ $this->assertSession()->linkNotExists('Child 4');
+
+ // Click the parent and make sure its children are not active, too.
+ $this->clickLink('Parent 1');
+ $this->checkFacetIsNotActive('Parent 1');
+ $this->assertSession()->linkNotExists('Child 1');
+ $this->assertSession()->linkNotExists('Child 2');
+ $this->assertSession()->linkNotExists('Child 3');
+ $this->assertSession()->linkNotExists('Child 4');
+ }
+
+ /**
+ * Verify the "Always expand hierarchy" option is working.
+ */
+ protected function verifyExpandHierarchyOption() {
+ // Expand the hierarchy and verify that all items are visible initially.
+ $this->drupalGet($this->facetEditPage);
+ $edit = [
+ 'facet_settings[expand_hierarchy]' => '1',
+ 'facet_settings[keep_hierarchy_parents_active]' => FALSE,
+ 'facet_settings[use_hierarchy]' => '1',
+ 'facet_settings[translate_entity][status]' => '1',
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->drupalGet('search-api-test-fulltext');
+
+ $this->assertFacetLabel('Parent 1');
+ $this->assertFacetLabel('Parent 2');
+ $this->assertFacetLabel('Child 1');
+ $this->assertFacetLabel('Child 2');
+ $this->assertFacetLabel('Child 3');
+ $this->assertFacetLabel('Child 4');
+ }
+
+ /**
+ * Tests sorting of hierarchy.
+ */
+ public function testHierarchySorting() {
+ // Expand the hierarchy and verify that all items are visible initially.
+ $edit = [
+ 'facet_settings[expand_hierarchy]' => '1',
+ 'facet_settings[keep_hierarchy_parents_active]' => FALSE,
+ 'facet_settings[use_hierarchy]' => '1',
+ 'facet_settings[translate_entity][status]' => '1',
+ 'facet_sorting[display_value_widget_order][status]' => '1',
+ 'facet_sorting[display_value_widget_order][settings][sort]' => 'ASC',
+ 'facet_sorting[count_widget_order][status]' => '0',
+ 'facet_sorting[active_widget_order][status]' => '0',
+ ];
+ $this->drupalGet($this->facetEditPage);
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertStringPosition('Parent 1', 'Parent 2');
+ $this->assertStringPosition('Child 1', 'Child 2');
+ $this->assertStringPosition('Child 2', 'Child 3');
+ $this->assertStringPosition('Child 3', 'Child 4');
+
+ $edit = [
+ 'facet_sorting[display_value_widget_order][settings][sort]' => 'DESC',
+ ];
+ $this->drupalGet($this->facetEditPage);
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertStringPosition('Parent 2', 'Parent 1');
+ $this->assertStringPosition('Child 4', 'Child 3');
+ $this->assertStringPosition('Child 3', 'Child 2');
+ $this->assertStringPosition('Child 2', 'Child 1');
+ }
+
+ /**
+ * Tests sorting by weight of a taxonomy term.
+ */
+ public function testWeightSort() {
+ $edit = [
+ 'facet_settings[translate_entity][status]' => '1',
+ 'facet_sorting[term_weight_widget_order][status]' => '1',
+ ];
+ $this->drupalGet($this->facetEditPage);
+ $this->submitForm($edit, 'Save');
+
+ $this->parents['Parent 1']->setWeight(15);
+ $this->parents['Parent 1']->save();
+ $this->parents['Parent 2']->setWeight(25);
+ $this->parents['Parent 2']->save();
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('Parent 1');
+ $this->assertFacetLabel('Parent 2');
+ $this->assertStringPosition('Parent 1', 'Parent 2');
+
+ $this->parents['Parent 2']->setWeight(5);
+ $this->parents['Parent 2']->save();
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('Parent 1');
+ $this->assertFacetLabel('Parent 2');
+ $this->assertStringPosition('Parent 2', 'Parent 1');
+ }
+
+ /**
+ * Verify the "Enable parent when child gets disabled" option is working.
+ */
+ protected function verifyEnableParentWhenChildGetsDisabledOption() {
+ // Make sure the option is disabled initially.
+ $this->drupalGet($this->facetEditPage);
+ $edit = [
+ 'facet_settings[expand_hierarchy]' => '1',
+ 'facet_settings[keep_hierarchy_parents_active]' => FALSE,
+ 'facet_settings[enable_parent_when_child_gets_disabled]' => FALSE,
+ 'facet_settings[use_hierarchy]' => '1',
+ 'facet_settings[translate_entity][status]' => '1',
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Enable a child under Parent 2.
+ $this->clickLink('Child 4');
+ $this->checkFacetIsActive('Child 4');
+ $this->checkFacetIsNotActive('Parent 2');
+
+ // Uncheck the facet again.
+ $this->clickLink('(-) Child 4');
+ $this->checkFacetIsNotActive('Child 4');
+ $this->checkFacetIsNotActive('Parent 2');
+
+ // Enable the option.
+ $this->drupalGet($this->facetEditPage);
+ $edit = [
+ 'facet_settings[expand_hierarchy]' => '1',
+ 'facet_settings[keep_hierarchy_parents_active]' => FALSE,
+ 'facet_settings[enable_parent_when_child_gets_disabled]' => '1',
+ 'facet_settings[use_hierarchy]' => '1',
+ 'facet_settings[translate_entity][status]' => '1',
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->drupalGet('search-api-test-fulltext');
+
+ $this->clickLink('Child 4');
+ $this->checkFacetIsActive('Child 4');
+ $this->clickLink('Child 3');
+ $this->checkFacetIsActive('Child 3');
+ $this->checkFacetIsActive('Child 4');
+ $this->checkFacetIsNotActive('Parent 2');
+
+ $this->clickLink('(-) Child 4');
+ $this->checkFacetIsActive('Child 3');
+ $this->checkFacetIsNotActive('Child 4');
+ $this->checkFacetIsNotActive('Parent 2');
+
+ $this->clickLink('(-) Child 3');
+ $this->checkFacetIsNotActive('Child 3');
+ $this->checkFacetIsNotActive('Child 4');
+ $this->checkFacetIsActive('Parent 2');
+ }
+
+ /**
+ * Setup a term structure for our test.
+ */
+ protected function createHierarchialTermStructure() {
+ // Generate 2 parent terms.
+ foreach (['Parent 1', 'Parent 2'] as $name) {
+ $this->parents[$name] = Term::create([
+ 'name' => $name,
+ 'description' => '',
+ 'vid' => $this->vocabulary->id(),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ ]);
+ $this->parents[$name]->save();
+ }
+
+ // Generate 4 child terms.
+ foreach (range(1, 4) as $i) {
+ $this->terms[$i] = Term::create([
+ 'name' => sprintf('Child %d', $i),
+ 'description' => '',
+ 'vid' => $this->vocabulary->id(),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ ]);
+ $this->terms[$i]->save();
+ }
+
+ // Build up the hierarchy.
+ $this->terms[1]->parent = [$this->parents['Parent 1']->id()];
+ $this->terms[1]->save();
+
+ $this->terms[2]->parent = [$this->parents['Parent 1']->id()];
+ $this->terms[2]->save();
+
+ $this->terms[3]->parent = [$this->parents['Parent 2']->id()];
+ $this->terms[3]->save();
+
+ $this->terms[4]->parent = [$this->parents['Parent 2']->id()];
+ $this->terms[4]->save();
+ }
+
+ /**
+ * Tests hierarchy breadcrumbs.
+ */
+ public function testHierarchyBreadcrumb() {
+ $this->drupalGet('admin/config/search/facets');
+ $this->clickLink('Configure', 1);
+ $default_config = [
+ 'filter_key' => 'f',
+ 'url_processor' => 'query_string',
+ 'breadcrumb[active]' => TRUE,
+ 'breadcrumb[group]' => TRUE,
+ ];
+ $this->submitForm($default_config, 'Save');
+
+ $block = [
+ 'region' => 'footer',
+ 'label' => 'Breadcrumbs',
+ 'provider' => 'system',
+ ];
+ $this->drupalPlaceBlock('system_breadcrumb_block', $block);
+ $this->resetAll();
+
+ $edit = [
+ 'facet_settings[expand_hierarchy]' => '1',
+ 'facet_settings[keep_hierarchy_parents_active]' => FALSE,
+ 'facet_settings[use_hierarchy]' => '1',
+ 'facet_settings[translate_entity][status]' => '1',
+ 'facet_sorting[display_value_widget_order][status]' => '1',
+ 'facet_sorting[display_value_widget_order][settings][sort]' => 'ASC',
+ 'facet_sorting[count_widget_order][status]' => '0',
+ 'facet_sorting[active_widget_order][status]' => '0',
+ ];
+ $this->drupalGet($this->facetEditPage);
+ $this->submitForm($edit, 'Save');
+
+ $initial_query = ['search_api_fulltext' => 'foo', 'test_param' => 1];
+ $this->drupalGet('search-api-test-fulltext', ['query' => $initial_query]);
+ $this->clickLink('Child 2');
+ $this->checkFacetIsActive('Child 2');
+
+ $this->assertSession()->pageTextContains('hierarchical facet: Parent 1');
+ $this->clickLink('hierarchical facet: Parent 1');
+ $this->checkFacetIsActive('Parent 1');
+ }
+
+ /**
+ * Creates several test entities with the term-reference field.
+ */
+ protected function insertExampleContent() {
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+
+ $this->entities[1] = $entity_test_storage->create([
+ 'name' => 'foo bar baz',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ $this->fieldName => [$this->parents['Parent 1']->id()],
+ ]);
+ $this->entities[1]->save();
+
+ $this->entities[2] = $entity_test_storage->create([
+ 'name' => 'foo test',
+ 'body' => 'bar test',
+ 'type' => 'item',
+ 'keywords' => ['orange', 'apple', 'grape'],
+ 'category' => 'item_category',
+ $this->fieldName => [$this->parents['Parent 2']->id()],
+ ]);
+ $this->entities[2]->save();
+
+ $this->entities[3] = $entity_test_storage->create([
+ 'name' => 'bar',
+ 'body' => 'test foobar',
+ 'type' => 'item',
+ $this->fieldName => [$this->terms[1]->id()],
+ ]);
+ $this->entities[3]->save();
+
+ $this->entities[4] = $entity_test_storage->create([
+ 'name' => 'foo baz',
+ 'body' => 'test test test',
+ 'type' => 'article',
+ 'keywords' => ['apple', 'strawberry', 'grape'],
+ 'category' => 'article_category',
+ $this->fieldName => [$this->terms[2]->id()],
+ ]);
+ $this->entities[4]->save();
+
+ $this->entities[5] = $entity_test_storage->create([
+ 'name' => 'bar baz',
+ 'body' => 'foo',
+ 'type' => 'article',
+ 'keywords' => ['orange', 'strawberry', 'grape', 'banana'],
+ 'category' => 'article_category',
+ $this->fieldName => [$this->terms[3]->id()],
+ ]);
+ $this->entities[5]->save();
+
+ $this->entities[6] = $entity_test_storage->create([
+ 'name' => 'bar baz',
+ 'body' => 'foo',
+ 'type' => 'article',
+ 'keywords' => ['orange', 'strawberry', 'grape', 'banana'],
+ 'category' => 'article_category',
+ $this->fieldName => [$this->terms[4]->id()],
+ ]);
+ $this->entities[6]->save();
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/IntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/IntegrationTest.php
new file mode 100644
index 000000000..59974200c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/IntegrationTest.php
@@ -0,0 +1,1212 @@
+drupalLogin($this->adminUser);
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals(5, $this->indexItems($this->indexId), '5 items were indexed.');
+
+ // Make absolutely sure the ::$blocks variable doesn't pass information
+ // along between tests.
+ $this->blocks = NULL;
+ }
+
+ /**
+ * Tests permissions.
+ */
+ public function testOverviewPermissions() {
+ $facet_overview = '/admin/config/search/facets';
+
+ // Login with a user that is not authorized to administer facets and test
+ // that we're correctly getting a 403 HTTP response code.
+ $this->drupalLogin($this->unauthorizedUser);
+ $this->drupalGet($facet_overview);
+ $this->assertSession()->statusCodeEquals(403);
+ $this->assertSession()->pageTextContains('You are not authorized to access this page');
+
+ // Login with a user that has the correct permissions and test for the
+ // correct HTTP response code.
+ $this->drupalLogin($this->adminUser);
+ $this->drupalGet($facet_overview);
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+ /**
+ * Tests facets admin pages availability.
+ */
+ public function testAdminPages() {
+ $pages = [
+ '/admin/config/search/facets',
+ '/admin/config/search/facets/add-facet',
+ '/admin/config/search/facets/facet-sources/views_page/edit',
+ ];
+
+ foreach ($pages as $page) {
+ $this->drupalGet($page);
+ $this->assertSession()->statusCodeEquals(200);
+ }
+ }
+
+ /**
+ * Tests various operations via the Facets' admin UI.
+ */
+ public function testFramework() {
+ $facet_name = "Test Facet name";
+ $facet_id = 'test_facet_name';
+
+ // Check if the overview is empty.
+ $this->checkEmptyOverview();
+
+ // Add a new facet and edit it. Check adding a duplicate.
+ $this->addFacet($facet_name);
+ $this->editFacet($facet_name);
+ $this->addFacetDuplicate($facet_name);
+
+ // By default, the view should show all entities.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+
+ // Create and place a block for "Test Facet name" facet.
+ $this->blocks[$facet_id] = $this->createBlock($facet_id);
+
+ // Verify that the facet results are correct.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('item');
+ $this->assertSession()->pageTextContains('article');
+
+ // Verify that facet blocks appear as expected.
+ $this->assertFacetBlocksAppear();
+
+ // Verify that the facet only shows when the facet source is visible, it
+ // should not show up on the user page.
+ $this->setOptionShowOnlyWhenFacetSourceVisible($facet_name);
+ $this->drupalGet('user/2');
+ $this->assertNoFacetBlocksAppear();
+
+ // Do not show the block on empty behaviors.
+ $this->clearIndex();
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Verify that no facet blocks appear. Empty behavior "None" is selected by
+ // default.
+ $this->assertNoFacetBlocksAppear();
+
+ // Verify that the "empty_text" appears as expected.
+ $this->setEmptyBehaviorFacetText($facet_name);
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->responseContains('block-test-facet-name');
+ $this->assertSession()->responseContains('No results found for this block!');
+
+ // Delete the block.
+ $this->deleteBlock($facet_id);
+
+ // Delete the facet and make sure the overview is empty again.
+ $this->deleteUnusedFacet($facet_name);
+ $this->checkEmptyOverview();
+ }
+
+ /**
+ * Tests that a block view also works.
+ */
+ public function testBlockView() {
+ $facet_id = 'block_view_facet';
+
+ $webAssert = $this->assertSession();
+ $this->addFacet('Block view facet', 'type', 'search_api:views_block__search_api_test_view__block_1');
+ $this->createBlock($facet_id);
+ $this->drupalGet('admin/config/search/facets/' . $facet_id . '/edit');
+ $webAssert->checkboxNotChecked('facet_settings[only_visible_when_facet_source_is_visible]');
+
+ // Place the views block in the footer of all pages.
+ $block_settings = [
+ 'region' => 'sidebar_first',
+ 'id' => 'view_block',
+ ];
+ $this->drupalPlaceBlock('views_block:search_api_test_view-block_1', $block_settings);
+
+ // By default, the view should show all entities.
+ $this->drupalGet('');
+ $webAssert->pageTextContains('Fulltext test index');
+ $webAssert->pageTextContains('Displaying 5 search results');
+ $webAssert->pageTextContains('item');
+ $webAssert->pageTextContains('article');
+
+ // Click the item link, and test that filtering of results actually works.
+ $this->clickLink('item');
+ $webAssert->pageTextContains('Displaying 3 search results');
+ }
+
+ /**
+ * Tests for deleting a block.
+ */
+ public function testBlockDelete() {
+ $name = 'Tawny-browed owl';
+ $id = 'tawny_browed_owl';
+
+ // Add a new facet.
+ $this->createFacet($name, $id);
+
+ $block = $this->blocks[$id];
+ $block_id = $block->label();
+
+ $this->drupalGet('admin/structure/block');
+ $this->assertSession()->pageTextContains($block_id);
+
+ $this->drupalGet('admin/structure/block/library/stark');
+ $this->assertSession()->pageTextContains($name);
+
+ $this->drupalGet('admin/config/search/facets/' . $id . '/delete');
+ $this->assertSession()->pageTextContains('The listed configuration will be deleted.');
+ $this->assertSession()->pageTextContains($block->label());
+ $this->submitForm([], 'Delete');
+
+ $this->drupalGet('admin/structure/block/library/stark');
+ $this->assertSession()->pageTextNotContains($name);
+ }
+
+ /**
+ * Tests that an url alias works correctly.
+ */
+ public function testUrlAlias() {
+ $facet_id = 'ab_facet';
+ $facet_edit_page = '/admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet('ab Facet', $facet_id);
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ $this->clickLink('item');
+ $url = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['f[0]' => 'ab_facet:item']]);
+ $this->assertSession()->addressEquals($url);
+
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm(['facet_settings[url_alias]' => 'llama'], 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ $this->clickLink('item');
+ $url = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['f[0]' => 'llama:item']]);
+ $this->assertSession()->addressEquals($url);
+ }
+
+ /**
+ * Tests facet dependencies.
+ */
+ public function testFacetDependencies() {
+ $facet_name = "DependableFacet";
+ $facet_id = 'dependablefacet';
+
+ $depending_facet_name = "DependingFacet";
+ $depending_facet_id = "dependingfacet";
+
+ $this->addFacet($facet_name);
+ $this->addFacet($depending_facet_name, 'keywords');
+
+ // Create both facets as blocks and add them on the page.
+ $this->blocks[$facet_id] = $this->createBlock($facet_id);
+ $this->blocks[$depending_facet_id] = $this->createBlock($depending_facet_id);
+
+ // Go to the view and test that both facets are shown. Item and article
+ // come from the DependableFacet, orange and grape come from DependingFacet.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('grape');
+ $this->assertFacetLabel('orange');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+ $this->assertFacetBlocksAppear();
+
+ // Change the visiblity settings of the DependingFacet.
+ $this->drupalGet('admin/config/search/facets/' . $depending_facet_id . '/edit');
+ $edit = [
+ 'facet_settings[dependent_processor][status]' => TRUE,
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][enable]' => TRUE,
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][condition]' => 'values',
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][values]' => 'item',
+ ];
+ $this->submitForm($edit, 'Save');
+
+ // Go to the view and test that only the types are shown.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->linkNotExists('grape');
+ $this->assertSession()->linkNotExists('orange');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ // Click on the item, and test that this shows the keywords.
+ $this->clickLink('item');
+ $this->assertFacetLabel('grape');
+ $this->assertFacetLabel('orange');
+
+ // Go back to the view, click on article and test that the keywords are
+ // hidden.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('article');
+ $this->assertSession()->linkNotExists('grape');
+ $this->assertSession()->linkNotExists('orange');
+
+ // Change the visibility settings to negate the previous settings.
+ $this->drupalGet('admin/config/search/facets/' . $depending_facet_id . '/edit');
+ $edit = [
+ 'facet_settings[dependent_processor][status]' => TRUE,
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][enable]' => TRUE,
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][condition]' => 'values',
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][values]' => 'item',
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][negate]' => TRUE,
+ ];
+ $this->submitForm($edit, 'Save');
+
+ // Go to the view and test only the type facet is shown.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+ $this->assertFacetLabel('grape');
+ $this->assertFacetLabel('orange');
+
+ // Click on the article, and test that this shows the keywords.
+ $this->clickLink('article');
+ $this->assertFacetLabel('grape');
+ $this->assertFacetLabel('orange');
+
+ // Go back to the view, click on item and test that the keywords are
+ // hidden.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('item');
+ $this->assertSession()->linkNotExists('grape');
+ $this->assertSession()->linkNotExists('orange');
+
+ // Disable negation again.
+ $this->drupalGet('admin/config/search/facets/' . $depending_facet_id . '/edit');
+ $edit = [
+ 'facet_settings[dependent_processor][status]' => TRUE,
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][enable]' => TRUE,
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][condition]' => 'values',
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][values]' => 'item',
+ 'facet_settings[dependent_processor][settings][' . $facet_id . '][negate]' => FALSE,
+ ];
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertSession()->linkNotExists('grape');
+ $this->clickLink('item');
+ $this->assertSession()->pageTextContains('Displaying 3 search results');
+ $this->assertSession()->linkExists('grape');
+ $this->clickLink('grape');
+ $this->assertSession()->pageTextContains('Displaying 1 search results');
+ // Disable item again, and the grape should not be reflected in the search
+ // result anymore.
+ $this->clickLink('item');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ }
+
+ /**
+ * Tests the facet's and/or functionality.
+ */
+ public function testAndOrFacet() {
+ $facet_name = 'test & facet';
+ $facet_id = 'test_facet';
+ $facet_edit_page = 'admin/config/search/facets/' . $facet_id . '/edit';
+
+ $this->createFacet($facet_name, $facet_id);
+
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm(['facet_settings[query_operator]' => 'and'], 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ $this->clickLink('item');
+ $this->checkFacetIsActive('item');
+ $this->assertSession()->linkNotExists('article');
+
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm(['facet_settings[query_operator]' => 'or'], 'Save');
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ $this->clickLink('item');
+ $this->checkFacetIsActive('item');
+ $this->assertFacetLabel('article');
+
+ // Verify the number of results for OR functionality.
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm(
+ [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => TRUE,
+ ],
+ 'Save'
+ );
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('item (3)');
+ $this->assertFacetLabel('article (2)');
+
+ }
+
+ /**
+ * Tests that we disallow unwanted values when creating a facet trough the UI.
+ */
+ public function testUnwantedValues() {
+ // Go to the Add facet page and make sure that returns a 200.
+ $facet_add_page = '/admin/config/search/facets/add-facet';
+ $this->drupalGet($facet_add_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Configure the facet source by selecting one of the Search API views.
+ $this->drupalGet($facet_add_page);
+ $this->submitForm(['facet_source_id' => 'search_api:views_page__search_api_test_view__page_1'], 'Configure facet source');
+
+ // Fill in all fields and make sure the 'field is required' message is no
+ // longer shown.
+ $facet_source_form = [
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'facet_source_configs[search_api:views_page__search_api_test_view__page_1][field_identifier]' => 'type',
+ ];
+ $this->submitForm($facet_source_form, 'Save');
+
+ $form_values = [
+ 'name' => 'name 1',
+ 'id' => 'name 1',
+ ];
+ $this->submitForm($form_values, 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
+
+ $form_values = [
+ 'name' => 'name 1',
+ 'id' => 'name:&1',
+ ];
+ $this->submitForm($form_values, 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
+
+ // Post the form with valid values, so we can test the next step.
+ $form_values = [
+ 'name' => 'name 1',
+ 'id' => 'name_1',
+ ];
+ $this->submitForm($form_values, 'Save');
+
+ // Create an array of values that are not allowed in the url.
+ $unwanted_values = [' ', '!', '@', '#', '$', '%', '^', '&'];
+ foreach ($unwanted_values as $unwanted_value) {
+ $form_values = [
+ 'facet_settings[url_alias]' => 'alias' . $unwanted_value . '1',
+ ];
+ $this->submitForm($form_values, 'Save');
+ $this->assertSession()->pageTextContains('The URL alias contains characters that are not allowed.');
+ }
+
+ // Post an alias with allowed values.
+ $form_values = [
+ 'facet_settings[url_alias]' => 'alias~-_.1',
+ ];
+ $this->submitForm($form_values, 'Save');
+ $this->assertSession()->pageTextContains('Facet name 1 has been updated.');
+ }
+
+ /**
+ * Tests the facet's exclude functionality.
+ */
+ public function testExcludeFacet() {
+ $facet_name = 'test & facet';
+ $facet_id = 'test_facet';
+ $facet_edit_page = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet($facet_name, $facet_id);
+
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->checkboxNotChecked('edit-facet-settings-exclude');
+ $this->submitForm(['facet_settings[exclude]' => TRUE], 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->checkboxChecked('edit-facet-settings-exclude');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('foo bar baz');
+ $this->assertSession()->pageTextContains('foo baz');
+ $this->assertFacetLabel('item');
+
+ $this->clickLink('item');
+ $this->checkFacetIsActive('item');
+ $this->assertSession()->pageTextContains('foo baz');
+ $this->assertSession()->pageTextContains('bar baz');
+ $this->assertSession()->pageTextNotContains('foo bar baz');
+
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm(['facet_settings[exclude]' => FALSE], 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->checkboxNotChecked('edit-facet-settings-exclude');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('foo bar baz');
+ $this->assertSession()->pageTextContains('foo baz');
+ $this->assertFacetLabel('item');
+
+ $this->clickLink('item');
+ $this->checkFacetIsActive('item');
+ $this->assertSession()->pageTextContains('foo bar baz');
+ $this->assertSession()->pageTextContains('foo test');
+ $this->assertSession()->pageTextContains('bar');
+ $this->assertSession()->pageTextNotContains('foo baz');
+ }
+
+ /**
+ * Tests the facet's exclude functionality for a date field.
+ */
+ public function testExcludeFacetDate() {
+ $field_name = 'created';
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ $entity_test_storage->create([
+ 'name' => 'foo new',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ $field_name => 1490000000,
+ ])->save();
+
+ $entity_test_storage->create([
+ 'name' => 'foo old',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ $field_name => 1460000000,
+ ])->save();
+
+ $this->indexItems($this->indexId);
+
+ $facet_id = "created";
+
+ // Create facet.
+ $facet_edit_page = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet("Created", $facet_id, $field_name);
+
+ $form = [
+ 'widget' => 'links',
+ 'facet_settings[exclude]' => 0,
+ 'facet_settings[date_item][status]' => 1,
+ 'facet_settings[date_item][settings][date_display]' => 'actual_date',
+ 'facet_settings[date_item][settings][granularity]' => SearchApiDate::FACETAPI_DATE_MONTH,
+ ];
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('foo old');
+ $this->assertSession()->pageTextContains('foo new');
+ $this->clickLink('March 2017');
+ $this->checkFacetIsActive('March 2017');
+ $this->assertSession()->pageTextContains('foo new');
+ $this->assertSession()->pageTextNotContains('foo old');
+
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->checkboxNotChecked('edit-facet-settings-exclude');
+ $this->submitForm(['facet_settings[exclude]' => 1], 'Save');
+ $this->assertSession()->checkboxChecked('edit-facet-settings-exclude');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('March 2017');
+ $this->checkFacetIsActive('March 2017');
+ $this->assertSession()->pageTextContains('foo old');
+ $this->assertSession()->pageTextNotContains('foo new');
+ }
+
+ /**
+ * Tests allow only one active item.
+ */
+ public function testAllowOneActiveItem() {
+ $facet_name = 'Spotted wood owl';
+ $facet_id = 'spotted_wood_owl';
+ $facet_edit_page = 'admin/config/search/facets/' . $facet_id;
+
+ $this->createFacet($facet_name, $facet_id, 'keywords');
+
+ $this->drupalGet($facet_edit_page . '/edit');
+ $edit = ['facet_settings[show_only_one_result]' => TRUE];
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertFacetLabel('grape');
+ $this->assertFacetLabel('orange');
+
+ $this->clickLink('grape');
+ $this->assertSession()->pageTextContains('Displaying 3 search results');
+ $this->checkFacetIsActive('grape');
+ $this->assertFacetLabel('orange');
+
+ $this->clickLink('orange');
+ $this->assertSession()->pageTextContains('Displaying 3 search results');
+ $this->assertFacetLabel('grape');
+ $this->checkFacetIsActive('orange');
+ }
+
+ /**
+ * Tests calculations of facet count.
+ */
+ public function testFacetCountCalculations() {
+ $this->addFacet('Type');
+ $this->addFacet('Keywords', 'keywords');
+ $this->createBlock('type');
+ $this->createBlock('keywords');
+
+ $edit = [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => '1',
+ 'facet_settings[query_operator]' => 'and',
+ ];
+ $this->drupalGet('admin/config/search/facets/keywords/edit');
+ $this->submitForm($edit, 'Save');
+ $this->drupalGet('admin/config/search/facets/type/edit');
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertFacetLabel('article (2)');
+ $this->assertFacetLabel('grape (3)');
+
+ // Make sure that after clicking on article, which has only 2 entities,
+ // there are only 2 items left in the results for other facets as well.
+ // In this case, that means we can't have 3 entities tagged with grape. Both
+ // remaining entities are tagged with grape and strawberry.
+ $this->clickPartialLink('article');
+ $this->assertSession()->pageTextNotContains('(3)');
+ $this->assertFacetLabel('grape (2)');
+ $this->assertFacetLabel('strawberry (2)');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertFacetLabel('article (2)');
+ $this->assertFacetLabel('grape (3)');
+
+ // Make sure that after clicking on grape, which has only 3 entities, there
+ // are only 3 items left in the results for other facets as well. In this
+ // case, that means 2 entities of type article and 1 item.
+ $this->clickPartialLink('grape');
+ $this->assertSession()->pageTextContains('Displaying 3 search results');
+ $this->assertFacetLabel('article (2)');
+ $this->assertFacetLabel('item (1)');
+ }
+
+ /**
+ * Tests what happens when a dependency is removed.
+ */
+ public function testOnViewRemoval() {
+ $id = "owl";
+ $name = "Owl";
+ $this->createFacet($name, $id);
+
+ $this->drupalGet('/admin/config/search/facets');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Check that the expected facet sources and the owl facet are shown.
+ $this->assertSession()->pageTextContains('search_api:views_block__search_api_test_view__block_1');
+ $this->assertSession()->pageTextContains('search_api:views_page__search_api_test_view__page_1');
+ $this->assertSession()->pageTextContains($name);
+
+ // Delete the view on which both facet sources are based.
+ $view = View::load('search_api_test_view');
+ $view->delete();
+
+ // Go back to the overview, make sure that the page doesn't show any errors
+ // and the facet/facet source are deleted.
+ $this->drupalGet('/admin/config/search/facets');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextNotContains('search_api:views_page__search_api_test_view__page_1');
+ $this->assertSession()->pageTextNotContains('search_api:views_block__search_api_test_view__block_1');
+ $this->assertSession()->pageTextNotContains($name);
+ }
+
+ /**
+ * Tests what happens when a dependency is removed.
+ */
+ public function testOnViewDisplayRemoval() {
+ $admin_user = $this->drupalCreateUser([
+ 'administer search_api',
+ 'administer facets',
+ 'access administration pages',
+ 'administer nodes',
+ 'access content overview',
+ 'administer content types',
+ 'administer blocks',
+ 'administer views',
+ ]);
+ $this->drupalLogin($admin_user);
+
+ $id = "owl";
+ $name = "Owl";
+ $this->createFacet($name, $id);
+
+ $this->drupalGet('/admin/config/search/facets');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Check that the expected facet sources and the owl facet are shown.
+ $this->assertSession()->pageTextContains('search_api:views_block__search_api_test_view__block_1');
+ $this->assertSession()->pageTextContains('search_api:views_page__search_api_test_view__page_1');
+ $this->assertSession()->pageTextContains($name);
+
+ // Delete the view display for the page.
+ $this->drupalGet('admin/structure/views/view/search_api_test_view');
+ $this->submitForm([], 'Delete Page');
+ $this->submitForm([], 'Save');
+
+ // Go back to the overview, make sure that the page doesn't show any errors
+ // and the facet/facet source are deleted.
+ $this->drupalGet('/admin/config/search/facets');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextNotContains('search_api:views_page__search_api_test_view__page_1');
+ $this->assertSession()->pageTextContains('search_api:views_block__search_api_test_view__block_1');
+ $this->assertSession()->pageTextNotContains($name);
+ }
+
+ /**
+ * Tests the hard limit setting.
+ */
+ public function testHardLimit() {
+ $this->createFacet('Owl', 'owl', 'keywords');
+
+ $edit = [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => '1',
+ 'facet_sorting[display_value_widget_order][status]' => TRUE,
+ 'facet_sorting[active_widget_order][status]' => FALSE,
+ ];
+ $this->drupalGet('admin/config/search/facets/owl/edit');
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertFacetLabel('grape (3)');
+ $this->assertFacetLabel('orange (3)');
+ $this->assertFacetLabel('apple (2)');
+ $this->assertFacetLabel('banana (1)');
+ $this->assertFacetLabel('strawberry (2)');
+
+ $edit['facet_settings[hard_limit]'] = 3;
+ $this->drupalGet('admin/config/search/facets/owl/edit');
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ // We're still testing for 5 search results here, the hard limit only limits
+ // the facets, not the search results.
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertFacetLabel('grape (3)');
+ $this->assertFacetLabel('orange (3)');
+ $this->assertFacetLabel('apple (2)');
+ $this->assertSession()->pageTextNotContains('banana (0)');
+ $this->assertSession()->pageTextNotContains('strawberry (0)');
+ }
+
+ /**
+ * Test minimum amount of items.
+ */
+ public function testMinimumAmount() {
+ $id = "elf_owl";
+ $name = "Elf owl";
+ $this->createFacet($name, $id);
+
+ // Show the amount of items.
+ $edit = [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => '1',
+ 'facet_settings[min_count]' => 1,
+ ];
+ $this->drupalGet('admin/config/search/facets/elf_owl/edit');
+ $this->submitForm($edit, 'Save');
+
+ // See that both article and item are showing.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertFacetLabel('article (2)');
+ $this->assertFacetLabel('item (3)');
+
+ // Make sure that a facet needs at least 3 results.
+ $edit = [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => '1',
+ 'facet_settings[min_count]' => 3,
+ ];
+ $this->drupalGet('admin/config/search/facets/elf_owl/edit');
+ $this->submitForm($edit, 'Save');
+
+ // See that article is now hidden, item should still be showing.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertSession()->pageTextNotContains('article');
+ $this->assertFacetLabel('item (3)');
+ }
+
+ /**
+ * Tests the visibility of facet source.
+ */
+ public function testFacetSourceVisibility() {
+ $this->createFacet('Vicuña', 'vicuna');
+ $edit = [
+ 'facet_settings[only_visible_when_facet_source_is_visible]' => FALSE,
+ ];
+ $this->drupalGet('admin/config/search/facets/vicuna/edit');
+ $this->submitForm($edit, 'Save');
+
+ // Test that the facet source is visible on the search page and user/2 page.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->drupalGet('user/2');
+ $this->assertFacetBlocksAppear();
+
+ // Change the facet to only show when it's source is visible.
+ $edit = [
+ 'facet_settings[only_visible_when_facet_source_is_visible]' => TRUE,
+ ];
+ $this->drupalGet('admin/config/search/facets/vicuna/edit');
+ $this->submitForm($edit, 'Save');
+
+ // Test that the facet still apears on the search page but is hidden on the
+ // user page.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->drupalGet('user/2');
+ $this->assertNoFacetBlocksAppear();
+ }
+
+ /**
+ * Tests behavior with multiple enabled facets and their interaction.
+ */
+ public function testMultipleFacets() {
+ // Create 2 facets.
+ $this->createFacet('Snow Owl', 'snow_owl');
+ // Clear all the caches between building the 2 facets - because things fail
+ // otherwise.
+ $this->resetAll();
+ $this->createFacet('Forest Owl', 'forest_owl', 'category');
+
+ // Make sure numbers are displayed.
+ $edit = [
+ 'widget_config[show_numbers]' => 1,
+ 'facet_settings[min_count]' => 0,
+ ];
+ $this->drupalGet('admin/config/search/facets/snow_owl/edit');
+ $this->submitForm($edit, 'Save');
+ $this->drupalGet('admin/config/search/facets/forest_owl/edit');
+ $this->submitForm($edit, 'Save');
+
+ // Go to the view and check the default behavior.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertFacetLabel('item (3)');
+ $this->assertFacetLabel('article (2)');
+ $this->assertFacetLabel('item_category (2)');
+ $this->assertFacetLabel('article_category (2)');
+
+ // Start filtering.
+ $this->clickPartialLink('item_category');
+ $this->assertSession()->pageTextContains('Displaying 2 search results');
+ $this->checkFacetIsActive('item_category');
+ $this->assertFacetLabel('item (2)');
+
+ // Go back to the overview and start another filter, from the second facet
+ // block this time.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->clickPartialLink('article (2)');
+ $this->assertSession()->pageTextContains('Displaying 2 search results');
+ $this->checkFacetIsActive('article');
+ $this->assertFacetLabel('article_category (2)');
+ $this->assertFacetLabel('item_category (0)');
+ }
+
+ /**
+ * Tests cloning of a facet.
+ */
+ public function testClone() {
+ $id = "western_screech_owl";
+ $name = "Western screech owl";
+ $this->createFacet($name, $id);
+
+ $this->drupalGet('admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Western screech owl');
+ $this->assertSession()->linkExists('Clone facet');
+ $this->clickLink('Clone facet');
+
+ $clone_edit = [
+ 'destination_facet_source' => 'search_api:views_block__search_api_test_view__block_1',
+ 'name' => 'Eastern screech owl',
+ 'id' => 'eastern_screech_owl',
+ ];
+ $this->submitForm($clone_edit, 'Duplicate');
+ $this->assertSession()->pageTextContains('Facet cloned to Eastern screech owl');
+
+ $this->drupalGet('admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Western screech owl');
+ $this->assertSession()->pageTextContains('Eastern screech owl');
+ }
+
+ /**
+ * Check that the disabling of the cache works.
+ */
+ public function testViewsCacheDisable() {
+ // Load the view, verify cache settings.
+ $view = Views::getView('search_api_test_view');
+ $view->setDisplay('page_1');
+ $current_cache = $view->display_handler->getOption('cache');
+ $this->assertEquals('none', $current_cache['type']);
+ $view->display_handler->setOption('cache', ['type' => 'tag']);
+ $view->save();
+ $current_cache = $view->display_handler->getOption('cache');
+ $this->assertEquals('tag', $current_cache['type']);
+
+ // Create a facet and check for the cache disabled message.
+ $id = "western_screech_owl";
+ $name = "Western screech owl";
+ $this->createFacet($name, $id);
+ $this->drupalGet('admin/config/search/facets/' . $id . '/settings');
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('Caching of view Search API Test Fulltext search view has been disabled.');
+
+ // Check the view's cache settings again to see if they've been updated.
+ $view = Views::getView('search_api_test_view');
+ $view->setDisplay('page_1');
+ $current_cache = $view->display_handler->getOption('cache');
+ $this->assertEquals('none', $current_cache['type']);
+ }
+
+ /**
+ * Tests that the configuration for showing a title works.
+ */
+ public function testShowTitle() {
+ $this->createFacet("Llama", 'llama');
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextNotContains('Llama');
+ $this->drupalGet('admin/config/search/facets/llama/edit');
+ $this->submitForm(['facet_settings[show_title]' => TRUE], 'Save');
+ $this->assertSession()->checkboxChecked('Show title of facet');
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->responseContains('Llama ');
+ $this->assertSession()->pageTextContains('Llama');
+ }
+
+ /**
+ * Configures empty behavior option to show a text on empty results.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ */
+ protected function setEmptyBehaviorFacetText($facet_name) {
+ $facet_id = $this->convertNameToMachineName($facet_name);
+
+ $facet_display_page = '/admin/config/search/facets/' . $facet_id . '/edit';
+
+ // Go to the facet edit page and make sure "edit facet %facet" is present.
+ $this->drupalGet($facet_display_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Configure the text for empty results behavior.
+ $edit = [
+ 'facet_settings[empty_behavior]' => 'text',
+ 'facet_settings[empty_behavior_container][empty_behavior_text][value]' => 'No results found for this block!',
+ ];
+ $this->submitForm($edit, 'Save');
+
+ }
+
+ /**
+ * Configures a facet to only be visible when accessing to the facet source.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ */
+ protected function setOptionShowOnlyWhenFacetSourceVisible($facet_name) {
+ $facet_id = $this->convertNameToMachineName($facet_name);
+
+ $facet_edit_page = '/admin/config/search/facets/' . $facet_id . '/edit';
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ $edit = [
+ 'facet_settings[only_visible_when_facet_source_is_visible]' => TRUE,
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => '0',
+ ];
+ $this->submitForm($edit, 'Save');
+ }
+
+ /**
+ * Tests that the facet overview is empty.
+ */
+ protected function checkEmptyOverview() {
+ $facet_overview = '/admin/config/search/facets';
+ $this->drupalGet($facet_overview);
+ $this->assertSession()->statusCodeEquals(200);
+
+ // The list overview has Field: field_name as description. This tests on the
+ // absence of that.
+ $this->assertSession()->pageTextNotContains('Field:');
+
+ // Check that the expected facet sources are shown.
+ $this->assertSession()->pageTextContains('search_api:views_block__search_api_test_view__block_1');
+ $this->assertSession()->pageTextContains('search_api:views_page__search_api_test_view__page_1');
+ }
+
+ /**
+ * Tests adding a facet trough the interface.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ * @param string $facet_type
+ * The field of the facet.
+ * @param string $source_id
+ * The facet source id.
+ */
+ protected function addFacet($facet_name, $facet_type = 'type', $source_id = 'search_api:views_page__search_api_test_view__page_1') {
+ $facet_id = $this->convertNameToMachineName($facet_name);
+
+ // Go to the Add facet page and make sure that returns a 200.
+ $facet_add_page = '/admin/config/search/facets/add-facet';
+ $this->drupalGet($facet_add_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ $form_values = [
+ 'name' => '',
+ 'id' => $facet_id,
+ ];
+
+ // Try filling out the form, but without having filled in a name for the
+ // facet to test for form errors.
+ $this->submitForm($form_values, 'Save');
+ $this->assertSession()->pageTextContains('Name field is required.');
+ $this->assertSession()->pageTextContains('Facet source field is required.');
+
+ // Make sure that when filling out the name, the form error disappears.
+ $form_values['name'] = $facet_name;
+ $this->submitForm($form_values, 'Save');
+ $this->assertSession()->pageTextNotContains('Name field is required.');
+
+ // Configure the facet source by selecting one of the Search API views.
+ $this->drupalGet($facet_add_page);
+ $this->submitForm(['facet_source_id' => '' . $source_id . ''], 'Configure facet source');
+
+ // The field is still required.
+ $this->submitForm($form_values, 'Save');
+ $this->assertSession()->pageTextContains('Field field is required.');
+
+ // Fill in all fields and make sure the 'field is required' message is no
+ // longer shown.
+ $facet_source_form = [
+ 'facet_source_id' => $source_id,
+ 'facet_source_configs[' . $source_id . '][field_identifier]' => $facet_type,
+ ];
+ $this->submitForm($form_values + $facet_source_form, 'Save');
+ $this->assertSession()->pageTextNotContains('field is required.');
+
+ // Make sure that the redirection to the display page is correct.
+ $this->assertSession()->pageTextContains('Facet ' . $facet_name . ' has been created.');
+ $this->assertSession()->addressEquals('admin/config/search/facets/' . $facet_id . '/edit');
+
+ $this->drupalGet('admin/config/search/facets');
+ }
+
+ /**
+ * Tests creating a facet with an existing machine name.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ * @param string $facet_type
+ * The type of facet to create.
+ */
+ protected function addFacetDuplicate($facet_name, $facet_type = 'type') {
+ $facet_id = $this->convertNameToMachineName($facet_name);
+
+ $facet_add_page = '/admin/config/search/facets/add-facet';
+ $this->drupalGet($facet_add_page);
+
+ $form_values = [
+ 'name' => $facet_name,
+ 'id' => $facet_id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ ];
+
+ $facet_source_configs['facet_source_configs[search_api:views_page__search_api_test_view__page_1][field_identifier]'] = $facet_type;
+
+ // Try to submit a facet with a duplicate machine name after form rebuilding
+ // via facet source submit.
+ $this->submitForm($form_values, 'Configure facet source');
+ $this->submitForm($form_values + $facet_source_configs, 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
+
+ // Try to submit a facet with a duplicate machine name after form rebuilding
+ // via facet source submit using AJAX.
+ $this->submitForm($form_values, 'Configure facet source');
+ $this->submitForm($form_values + $facet_source_configs, 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
+ }
+
+ /**
+ * Tests editing of a facet through the UI.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ */
+ protected function editFacet($facet_name) {
+ $facet_id = $this->convertNameToMachineName($facet_name);
+
+ $facet_edit_page = '/admin/config/search/facets/' . $facet_id . '/settings';
+
+ // Go to the facet edit page and make sure "edit facet %facet" is present.
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->responseContains('Facet settings for ' . $facet_name . ' facet');
+
+ // Check if it's possible to change the machine name.
+ $elements = $this->xpath('//form[@id="facets-facet-settings-form"]/div[contains(@class, "form-item-id")]/input[@disabled]');
+ $this->assertEquals(count($elements), 1, 'Machine name cannot be changed.');
+
+ // Change the facet name to add in "-2" to test editing of a facet works.
+ $form_values = ['name' => $facet_name . ' - 2'];
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm($form_values, 'Save');
+
+ // Make sure that the redirection back to the overview was successful and
+ // the edited facet is shown on the overview page.
+ $this->assertSession()->pageTextContains('Facet ' . $facet_name . ' - 2 has been updated.');
+
+ // Make sure the "-2" suffix is still on the facet when editing a facet.
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->responseContains('Facet settings for ' . $facet_name . ' - 2 facet');
+
+ // Edit the form and change the facet's name back to the initial name.
+ $form_values = ['name' => $facet_name];
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm($form_values, 'Save');
+
+ // Make sure that the redirection back to the overview was successful and
+ // the edited facet is shown on the overview page.
+ $this->assertSession()->pageTextContains('Facet ' . $facet_name . ' has been updated.');
+
+ $facet_edit_page = '/admin/config/search/facets/' . $facet_id . '/edit';
+ $this->drupalGet($facet_edit_page);
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('View Search API Test Fulltext search view, display Page');
+ }
+
+ /**
+ * Deletes a facet through the UI that still has usages.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ */
+ protected function deleteUsedFacet($facet_name) {
+ $facet_id = $this->convertNameToMachineName($facet_name);
+
+ $facet_delete_page = '/admin/config/search/facets/' . $facet_id . '/delete';
+
+ // Go to the facet delete page and make the warning is shown.
+ $this->drupalGet($facet_delete_page);
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Check that the facet by testing for the message and the absence of the
+ // facet name on the overview.
+ $this->assertSession()->responseContains("The facet is currently used in a block and thus can't be removed. Remove the block first.");
+ }
+
+ /**
+ * Deletes a facet through the UI.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ */
+ protected function deleteUnusedFacet($facet_name) {
+ $facet_id = $this->convertNameToMachineName($facet_name);
+
+ $facet_delete_page = '/admin/config/search/facets/' . $facet_id . '/delete';
+ $facet_overview = '/admin/config/search/facets';
+
+ // Go to the facet delete page and make the warning is shown.
+ $this->drupalGet($facet_delete_page);
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains("This action cannot be undone.");
+
+ // Click the cancel link and see that we redirect to the overview page.
+ $this->clickLink("Cancel");
+ $this->assertSession()->addressEquals($facet_overview);
+
+ // Back to the delete page.
+ $this->drupalGet($facet_delete_page);
+
+ // Actually submit the confirmation form.
+ $this->submitForm([], 'Delete');
+
+ // Check that the facet by testing for the message and the absence of the
+ // facet name on the overview.
+ $this->assertSession()->pageTextContains('The facet ' . $facet_name . ' has been deleted.');
+
+ // Refresh the page because on the previous page the $facet_name is still
+ // visible (in the message).
+ $this->drupalGet($facet_overview);
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextNotContains($facet_name);
+ }
+
+ /**
+ * Add fields to Search API index.
+ */
+ protected function addFieldsToIndex() {
+ $edit = [
+ 'fields[entity:node/nid][indexed]' => 1,
+ 'fields[entity:node/title][indexed]' => 1,
+ 'fields[entity:node/title][type]' => 'text',
+ 'fields[entity:node/title][boost]' => '21.0',
+ 'fields[entity:node/body][indexed]' => 1,
+ 'fields[entity:node/uid][indexed]' => 1,
+ 'fields[entity:node/uid][type]' => 'search_api_test_data_type',
+ ];
+
+ $this->drupalGet('admin/config/search/search-api/index/webtest_index/fields');
+ $this->submitForm($edit, 'Save changes');
+ $this->assertSession()->pageTextContains('The changes were successfully saved.');
+ }
+
+ /**
+ * Go to the Delete Facet Page using the facet name.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ */
+ protected function goToDeleteFacetPage($facet_name) {
+ $facet_id = $this->convertNameToMachineName($facet_name);
+
+ $facet_delete_page = '/admin/config/search/facets/' . $facet_id . '/delete';
+
+ // Go to the facet delete page and make the warning is shown.
+ $this->drupalGet($facet_delete_page);
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/LanguageIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/LanguageIntegrationTest.php
new file mode 100644
index 000000000..75b9076c2
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/LanguageIntegrationTest.php
@@ -0,0 +1,289 @@
+adminUser = $this->drupalCreateUser([
+ 'administer search_api',
+ 'administer facets',
+ 'access administration pages',
+ 'administer nodes',
+ 'access content overview',
+ 'administer content types',
+ 'administer blocks',
+ 'translate configuration',
+ ]);
+ $this->drupalLogin($this->adminUser);
+
+ ConfigurableLanguage::create([
+ 'id' => 'xx-lolspeak',
+ 'label' => 'Lolspeak',
+ ])->save();
+ ConfigurableLanguage::create([
+ 'id' => 'nl',
+ 'label' => 'Dutch',
+ ])->save();
+ ConfigurableLanguage::create([
+ 'id' => 'es',
+ 'label' => 'Spanish',
+ ])->save();
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals($this->indexItems($this->indexId), 5, '5 items were indexed.');
+
+ // Make absolutely sure the ::$blocks variable doesn't pass information
+ // along between tests.
+ $this->blocks = NULL;
+ }
+
+ /**
+ * Tests that a facet works on a page with language prefix.
+ *
+ * @see https://www.drupal.org/node/2712557
+ */
+ public function testLanguageIntegration() {
+ $facet_id = 'owl';
+ $facet_name = 'Owl';
+ $this->createFacet($facet_name, $facet_id);
+
+ // Go to the search view with a language prefix and click on one of the
+ // facets.
+ $this->drupalGet('xx-lolspeak/search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('item');
+ $this->assertSession()->pageTextContains('article');
+ $this->clickLink('item');
+
+ // Check that the language code is still in the url.
+ $this->assertNotFalse(strpos($this->getUrl(), 'xx-lolspeak/'), 'Found the language code in the url');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('item');
+ $this->assertSession()->pageTextContains('article');
+ }
+
+ /**
+ * Tests that special characters work such as äüö work.
+ *
+ * @see https://www.drupal.org/node/2838247
+ * @see https://www.drupal.org/node/2838697
+ */
+ public function testSpecialCharacters() {
+ $id = 'water_bear';
+ $name = 'Water bear';
+ $this->createFacet($name, $id, 'keywords');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->assertFacetLabel('orange');
+
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ $entity_test_storage->create([
+ 'name' => 'special-chars 1',
+ 'body' => 'test test test',
+ 'type' => 'article',
+ 'keywords' => ['ƒäüö', 'test_key-word', 'special^%s', 'Key Word'],
+ 'category' => 'article_category',
+ ])->save();
+ $entity_test_storage->create([
+ 'name' => 'special-chars 2',
+ 'body' => 'test test test',
+ 'type' => 'article',
+ 'keywords' => ['ƒäüö', 'special^%s', 'aáå'],
+ 'category' => 'article_category',
+ ])->save();
+ $this->assertEquals(2, $this->indexItems($this->indexId), '2 items were indexed.');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->assertFacetLabel('orange');
+ $this->assertFacetLabel('ƒäüö');
+ $this->assertFacetLabel('aáå');
+ $this->assertFacetLabel('special^%s');
+ $this->assertFacetLabel('test_key-word');
+ $this->assertFacetLabel('Key Word');
+ }
+
+ /**
+ * Tests the url alias translation.
+ *
+ * @see https://www.drupal.org/node/2893374
+ */
+ public function testUrlAliasTranslation() {
+ $facet_id = 'barn_owl';
+ $facet_name = 'Barn owl';
+ $this->createFacet($facet_name, $facet_id);
+
+ // Go to the search view with a language prefix and click on one of the
+ // facets.
+ $this->drupalGet('xx-lolspeak/search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->clickLink('item');
+
+ // Check that the language code is still in the url.
+ $this->assertTrue((bool) strpos($this->getUrl(), 'xx-lolspeak/'), 'Found the language code in the url');
+ $this->assertTrue((bool) strpos($this->getUrl(), 'barn_owl'), 'Found the facet in the url');
+
+ // Translate the facet.
+ $this->drupalGet('admin/config/search/facets/' . $facet_id . '/edit/translate/xx-lolspeak/add');
+ $this->submitForm(['translation[config_names][facets.facet.barn_owl][url_alias]' => 'tyto_alba'], 'Save translation');
+ $this->drupalGet('admin/config/search/facets/' . $facet_id . '/edit/translate/nl/add');
+ $this->submitForm(['translation[config_names][facets.facet.barn_owl][url_alias]' => 'uil'], 'Save translation');
+ $this->drupalGet('admin/config/search/facets/' . $facet_id . '/edit/translate/es/add');
+ $this->submitForm(['translation[config_names][facets.facet.barn_owl][url_alias]' => 'buho'], 'Save translation');
+
+ // Go to the search view again and check that we now have the translated
+ // facet in the url.
+ $this->drupalGet('xx-lolspeak/search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->clickLink('item');
+ $this->assertTrue((bool) strpos($this->getUrl(), 'xx-lolspeak/'), 'Found the language code in the url');
+ $this->assertTrue((bool) strpos($this->getUrl(), 'tyto_alba'), 'Found the facet in the url');
+
+ \Drupal::service('module_installer')->install(['locale']);
+ $block = $this->drupalPlaceBlock('language_block:' . LanguageInterface::TYPE_INTERFACE, [
+ 'id' => 'test_language_block',
+ ]);
+
+ $this->drupalGet('xx-lolspeak/search-api-test-fulltext');
+ $this->assertSession()->pageTextContains($block->label());
+ $this->clickLink('item');
+
+ /** @var \Behat\Mink\Element\NodeElement[] $links */
+ $links = $this->findFacetLink('item');
+ $this->assertEquals('is-active', $links[0]->getParent()->getAttribute('class'));
+
+ $this->clickLink('English');
+ /** @var \Behat\Mink\Element\NodeElement[] $links */
+ $links = $this->findFacetLink('item');
+ $this->assertEquals('is-active', $links[0]->getParent()->getAttribute('class'));
+ $this->assertFalse((bool) strpos($this->getUrl(), 'xx-lolspeak/'), 'Found the language code in the url');
+ $this->assertFalse((bool) strpos($this->getUrl(), 'tyto_alba'), 'Found the facet in the url');
+ $this->assertTrue((bool) strpos($this->getUrl(), 'barn_owl'), 'Found the facet in the url');
+
+ $this->clickLink('Lolspeak');
+ /** @var \Behat\Mink\Element\NodeElement[] $links */
+ $links = $this->findFacetLink('item');
+ $this->assertEquals('is-active', $links[0]->getParent()->getAttribute('class'));
+ $this->assertTrue((bool) strpos($this->getUrl(), 'xx-lolspeak/'), 'Found the language code in the url');
+ $this->assertTrue((bool) strpos($this->getUrl(), 'tyto_alba'), 'Found the facet in the url');
+ $this->assertFalse((bool) strpos($this->getUrl(), 'barn_owl'), 'Found the facet in the url');
+
+ $this->clickLink('Dutch');
+ /** @var \Behat\Mink\Element\NodeElement[] $links */
+ $links = $this->findFacetLink('item');
+ $this->assertEquals('is-active', $links[0]->getParent()->getAttribute('class'));
+ $this->assertTrue((bool) strpos($this->getUrl(), 'nl/'), 'Found the language code in the url');
+ $this->assertTrue((bool) strpos($this->getUrl(), 'uil'), 'Found the facet in the url');
+
+ $this->clickLink('Spanish');
+ /** @var \Behat\Mink\Element\NodeElement[] $links */
+ $links = $this->findFacetLink('item');
+ $this->assertEquals('is-active', $links[0]->getParent()->getAttribute('class'));
+ $this->assertTrue((bool) strpos($this->getUrl(), 'es/'), 'Found the language code in the url');
+ $this->assertTrue((bool) strpos($this->getUrl(), 'buho'), 'Found the facet in the url');
+
+ $this->clickLink('English');
+ /** @var \Behat\Mink\Element\NodeElement[] $links */
+ $links = $this->findFacetLink('item');
+ $this->assertEquals('is-active', $links[0]->getParent()->getAttribute('class'));
+ $this->assertTrue((bool) strpos($this->getUrl(), 'barn_owl'), 'Found the facet in the url');
+
+ }
+
+ /**
+ * Tests facets where the count is different per language.
+ *
+ * @see https://www.drupal.org/node/2827808
+ */
+ public function testLanguageDifferences() {
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ $entity_test_storage->create([
+ 'name' => 'foo bar baz',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange', 'lol'],
+ 'category' => 'item_category',
+ 'langcode' => 'xx-lolspeak',
+ ])->save();
+ $entity_test_storage->create([
+ 'name' => 'foo bar baz',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange', 'rofl'],
+ 'category' => 'item_category',
+ 'langcode' => 'xx-lolspeak',
+ ])->save();
+
+ $id = 'water_bear';
+ $this->createFacet('Water bear', $id, 'keywords');
+
+ $this->drupalGet('admin/config/search/search-api/index/' . $this->indexId . '/edit');
+
+ $this->assertEquals(2, $this->indexItems($this->indexId), '2 items were indexed.');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetBlocksAppear();
+ $this->assertSession()->pageTextContains('orange');
+ $this->assertSession()->pageTextContains('grape');
+ $this->assertSession()->pageTextContains('rofl');
+
+ $this->submitForm(['language' => 'xx-lolspeak'], 'Search');
+ $this->assertFacetBlocksAppear();
+ $this->assertSession()->pageTextContains('orange');
+ $this->assertSession()->pageTextContains('rofl');
+ $this->assertSession()->pageTextNotContains('grape');
+
+ $this->submitForm(['language' => 'en'], 'Search');
+ $this->assertFacetBlocksAppear();
+ $this->assertSession()->pageTextContains('orange');
+ $this->assertSession()->pageTextContains('grape');
+ $this->assertSession()->pageTextNotContains('rofl');
+ }
+
+ /**
+ * Tests the admin translation screen.
+ */
+ public function testAdminTranslation() {
+ $id = 'water_bear';
+ $this->createFacet('Water bear', $id);
+ // Translate the facet.
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit/translate/xx-lolspeak/add');
+ $this->submitForm(['translation[config_names][facets.facet.water_bear][name]' => 'Tardigrade'], 'Save translation');
+
+ $this->drupalGet('admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Water bear');
+ $this->drupalGet('xx-lolspeak/admin/config/search/facets');
+ $this->assertSession()->pageTextContains('Tardigrade');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/ProcessorIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/ProcessorIntegrationTest.php
new file mode 100644
index 000000000..a5c18dd16
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/ProcessorIntegrationTest.php
@@ -0,0 +1,941 @@
+drupalLogin($this->adminUser);
+
+ // Set up example content types and insert 10 new content items.
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals($this->indexItems($this->indexId), 5, '5 items were indexed.');
+ $this->insertExampleContent();
+ $this->assertEquals($this->indexItems($this->indexId), 5, '5 items were indexed.');
+ }
+
+ /**
+ * Tests for the processors behavior in the backend.
+ */
+ public function testProcessorAdmin() {
+ $facet_name = "Guanaco";
+ $facet_id = "guanaco";
+
+ $this->createFacet($facet_name, $facet_id);
+
+ // Go to the processors form and check that the count limit processor is not
+ // checked.
+ $this->drupalGet('admin/config/search/facets/' . $facet_id . '/edit');
+ $this->assertSession()->checkboxNotChecked('edit-facet-settings-count-limit-status');
+
+ $form = ['facet_settings[count_limit][status]' => TRUE];
+ $this->submitForm($form, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->checkboxChecked('edit-facet-settings-count-limit-status');
+
+ // Enable the sort processor and change sort direction, check that the
+ // change is persisted.
+ $form = [
+ 'facet_sorting[active_widget_order][status]' => TRUE,
+ 'facet_sorting[active_widget_order][settings][sort]' => 'DESC',
+ ];
+ $this->submitForm($form, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->checkboxChecked('edit-facet-sorting-active-widget-order-status');
+ $this->assertSession()->checkboxChecked('edit-facet-sorting-active-widget-order-settings-sort-desc');
+
+ // Add an extra processor so we can test the weights as well.
+ $form = [
+ 'facet_settings[hide_non_narrowing_result_processor][status]' => TRUE,
+ 'facet_settings[count_limit][status]' => TRUE,
+ ];
+ $this->submitForm($form, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->checkboxChecked('edit-facet-settings-count-limit-status');
+ $this->assertSession()->checkboxChecked('edit-facet-settings-hide-non-narrowing-result-processor-status');
+ $this->assertOptionSelected('edit-processors-count-limit-weights-build', 50);
+ $this->assertOptionSelected('edit-processors-hide-non-narrowing-result-processor-weights-build', 40);
+
+ // Change the weight of one of the processors and test that the weight
+ // change persisted.
+ $form = [
+ 'facet_settings[hide_non_narrowing_result_processor][status]' => TRUE,
+ 'facet_settings[count_limit][status]' => TRUE,
+ 'processors[hide_non_narrowing_result_processor][weights][build]' => 5,
+ ];
+ $this->submitForm($form, 'Save');
+ $this->assertSession()->checkboxChecked('edit-facet-settings-count-limit-status');
+ $this->assertSession()->checkboxChecked('edit-facet-settings-hide-non-narrowing-result-processor-status');
+ $this->assertOptionSelected('edit-processors-count-limit-weights-build', 50);
+ $this->assertOptionSelected('edit-processors-hide-non-narrowing-result-processor-weights-build', 5);
+ }
+
+ /**
+ * Tests the for processors in the frontend with a 'keywords' facet.
+ */
+ public function testProcessorIntegration() {
+ $facet_name = "Vicuña";
+ $facet_id = "vicuna";
+ $this->editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+
+ $this->createFacet($facet_name, $facet_id, 'keywords');
+ $this->drupalGet($this->editForm);
+ $this->submitForm(['facet_settings[query_operator]' => 'and'], 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 10 search results');
+ $this->assertSession()->pageTextContains('grape');
+ $this->assertSession()->pageTextContains('orange');
+ $this->assertSession()->pageTextContains('apple');
+ $this->assertSession()->pageTextContains('strawberry');
+ $this->assertSession()->pageTextContains('banana');
+
+ $this->checkCountLimitProcessor();
+ $this->checkExcludeItems();
+ $this->checkHideNonNarrowingProcessor();
+ $this->checkHideActiveItems();
+ }
+
+ /**
+ * Tests the for processors in the frontend with a 'boolean' facet.
+ */
+ public function testBooleanProcessorIntegration() {
+ $field_name = 'field_boolean';
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'type' => 'boolean',
+ ]);
+ $field_storage->save();
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => 'item',
+ ]);
+ $field->save();
+
+ $index = $this->getIndex();
+
+ // Index a boolean field.
+ $boolean_field = new Field($index, $field_name);
+ $boolean_field->setType('integer');
+ $boolean_field->setPropertyPath($field_name);
+ $boolean_field->setDatasourceId('entity:entity_test_mulrev_changed');
+ $boolean_field->setLabel('BooleanField');
+ $index->addField($boolean_field);
+
+ $index->save();
+ $this->indexItems($this->indexId);
+
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ $entity_test_storage->create([
+ 'name' => 'foo bar baz',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ $field_name => TRUE,
+ ])->save();
+ $entity_test_storage->create([
+ 'name' => 'quux quuux',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['apple'],
+ 'category' => 'item_category',
+ $field_name => FALSE,
+ ])->save();
+
+ $this->indexItems($this->indexId);
+
+ $facet_name = "Boolean";
+ $facet_id = "boolean";
+
+ // Create facet.
+ $this->editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet($facet_name, $facet_id, $field_name);
+
+ // Check values.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('1');
+ $this->assertFacetLabel('0');
+
+ $form = [
+ 'facet_settings[boolean_item][status]' => TRUE,
+ 'facet_settings[boolean_item][settings][on_value]' => 'Yes',
+ 'facet_settings[boolean_item][settings][off_value]' => 'No',
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->checkboxChecked('edit-facet-settings-boolean-item-status');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('Yes');
+ $this->assertFacetLabel('No');
+
+ $form = [
+ 'facet_settings[boolean_item][status]' => TRUE,
+ 'facet_settings[boolean_item][settings][on_value]' => 'Øn',
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('Øn');
+ $this->assertEmpty($this->findFacetLink('1'));
+ $this->assertEmpty($this->findFacetLink('0'));
+
+ $form = [
+ 'facet_settings[boolean_item][status]' => TRUE,
+ 'facet_settings[boolean_item][settings][off_value]' => 'Øff',
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('Øff');
+ $this->assertEmpty($this->findFacetLink('1'));
+ $this->assertEmpty($this->findFacetLink('0'));
+ }
+
+ /**
+ * Tests the for configuration of granularity processor.
+ */
+ public function testNumericGranularity() {
+ $field_name = 'field_integer';
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'type' => 'integer',
+ ]);
+ $field_storage->save();
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => 'item',
+ ]);
+ $field->save();
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => 'article',
+ ]);
+ $field->save();
+
+ $index = $this->getIndex();
+
+ // Index the taxonomy and entity reference fields.
+ $integerField = new Field($index, $field_name);
+ $integerField->setType('integer');
+ $integerField->setPropertyPath($field_name);
+ $integerField->setDatasourceId('entity:entity_test_mulrev_changed');
+ $integerField->setLabel('IntegerField');
+ $index->addField($integerField);
+
+ $index->save();
+ $this->indexItems($this->indexId);
+
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+
+ foreach ([30, 35, 40, 100] as $val) {
+ $entity_test_storage->create([
+ 'name' => 'foo bar baz',
+ 'body' => 'test test int',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ $field_name => $val,
+ ])->save();
+ }
+
+ $this->indexItems($this->indexId);
+
+ $facet_id = "integer";
+
+ // Create facet.
+ $this->editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet("Integer", $facet_id, $field_name);
+
+ // Check values.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('100');
+ $this->assertFacetLabel('30');
+ $this->assertFacetLabel('35');
+ $this->assertFacetLabel('40');
+
+ $form = [
+ 'facet_settings[granularity_item][status]' => TRUE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ // Check values.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('30 - 31 (1)');
+ $this->assertFacetLabel('35 - 36');
+ $this->assertFacetLabel('40 - 41');
+ $this->assertFacetLabel('100 - 101');
+
+ $form = [
+ 'facet_settings[granularity_item][status]' => TRUE,
+ 'facet_settings[granularity_item][settings][granularity]' => 10,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ // Check values.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('30 - 40 (2)');
+ $this->assertEmpty($this->findFacetLink('35 - 36'));
+ $this->assertFacetLabel('40 - 50');
+ $this->assertFacetLabel('100 - 110');
+ }
+
+ /**
+ * Tests the for sorting processors in the frontend with a 'keywords' facet.
+ */
+ public function testSortingWidgets() {
+ $facet_name = "Huacaya alpaca";
+ $facet_id = "huacaya_alpaca";
+ $this->editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+
+ $this->createFacet($facet_name, $facet_id, 'keywords');
+
+ $this->checkSortByActive();
+ $this->checkSortByCount();
+ $this->checkSortByDisplay();
+ $this->checkSortByRaw();
+ }
+
+ /**
+ * Tests sorting of results.
+ */
+ public function testResultSorting() {
+ $id = 'burrowing_owl';
+ $name = 'Burrowing owl';
+ $this->editForm = 'admin/config/search/facets/' . $id . '/edit';
+
+ $this->createFacet($name, $id, 'keywords');
+ $this->disableAllFacetSorts();
+
+ $values = [
+ 'facet_sorting[display_value_widget_order][status]' => TRUE,
+ 'widget_config[show_numbers]' => TRUE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($values, 'Save');
+
+ $expected_results = [
+ 'apple',
+ 'banana',
+ 'grape',
+ 'orange',
+ 'strawberry',
+ ];
+
+ $this->drupalGet('search-api-test-fulltext');
+ foreach ($expected_results as $k => $link) {
+ if ($k > 0) {
+ $x = $expected_results[($k - 1)];
+ $y = $expected_results[$k];
+ $this->assertStringPosition($x, $y);
+ }
+ }
+
+ // Sort by count, then by display value.
+ $values['facet_sorting[count_widget_order][status]'] = TRUE;
+ $values['facet_sorting[count_widget_order][settings][sort]'] = 'ASC';
+ $values['processors[count_widget_order][weights][sort]'] = 1;
+ $values['facet_sorting[display_value_widget_order][status]'] = TRUE;
+ $values['processors[display_value_widget_order][weights][sort]'] = 2;
+ $this->disableAllFacetSorts();
+ $this->submitForm($values, 'Save');
+
+ $expected_results = [
+ 'banana',
+ 'apple',
+ 'strawberry',
+ 'grape',
+ 'orange',
+ ];
+
+ $this->drupalGet('search-api-test-fulltext');
+ foreach ($expected_results as $k => $link) {
+ if ($k > 0) {
+ $x = $expected_results[($k - 1)];
+ $y = $expected_results[$k];
+ $this->assertStringPosition($x, $y);
+ }
+ }
+
+ $values['facet_sorting[display_value_widget_order][status]'] = TRUE;
+ $values['facet_sorting[count_widget_order][status]'] = TRUE;
+ $values['facet_sorting[count_widget_order][settings][sort]'] = 'ASC';
+ $this->drupalGet($this->editForm);
+ $this->submitForm($values, 'Save');
+ $this->assertSession()->checkboxChecked('edit-facet-sorting-display-value-widget-order-status');
+ $this->assertSession()->checkboxChecked('edit-facet-sorting-count-widget-order-status');
+
+ $expected_results = [
+ 'banana',
+ 'apple',
+ 'strawberry',
+ 'grape',
+ 'orange',
+ ];
+
+ $this->drupalGet('search-api-test-fulltext');
+ foreach ($expected_results as $k => $link) {
+ if ($k > 0) {
+ $x = $expected_results[($k - 1)];
+ $y = $expected_results[$k];
+ $this->assertStringPosition($x, $y);
+ }
+ }
+ }
+
+ /**
+ * Tests the count limit processor.
+ */
+ protected function checkCountLimitProcessor() {
+ $this->drupalGet($this->editForm);
+
+ $form = [
+ 'widget_config[show_numbers]' => TRUE,
+ 'facet_settings[count_limit][status]' => TRUE,
+ ];
+ $this->submitForm($form, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->checkboxChecked('edit-facet-settings-count-limit-status');
+ $form = [
+ 'widget_config[show_numbers]' => TRUE,
+ 'facet_settings[count_limit][status]' => TRUE,
+ ];
+ $this->submitForm($form, 'Save');
+
+ $form = [
+ 'widget_config[show_numbers]' => TRUE,
+ 'facet_settings[count_limit][status]' => TRUE,
+ 'facet_settings[count_limit][settings][minimum_items]' => 5,
+ ];
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 10 search results');
+ $this->assertFacetLabel('grape (6)');
+ $this->assertSession()->pageTextNotContains('apple');
+
+ $form = [
+ 'widget_config[show_numbers]' => TRUE,
+ 'facet_settings[count_limit][status]' => TRUE,
+ 'facet_settings[count_limit][settings][minimum_items]' => 1,
+ 'facet_settings[count_limit][settings][maximum_items]' => 5,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 10 search results');
+ $this->assertSession()->pageTextNotContains('grape');
+ $this->assertFacetLabel('apple (4)');
+
+ $form = [
+ 'widget_config[show_numbers]' => FALSE,
+ 'facet_settings[count_limit][status]' => FALSE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ }
+
+ /**
+ * Tests the exclude items.
+ */
+ protected function checkExcludeItems() {
+ $form = [
+ 'facet_settings[exclude_specified_items][status]' => TRUE,
+ ];
+ $this->submitForm($form, 'Save');
+
+ $form = [
+ 'facet_settings[exclude_specified_items][status]' => TRUE,
+ 'facet_settings[exclude_specified_items][settings][exclude]' => 'banana',
+ ];
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 10 search results');
+ $this->assertSession()->pageTextContains('grape');
+ $this->assertSession()->pageTextNotContains('banana');
+
+ $form = [
+ 'facet_settings[exclude_specified_items][status]' => TRUE,
+ 'facet_settings[exclude_specified_items][settings][exclude]' => '(.*)berry',
+ 'facet_settings[exclude_specified_items][settings][regex]' => TRUE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 10 search results');
+ $this->assertSession()->pageTextNotContains('strawberry');
+ $this->assertSession()->pageTextContains('grape');
+
+ $form = [
+ 'facet_settings[exclude_specified_items][status]' => FALSE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ }
+
+ /**
+ * Tests hiding non-narrowing results.
+ */
+ protected function checkHideNonNarrowingProcessor() {
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 10 search results');
+ $this->assertFacetLabel('apple');
+
+ $this->clickLink('apple');
+ $this->assertSession()->pageTextContains('Displaying 4 search results');
+ $this->assertFacetLabel('grape');
+
+ $form = [
+ 'facet_settings[hide_non_narrowing_result_processor][status]' => TRUE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 10 search results');
+ $this->assertFacetLabel('apple');
+
+ $this->clickLink('apple');
+ $this->assertSession()->pageTextContains('Displaying 4 search results');
+ $this->assertSession()->linkNotExists('grape');
+
+ $form = [
+ 'facet_settings[hide_non_narrowing_result_processor][status]' => FALSE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ }
+
+ /**
+ * Tests hiding active results.
+ */
+ protected function checkHideActiveItems() {
+ $form = [
+ 'facet_settings[hide_active_items_processor][status]' => TRUE,
+ ];
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 10 search results');
+ $this->assertFacetLabel('grape');
+ $this->assertFacetLabel('banana');
+
+ $this->clickLink('grape');
+ $this->assertSession()->pageTextContains('Displaying 6 search results');
+ $this->assertSession()->linkNotExists('grape');
+ $this->assertFacetLabel('banana');
+
+ $form = [
+ 'facet_settings[hide_active_items_processor][status]' => FALSE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ }
+
+ /**
+ * Tests the active widget order.
+ */
+ protected function checkSortByActive() {
+ $this->disableAllFacetSorts();
+ $form = [
+ 'facet_sorting[active_widget_order][status]' => TRUE,
+ 'facet_sorting[active_widget_order][settings][sort]' => 'ASC',
+ ];
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('strawberry');
+ $this->assertStringPosition('strawberry', 'grape');
+
+ $form = [
+ 'facet_sorting[active_widget_order][status]' => TRUE,
+ 'facet_sorting[active_widget_order][settings][sort]' => 'DESC',
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('strawberry');
+ $this->assertStringPosition('grape', 'strawberry');
+
+ $form = [
+ 'facet_sorting[active_widget_order][status]' => FALSE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ }
+
+ /**
+ * Tests the active widget order.
+ */
+ protected function checkSortByCount() {
+ $this->disableAllFacetSorts();
+ $form = [
+ 'widget_config[show_numbers]' => TRUE,
+ 'facet_sorting[count_widget_order][status]' => TRUE,
+ 'facet_sorting[count_widget_order][settings][sort]' => 'ASC',
+ ];
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertStringPosition('banana', 'apple');
+ $this->assertStringPosition('banana', 'strawberry');
+ $this->assertStringPosition('apple', 'orange');
+
+ $form = [
+ 'facet_sorting[count_widget_order][status]' => TRUE,
+ 'facet_sorting[count_widget_order][settings][sort]' => 'DESC',
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertStringPosition('apple', 'banana');
+ $this->assertStringPosition('strawberry', 'banana');
+ $this->assertStringPosition('orange', 'apple');
+
+ $form = [
+ 'widget_config[show_numbers]' => FALSE,
+ 'facet_sorting[count_widget_order][status]' => FALSE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ }
+
+ /**
+ * Tests the display order.
+ */
+ protected function checkSortByDisplay() {
+ $this->disableAllFacetSorts();
+ $form = ['facet_sorting[display_value_widget_order][status]' => TRUE];
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertStringPosition('grape', 'strawberry');
+ $this->assertStringPosition('apple', 'banana');
+
+ $form = [
+ 'facet_sorting[display_value_widget_order][status]' => TRUE,
+ 'facet_sorting[display_value_widget_order][settings][sort]' => 'DESC',
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertStringPosition('strawberry', 'grape');
+ $this->assertStringPosition('banana', 'apple');
+
+ $form = ['facet_sorting[display_value_widget_order][status]' => FALSE];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ }
+
+ /**
+ * Tests the display order.
+ */
+ protected function checkSortByRaw() {
+ $this->disableAllFacetSorts();
+ $form = [
+ 'facet_sorting[raw_value_widget_order][status]' => TRUE,
+ ];
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertStringPosition('grape', 'strawberry');
+ $this->assertStringPosition('apple', 'banana');
+
+ $form = [
+ 'facet_sorting[raw_value_widget_order][status]' => TRUE,
+ 'facet_sorting[raw_value_widget_order][settings][sort]' => 'DESC',
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertStringPosition('strawberry', 'grape');
+ $this->assertStringPosition('banana', 'apple');
+
+ $form = [
+ 'facet_sorting[raw_value_widget_order][status]' => FALSE,
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ }
+
+ /**
+ * Disables all sorting processors for a clean testing base.
+ */
+ protected function disableAllFacetSorts($path = FALSE) {
+ $settings = [
+ 'facet_sorting[raw_value_widget_order][status]' => FALSE,
+ 'facet_sorting[display_value_widget_order][status]' => FALSE,
+ 'facet_sorting[count_widget_order][status]' => FALSE,
+ 'facet_sorting[active_widget_order][status]' => FALSE,
+ ];
+ if (!$path) {
+ $path = $this->editForm;
+ }
+ $this->drupalGet($path);
+ $this->submitForm($settings, 'Save');
+ }
+
+ /**
+ * Checks if the list processor changes machine name to the display label.
+ */
+ public function testListProcessor() {
+ entity_test_create_bundle('basic', "Basic page", 'entity_test_mulrev_changed');
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+
+ // Add an entity with basic page content type.
+ $entity_test_storage->create([
+ 'name' => 'AC0871108',
+ 'body' => 'Eamus Catuli',
+ 'type' => 'basic',
+ ])->save();
+ $this->indexItems($this->indexId);
+
+ $facet_name = "Eamus Catuli";
+ $facet_id = "eamus_catuli";
+ $editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet($facet_name, $facet_id);
+
+ // Go to the overview and check that the machine names are used as facets.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 11 search results');
+ $this->assertFacetLabel('basic');
+
+ // Edit the facet to use the list_item processor.
+ $edit = [
+ 'facet_settings[list_item][status]' => TRUE,
+ ];
+ $this->drupalGet($editForm);
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->checkboxChecked('edit-facet-settings-list-item-status');
+
+ // Go back to the overview and check that now the label is being used
+ // instead.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 11 search results');
+ $this->assertFacetLabel('Basic page');
+ }
+
+ /**
+ * Test pre query processor.
+ */
+ public function testPreQueryProcessor() {
+ $facet_name = "Eamus Catuli";
+ $facet_id = "eamus_catuli";
+ $editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet($facet_name, $facet_id);
+
+ $edit = [
+ 'facet_settings[test_pre_query][status]' => TRUE,
+ 'facet_settings[test_pre_query][settings][test_value]' => 'Llama',
+ ];
+ $this->drupalGet($editForm);
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('Llama');
+ }
+
+ /**
+ * Tests the facet support for a widget.
+ */
+ public function testSupportsFacet() {
+ $id = 'masked_owl';
+ $this->createFacet('Australian masked owl', $id);
+
+ // Go to the facet edit page and check to see if the custom processor shows
+ // up.
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->assertSession()->pageTextContains('test pre query');
+
+ // Make the ::supportsFacet method on the custom processor return false.
+ \Drupal::state()->set('facets_test_supports_facet', FALSE);
+
+ // Go to the facet edit page and check to see if the custom processor is
+ // now hidden.
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->assertSession()->pageTextNotContains('test pre query');
+ }
+
+ /**
+ * Test HideOnlyOneItemProcessor.
+ *
+ * Test if after clicking an item that has only one item, the facet block no
+ * longer shows.
+ */
+ public function testHideOnlyOneItemProcessor() {
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+
+ // Load all items and delete them.
+ $all = $entity_test_storage->loadMultiple();
+ foreach ($all as $item) {
+ $item->delete();
+ }
+ $entity_test_storage->create([
+ 'name' => 'baz baz',
+ 'body' => 'foo test',
+ 'type' => 'article',
+ 'keywords' => ['kiwi'],
+ 'category' => 'article_category',
+ ])->save();
+ $this->indexItems($this->indexId);
+
+ $facet_name = 'Drupalcon Vienna';
+ $facet_id = 'drupalcon_vienna';
+ $this->editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet($facet_name, $facet_id, 'keywords');
+
+ $form = [
+ 'facet_settings[hide_1_result_facet][status]' => TRUE,
+ 'facet_settings[query_operator]' => 'and',
+ ];
+ $this->drupalGet($this->editForm);
+ $this->submitForm($form, 'Save');
+ $this->drupalGet('search-api-test-fulltext');
+
+ $this->assertSession()->pageTextContains('Displaying 1 search results');
+ $this->assertNoFacetBlocksAppear();
+ }
+
+ /**
+ * Tests that processors are hidden when the correct fields aren't there.
+ */
+ public function testHiddenProcessors() {
+ $facet_id = 'alpaca';
+ $this->editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet('Alpaca', $facet_id);
+ $this->drupalGet($this->editForm);
+ $this->assertSession()->pageTextNotContains('Boolean item label');
+ $this->assertSession()->pageTextNotContains('Transform UID to user name');
+ $this->assertSession()->pageTextNotContains('Transform entity ID to label');
+ $this->assertSession()->pageTextNotContains('Sort by taxonomy term weight');
+ }
+
+ /**
+ * Tests the configuration of the processors.
+ */
+ public function testProcessorConfig() {
+ $this->createFacet('Llama', 'llama');
+
+ $facet_id = 'alpaca';
+ $this->editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet('Alpaca', $facet_id);
+ $this->drupalGet($this->editForm);
+
+ $facet = Facet::load($facet_id);
+
+ /** @var \Drupal\facets\Processor\ProcessorInterface $processor */
+ foreach ($facet->getProcessors(FALSE) as $processor) {
+ // Sort processors have a different form key, so don't bother for now.
+ if ($processor instanceof SortProcessorInterface) {
+ continue;
+ }
+ // These processors are hidden by default, see also
+ // ::testHiddenProcessors.
+ $hiddenProcessors = [
+ 'boolean_item',
+ 'translate_entity',
+ 'translate_entity_aggregated_fields',
+ 'uid_to_username_callback',
+ ];
+ if (in_array($processor->getPluginId(), $hiddenProcessors)) {
+ continue;
+ }
+
+ $this->submitForm(["facet_settings[{$processor->getPluginId()}][status]" => '1'], 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ }
+ }
+
+ /**
+ * Tests the list item processor with underscores in the bundle.
+ */
+ public function testEntityTranslateWithUnderScores() {
+ entity_test_create_bundle('test_with_underscore', "Test with underscore", 'entity_test_mulrev_changed');
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+
+ // Add an entity with basic page content type.
+ $entity_test_storage->create([
+ 'name' => 'llama',
+ 'body' => 'llama.',
+ 'type' => 'test_with_underscore',
+ ])->save();
+ $this->indexItems($this->indexId);
+
+ $facet_id = 'owl';
+ $editForm = 'admin/config/search/facets/' . $facet_id . '/edit';
+ $this->createFacet('Owl', $facet_id);
+
+ // Go to the overview and check that the machine names are used as facets.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 11 search results');
+ $this->assertFacetLabel('test_with_underscore');
+
+ // Edit the facet to use the list_item processor.
+ $edit = [
+ 'facet_settings[list_item][status]' => TRUE,
+ ];
+ $this->drupalGet($editForm);
+ $this->submitForm($edit, 'Save');
+
+ // Go back to the overview and check that now the label is being used
+ // instead.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 11 search results');
+ $this->assertFacetLabel('Test with underscore');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetJsonAnonTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetJsonAnonTest.php
new file mode 100644
index 000000000..d7cf6d885
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetJsonAnonTest.php
@@ -0,0 +1,26 @@
+grantPermissionsToTestedRole(['administer facets']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createEntity() {
+ $facet = Facet::create();
+ $facet->set('id', 'owl');
+ $facet->set('uuid', 'uuid-owl');
+ $facet->setWidget('links', ['show_numbers' => TRUE]);
+ $facet->addProcessor([
+ 'processor_id' => 'url_processor_handler',
+ 'weights' => ['pre_query' => -10, 'build' => -10],
+ 'settings' => [],
+ ]);
+ $facet->setEmptyBehavior(['behavior' => 'none']);
+ $facet->save();
+
+ return $facet;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedNormalizedEntity() {
+ return [
+ 'dependencies' => [],
+ 'empty_behavior' => ['behavior' => 'none'],
+ 'enable_parent_when_child_gets_disabled' => TRUE,
+ 'exclude' => FALSE,
+ 'keep_hierarchy_parents_active' => FALSE,
+ 'expand_hierarchy' => FALSE,
+ 'facet_source_id' => NULL,
+ 'field_identifier' => NULL,
+ 'hard_limit' => NULL,
+ 'hierarchy' => [
+ 'config' => [],
+ 'type' => 'taxonomy',
+ ],
+ 'id' => 'owl',
+ 'langcode' => 'en',
+ 'min_count' => 1,
+ 'name' => NULL,
+ 'only_visible_when_facet_source_is_visible' => NULL,
+ 'processor_configs' => [
+ 'url_processor_handler' => [
+ 'processor_id' => 'url_processor_handler',
+ 'settings' => [],
+ 'weights' => [
+ 'build' => -10,
+ 'pre_query' => -10,
+ ],
+ ],
+ ],
+ 'query_operator' => NULL,
+ 'show_only_one_result' => FALSE,
+ 'show_title' => NULL,
+ 'status' => TRUE,
+ 'url_alias' => NULL,
+ 'use_hierarchy' => FALSE,
+ 'uuid' => 'uuid-owl',
+ 'weight' => NULL,
+ 'widget' => [
+ 'config' => [
+ 'show_numbers' => TRUE,
+ ],
+ 'type' => 'links',
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedPostEntity() {
+ // @todo Update after https://www.drupal.org/node/2300677.
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetSourceJsonAnonTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetSourceJsonAnonTest.php
new file mode 100644
index 000000000..b4e60b477
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetSourceJsonAnonTest.php
@@ -0,0 +1,26 @@
+grantPermissionsToTestedRole(['administer facets']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createEntity() {
+ $entity = FacetSource::create();
+ $entity->set('id', 'red_panda')
+ ->set('name', 'Red panda')
+ ->set('uuid', 'red-panda-uuid')
+ ->save();
+
+ return $entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getExpectedNormalizedEntity() {
+ return [
+ 'breadcrumb' => [],
+ 'dependencies' => [],
+ 'filter_key' => NULL,
+ 'id' => 'red_panda',
+ 'langcode' => 'en',
+ 'name' => 'Red panda',
+ 'uuid' => 'red-panda-uuid',
+ 'status' => TRUE,
+ 'url_processor' => 'query_string',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedPostEntity() {
+ // @todo Update after https://www.drupal.org/node/2300677.
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetSourceXmlAnonTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetSourceXmlAnonTest.php
new file mode 100644
index 000000000..0392164e0
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/Rest/FacetSourceXmlAnonTest.php
@@ -0,0 +1,28 @@
+findFacetLink($label);
+
+ $message = ($message ? $message : strtr('Link with label %label found.', ['%label' => $label]));
+ return $this->assertArrayHasKey($index, $links, $message, $group);
+ }
+
+ /**
+ * Check if a facet is active by providing a label for it.
+ *
+ * We'll check by activeness by seeing that there's a span with (-) in the
+ * same link as the label.
+ *
+ * @param string $label
+ * The label of a facet that should be active.
+ *
+ * @return bool
+ * Returns true when the facet is found and is active.
+ */
+ protected function checkFacetIsActive($label) {
+ $links = $this->findFacetLink($label);
+ return $this->assertArrayHasKey(0, $links);
+ }
+
+ /**
+ * Check if a facet is not active by providing a label for it.
+ *
+ * We'll check by activeness by seeing that there's no span with (-) in the
+ * same link as the label.
+ *
+ * @param string $label
+ * The label of a facet that should be active.
+ *
+ * @return bool
+ * Returns true when the facet is found and is active.
+ */
+ protected function checkFacetIsNotActive($label) {
+ $label = (string) $label;
+ $label = strip_tags($label);
+ $links = $this->xpath('//a/span[1][normalize-space(text())=:label]', [':label' => $label]);
+ return $this->assertArrayHasKey(0, $links);
+ }
+
+ /**
+ * Asserts that a facet block does not appear.
+ */
+ protected function assertNoFacetBlocksAppear() {
+ foreach ($this->blocks as $block) {
+ $xpath = $this->xpath('//div[@id = :id]/div[@class="facet-empty"]', [':id' => 'block-' . $block->id()]);
+ if (!$xpath) {
+ $this->assertEmpty($xpath);
+ }
+ else {
+ $this->assertTrue($this->xpath('//div[@id = :id]/div[@class="facet-empty"]', [':id' => 'block-' . $block->id()]));
+ }
+ }
+ }
+
+ /**
+ * Asserts that a facet block appears.
+ */
+ protected function assertFacetBlocksAppear() {
+ foreach ($this->blocks as $block) {
+ $this->xpath('//div[@id = :id]', [':id' => 'block-' . $block->id()]);
+ $this->assertSession()->pageTextContains($block->label());
+ }
+ }
+
+ /**
+ * Asserts that a string is found before another string in the source.
+ *
+ * This uses the simpletest's getRawContent method to search in the source of
+ * the page for the position of 2 strings and that the first argument is
+ * before the second argument's position.
+ *
+ * @param string $x
+ * A string.
+ * @param string $y
+ * Another string.
+ */
+ protected function assertStringPosition($x, $y) {
+ $this->assertSession()->pageTextContains($x);
+ $this->assertSession()->pageTextContains($y);
+
+ $x_position = strpos($this->getTextContent(), $x);
+ $y_position = strpos($this->getTextContent(), $y);
+
+ $message = new FormattableMarkup(
+ 'Assert that %x is before %y in the source',
+ [
+ '%x' => $x,
+ '%y' => $y,
+ ]
+ );
+ $this->assertTrue($x_position < $y_position, $message);
+ }
+
+ /**
+ * Clicks the test facet.
+ */
+ protected function clickFacet() {
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ $this->clickLink('item');
+
+ $this->assertSession()->statusCodeEquals(200);
+ $this->checkFacetIsActive('item');
+ $this->assertFacetLabel('article');
+ }
+
+ /**
+ * Click a link by partial name.
+ *
+ * @param string $label
+ * The label of a link to click.
+ */
+ protected function clickPartialLink($label) {
+ $label = (string) $label;
+
+ $xpath = $this->assertSession()->buildXPathQuery('//a[starts-with(normalize-space(), :label)]', [':label' => $label]);
+ $links = $this->getSession()->getPage()->findAll('xpath', $xpath);
+ $links[0]->click();
+ }
+
+ /**
+ * Use xpath to find a facet link.
+ *
+ * @param string $label
+ * Label of a link to find.
+ *
+ * @return array
+ * An array of links with the facets.
+ */
+ protected function findFacetLink($label) {
+ $label = (string) $label;
+ $label = strip_tags($label);
+ $matches = [];
+
+ if (preg_match('/(.*)\s\((\d+)\)/', $label, $matches)) {
+ $links = $this->xpath(
+ '//a//span[normalize-space(text())=:label]/following-sibling::span[normalize-space(text())=:count]',
+ [
+ ':label' => $matches[1],
+ ':count' => '(' . $matches[2] . ')',
+ ]
+ );
+ }
+ else {
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => $label]);
+ }
+
+ return $links;
+ }
+
+ /**
+ * Convert facet name to machine name.
+ *
+ * @param string $facet_name
+ * The name of the facet.
+ *
+ * @return string
+ * The facet name changed to a machine name.
+ */
+ protected function convertNameToMachineName($facet_name) {
+ return preg_replace('@[^a-zA-Z0-9_]+@', '_', strtolower($facet_name));
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/UrlIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/UrlIntegrationTest.php
new file mode 100644
index 000000000..b8afb95d1
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/UrlIntegrationTest.php
@@ -0,0 +1,242 @@
+drupalLogin($this->adminUser);
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals($this->indexItems($this->indexId), 5, '5 items were indexed.');
+ }
+
+ /**
+ * Tests various url integration things.
+ */
+ public function testUrlIntegration() {
+ $id = 'facet';
+ $name = '&^Facet@#1';
+ $this->createFacet($name, $id);
+
+ $url = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['f[0]' => 'facet:item']]);
+ $this->clickFacet();
+ $this->assertSession()->addressEquals($url);
+
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = Facet::load($id);
+ $this->assertInstanceOf(FacetInterface::class, $facet);
+ $config = $facet->getFacetSourceConfig();
+ $this->assertInstanceOf(FacetSourceInterface::class, $config);
+ $this->assertEquals('f', $config->getFilterKey());
+
+ $facet = NULL;
+ $config = NULL;
+
+ // Go to the only enabled facet source's config and change the filter key.
+ $this->drupalGet('admin/config/search/facets');
+ $this->clickLink('Configure', 1);
+
+ $edit = [
+ 'filter_key' => 'y',
+ 'url_processor' => 'query_string',
+ ];
+ $this->submitForm($edit, 'Save');
+
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = Facet::load($id);
+ $config = $facet->getFacetSourceConfig();
+ $this->assertInstanceOf(FacetSourceInterface::class, $config);
+ $this->assertEquals('y', $config->getFilterKey());
+
+ $facet = NULL;
+ $config = NULL;
+
+ $url_2 = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['y[0]' => 'facet:item']]);
+ $this->clickFacet();
+ $this->assertSession()->addressEquals($url_2);
+
+ // Go to the only enabled facet source's config and change the url
+ // processor.
+ $this->drupalGet('admin/config/search/facets');
+ $this->clickLink('Configure', 1);
+
+ $edit = [
+ 'filter_key' => 'y',
+ 'url_processor' => 'dummy_query',
+ ];
+ $this->submitForm($edit, 'Save');
+
+ /** @var \Drupal\facets\FacetInterface $facet */
+ $facet = Facet::load($id);
+ $config = $facet->getFacetSourceConfig();
+ $this->assertInstanceOf(FacetSourceInterface::class, $config);
+ $this->assertEquals('y', $config->getFilterKey());
+
+ $facet = NULL;
+ $config = NULL;
+
+ $url_3 = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['y[0]' => 'facet||item']]);
+ $this->clickFacet();
+ $this->assertSession()->addressEquals($url_3);
+ }
+
+ /**
+ * Tests the url when a colon is used in the value.
+ */
+ public function testColonValue() {
+ $id = 'water_bear';
+ $name = 'Water bear';
+ $this->createFacet($name, $id, 'keywords');
+
+ // Add a new entity that has a colon in one of it's keywords.
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ $entity_test_storage->create([
+ 'name' => 'Entity with colon',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange', 'test:colon'],
+ 'category' => 'item_category',
+ ])->save();
+ // Make sure the new item is indexed.
+ $this->assertEquals(1, $this->indexItems($this->indexId));
+
+ // Go to the overview and test that we have the expected links.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('test:colon');
+ $this->assertFacetLabel('orange');
+ $this->assertFacetLabel('banana');
+
+ // Click the link with the colon.
+ $this->clickLink('test:colon');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Make sure 'test:colon' is active.
+ $url = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['f[0]' => 'water_bear:test:colon']]);
+ $this->assertSession()->addressEquals($url);
+ $this->checkFacetIsActive('test:colon');
+ $this->assertFacetLabel('orange');
+ $this->assertFacetLabel('banana');
+ }
+
+ /**
+ * Regression test for #2871475.
+ *
+ * @link https://drupal.org/node/2871475
+ */
+ public function testIncompleteFacetUrl() {
+ $id = 'owl';
+ $name = 'Owl';
+ $this->createFacet($name, $id);
+
+ $url = Url::fromUserInput('/search-api-test-fulltext');
+ $this->clickFacet();
+ $this->assertSession()->addressEquals($url);
+
+ // Build the path as described in #2871475.
+ $path = 'search-api-test-fulltext';
+ $options['absolute'] = TRUE;
+ $url = $this->buildUrl($path, $options);
+ $url .= '?f';
+
+ // Visit the page.
+ $session = $this->getSession();
+ $this->prepareRequest();
+ $session->visit($url);
+
+ // Check that no errors occurred.
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+ /**
+ * Regression test for #2898189.
+ *
+ * @link https://www.drupal.org/node/2898189
+ */
+ public function testResetPager() {
+ $id = 'owl';
+ $name = 'Owl';
+ $this->createFacet($name, $id);
+
+ // Set view pager option to 2 items, so we can check the pager rest on the
+ // facet links.
+ $view = Views::getView('search_api_test_view');
+ $view->setDisplay('page_1');
+ $pagerOptions = $view->display_handler->getOption('pager');
+ $pagerOptions['options']['items_per_page'] = 2;
+ $view->display_handler->setOption('pager', $pagerOptions);
+ $view->save();
+
+ $content_types = ['item', 'article'];
+ foreach ($content_types as $content_type) {
+ $this->drupalGet('search-api-test-fulltext');
+ $this->clickLink('2');
+ $this->assertNotFalse(strpos($this->getUrl(), 'page=1'));
+ $this->clickLink($content_type);
+ $this->assertFalse(strpos($this->getUrl(), 'page=1'));
+ }
+ }
+
+ /**
+ * Tests that creating a facet with a duplicate url alias emits a warning.
+ */
+ public function testCreatingDuplicateUrlAlias() {
+ $this->createFacet('Owl', 'owl');
+ $this->createFacet('Another owl', 'another_owl');
+ $this->drupalGet('admin/config/search/facets/another_owl/edit');
+ $this->submitForm(['facet_settings[url_alias]' => 'owl'], 'Save');
+ $this->assertSession()->pageTextContains('This alias is already in use for another facet defined on the same source.');
+ }
+
+ /**
+ * Tests that modules can change the facet url.
+ */
+ public function testFacetUrlCanBeChanged() {
+ $modules = ['facets_events_test'];
+ $success = $this->container->get('module_installer')->install($modules, TRUE);
+ $this->assertTrue($success, new FormattableMarkup('Enabled modules: %modules', ['%modules' => implode(', ', $modules)]));
+ $this->rebuildAll();
+
+ $id = 'facet';
+ $name = 'Facet';
+ $this->createFacet($name, $id);
+
+ $this->clickFacet();
+ $url = urldecode($this->getSession()->getCurrentUrl());
+ $this->assertStringContainsString('test=fun', $url);
+ $this->assertStringContainsString('f[0]=facet:item', $url);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/WidgetIntegrationTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/WidgetIntegrationTest.php
new file mode 100644
index 000000000..539ff1818
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Functional/WidgetIntegrationTest.php
@@ -0,0 +1,252 @@
+drupalLogin($this->adminUser);
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->assertEquals($this->indexItems($this->indexId), 5, '5 items were indexed.');
+ }
+
+ /**
+ * Tests checkbox widget.
+ */
+ public function testCheckboxWidget() {
+ $id = 't';
+ $this->createFacet('Facet & checkbox~', $id);
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->submitForm(['widget' => 'checkbox'], 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+ }
+
+ /**
+ * Tests links widget's basic functionality.
+ */
+ public function testLinksWidget() {
+ $id = 'links_widget';
+ $this->createFacet('>.Facet &* Links', $id);
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->submitForm(['widget' => 'links'], 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ $this->clickLink('item');
+ $this->checkFacetIsActive('item');
+ }
+
+ /**
+ * Tests dropdown widget's basic functionality.
+ */
+ public function testDropdownWidget() {
+ $id = 'select_widget';
+ $this->createFacet('Select', $id);
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->submitForm(
+ [
+ 'widget' => 'dropdown',
+ ],
+ 'Configure widget'
+ );
+ $this->submitForm(
+ [
+ 'widget' => 'dropdown',
+ 'facet_settings[show_only_one_result]' => TRUE,
+ ],
+ 'Save'
+ );
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+ }
+
+ /**
+ * Tests the functionality of a widget to hide/show the item-count.
+ */
+ public function testLinksShowHideCount() {
+ $id = 'links_widget';
+ $facet_edit_page = 'admin/config/search/facets/' . $id . '/edit';
+
+ $this->createFacet('>.Facet &* Links', $id);
+
+ // Go to the view and check that the facet links are shown with their
+ // default settings.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm(
+ [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => TRUE,
+ ],
+ 'Save'
+ );
+
+ // Go back to the same view and check that links now display the count.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item (3)');
+ $this->assertFacetLabel('article (2)');
+
+ $edit = [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => TRUE,
+ 'facet_settings[query_operator]' => 'or',
+ ];
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm($edit, 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item (3)');
+ $this->assertFacetLabel('article (2)');
+ $this->clickPartialLink('item');
+ $this->assertFacetLabel('item (3)');
+ $this->assertFacetLabel('article (2)');
+
+ $this->drupalGet($facet_edit_page);
+ $this->submitForm(
+ [
+ 'widget' => 'links',
+ 'widget_config[show_numbers]' => FALSE,
+ ],
+ 'Save'
+ );
+
+ // The count should be hidden again.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+ }
+
+ /**
+ * Tests custom widget.
+ *
+ * ::requiredFacetProperties in the custom widget requires the
+ * hide_non_narrowing_result_processor processor, so check that it's enabled
+ * after the custom widget is selected.
+ */
+ public function testCustomWidget() {
+ $id = 'custom_widget';
+ $this->createFacet('Custom widget.', $id);
+
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+
+ $this->assertSession()->checkboxNotChecked('edit-facet-settings-hide-non-narrowing-result-processor-status');
+ $this->assertSession()->checkboxNotChecked('edit-facet-settings-show-only-one-result');
+
+ $this->submitForm(['widget' => 'custom_widget'], 'Configure widget');
+ $this->submitForm(['widget' => 'custom_widget'], 'Save');
+
+ $this->assertSession()->checkboxChecked('edit-facet-settings-hide-non-narrowing-result-processor-status');
+ $this->assertSession()->checkboxChecked('edit-facet-settings-show-only-one-result');
+ }
+
+ /**
+ * Tests the facet support for a widget.
+ */
+ public function testSupportsFacet() {
+ $id = 'masked_owl';
+ $this->createFacet('Australian masked owl', $id);
+
+ // Go to the facet edit page and check to see if the custom widget shows
+ // up.
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->assertSession()->pageTextContains('Custom widget');
+
+ // Make the ::supportsFacet method on the custom widget return false.
+ \Drupal::state()->set('facets_test_supports_facet', FALSE);
+
+ // Go to the facet edit page and check to see if the custom widget is now
+ // hidden.
+ $this->drupalGet('admin/config/search/facets/' . $id . '/edit');
+ $this->assertSession()->pageTextNotContains('Custom widget');
+ }
+
+ /**
+ * Tests the all link.
+ */
+ public function testAllLink() {
+ $id = 'kepler_16b';
+ $this->createFacet('Kepler 16b', $id);
+ $editUrl = 'admin/config/search/facets/' . $id . '/edit';
+ $this->drupalGet($editUrl);
+ $this->submitForm(['widget' => 'links'], 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+
+ $this->clickLink('item');
+ $this->checkFacetIsActive('item');
+
+ // Enable the all (reset) link.
+ $this->drupalGet($editUrl);
+ $this->submitForm(['widget_config[show_reset_link]' => TRUE], 'Save');
+
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+ $this->findFacetLink('Show all');
+
+ // Change the text.
+ $edit = [
+ 'widget_config[show_reset_link]' => TRUE,
+ 'widget_config[reset_text]' => 'Planets',
+ ];
+ $this->drupalGet($editUrl);
+ $this->submitForm($edit, 'Save');
+
+ // Check that the new text appears and no facets are active.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertFacetLabel('item');
+ $this->assertFacetLabel('article');
+ $this->findFacetLink('Planets (5)');
+ $this->checkFacetIsNotActive('item');
+ $this->checkFacetIsNotActive('article');
+
+ // Click one of the facets.
+ $this->clickLink('item');
+ $this->checkFacetIsActive('item');
+
+ // Click the rest link.
+ $this->clickLink('Planets');
+ $this->checkFacetIsNotActive('item');
+ $this->checkFacetIsNotActive('article');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/AjaxBehaviorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/AjaxBehaviorTest.php
new file mode 100644
index 000000000..5391c2660
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/AjaxBehaviorTest.php
@@ -0,0 +1,206 @@
+getDisplay('page_1');
+ $display['display_options']['use_ajax'] = TRUE;
+ $view->save();
+ }
+
+ /**
+ * Tests ajax links.
+ */
+ public function testAjaxLinks() {
+ // Create facets.
+ $this->createFacet('owl');
+ $this->createFacet('duck', 'keywords');
+
+ // Go to the views page.
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Make sure the blocks are shown on the page.
+ $page = $this->getSession()->getPage();
+ $block_owl = $page->findById('block-owl-block');
+ $block_owl->isVisible();
+ $block_duck = $page->findById('block-duck-block');
+ $block_duck->isVisible();
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+
+ // Check that the article link exists (and is formatted like a facet) link.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']);
+ $this->assertNotEmpty($links);
+
+ // Click the item facet.
+ $this->clickLink('item');
+
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->pageTextContains('Displaying 3 search results');
+
+ // Check that the article facet is now gone.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']);
+ $this->assertEmpty($links);
+
+ // Click the item facet again, and check that the article facet is back.
+ $this->clickLink('item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']);
+ $this->assertNotEmpty($links);
+
+ // Check that the strawberry link disappears when filtering on items.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'strawberry']);
+ $this->assertNotEmpty($links);
+ $this->clickLink('item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'strawberry']);
+ $this->assertEmpty($links);
+ }
+
+ /**
+ * Tests ajax dropdown.
+ */
+ public function testAjaxDropdown() {
+ // Create facets.
+ $this->createFacet('owl');
+ $this->createFacet('duck', 'category', 'dropdown', []);
+
+ // Go to the views page.
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Make sure the blocks are shown on the page.
+ $page = $this->getSession()->getPage();
+ $block_owl = $page->findById('block-owl-block');
+ $block_owl->isVisible();
+ $block_duck = $page->findById('block-duck-block');
+ $block_duck->isVisible();
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+
+ // Check that the article_category option disappears when filtering on item.
+ $dropdown_entry = $this->xpath('//*[@id="block-duck-block"]/div/select/option[normalize-space(text())=:label]', [':label' => 'article_category']);
+ $this->assertNotEmpty($dropdown_entry);
+ $block_owl->clickLink('item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $dropdown_entry = $this->xpath('//*[@id="block-duck-block"]/div/select/option[normalize-space(text())=:label]', [':label' => 'article_category']);
+ $this->assertEmpty($dropdown_entry);
+
+ // Click the item facet again.
+ $block_owl->clickLink('item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+
+ // Select the article_category in the dropdown.
+ $dropdown = $this->xpath('//*[@id="block-duck-block"]/div/select');
+ $dropdown[0]->selectOption('article_category');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+
+ $this->assertSession()->pageTextContains('Displaying 2 search results');
+
+ // Check that the article link exists (and is formatted like a facet) link.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']);
+ $this->assertNotEmpty($links);
+ // Check that the item link didn't exists.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'item']);
+ $this->assertEmpty($links);
+ }
+
+ /**
+ * Tests ajax checkbox.
+ */
+ public function testAjaxCheckbox() {
+ // Create facets.
+ $this->createFacet('owl');
+ $this->createFacet('duck', 'keywords', 'checkbox');
+
+ // Go to the views page.
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Make sure the blocks are shown on the page.
+ $page = $this->getSession()->getPage();
+ $block_owl = $page->findById('block-owl-block');
+ $block_owl->isVisible();
+ $block_duck = $page->findById('block-duck-block');
+ $block_duck->isVisible();
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+
+ // Check that the article link exists (and is formatted like a facet) link.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']);
+ $this->assertNotEmpty($links);
+
+ // Click the item facet.
+ $this->clickLink('item');
+
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->pageTextContains('Displaying 3 search results');
+
+ // Check that the article facet is now gone.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']);
+ $this->assertEmpty($links);
+
+ // Click the item facet again, and check that the article facet is back.
+ $this->clickLink('item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']);
+ $this->assertNotEmpty($links);
+
+ // Check that the strawberry link disappears when filtering on items.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'strawberry']);
+ $this->assertNotEmpty($links);
+ $this->clickLink('item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'strawberry']);
+ $this->assertEmpty($links);
+ $this->clickLink('item');
+
+ $this->getSession()->getPage()->checkField('strawberry');
+ // Check that the article link exists (and is formatted like a facet) link.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']);
+ $this->assertNotEmpty($links);
+ // Check that the item link didn't exists.
+ $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'item']);
+ $this->assertEmpty($links);
+ }
+
+ /**
+ * Tests links with exposed filters.
+ */
+ public function testLinksWithExposedFilter() {
+ $view = View::load('search_api_test_view');
+ $display = $view->getDisplay('page_1');
+ $display['display_options']['filters']['search_api_fulltext']['expose']['required'] = TRUE;
+ $view->save();
+
+ $this->createFacet('owl');
+ $this->drupalGet('search-api-test-fulltext');
+
+ $page = $this->getSession()->getPage();
+ $block_owl = $page->findById('block-owl-block');
+ $block_owl->isVisible();
+
+ $this->assertSession()->fieldExists('edit-search-api-fulltext')->setValue('baz');
+ $this->click('.form-submit');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->pageTextContains('Displaying 3 search results');
+
+ $this->clickLink('item');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->pageTextContains('Displaying 1 search results');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/JsBase.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/JsBase.php
new file mode 100644
index 000000000..a47c6fb77
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/JsBase.php
@@ -0,0 +1,183 @@
+drupalCreateUser([
+ 'administer search_api',
+ 'administer facets',
+ 'access administration pages',
+ 'administer blocks',
+ ]);
+ $this->drupalLogin($admin_user);
+
+ $this->insertExampleContent();
+ }
+
+ /**
+ * Setup and insert test content.
+ */
+ protected function insertExampleContent() {
+ entity_test_create_bundle('item', NULL, 'entity_test_mulrev_changed');
+ entity_test_create_bundle('article', NULL, 'entity_test_mulrev_changed');
+
+ $entity_test_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+ $entity_1 = $entity_test_storage->create([
+ 'name' => 'foo bar baz',
+ 'body' => 'test test',
+ 'type' => 'item',
+ 'keywords' => ['orange'],
+ 'category' => 'item_category',
+ ]);
+ $entity_1->save();
+ $entity_2 = $entity_test_storage->create([
+ 'name' => 'foo test',
+ 'body' => 'bar test',
+ 'type' => 'item',
+ 'keywords' => ['orange', 'apple', 'grape'],
+ 'category' => 'item_category',
+ ]);
+ $entity_2->save();
+ $entity_3 = $entity_test_storage->create([
+ 'name' => 'bar',
+ 'body' => 'test foobar',
+ 'type' => 'item',
+ ]);
+ $entity_3->save();
+ $entity_4 = $entity_test_storage->create([
+ 'name' => 'foo baz',
+ 'body' => 'test test test',
+ 'type' => 'article',
+ 'keywords' => ['apple', 'strawberry', 'grape'],
+ 'category' => 'article_category',
+ ]);
+ $entity_4->save();
+ $entity_5 = $entity_test_storage->create([
+ 'name' => 'bar baz',
+ 'body' => 'foo',
+ 'type' => 'article',
+ 'keywords' => ['orange', 'strawberry', 'grape', 'banana'],
+ 'category' => 'article_category',
+ ]);
+ $entity_5->save();
+
+ $inserted_entities = \Drupal::entityQuery('entity_test_mulrev_changed')
+ ->count()
+ ->execute();
+ $this->assertEquals(5, $inserted_entities, "5 items inserted.");
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load('database_search_index');
+ $indexed_items = $index->indexItems();
+ $this->assertEquals(5, $indexed_items, '5 items indexed.');
+ }
+
+ /**
+ * Create and place a facet block in the first sidebar.
+ *
+ * @param string $id
+ * Create a block for a facet.
+ */
+ protected function createBlock($id) {
+ $config = \Drupal::configFactory();
+ $settings = [
+ 'plugin' => 'facet_block:' . $id,
+ 'region' => 'sidebar_first',
+ 'id' => $id . '_block',
+ 'theme' => $config->get('system.theme')->get('default'),
+ 'label' => ucfirst($id) . ' block',
+ 'visibility' => [],
+ 'weight' => 0,
+ ];
+
+ foreach (['region', 'id', 'theme', 'plugin', 'weight', 'visibility'] as $key) {
+ $values[$key] = $settings[$key];
+ // Remove extra values that do not belong in the settings array.
+ unset($settings[$key]);
+ }
+ $values['settings'] = $settings;
+ $block = Block::create($values);
+ $block->save();
+ }
+
+ /**
+ * Create a facet.
+ *
+ * @param string $id
+ * The id of the facet.
+ * @param string $field
+ * The field name.
+ * @param string $widget_type
+ * The type of the facet widget. links by default.
+ * @param array $widget_settings
+ * The widget config.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ */
+ protected function createFacet($id, $field = 'type', $widget_type = 'links', array $widget_settings = [
+ 'show_numbers' => TRUE,
+ 'soft_limit' => 0,
+ ]) {
+ $facet_storage = \Drupal::entityTypeManager()->getStorage('facets_facet');
+ // Create and save a facet with a checkbox widget.
+ $facet_storage->create([
+ 'id' => $id,
+ 'name' => strtoupper($id),
+ 'url_alias' => $id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'field_identifier' => $field,
+ 'empty_behavior' => ['behavior' => 'none'],
+ 'weight' => 1,
+ 'widget' => [
+ 'type' => $widget_type,
+ 'config' => $widget_settings,
+ ],
+ 'processor_configs' => [
+ 'url_processor_handler' => [
+ 'processor_id' => 'url_processor_handler',
+ 'weights' => ['pre_query' => -10, 'build' => -10],
+ 'settings' => [],
+ ],
+ ],
+ 'query_operator' => 'AND',
+ 'use_hierarchy' => FALSE,
+ 'hierarchy' => ['type' => 'taxonomy', 'config' => []],
+ ])->save();
+ $this->createBlock($id);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/WidgetJSTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/WidgetJSTest.php
new file mode 100644
index 000000000..05b62c9ba
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/FunctionalJavascript/WidgetJSTest.php
@@ -0,0 +1,435 @@
+drupalGet('admin/config/search/facets/add-facet');
+
+ $page = $this->getSession()->getPage();
+
+ // Select one of the options from the facet source dropdown and wait for the
+ // result to show.
+ $page->selectFieldOption('edit-facet-source-id', 'search_api:views_page__search_api_test_view__page_1');
+ $this->getSession()->wait(6000, "jQuery('.facet-source-field-wrapper').length > 0");
+
+ $page->selectFieldOption('facet_source_configs[search_api:views_page__search_api_test_view__page_1][field_identifier]', 'type');
+
+ // Check that after choosing the field, the name is already filled in.
+ $field_value = $this->getSession()->getPage()->findField('edit-name')->getValue();
+ $this->assertEquals('Type', $field_value);
+ }
+
+ /**
+ * Tests show more / less links.
+ */
+ public function testLinksShowMoreLess() {
+ $facet_storage = \Drupal::entityTypeManager()->getStorage('facets_facet');
+ $id = 'owl';
+
+ // Create and save a facet with a checkbox widget on the 'type' field.
+ $facet_storage->create([
+ 'id' => $id,
+ 'name' => strtoupper($id),
+ 'url_alias' => $id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'field_identifier' => 'type',
+ 'empty_behavior' => ['behavior' => 'none'],
+ 'weight' => 1,
+ 'widget' => [
+ 'type' => 'links',
+ 'config' => [
+ 'show_numbers' => TRUE,
+ 'soft_limit' => 1,
+ 'soft_limit_settings' => [
+ 'show_less_label' => 'Show less',
+ 'show_more_label' => 'Show more',
+ ],
+ ],
+ ],
+ 'processor_configs' => [
+ 'url_processor_handler' => [
+ 'processor_id' => 'url_processor_handler',
+ 'weights' => ['pre_query' => -10, 'build' => -10],
+ 'settings' => [],
+ ],
+ ],
+ 'use_hierarchy' => FALSE,
+ 'hierarchy' => ['type' => 'taxonomy', 'config' => []],
+ ])->save();
+ $this->createBlock($id);
+
+ // Go to the views page.
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Make sure the block is shown on the page.
+ $page = $this->getSession()->getPage();
+ $block = $page->findById('block-owl-block');
+ $block->isVisible();
+
+ // Make sure the show more / show less links are shown.
+ $this->assertSession()->linkExists('Show more');
+
+ // Change the link label of show more into "Moar Llamas".
+ $facet = Facet::load('owl');
+ $facet->setWidget('links', [
+ 'show_numbers' => TRUE,
+ 'soft_limit' => 1,
+ 'soft_limit_settings' => [
+ 'show_less_label' => 'Show less',
+ 'show_more_label' => 'Moar Llamas',
+ ],
+ ]);
+ $facet->save();
+
+ // Check that the new configuration is used now.
+ $this->drupalGet('search-api-test-fulltext');
+ $this->assertSession()->linkNotExists('Show more');
+ $this->assertSession()->linkExists('Moar Llamas');
+ }
+
+ /**
+ * Tests checkbox widget.
+ */
+ public function testCheckboxWidget() {
+ $facet_storage = \Drupal::entityTypeManager()->getStorage('facets_facet');
+ $id = 'llama';
+
+ // Create and save a facet with a checkbox widget on the 'type' field.
+ $facet_storage->create([
+ 'id' => $id,
+ 'name' => strtoupper($id),
+ 'url_alias' => $id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'field_identifier' => 'type',
+ 'empty_behavior' => ['behavior' => 'none'],
+ 'widget' => [
+ 'type' => 'checkbox',
+ 'config' => [
+ 'show_numbers' => TRUE,
+ ],
+ ],
+ 'processor_configs' => [
+ 'url_processor_handler' => [
+ 'processor_id' => 'url_processor_handler',
+ 'weights' => ['pre_query' => -10, 'build' => -10],
+ 'settings' => [],
+ ],
+ ],
+ 'use_hierarchy' => FALSE,
+ 'hierarchy' => ['type' => 'taxonomy', 'config' => []],
+ ])->save();
+ $this->createBlock($id);
+
+ // Go to the views page.
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Make sure the block is shown on the page.
+ $page = $this->getSession()->getPage();
+ $block = $page->findById('block-llama-block');
+ $this->assertTrue($block->isVisible());
+
+ // The checkboxes should be wrapped in a container with a CSS class that
+ // correctly identifies the widget type.
+ $this->assertCount(1, $block->findAll('css', 'div.facets-widget-checkbox ul'));
+
+ // The checkboxes should be wrapped in a list element that has the expected
+ // CSS classes to identify it as well as the data attributes that enable the
+ // JS functionality.
+ $this->assertCount(1, $block->findAll('css', 'ul.facet-inactive.item-list__checkbox.js-facets-widget.js-facets-checkbox-links'));
+ $this->assertCount(1, $block->findAll('css', 'ul[data-drupal-facet-id="llama"]'));
+ $this->assertCount(1, $block->findAll('css', 'ul[data-drupal-facet-alias="llama"]'));
+
+ // There should be two list items that can be identified by CSS class.
+ $list_items = $block->findAll('css', 'ul li.facet-item');
+ $this->assertCount(2, $list_items);
+
+ // The list items should contain a checkbox, a label and a hidden link that
+ // leads to the updated search results. None of the checkboxes should be
+ // checked.
+ $expected = [
+ [
+ 'item',
+ 3,
+ base_path() . 'search-api-test-fulltext?f%5B0%5D=llama%3Aitem',
+ FALSE,
+ ],
+ [
+ 'article',
+ 2,
+ base_path() . 'search-api-test-fulltext?f%5B0%5D=llama%3Aarticle',
+ FALSE,
+ ],
+ ];
+ $this->assertListItems($expected, $list_items);
+
+ // Checking one of the checkboxes should cause a redirect to a page with
+ // updated search results.
+ $checkbox = $page->findField('item (3)');
+ $checkbox->click();
+ $current_url = $this->getSession()->getCurrentUrl();
+ $this->assertStringContainsString('search-api-test-fulltext?f%5B0%5D=llama%3Aitem', $current_url);
+
+ // Now the chosen keyword should be checked and the hidden links should be
+ // updated.
+ $expected = [
+ [
+ 'item',
+ 3,
+ base_path() . 'search-api-test-fulltext',
+ TRUE,
+ ],
+ [
+ 'article',
+ 2,
+ base_path() . 'search-api-test-fulltext?f%5B0%5D=llama%3Aarticle',
+ FALSE,
+ ],
+ ];
+ $this->assertListItems($expected, $block->findAll('css', 'ul li.facet-item'));
+
+ // Unchecking a checkbox should remove the keyword from the search.
+ $checkbox = $page->findField('item (3)');
+ $checkbox->click();
+ $current_url = $this->getSession()->getCurrentUrl();
+ $this->assertStringContainsString('search-api-test-fulltext', $current_url);
+ $expected = [
+ [
+ 'item',
+ 3,
+ base_path() . 'search-api-test-fulltext?f%5B0%5D=llama%3Aitem',
+ FALSE,
+ ],
+ [
+ 'article',
+ 2,
+ base_path() . 'search-api-test-fulltext?f%5B0%5D=llama%3Aarticle',
+ FALSE,
+ ],
+ ];
+ $this->assertListItems($expected, $block->findAll('css', 'ul li.facet-item'));
+ }
+
+ /**
+ * Checks that the list items that wrap checkboxes are rendered correctly.
+ *
+ * @param array[] $expected
+ * An array of expected properties, each an array with the following values:
+ * - The expected checkbox value.
+ * - The expected number of results, displayed in the checkbox label.
+ * - The URI leading to the updated search results.
+ * - A boolean indicating whether the checkbox is expected to be checked.
+ * @param \Behat\Mink\Element\NodeElement[] $list_items
+ * The list items to check.
+ */
+ protected function assertListItems(array $expected, array $list_items): void {
+ $this->assertCount(count($expected), $list_items);
+
+ foreach ($expected as $key => [$keyword, $count, $uri, $selected]) {
+ $list_item = $list_items[$key];
+
+ // The list element should be visible.
+ $this->assertTrue($list_item->isVisible());
+
+ // It should contain 1 input element (the checkbox). It should have the
+ // expected ID and CSS class.
+ $item_id = "llama-{$keyword}";
+ $this->assertCount(1, $list_item->findAll('css', 'input'));
+ $this->assertCount(1, $list_item->findAll('css', "input#{$item_id}[type='checkbox'].facets-checkbox"));
+
+ // It should contain a label for the checkbox.
+ $labels = $list_item->findAll('css', "label[for=$item_id]");
+ $this->assertCount(1, $labels);
+ // The label should contain the search keyword and the result count. Since
+ // there can be multiple spaces or newlines between the keyword and the
+ // count, reduce them to a single space before asserting. The keyword and
+ // the count should be wrapped in elements with semantic classes.
+ $label = reset($labels);
+ $expected_text = "$keyword ($count) ";
+ $this->assertTrue($label->isVisible());
+ $this->assertEquals($expected_text, trim(preg_replace('/\s+/', ' ', $label->getHtml())));
+
+ // There should be a hidden link that leads to the updated search results.
+ // If a user checks a checkbox this hidden link is followed in JS.
+ $links = $list_item->findAll('css', 'a');
+ $this->assertCount(1, $links);
+ $link = reset($links);
+ // The link should not be visible.
+ $this->assertFalse($link->isVisible());
+ // The link should indicate that search engines shouldn't follow it.
+ $this->assertEquals('nofollow', $link->getAttribute('rel'));
+ // The link should have CSS classes that allow to attach our JS code.
+ $this->assertEquals($item_id, $link->getAttribute('data-drupal-facet-item-id'));
+ $this->assertEquals($keyword, $link->getAttribute('data-drupal-facet-item-value'));
+ // The link text should include the keyword as well as the count.
+ $this->assertStringContainsString($expected_text, trim(preg_replace('/\s+/', ' ', $link->getHtml())));
+ }
+ }
+
+ /**
+ * Tests dropdown widget.
+ */
+ public function testDropdownWidget() {
+ $facet_storage = \Drupal::entityTypeManager()->getStorage('facets_facet');
+ $id = 'llama';
+
+ // Create and save a facet with a checkbox widget on the 'type' field.
+ $facet_storage->create([
+ 'id' => $id,
+ 'name' => strtoupper($id),
+ 'url_alias' => $id,
+ 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
+ 'field_identifier' => 'type',
+ 'empty_behavior' => ['behavior' => 'none'],
+ 'show_only_one_result' => TRUE,
+ 'widget' => [
+ 'type' => 'dropdown',
+ 'config' => [
+ 'show_numbers' => TRUE,
+ 'default_option_label' => '- All -',
+ ],
+ ],
+ 'processor_configs' => [
+ 'url_processor_handler' => [
+ 'processor_id' => 'url_processor_handler',
+ 'weights' => ['pre_query' => -10, 'build' => -10],
+ 'settings' => [],
+ ],
+ ],
+ 'use_hierarchy' => FALSE,
+ 'hierarchy' => ['type' => 'taxonomy', 'config' => []],
+ ])->save();
+ $this->createBlock($id);
+
+ // Go to the views page.
+ $this->drupalGet('search-api-test-fulltext');
+
+ // Make sure the block is shown on the page.
+ $page = $this->getSession()->getPage();
+ $block = $page->findById('block-llama-block');
+ $this->assertTrue($block->isVisible());
+
+ // There should be a single select element in the block.
+ $this->assertCount(1, $block->findAll('css', 'select'));
+
+ // The select element should be wrapped in a container with a CSS class that
+ // correctly identifies the widget type.
+ $this->assertCount(1, $block->findAll('css', 'div.facets-widget-dropdown select'));
+
+ // The select element should have the expected CSS classes to identify it as
+ // well as the data attributes that enable the JS functionality.
+ $this->assertCount(1, $block->findAll('css', 'select.facet-inactive.item-list__dropdown.facets-dropdown.js-facets-widget.js-facets-dropdown'));
+ $this->assertCount(1, $block->findAll('css', 'select[data-drupal-facet-id="llama"]'));
+ $this->assertCount(1, $block->findAll('css', 'select[data-drupal-facet-alias="llama"]'));
+
+ // The select element should have an accessible label.
+ $this->assertCount(1, $block->findAll('css', 'select[aria-labelledby="facet_llama_label"]'));
+ $this->assertCount(1, $block->findAll('css', 'label#facet_llama_label'));
+ $this->assertEquals('Facet LLAMA', $block->find('css', 'label')->getHtml());
+
+ // The select element should be visible.
+ $dropdown = $block->find('css', 'select');
+ $this->assertTrue($dropdown->isVisible());
+
+ // There should be 3 options in the expected order.
+ $options = $dropdown->findAll('css', 'option');
+
+ $expected = [
+ // The first option is the default option, it doesn't have a value and it
+ // should be selected.
+ [
+ '- All -',
+ '',
+ TRUE,
+ ],
+ // The second option should have the expected option text, have the URI
+ // that points to the updated search result as the value, and is not
+ // selected.
+ [
+ 'item (3)',
+ base_path() . 'search-api-test-fulltext?f%5B0%5D=llama%3Aitem',
+ FALSE,
+ ],
+ // The third option is similar.
+ [
+ 'article (2)',
+ base_path() . 'search-api-test-fulltext?f%5B0%5D=llama%3Aarticle',
+ FALSE,
+ ],
+ ];
+ $this->assertSelectOptions($expected, $options);
+
+ // Selecting one of the options should cause a redirect to a page with
+ // updated search results.
+ $dropdown->selectOption('item (3)');
+ $this->getSession()->wait(6000, "window.location.search != ''");
+ $current_url = $this->getSession()->getCurrentUrl();
+ $this->assertStringContainsString('search-api-test-fulltext?f%5B0%5D=llama%3Aitem', $current_url);
+
+ // Now the clicked option should be selected and the URIs in the option
+ // values should be updated.
+ $dropdown = $block->find('css', 'select');
+ $this->assertTrue($dropdown->isVisible());
+ $options = $dropdown->findAll('css', 'option');
+
+ $expected = [
+ // The first option is the default option, it should point to the original
+ // search result (without any chosen facets) and should not be selected.
+ [
+ '- All -',
+ base_path() . 'search-api-test-fulltext',
+ FALSE,
+ ],
+ // The second option should now be selected, and since clicking it again
+ // would negate it, it should also link to the search page without any
+ // chosen facets.
+ [
+ 'item (3)',
+ base_path() . 'search-api-test-fulltext',
+ TRUE,
+ ],
+ // The third option remains unchanged.
+ [
+ 'article (2)',
+ base_path() . 'search-api-test-fulltext?f%5B0%5D=llama%3Aarticle',
+ FALSE,
+ ],
+ ];
+ $this->assertSelectOptions($expected, $options);
+ }
+
+ /**
+ * Checks that the given select option elements have the selected properties.
+ *
+ * @param array[] $expected
+ * An array of expected properties, each an array with the following values:
+ * - The expected option text.
+ * - The expected option value.
+ * - A boolean indicating whether the option is expected to be selected.
+ * @param \Behat\Mink\Element\NodeElement[] $options
+ * The list of options to check.
+ */
+ protected function assertSelectOptions(array $expected, array $options): void {
+ $this->assertCount(count($expected), $options);
+ foreach ($expected as $key => [$text, $value, $selected]) {
+ $option = $options[$key];
+ // There can be multiple spaces or newlines between the value text and the
+ // number of results. Reduce them to a single space before asserting.
+ $this->assertEquals($text, trim(preg_replace('/\s+/', ' ', $option->getText())));
+ $this->assertEquals($value, $option->getValue());
+ $this->assertEquals($selected, $option->isSelected());
+ }
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetFacetSourceTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetFacetSourceTest.php
new file mode 100644
index 000000000..e29daf4ca
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetFacetSourceTest.php
@@ -0,0 +1,214 @@
+installEntitySchema('facets_facet');
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('search_api_task');
+
+ \Drupal::state()->set('search_api_use_tracking_batch', FALSE);
+
+ // Set tracking page size so tracking will work properly.
+ \Drupal::configFactory()
+ ->getEditable('search_api.settings')
+ ->set('tracking_page_size', 100)
+ ->save();
+
+ $this->installConfig([
+ 'search_api_test_example_content',
+ 'search_api_test_db',
+ ]);
+
+ $this->installConfig('search_api_test_views');
+ }
+
+ /**
+ * Tests facet source behavior for the facet entity.
+ *
+ * @covers ::getFacetSourceId
+ * @covers ::setFacetSourceId
+ * @covers ::getFacetSources
+ * @covers ::getFacetSource
+ * @covers ::getFacetSourceConfig
+ */
+ public function testFacetSource() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertNull($entity->getFacetSourceId());
+
+ // Check that the facet source is in the list of search api displays.
+ $displays = $this->container
+ ->get('plugin.manager.search_api.display')
+ ->getDefinitions();
+ $this->assertArrayHasKey('views_page:search_api_test_view__page_1', $displays);
+
+ // Check that has transformed into a facet source as expected.
+ $facet_sources = $this->container
+ ->get('plugin.manager.facets.facet_source')
+ ->getDefinitions();
+ $this->assertArrayHasKey('search_api:views_page__search_api_test_view__page_1', $facet_sources);
+
+ // Check the behavior of the facet sources.
+ $display_name = 'search_api:views_page__search_api_test_view__page_1';
+ $entity->setFacetSourceId($display_name);
+ $this->assertEquals($display_name, $entity->getFacetSourceId());
+ $this->assertInstanceOf(SearchApiDisplay::class, $entity->getFacetSources()[$display_name]);
+ $this->assertInstanceOf(SearchApiDisplay::class, $entity->getFacetSource());
+ $this->assertInstanceOf(FacetSourceInterface::class, $entity->getFacetSourceConfig());
+ $this->assertEquals($display_name, $entity->getFacetSourceConfig()->getName());
+ $this->assertEquals('f', $entity->getFacetSourceConfig()->getFilterKey());
+ }
+
+ /**
+ * Tests invalid query type.
+ *
+ * The error here is triggered because no field id is set.
+ *
+ * @covers ::getQueryType
+ * @covers ::getFacetSource
+ */
+ public function testInvalidQueryType() {
+ $entity = new Facet([], 'facets_facet');
+ $entity->setWidget('links');
+ $entity->setFacetSourceId('search_api:views_page__search_api_test_view__page_1');
+
+ $this->expectException(InvalidQueryTypeException::class);
+ $entity->getQueryType();
+ }
+
+ /**
+ * Tests valid query type.
+ *
+ * @covers ::getQueryType
+ * @covers ::getFacetSource
+ */
+ public function testQueryType() {
+ $entity = new Facet([], 'facets_facet');
+ $entity->setWidget('links');
+ $entity->setFacetSourceId('search_api:views_page__search_api_test_view__page_1');
+ $entity->setFieldIdentifier('name');
+
+ $selectedQueryType = $entity->getQueryType();
+ $this->assertEquals('search_api_string', $selectedQueryType);
+ }
+
+ /**
+ * Tests the selection of a query type.
+ *
+ * @covers ::getQueryType
+ * @covers ::pickQueryType
+ */
+ public function testQueryTypeJugglingInvalidWidget() {
+ $entity = new Facet([], 'facets_facet');
+ $entity->setWidget('widget_invalid_qt');
+ $entity->setFacetSourceId('search_api:views_page__search_api_test_view__page_1');
+ $entity->setFieldIdentifier('name');
+
+ $this->expectException(InvalidQueryTypeException::class);
+ $entity->getQueryType();
+ }
+
+ /**
+ * Tests the selection of a query type.
+ *
+ * @covers ::getQueryType
+ * @covers ::pickQueryType
+ */
+ public function testQueryTypeJugglingInvalidProcessor() {
+ $entity = new Facet([], 'facets_facet');
+ $entity->setWidget('links');
+ $entity->setFacetSourceId('search_api:views_page__search_api_test_view__page_1');
+ $entity->setFieldIdentifier('name');
+ $entity->addProcessor([
+ 'processor_id' => 'invalid_qt',
+ 'weights' => [],
+ 'settings' => [],
+ ]);
+
+ $this->expectException(InvalidQueryTypeException::class);
+ $entity->getQueryType();
+ }
+
+ /**
+ * Tests the selection of a query type.
+ *
+ * @covers ::getQueryType
+ * @covers ::pickQueryType
+ */
+ public function testQueryTypeJugglingInvalidCombo() {
+ $entity = new Facet([], 'facets_facet');
+ $entity->setWidget('widget_date_qt');
+ $entity->setFacetSourceId('search_api:views_page__search_api_test_view__page_1');
+ $entity->setFieldIdentifier('name');
+ $processor = [
+ 'processor_id' => 'test_pre_query',
+ 'weights' => [],
+ 'settings' => [],
+ ];
+ $entity->addProcessor($processor);
+
+ $this->expectException(InvalidQueryTypeException::class);
+ $entity->getQueryType();
+ }
+
+ /**
+ * Test the data definitions.
+ *
+ * @covers \Drupal\facets\Plugin\facets\facet_source\SearchApiDisplay::getDataDefinition
+ */
+ public function testDataDefinitions() {
+ // Create and configure facet.
+ $entity = new Facet([], 'facets_facet');
+ $display_name = 'search_api:views_page__search_api_test_view__page_1';
+ $entity->setFacetSourceId($display_name);
+
+ $this->assertInstanceOf(DataDefinitionInterface::class, $entity->getFacetSource()->getDataDefinition('id'));
+ $this->assertInstanceOf(DataDefinitionInterface::class, $entity->getFacetSource()->getDataDefinition('name'));
+ $this->assertInstanceOf(DataDefinitionInterface::class, $entity->getFacetSource()->getDataDefinition('category'));
+
+ // When trying to get a field that doesn't exist, an error should be thrown.
+ $this->expectException(Exception::class);
+ $entity->getFacetSource()->getDataDefinition('llama');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetSourceTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetSourceTest.php
new file mode 100644
index 000000000..c5e722df8
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetSourceTest.php
@@ -0,0 +1,85 @@
+installEntitySchema('facets_facet');
+ }
+
+ /**
+ * Tests constructor.
+ *
+ * @covers ::getName
+ * @covers ::getFilterKey
+ * @covers ::getUrlProcessorName
+ */
+ public function testConstruct() {
+ $fs = new FacetSource(
+ [
+ 'id' => 'llama',
+ 'name' => 'Llama',
+ 'filter_key' => 'u',
+ 'url_processor' => 'monkey',
+ ], 'facets_facet_source'
+ );
+
+ $this->assertEquals('u', $fs->getFilterKey());
+ $this->assertEquals('monkey', $fs->getUrlProcessorName());
+ $this->assertEquals('Llama', $fs->getName());
+ }
+
+ /**
+ * Tests simple getters / setters.
+ *
+ * @covers ::getName
+ * @covers ::setFilterKey
+ * @covers ::getFilterKey
+ * @covers ::setUrlProcessor
+ * @covers ::getUrlProcessorName
+ * @covers ::setBreadcrumbSettings
+ * @covers ::getBreadcrumbSettings
+ */
+ public function testGetterSetters() {
+ $fs = new FacetSource(['id' => 'llama'], 'facets_facet_source');
+
+ $this->assertNull($fs->getFilterKey());
+ $this->assertNull($fs->getName());
+ $this->assertEquals('query_string', $fs->getUrlProcessorName());
+ $this->assertEmpty($fs->getBreadcrumbSettings());
+
+ $fs->setFilterKey('ab');
+ $this->assertEquals('ab', $fs->getFilterKey());
+
+ $fs->setUrlProcessor('test');
+ $this->assertEquals('test', $fs->getUrlProcessorName());
+
+ $breadcrumb_settings = ['active' => 1, 'group' => 1];
+ $fs->setBreadcrumbSettings($breadcrumb_settings);
+ $this->assertEquals($breadcrumb_settings, $fs->getBreadcrumbSettings());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetTest.php
new file mode 100644
index 000000000..1cad87888
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Entity/FacetTest.php
@@ -0,0 +1,428 @@
+installEntitySchema('facets_facet');
+ }
+
+ /**
+ * Tests for getters that don't have setters.
+ *
+ * @covers ::getDescription
+ * @covers ::getName
+ */
+ public function testDescription() {
+ $entity = new Facet(['description' => 'Owls'], 'facets_facet');
+ $this->assertEquals('Owls', $entity->getDescription());
+
+ $entity = new Facet(['description' => 'Owls', 'name' => 'owl'], 'facets_facet');
+ $this->assertEquals('owl', $entity->getName());
+ }
+
+ /**
+ * Tests widget behavior.
+ *
+ * @covers ::setWidget
+ * @covers ::getWidget
+ * @covers ::getWidgetManager
+ * @covers ::getWidgetInstance
+ */
+ public function testWidget() {
+ $entity = new Facet([], 'facets_facet');
+ $entity->setWidget('links');
+
+ $manager = $entity->getWidgetManager();
+ $this->assertInstanceOf(WidgetPluginManager::class, $manager);
+
+ $config = [
+ 'soft_limit' => 0,
+ 'show_numbers' => FALSE,
+ 'soft_limit_settings' => [
+ 'show_less_label' => 'Show less',
+ 'show_more_label' => 'Show more',
+ ],
+ 'show_reset_link' => FALSE,
+ 'hide_reset_when_no_selection' => FALSE,
+ 'reset_text' => 'Show all',
+ ];
+ $this->assertEquals(['type' => 'links', 'config' => $config], $entity->getWidget());
+ $this->assertInstanceOf(LinksWidget::class, $entity->getWidgetInstance());
+ $this->assertFalse($entity->getWidgetInstance()->getConfiguration()['show_numbers']);
+
+ $config['show_numbers'] = TRUE;
+ $entity->setWidget('links', $config);
+ $this->assertEquals(['type' => 'links', 'config' => $config], $entity->getWidget());
+ $this->assertInstanceOf(LinksWidget::class, $entity->getWidgetInstance());
+ $this->assertTrue($entity->getWidgetInstance()->getConfiguration()['show_numbers']);
+ }
+
+ /**
+ * Tests an empty widget.
+ *
+ * @covers ::getWidget
+ * @covers ::getWidgetInstance
+ */
+ public function testEmptyWidget() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertNull($entity->getWidget());
+
+ $this->expectException(InvalidProcessorException::class);
+ $entity->getWidgetInstance();
+ }
+
+ /**
+ * Tests widget processor behavior.
+ *
+ * @covers ::getProcessorsByStage
+ * @covers ::getProcessors
+ * @covers ::getProcessorConfigs
+ * @covers ::addProcessor
+ * @covers ::removeProcessor
+ * @covers ::loadProcessors
+ */
+ public function testProcessor() {
+ $entity = new Facet([], 'facets_facet');
+
+ $this->assertEmpty($entity->getProcessorConfigs());
+ $this->assertEmpty($entity->getProcessors());
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_PRE_QUERY));
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_POST_QUERY));
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_BUILD));
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_SORT));
+
+ $id = 'hide_non_narrowing_result_processor';
+ $config = [
+ 'processor_id' => $id,
+ 'weights' => [],
+ 'settings' => [],
+ ];
+ $entity->addProcessor($config);
+ $this->assertEquals([$id => $config], $entity->getProcessorConfigs());
+
+ $this->assertNotEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_BUILD));
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_SORT));
+ $processors = $entity->getProcessors();
+ $this->assertArrayHasKey('hide_non_narrowing_result_processor', $processors);
+ $this->assertInstanceOf(HideNonNarrowingResultProcessor::class, $processors['hide_non_narrowing_result_processor']);
+
+ $entity->removeProcessor($id);
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_BUILD));
+ $this->assertEmpty($entity->getProcessorsByStage(ProcessorInterface::STAGE_SORT));
+ }
+
+ /**
+ * Query type with no defined facet source.
+ *
+ * @covers ::getQueryType
+ */
+ public function testGetQueryTypeWithNoFacetSource() {
+ $entity = new Facet([], 'facets_facet');
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('No facet source defined for facet.');
+ $entity->getQueryType();
+ }
+
+ /**
+ * Tests query operator.
+ *
+ * @covers ::setQueryOperator
+ * @covers ::getQueryOperator
+ */
+ public function testQueryOperator() {
+ $entity = new Facet([], 'facets_facet');
+
+ $this->assertEquals('or', $entity->getQueryOperator());
+ $entity->setQueryOperator('and');
+ $this->assertEquals('and', $entity->getQueryOperator());
+ }
+
+ /**
+ * Tests exclude operator.
+ *
+ * @covers ::getExclude
+ * @covers ::setExclude
+ */
+ public function testExclude() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertFalse($entity->getExclude());
+ $entity->setExclude(TRUE);
+ $this->assertTrue($entity->getExclude());
+ }
+
+ /**
+ * Tests facet weight.
+ *
+ * @covers ::setWeight
+ * @covers ::getWeight
+ */
+ public function testWeight() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertNull($entity->getWeight());
+ $entity->setWeight(12);
+ $this->assertEquals(12, $entity->getWeight());
+
+ }
+
+ /**
+ * Tests facet visibility.
+ *
+ * @covers ::setOnlyVisibleWhenFacetSourceIsVisible
+ * @covers ::getOnlyVisibleWhenFacetSourceIsVisible
+ */
+ public function testOnlyVisible() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertNull($entity->getOnlyVisibleWhenFacetSourceIsVisible());
+ $entity->setOnlyVisibleWhenFacetSourceIsVisible(TRUE);
+ $this->assertTrue($entity->getOnlyVisibleWhenFacetSourceIsVisible());
+
+ }
+
+ /**
+ * Tests facet only one result.
+ *
+ * @covers ::getShowOnlyOneResult
+ * @covers ::setShowOnlyOneResult
+ */
+ public function testOnlyOneResult() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertFalse($entity->getShowOnlyOneResult());
+ $entity->setShowOnlyOneResult(TRUE);
+ $this->assertTrue($entity->getShowOnlyOneResult());
+ }
+
+ /**
+ * Tests url alias.
+ *
+ * @covers ::getUrlAlias
+ * @covers ::setUrlAlias
+ */
+ public function testUrlAlias() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertNull($entity->getUrlAlias());
+
+ $entity->setUrlAlias('owl');
+ $this->assertEquals('owl', $entity->getUrlAlias());
+
+ $entity = new Facet(['url_alias' => 'llama'], 'facets_facet');
+ $this->assertEquals('llama', $entity->getUrlAlias());
+ }
+
+ /**
+ * Tests results behavior.
+ *
+ * @covers ::setResults
+ * @covers ::getResults
+ * @covers ::isActiveValue
+ * @covers ::getActiveItems
+ * @covers ::setActiveItems
+ * @covers ::setActiveItem
+ * @covers ::isActiveValue
+ */
+ public function testResults() {
+ $entity = new Facet([], 'facets_facet');
+ /** @var \Drupal\facets\Result\ResultInterface[] $results */
+ $results = [
+ new Result($entity, 'llama', 'llama', 10),
+ new Result($entity, 'badger', 'badger', 15),
+ new Result($entity, 'owl', 'owl', 5),
+ ];
+
+ $this->assertEmpty($entity->getResults());
+
+ $entity->setResults($results);
+ $this->assertEquals($results, $entity->getResults());
+
+ $this->assertEmpty($entity->getActiveItems());
+ $this->assertFalse($entity->isActiveValue('llama'));
+
+ $entity->setActiveItem('llama');
+ $this->assertEquals(['llama'], $entity->getActiveItems());
+ $this->assertTrue($entity->isActiveValue('llama'));
+ $this->assertFalse($entity->isActiveValue('owl'));
+
+ $this->assertFalse($entity->getResults()[0]->isActive());
+ $entity->setResults($results);
+ $this->assertTrue($entity->getResults()[0]->isActive());
+
+ $this->assertTrue($entity->isActiveValue('llama'));
+ $this->assertFalse($entity->isActiveValue('badger'));
+ $this->assertFalse($entity->isActiveValue('owl'));
+
+ $entity->setActiveItems(['badger', 'owl']);
+ $this->assertFalse($entity->isActiveValue('llama'));
+ $this->assertTrue($entity->isActiveValue('badger'));
+ $this->assertTrue($entity->isActiveValue('owl'));
+ }
+
+ /**
+ * Tests field identifier.
+ *
+ * @covers ::getFieldIdentifier
+ * @covers ::setFieldIdentifier
+ * @covers ::getFieldAlias
+ */
+ public function testFieldIdentifier() {
+ $entity = new Facet([], 'facets_facet');
+
+ $this->assertEmpty($entity->getFieldIdentifier());
+
+ $entity->setFieldIdentifier('field_owl');
+ $this->assertEquals('field_owl', $entity->getFieldIdentifier());
+ $this->assertEquals('field_owl', $entity->getFieldAlias());
+ }
+
+ /**
+ * Tests empty behavior.
+ *
+ * @covers ::setEmptyBehavior
+ * @covers ::getEmptyBehavior
+ */
+ public function testEmptyBehavior() {
+ $entity = new Facet([], 'facets_facet');
+
+ $this->assertEmpty($entity->getEmptyBehavior());
+
+ $entity->setEmptyBehavior(['behavior' => 'none']);
+ $this->assertEquals(['behavior' => 'none'], $entity->getEmptyBehavior());
+ }
+
+ /**
+ * Tests hard limit.
+ *
+ * @covers ::setHardLimit
+ * @covers ::getHardLimit
+ */
+ public function testHardLimit() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertEquals(0, $entity->getHardLimit());
+ $entity->setHardLimit(50);
+ $this->assertEquals(50, $entity->getHardLimit());
+ }
+
+ /**
+ * Tests minimum count.
+ *
+ * @covers ::setMinCount
+ * @covers ::getMinCount
+ */
+ public function testMinCount() {
+ $entity = new Facet([], 'facets_facet');
+ $this->assertEquals(1, $entity->getMinCount());
+ $entity->setMinCount(50);
+ $this->assertEquals(50, $entity->getMinCount());
+ }
+
+ /**
+ * Tests hierarchy settings.
+ *
+ * @covers ::getHierarchy
+ * @covers ::setUseHierarchy
+ * @covers ::getUseHierarchy
+ * @covers ::setExpandHierarchy
+ * @covers ::getExpandHierarchy
+ * @covers ::setEnableParentWhenChildGetsDisabled
+ * @covers ::getEnableParentWhenChildGetsDisabled
+ * @covers ::getHierarchyManager
+ * @covers ::getHierarchyInstance
+ */
+ public function testHierarchySettings() {
+ $entity = Facet::create();
+
+ $entity->setUseHierarchy(FALSE);
+ $this->assertFalse($entity->getUseHierarchy());
+ $entity->setUseHierarchy(TRUE);
+ $this->assertTrue($entity->getUseHierarchy());
+
+ $entity->setExpandHierarchy(FALSE);
+ $this->assertFalse($entity->getExpandHierarchy());
+ $entity->setExpandHierarchy(TRUE);
+ $this->assertTrue($entity->getExpandHierarchy());
+
+ $entity->setEnableParentWhenChildGetsDisabled(FALSE);
+ $this->assertFalse($entity->getEnableParentWhenChildGetsDisabled());
+ $entity->setEnableParentWhenChildGetsDisabled(TRUE);
+ $this->assertTrue($entity->getEnableParentWhenChildGetsDisabled());
+
+ $entity->setHierarchy('taxonomy');
+ $manager = $entity->getHierarchyManager();
+ $this->assertInstanceOf(HierarchyPluginManager::class, $manager);
+ $this->assertInstanceOf(Taxonomy::class, $entity->getHierarchyInstance());
+
+ $this->assertEquals(['type' => 'taxonomy', 'config' => []], $entity->getHierarchy());
+ }
+
+ /**
+ * Tests that the block caches are cleared from API calls.
+ *
+ * @covers ::postSave
+ * @covers ::postDelete
+ * @covers ::clearBlockCache
+ */
+ public function testBlockCache() {
+ // Block processing requires the system module.
+ $this->enableModules(['system']);
+
+ // Create our facet.
+ $entity = Facet::create([
+ 'id' => 'test_facet',
+ 'name' => 'Test facet',
+ ]);
+ $entity->setWidget('links');
+ $entity->setEmptyBehavior(['behavior' => 'none']);
+
+ $block_id = 'facet_block' . PluginBase::DERIVATIVE_SEPARATOR . $entity->id();
+
+ // Check we don't have a block yet.
+ $this->assertFalse($this->container->get('plugin.manager.block')->hasDefinition($block_id));
+
+ // Save our facet.
+ $entity->save();
+
+ // Check our block exists.
+ $this->assertTrue($this->container->get('plugin.manager.block')->hasDefinition($block_id));
+
+ // Delete our facet.
+ $entity->delete();
+
+ // Check our block exists.
+ $this->assertFalse($this->container->get('plugin.manager.block')->hasDefinition($block_id));
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/FacetManager/DefaultFacetManagerTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/FacetManager/DefaultFacetManagerTest.php
new file mode 100644
index 000000000..a840b44c4
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/FacetManager/DefaultFacetManagerTest.php
@@ -0,0 +1,128 @@
+installEntitySchema('facets_facet');
+ }
+
+ /**
+ * Tests the getEnabledFacets method.
+ *
+ * @covers ::getEnabledFacets
+ */
+ public function testGetEnabledFacets() {
+ /** @var \Drupal\facets\FacetManager\DefaultFacetManager $dfm */
+ $dfm = \Drupal::service('facets.manager');
+ $returnValue = $dfm->getEnabledFacets();
+ $this->assertEmpty($returnValue);
+
+ // Create a facet.
+ $entity = $this->createAndSaveFacet('test_facet');
+
+ $returnValue = $dfm->getEnabledFacets();
+ $this->assertNotEmpty($returnValue);
+ $this->assertSame($entity->id(), $returnValue['test_facet']->id());
+ }
+
+ /**
+ * Tests the getFacetsByFacetSourceId method.
+ *
+ * @covers ::getFacetsByFacetSourceId
+ */
+ public function testGetFacetsByFacetSourceId() {
+ /** @var \Drupal\facets\FacetManager\DefaultFacetManager $dfm */
+ $dfm = \Drupal::service('facets.manager');
+ $this->assertEmpty($dfm->getFacetsByFacetSourceId('planets'));
+
+ // Create 2 different facets with a unique facet source id.
+ $entity = $this->createAndSaveFacet('Jupiter');
+ $entity->setFacetSourceId('planets');
+ $entity->save();
+ $entity = $this->createAndSaveFacet('Pluto');
+ $entity->setFacetSourceId('former_planets');
+ $entity->save();
+
+ $planetFacets = $dfm->getFacetsByFacetSourceId('planets');
+ $this->assertNotEmpty($planetFacets);
+ $this->assertCount(1, $planetFacets);
+ $this->assertSame('Jupiter', $planetFacets['Jupiter']->id());
+
+ $formerPlanetFacets = $dfm->getFacetsByFacetSourceId('former_planets');
+ $this->assertNotEmpty($formerPlanetFacets);
+ $this->assertCount(1, $formerPlanetFacets);
+ $this->assertSame('Pluto', $formerPlanetFacets['Pluto']->id());
+
+ // Make pluto a planet again.
+ $entity->setFacetSourceId('planets');
+ $entity->save();
+
+ // Test that we now hit the static cache.
+ $planetFacets = $dfm->getFacetsByFacetSourceId('planets');
+ $this->assertNotEmpty($planetFacets);
+ $this->assertCount(1, $planetFacets);
+
+ // Change the 'facets' property on the manager to public, so we can
+ // overwrite it here. This is because otherwise we run into the static
+ // caches.
+ $facetsProperty = new \ReflectionProperty($dfm, 'facets');
+ $facetsProperty->setAccessible(TRUE);
+ $facetsProperty->setValue($dfm, []);
+
+ // Now that the static cache is reset, test that we have 2 planets.
+ $planetFacets = $dfm->getFacetsByFacetSourceId('planets');
+ $this->assertNotEmpty($planetFacets);
+ $this->assertCount(2, $planetFacets);
+ $this->assertSame('Jupiter', $planetFacets['Jupiter']->id());
+ $this->assertSame('Pluto', $planetFacets['Pluto']->id());
+ }
+
+ /**
+ * Create and save a facet, for usage in test-scenario's.
+ *
+ * @param string $id
+ * The id.
+ *
+ * @return \Drupal\facets\FacetInterface
+ * The newly created facet.
+ */
+ protected function createAndSaveFacet($id) {
+ // Create a facet.
+ $entity = Facet::create([
+ 'id' => $id,
+ 'name' => 'Test facet',
+ ]);
+ $entity->setWidget('links');
+ $entity->setEmptyBehavior(['behavior' => 'none']);
+ $entity->setFacetSourceId('fluffy');
+ $entity->save();
+
+ return $entity;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Plugin/query_type/SearchApiDateTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Plugin/query_type/SearchApiDateTest.php
new file mode 100644
index 000000000..3eb7cfa74
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Kernel/Plugin/query_type/SearchApiDateTest.php
@@ -0,0 +1,351 @@
+installEntitySchema('facets_facet');
+ }
+
+ /**
+ * Tests string query type without executing the query with an "AND" operator.
+ *
+ * @dataProvider resultsProvider
+ */
+ public function testQueryTypeAnd($granularity, $original_results, $grouped_results) {
+ $backend = $this->prophesize(BackendInterface::class);
+ $backend->getSupportedFeatures()->willReturn([]);
+ $server = $this->prophesize(ServerInterface::class);
+ $server->getBackend()->willReturn($backend);
+ $index = $this->prophesize(IndexInterface::class);
+ $index->getServerInstance()->willReturn($server);
+ $query = $this->prophesize(SearchApiQuery::class);
+ $query->getIndex()->willReturn($index);
+
+ $facet = new Facet(
+ ['query_operator' => 'AND', 'widget' => 'links'],
+ 'facets_facet'
+ );
+ $facet->addProcessor([
+ 'processor_id' => 'date_item',
+ 'weights' => [],
+ 'settings' => [
+ 'granularity' => $granularity,
+ 'date_format' => '',
+ 'date_display' => 'actual_date',
+ ],
+ ]);
+
+ $query_type = new SearchApiDate(
+ [
+ 'facet' => $facet,
+ 'query' => $query->reveal(),
+ 'results' => $original_results,
+ ],
+ 'search_api_date',
+ []
+ );
+
+ $built_facet = $query_type->build();
+ $this->assertInstanceOf(FacetInterface::class, $built_facet);
+
+ $results = $built_facet->getResults();
+ $this->assertSame('array', gettype($results));
+
+ foreach ($grouped_results as $k => $result) {
+ $this->assertInstanceOf(ResultInterface::class, $results[$k]);
+ $this->assertEquals($result['count'], $results[$k]->getCount());
+ $this->assertEquals($result['filter'], $results[$k]->getDisplayValue());
+ }
+ }
+
+ /**
+ * Data provider for date results and different groupings.
+ */
+ public function resultsProvider() {
+ return [
+ 'Year' => [
+ SearchApiDate::FACETAPI_DATE_YEAR,
+ [
+ ['count' => 1, 'filter' => '984711763'],
+ ['count' => 1, 'filter' => '1268900542'],
+ ['count' => 1, 'filter' => '1269963121'],
+ ['count' => 1, 'filter' => '1306314733'],
+ ['count' => 1, 'filter' => '1464167533'],
+ ['count' => 2, 'filter' => '1464167534'],
+ ['count' => 1, 'filter' => '1464172214'],
+ ['count' => 1, 'filter' => '1464174734'],
+ ['count' => 1, 'filter' => '1464202800'],
+ ['count' => 1, 'filter' => '1464250210'],
+ ['count' => 1, 'filter' => '1464250230'],
+ ['count' => 1, 'filter' => '1464926723'],
+ ['count' => 1, 'filter' => '1465930475'],
+ ],
+ [
+ '2001' => ['count' => 1, 'filter' => 2001],
+ '2010' => ['count' => 2, 'filter' => 2010],
+ '2011' => ['count' => 1, 'filter' => 2011],
+ '2016' => ['count' => 10, 'filter' => 2016],
+ ],
+ ],
+ 'Month' => [
+ SearchApiDate::FACETAPI_DATE_MONTH,
+ [
+ ['count' => 1, 'filter' => '984711763'],
+ ['count' => 1, 'filter' => '1268900542'],
+ ['count' => 1, 'filter' => '1269963121'],
+ ['count' => 1, 'filter' => '1306314733'],
+ ['count' => 1, 'filter' => '1464167533'],
+ ['count' => 2, 'filter' => '1464167534'],
+ ['count' => 1, 'filter' => '1464172214'],
+ ['count' => 1, 'filter' => '1464174734'],
+ ['count' => 1, 'filter' => '1464202800'],
+ ['count' => 1, 'filter' => '1464250210'],
+ ['count' => 1, 'filter' => '1464250230'],
+ ['count' => 1, 'filter' => '1464926723'],
+ ['count' => 1, 'filter' => '1465930475'],
+ ],
+ [
+ '2001-03' => ['count' => 1, 'filter' => 'March 2001'],
+ '2010-03' => ['count' => 2, 'filter' => 'March 2010'],
+ '2011-05' => ['count' => 1, 'filter' => 'May 2011'],
+ '2016-05' => ['count' => 8, 'filter' => 'May 2016'],
+ '2016-06' => ['count' => 2, 'filter' => 'June 2016'],
+ ],
+ ],
+ 'Day' => [
+ SearchApiDate::FACETAPI_DATE_DAY,
+ [
+ ['count' => 1, 'filter' => '984711763'],
+ ['count' => 1, 'filter' => '1268900542'],
+ ['count' => 1, 'filter' => '1269963121'],
+ ['count' => 1, 'filter' => '1306314733'],
+ ['count' => 1, 'filter' => '1464167533'],
+ ['count' => 2, 'filter' => '1464167534'],
+ ['count' => 1, 'filter' => '1464172214'],
+ ['count' => 1, 'filter' => '1464174734'],
+ ['count' => 1, 'filter' => '1464202800'],
+ ['count' => 1, 'filter' => '1464250210'],
+ ['count' => 1, 'filter' => '1464250230'],
+ ['count' => 1, 'filter' => '1464926723'],
+ ['count' => 1, 'filter' => '1465930475'],
+ ],
+ [
+ '2001-03-16' => ['count' => 1, 'filter' => '16 March 2001'],
+ '2010-03-18' => ['count' => 1, 'filter' => '18 March 2010'],
+ '2010-03-31' => ['count' => 1, 'filter' => '31 March 2010'],
+ '2011-05-25' => ['count' => 1, 'filter' => '25 May 2011'],
+ '2016-05-25' => ['count' => 5, 'filter' => '25 May 2016'],
+ '2016-05-26' => ['count' => 3, 'filter' => '26 May 2016'],
+ '2016-06-03' => ['count' => 1, 'filter' => '03 June 2016'],
+ '2016-06-15' => ['count' => 1, 'filter' => '15 June 2016'],
+ ],
+ ],
+ 'Hour' => [
+ SearchApiDate::FACETAPI_DATE_HOUR,
+ [
+ ['count' => 1, 'filter' => '984711763'],
+ ['count' => 1, 'filter' => '1268900542'],
+ ['count' => 1, 'filter' => '1269963121'],
+ ['count' => 1, 'filter' => '1306314733'],
+ ['count' => 1, 'filter' => '1464167533'],
+ ['count' => 2, 'filter' => '1464167534'],
+ ['count' => 1, 'filter' => '1464172214'],
+ ['count' => 1, 'filter' => '1464174734'],
+ ['count' => 1, 'filter' => '1464202800'],
+ ['count' => 1, 'filter' => '1464250210'],
+ ['count' => 1, 'filter' => '1464250230'],
+ ['count' => 1, 'filter' => '1464926723'],
+ ['count' => 1, 'filter' => '1465930475'],
+ ],
+ [
+ '2001-03-16T14' => ['count' => 1, 'filter' => '16/03/2001 14h'],
+ '2010-03-18T19' => ['count' => 1, 'filter' => '18/03/2010 19h'],
+ '2010-03-31T02' => ['count' => 1, 'filter' => '31/03/2010 02h'],
+ '2011-05-25T19' => ['count' => 1, 'filter' => '25/05/2011 19h'],
+ '2016-05-25T19' => ['count' => 3, 'filter' => '25/05/2016 19h'],
+ '2016-05-25T20' => ['count' => 1, 'filter' => '25/05/2016 20h'],
+ '2016-05-25T21' => ['count' => 1, 'filter' => '25/05/2016 21h'],
+ '2016-05-26T05' => ['count' => 1, 'filter' => '26/05/2016 05h'],
+ '2016-05-26T18' => ['count' => 2, 'filter' => '26/05/2016 18h'],
+ '2016-06-03T14' => ['count' => 1, 'filter' => '03/06/2016 14h'],
+ '2016-06-15T04' => ['count' => 1, 'filter' => '15/06/2016 04h'],
+ ],
+ ],
+ 'Minute' => [
+ SearchApiDate::FACETAPI_DATE_MINUTE,
+ [
+ ['count' => 1, 'filter' => '984711763'],
+ ['count' => 1, 'filter' => '1268900542'],
+ ['count' => 1, 'filter' => '1269963121'],
+ ['count' => 1, 'filter' => '1306314733'],
+ ['count' => 1, 'filter' => '1464167533'],
+ ['count' => 2, 'filter' => '1464167534'],
+ ['count' => 1, 'filter' => '1464172214'],
+ ['count' => 1, 'filter' => '1464174734'],
+ ['count' => 1, 'filter' => '1464202800'],
+ ['count' => 1, 'filter' => '1464250210'],
+ ['count' => 1, 'filter' => '1464250230'],
+ ['count' => 1, 'filter' => '1464926723'],
+ ['count' => 1, 'filter' => '1465930475'],
+ ],
+ [
+ '2001-03-16T14:02' => ['count' => 1, 'filter' => '16/03/2001 14:02'],
+ '2010-03-18T19:22' => ['count' => 1, 'filter' => '18/03/2010 19:22'],
+ '2010-03-31T02:32' => ['count' => 1, 'filter' => '31/03/2010 02:32'],
+ '2011-05-25T19:12' => ['count' => 1, 'filter' => '25/05/2011 19:12'],
+ '2016-05-25T19:12' => ['count' => 3, 'filter' => '25/05/2016 19:12'],
+ '2016-05-25T20:30' => ['count' => 1, 'filter' => '25/05/2016 20:30'],
+ '2016-05-25T21:12' => ['count' => 1, 'filter' => '25/05/2016 21:12'],
+ '2016-05-26T05:00' => ['count' => 1, 'filter' => '26/05/2016 05:00'],
+ '2016-05-26T18:10' => ['count' => 2, 'filter' => '26/05/2016 18:10'],
+ '2016-06-03T14:05' => ['count' => 1, 'filter' => '03/06/2016 14:05'],
+ '2016-06-15T04:54' => ['count' => 1, 'filter' => '15/06/2016 04:54'],
+ ],
+ ],
+ 'Second' => [
+ SearchApiDate::FACETAPI_DATE_SECOND,
+ [
+ ['count' => 1, 'filter' => '984711763'],
+ ['count' => 1, 'filter' => '1268900542'],
+ ['count' => 1, 'filter' => '1269963121'],
+ ['count' => 1, 'filter' => '1306314733'],
+ ['count' => 1, 'filter' => '1464167533'],
+ ['count' => 2, 'filter' => '1464167534'],
+ ['count' => 1, 'filter' => '1464172214'],
+ ['count' => 1, 'filter' => '1464174734'],
+ ['count' => 1, 'filter' => '1464202800'],
+ ['count' => 1, 'filter' => '1464250210'],
+ ['count' => 1, 'filter' => '1464250230'],
+ ['count' => 1, 'filter' => '1464926723'],
+ ['count' => 1, 'filter' => '1465930475'],
+ ],
+ [
+ '2001-03-16T14:02:43' => [
+ 'count' => 1,
+ 'filter' => '16/03/2001 14:02:43',
+ ],
+ '2010-03-18T19:22:22' => [
+ 'count' => 1,
+ 'filter' => '18/03/2010 19:22:22',
+ ],
+ '2010-03-31T02:32:01' => [
+ 'count' => 1,
+ 'filter' => '31/03/2010 02:32:01',
+ ],
+ '2011-05-25T19:12:13' => [
+ 'count' => 1,
+ 'filter' => '25/05/2011 19:12:13',
+ ],
+ '2016-05-25T19:12:13' => [
+ 'count' => 1,
+ 'filter' => '25/05/2016 19:12:13',
+ ],
+ '2016-05-25T19:12:14' => [
+ 'count' => 2,
+ 'filter' => '25/05/2016 19:12:14',
+ ],
+ '2016-05-25T20:30:14' => [
+ 'count' => 1,
+ 'filter' => '25/05/2016 20:30:14',
+ ],
+ '2016-05-25T21:12:14' => [
+ 'count' => 1,
+ 'filter' => '25/05/2016 21:12:14',
+ ],
+ '2016-05-26T05:00:00' => [
+ 'count' => 1,
+ 'filter' => '26/05/2016 05:00:00',
+ ],
+ '2016-05-26T18:10:10' => [
+ 'count' => 1,
+ 'filter' => '26/05/2016 18:10:10',
+ ],
+ '2016-05-26T18:10:30' => [
+ 'count' => 1,
+ 'filter' => '26/05/2016 18:10:30',
+ ],
+ '2016-06-03T14:05:23' => [
+ 'count' => 1,
+ 'filter' => '03/06/2016 14:05:23',
+ ],
+ '2016-06-15T04:54:35' => [
+ 'count' => 1,
+ 'filter' => '15/06/2016 04:54:35',
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Tests string query type without results.
+ */
+ public function testEmptyResults() {
+ $query = new SearchApiQuery([], 'search_api_query', []);
+ $facet = new Facet([], 'facets_facet');
+
+ $facet->addProcessor([
+ 'processor_id' => 'date_item',
+ 'weights' => [],
+ 'settings' => [
+ 'granularity' => SearchApiDate::FACETAPI_DATE_YEAR,
+ 'date_format' => '',
+ 'date_display' => 'actual_date',
+ ],
+ ]);
+
+ $query_type = new SearchApiDate(
+ [
+ 'facet' => $facet,
+ 'query' => $query,
+ ],
+ 'search_api_string',
+ []
+ );
+
+ $built_facet = $query_type->build();
+ $this->assertInstanceOf(FacetInterface::class, $built_facet);
+
+ $results = $built_facet->getResults();
+ $this->assertSame('array', gettype($results));
+ $this->assertEmpty($results);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/FacetSource/FacetSourcePluginManagerTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/FacetSource/FacetSourcePluginManagerTest.php
new file mode 100644
index 000000000..1c76ed338
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/FacetSource/FacetSourcePluginManagerTest.php
@@ -0,0 +1,138 @@
+discovery = $this->createMock(DiscoveryInterface::class);
+
+ $this->factory = $this->getMockBuilder(DefaultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->moduleHandler = $this->createMock(ModuleHandlerInterface::class);
+
+ $this->cache = $this->createMock(CacheBackendInterface::class);
+
+ $namespaces = new \ArrayObject();
+
+ $this->sut = new FacetSourcePluginManager($namespaces, $this->cache, $this->moduleHandler);
+ $discovery_property = new \ReflectionProperty($this->sut, 'discovery');
+ $discovery_property->setAccessible(TRUE);
+ $discovery_property->setValue($this->sut, $this->discovery);
+ $factory_property = new \ReflectionProperty($this->sut, 'factory');
+ $factory_property->setAccessible(TRUE);
+ $factory_property->setValue($this->sut, $this->factory);
+ }
+
+ /**
+ * Tests plugin manager constructor.
+ */
+ public function testConstruct() {
+ $namespaces = new \ArrayObject();
+ $sut = new FacetSourcePluginManager($namespaces, $this->cache, $this->moduleHandler);
+ $this->assertInstanceOf(FacetSourcePluginManager::class, $sut);
+ }
+
+ /**
+ * Tests plugin manager's getDefinitions method.
+ */
+ public function testGetDefinitions() {
+ $definitions = [
+ 'foo' => [
+ 'id' => 'foo_bar',
+ 'label' => 'Foo bar',
+ 'description' => 'test',
+ 'display_id' => 'foo',
+ ],
+ ];
+ $this->discovery->expects($this->once())
+ ->method('getDefinitions')
+ ->willReturn($definitions);
+ $this->assertSame($definitions, $this->sut->getDefinitions());
+ }
+
+ /**
+ * Tests plugin manager definitions.
+ *
+ * @dataProvider invalidDefinitions
+ */
+ public function testInvalidDefinitions($invalid_definition) {
+ $definitions = ['foo' => [$invalid_definition]];
+
+ $this->discovery->expects($this->once())
+ ->method('getDefinitions')
+ ->willReturn($definitions);
+
+ $this->expectException(PluginException::class);
+ $this->sut->getDefinitions();
+ }
+
+ /**
+ * Provides invalid definitions.
+ *
+ * @return array
+ * An invalid data provider.
+ */
+ public function invalidDefinitions() {
+ return [
+ 'only id' => ['id' => 'owl'],
+ 'only display_id' => ['display_id' => 'search_api:owl'],
+ 'only label' => ['label' => 'Owl'],
+ 'no label' => ['id' => 'owl', 'display_id' => 'Owl'],
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ActiveWidgetOrderProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ActiveWidgetOrderProcessorTest.php
new file mode 100644
index 000000000..f1eba7dbc
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ActiveWidgetOrderProcessorTest.php
@@ -0,0 +1,84 @@
+setActiveState(TRUE);
+ $original_results[2]->setActiveState(TRUE);
+ $original_results[3]->setActiveState(TRUE);
+
+ $this->originalResults = $original_results;
+
+ $this->processor = new ActiveWidgetOrderProcessor([], 'active_widget_order', []);
+ }
+
+ /**
+ * Tests sorting.
+ */
+ public function testSorting() {
+ $sort_value = $this->processor->sortResults($this->originalResults[0], $this->originalResults[1]);
+ $this->assertEquals(1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[1], $this->originalResults[2]);
+ $this->assertEquals(0, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[2], $this->originalResults[3]);
+ $this->assertEquals(0, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[3], $this->originalResults[4]);
+ $this->assertEquals(-1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[3], $this->originalResults[3]);
+ $this->assertEquals(0, $sort_value);
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testDefaultConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals(['sort' => 'DESC'], $config);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/BooleanItemProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/BooleanItemProcessorTest.php
new file mode 100644
index 000000000..e8df066f3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/BooleanItemProcessorTest.php
@@ -0,0 +1,97 @@
+originalResults = [
+ new Result($facet, 0, 0, 10),
+ new Result($facet, 1, 1, 15),
+ ];
+
+ $this->processor = new BooleanItemProcessor([], 'boolean_item_processor', []);
+ }
+
+ /**
+ * Tests filtering of results.
+ */
+ public function testBuild() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ // The default values for on / off are On and Off.
+ $this->assertEquals('Off', $filtered_results[0]->getDisplayValue());
+ $this->assertEquals('On', $filtered_results[1]->getDisplayValue());
+
+ // Overwrite the on/off values.
+ $configuration = ['on_value' => 'True', 'off_value' => 'False'];
+ $this->processor->setConfiguration($configuration);
+
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+ $this->assertEquals('False', $filtered_results[0]->getDisplayValue());
+ $this->assertEquals('True', $filtered_results[1]->getDisplayValue());
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals(['on_value' => 'On', 'off_value' => 'Off'], $config);
+ }
+
+ /**
+ * Tests testDescription().
+ */
+ public function testDescription() {
+ $this->assertEquals('', $this->processor->getDescription());
+ }
+
+ /**
+ * Tests isHidden().
+ */
+ public function testIsHidden() {
+ $this->assertEquals(FALSE, $this->processor->isHidden());
+ }
+
+ /**
+ * Tests isLocked().
+ */
+ public function testIsLocked() {
+ $this->assertEquals(FALSE, $this->processor->isLocked());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/CountLimitProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/CountLimitProcessorTest.php
new file mode 100644
index 000000000..526df50b3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/CountLimitProcessorTest.php
@@ -0,0 +1,236 @@
+originalResults = [
+ new Result($facet, 'llama', 'llama', 10),
+ new Result($facet, 'badger', 'badger', 5),
+ new Result($facet, 'duck', 'duck', 15),
+ ];
+
+ $processor_id = 'count_limit';
+ $this->processor = new CountLimitProcessor([], $processor_id, []);
+
+ $processor_definitions = [
+ $processor_id => [
+ 'id' => $processor_id,
+ 'class' => CountLimitProcessor::class,
+ ],
+ ];
+
+ $manager = $this->getMockBuilder(ProcessorPluginManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $manager->expects($this->any())
+ ->method('getDefinitions')
+ ->willReturn($processor_definitions);
+ $manager->expects($this->any())
+ ->method('createInstance')
+ ->willReturn($this->processor);
+
+ $container_builder = new ContainerBuilder();
+ $container_builder->set('plugin.manager.facets.processor', $manager);
+ \Drupal::setContainer($container_builder);
+
+ }
+
+ /**
+ * Tests no filtering happens.
+ */
+ public function testNoFilter() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'count_limit',
+ 'weights' => [],
+ 'settings' => ['minimum_items' => 4],
+ ]);
+ $this->processor->setConfiguration(['minimum_items' => 4]);
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(3, $sorted_results);
+
+ $this->assertEquals('llama', $sorted_results[0]->getDisplayValue());
+ $this->assertEquals('badger', $sorted_results[1]->getDisplayValue());
+ $this->assertEquals('duck', $sorted_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests no filtering happens.
+ */
+ public function testMinEqualsValue() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'count_limit',
+ 'weights' => [],
+ 'settings' => ['minimum_items' => 5],
+ ]);
+ $this->processor->setConfiguration(['minimum_items' => 5]);
+
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(3, $sorted_results);
+
+ $this->assertEquals('llama', $sorted_results[0]->getDisplayValue());
+ $this->assertEquals('badger', $sorted_results[1]->getDisplayValue());
+ $this->assertEquals('duck', $sorted_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests between minimum and maximum values.
+ */
+ public function testBetweenMinAndMaxValue() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'count_limit',
+ 'weights' => [],
+ 'settings' => [],
+ ]);
+
+ $this->processor->setConfiguration(
+ [
+ 'minimum_items' => 6,
+ 'maximum_items' => 14,
+ ]
+ );
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+ $this->assertCount(1, $sorted_results);
+ $this->assertEquals('llama', $sorted_results[0]->getDisplayValue());
+
+ $this->processor->setConfiguration(
+ [
+ 'minimum_items' => 60,
+ 'maximum_items' => 140,
+ ]
+ );
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+ $this->assertCount(0, $sorted_results);
+
+ $this->processor->setConfiguration(
+ [
+ 'minimum_items' => 1,
+ 'maximum_items' => 10,
+ ]
+ );
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+ $this->assertCount(2, $sorted_results);
+ }
+
+ /**
+ * Tests maximum values.
+ */
+ public function testMaxValue() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'count_limit',
+ 'weights' => [],
+ 'settings' => [],
+ ]);
+
+ $this->processor->setConfiguration(['maximum_items' => 14]);
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+ $this->assertCount(2, $sorted_results);
+ $this->assertEquals('llama', $sorted_results[0]->getDisplayValue());
+ $this->assertEquals('badger', $sorted_results[1]->getDisplayValue());
+
+ $this->processor->setConfiguration(['maximum_items' => 140]);
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+ $this->assertCount(3, $sorted_results);
+ $this->assertEquals('llama', $sorted_results[0]->getDisplayValue());
+ $this->assertEquals('badger', $sorted_results[1]->getDisplayValue());
+ $this->assertEquals('duck', $sorted_results[2]->getDisplayValue());
+
+ $this->processor->setConfiguration(['maximum_items' => 1]);
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+ $this->assertCount(0, $sorted_results);
+ }
+
+ /**
+ * Tests filtering of results.
+ */
+ public function testFilterResults() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'count_limit',
+ 'weights' => [],
+ 'settings' => ['minimum_items' => 8],
+ ]);
+ $this->processor->setConfiguration(['minimum_items' => 8]);
+
+ $sorted_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(2, $sorted_results);
+
+ $this->assertEquals('llama', $sorted_results[0]->getDisplayValue());
+ $this->assertEquals('duck', $sorted_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals(['minimum_items' => 1, 'maximum_items' => 0], $config);
+ }
+
+ /**
+ * Tests testDescription().
+ */
+ public function testDescription() {
+ $this->assertEquals('', $this->processor->getDescription());
+ }
+
+ /**
+ * Tests isHidden().
+ */
+ public function testIsHidden() {
+ $this->assertEquals(FALSE, $this->processor->isHidden());
+ }
+
+ /**
+ * Tests isLocked().
+ */
+ public function testIsLocked() {
+ $this->assertEquals(FALSE, $this->processor->isLocked());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/CountWidgetOrderProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/CountWidgetOrderProcessorTest.php
new file mode 100644
index 000000000..ab1da5c9f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/CountWidgetOrderProcessorTest.php
@@ -0,0 +1,69 @@
+originalResults = [
+ new Result($facet, 'llama', 'llama', 10),
+ new Result($facet, 'badger', 'badger', 5),
+ new Result($facet, 'duck', 'duck', 15),
+ ];
+
+ $this->processor = new CountWidgetOrderProcessor([], 'count_widget_order', []);
+ }
+
+ /**
+ * Tests sorting.
+ */
+ public function testSorting() {
+ $sort_value = $this->processor->sortResults($this->originalResults[0], $this->originalResults[1]);
+ $this->assertEquals(1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[1], $this->originalResults[2]);
+ $this->assertEquals(-1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[2], $this->originalResults[2]);
+ $this->assertEquals(0, $sort_value);
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testDefaultConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals(['sort' => 'DESC'], $config);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/DependentFacetProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/DependentFacetProcessorTest.php
new file mode 100644
index 000000000..ac717d472
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/DependentFacetProcessorTest.php
@@ -0,0 +1,124 @@
+results = [
+ new Result($facet, 'snow_owl', 'Snow owl', 2),
+ new Result($facet, 'forest_owl', 'Forest owl', 3),
+ new Result($facet, 'sand_owl', 'Sand owl', 8),
+ new Result($facet, 'church_owl', 'Church owl', 1),
+ new Result($facet, 'barn_owl', 'Barn owl', 1),
+ ];
+ }
+
+ /**
+ * Tests to no-config case.
+ */
+ public function testNotConfigured() {
+ $facetManager = $this->prophesize(DefaultFacetManager::class)
+ ->reveal();
+ $etm = $this->prophesize(EntityTypeManagerInterface::class)
+ ->reveal();
+ $dfp = new DependentFacetProcessor([], 'dependent_facet_processor', [], $facetManager, $etm);
+
+ $facet = new Facet(['id' => 'owl', 'name' => 'øwl'], 'facets_facet');
+
+ $computed = $dfp->build($facet, $this->results);
+ $this->assertEquals($computed, $this->results);
+ }
+
+ /**
+ * Tests the case where no facets are enabled.
+ */
+ public function testNoEnabledFacets() {
+ $facetManager = $this->prophesize(DefaultFacetManager::class)
+ ->reveal();
+ $etm = $this->prophesize(EntityTypeManagerInterface::class)
+ ->reveal();
+ $configuration = ['owl' => ['enable' => FALSE, 'condition' => 'not_empty']];
+ $dfp = new DependentFacetProcessor($configuration, 'dependent_facet_processor', [], $facetManager, $etm);
+
+ $facet = new Facet(['id' => 'owl', 'name' => 'øwl'], 'facets_facet');
+
+ $computed = $dfp->build($facet, $this->results);
+ $this->assertEquals($computed, $this->results);
+ }
+
+ /**
+ * Tests that facet is not empty.
+ *
+ * @dataProvider provideNegated
+ */
+ public function testNotEmpty($negated) {
+ $facet = new Facet(['id' => 'owl', 'name' => 'øwl'], 'facets_facet');
+ $facet->setActiveItem('snow_owl');
+
+ $facetManager = $this->prophesize(DefaultFacetManager::class);
+ $facetManager->returnBuiltFacet($facet)->willReturn($facet);
+
+ $entityStorage = $this->prophesize(EntityStorageInterface::class);
+ $entityStorage->load('owl')->willReturn($facet);
+
+ $etm = $this->prophesize(EntityTypeManagerInterface::class);
+ $etm->getStorage('facets_facet')->willReturn($entityStorage->reveal());
+
+ $configuration = [
+ 'owl' => [
+ 'enable' => TRUE,
+ 'negate' => $negated,
+ 'condition' => 'not_empty',
+ ],
+ ];
+ $dfp = new DependentFacetProcessor($configuration, 'dependent_facet_processor', [], $facetManager->reveal(), $etm->reveal());
+
+ $computed = $dfp->build($facet, $this->results);
+
+ if ($negated) {
+ $this->assertEquals($computed, []);
+ }
+ else {
+ $this->assertEquals($computed, $this->results);
+ }
+ }
+
+ /**
+ * Provides test cases with data.
+ *
+ * @return array
+ * An array of test data.
+ */
+ public function provideNegated() {
+ return [
+ 'negated' => [TRUE],
+ 'normal' => [FALSE],
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/DisplayValueWidgetOrderProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/DisplayValueWidgetOrderProcessorTest.php
new file mode 100644
index 000000000..542adb601
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/DisplayValueWidgetOrderProcessorTest.php
@@ -0,0 +1,114 @@
+originalResults = [
+ new Result($facet, 'thetans', 'thetans', 10),
+ new Result($facet, 'xenu', 'xenu', 5),
+ new Result($facet, 'Tom', 'Tom', 15),
+ new Result($facet, 'Hubbard', 'Hubbard', 666),
+ new Result($facet, 'FALSE', 'FALSE', 1),
+ new Result($facet, '1977', '1977', 20),
+ new Result($facet, '2', '2', 22),
+ ];
+
+ $transliteration = $this->getMockBuilder(TransliterationInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $transliteration
+ ->expects($this->any())
+ ->method('removeDiacritics')
+ ->will($this->returnArgument(0));
+
+ $this->processor = new DisplayValueWidgetOrderProcessor([], 'display_value_widget_order', [], $transliteration);
+ }
+
+ /**
+ * Tests sorting.
+ */
+ public function testSorting() {
+ $result_count = $this->processor->sortResults($this->originalResults[0], $this->originalResults[1]);
+ $this->assertEquals(-1, $result_count);
+
+ $result_count = $this->processor->sortResults($this->originalResults[1], $this->originalResults[2]);
+ $this->assertEquals(1, $result_count);
+
+ $result_count = $this->processor->sortResults($this->originalResults[2], $this->originalResults[3]);
+ $this->assertEquals(1, $result_count);
+
+ $result_count = $this->processor->sortResults($this->originalResults[3], $this->originalResults[4]);
+ $this->assertEquals(1, $result_count);
+
+ $result_count = $this->processor->sortResults($this->originalResults[4], $this->originalResults[5]);
+ $this->assertEquals(1, $result_count);
+
+ $result_count = $this->processor->sortResults($this->originalResults[5], $this->originalResults[6]);
+ $this->assertEquals(1, $result_count);
+
+ $result_count = $this->processor->sortResults($this->originalResults[6], $this->originalResults[5]);
+ $this->assertEquals(-1, $result_count);
+
+ $result_count = $this->processor->sortResults($this->originalResults[3], $this->originalResults[3]);
+ $this->assertEquals(0, $result_count);
+ }
+
+ /**
+ * Tests that sorting uses the display value.
+ */
+ public function testUseActualDisplayValue() {
+ $facet = new Facet([], 'facets_facet');
+ $original = [
+ new Result($facet, 'bb_test', 'Test AA', 10),
+ new Result($facet, 'aa_test', 'Test BB', 10),
+ ];
+
+ $sorted_results = $this->processor->sortResults($original[0], $original[1]);
+ $this->assertEquals(-1, $sorted_results);
+
+ $sorted_results = $this->processor->sortResults($original[1], $original[0]);
+ $this->assertEquals(1, $sorted_results);
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testDefaultConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals(['sort' => 'ASC'], $config);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ExcludeSpecifiedItemsProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ExcludeSpecifiedItemsProcessorTest.php
new file mode 100644
index 000000000..96daaabd9
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ExcludeSpecifiedItemsProcessorTest.php
@@ -0,0 +1,416 @@
+originalResults = [
+ new Result($facet, 'llama', 'llama', 10),
+ new Result($facet, 'badger', 'badger', 5),
+ new Result($facet, 'duck', 'duck', 15),
+ new Result($facet, 'snbke', 'snbke', 10),
+ new Result($facet, 'snake', 'snake', 10),
+ new Result($facet, 'snaake', 'snaake', 10),
+ new Result($facet, 'snaaake', 'snaaake', 10),
+ new Result($facet, 'snaaaake', 'snaaaake', 10),
+ new Result($facet, 'snaaaaake', 'snaaaaake', 10),
+ new Result($facet, 'snaaaaaake', 'snaaaaaake', 10),
+ ];
+
+ $processor_id = 'exclude_specified_items';
+ $this->processor = new ExcludeSpecifiedItemsProcessor([], $processor_id, [
+ 'id' => "display_value_widget_order",
+ 'label' => "Sort by display value",
+ 'description' => "Sorts the widget results by display value.",
+ 'default_enabled' => TRUE,
+ 'stages' => [
+ "build" => 50,
+ ],
+ ]);
+
+ $processor_definitions = [
+ $processor_id => [
+ 'id' => $processor_id,
+ 'class' => ExcludeSpecifiedItemsProcessor::class,
+ ],
+ ];
+
+ $manager = $this->getMockBuilder(ProcessorPluginManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $manager->expects($this->any())
+ ->method('getDefinitions')
+ ->willReturn($processor_definitions);
+ $manager->expects($this->any())
+ ->method('createInstance')
+ ->willReturn($this->processor);
+
+ $container_builder = new ContainerBuilder();
+ $container_builder->set('plugin.manager.facets.processor', $manager);
+ \Drupal::setContainer($container_builder);
+ }
+
+ /**
+ * Tests no filtering happens.
+ */
+ public function testNoFilter() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'exclude_specified_items',
+ 'weights' => [],
+ 'settings' => [
+ 'exclude' => 'alpaca',
+ 'regex' => 0,
+ ],
+ ]);
+ $this->processor->setConfiguration([
+ 'exclude' => 'alpaca',
+ 'regex' => 0,
+ ]);
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(count($this->originalResults), $filtered_results);
+ }
+
+ /**
+ * Tests filtering happens for string filter.
+ */
+ public function testStringFilter() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'exclude_specified_items',
+ 'weights' => [],
+ 'settings' => [
+ 'exclude' => 'alpaca',
+ 'regex' => 0,
+ ],
+ ]);
+ $this->processor->setConfiguration([
+ 'exclude' => 'llama',
+ 'regex' => 0,
+ ]);
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount((count($this->originalResults) - 1), $filtered_results);
+
+ foreach ($filtered_results as $result) {
+ $this->assertNotEquals('llama', $result->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests filtering happens for string filter.
+ */
+ public function testMultiString() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'exclude_specified_items',
+ 'weights' => [],
+ 'settings' => [
+ 'exclude' => 'alpaca',
+ 'regex' => 0,
+ ],
+ ]);
+ $this->processor->setConfiguration([
+ 'exclude' => 'llama,badger',
+ 'regex' => 0,
+ ]);
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount((count($this->originalResults) - 2), $filtered_results);
+
+ foreach ($filtered_results as $result) {
+ $this->assertNotEquals('llama', $result->getDisplayValue());
+ $this->assertNotEquals('badger', $result->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests filtering happens for string filter.
+ */
+ public function testMultiStringTrim() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'exclude_specified_items',
+ 'weights' => [],
+ 'settings' => [
+ 'exclude' => 'alpaca',
+ 'regex' => 0,
+ ],
+ ]);
+ $this->processor->setConfiguration([
+ 'exclude' => 'llama, badger',
+ 'regex' => 0,
+ ]);
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount((count($this->originalResults) - 2), $filtered_results);
+
+ foreach ($filtered_results as $result) {
+ $this->assertNotEquals('llama', $result->getDisplayValue());
+ $this->assertNotEquals('badger', $result->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests invert filtering happens for string filter.
+ */
+ public function testInvertStringFilter() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'exclude_specified_items',
+ 'weights' => [],
+ 'settings' => [
+ 'exclude' => 'alpaca',
+ 'regex' => 0,
+ 'invert' => 1,
+ ],
+ ]);
+ $this->processor->setConfiguration([
+ 'exclude' => 'llama',
+ 'regex' => 0,
+ 'invert' => 1,
+ ]);
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(1, $filtered_results);
+
+ foreach ($filtered_results as $result) {
+ $this->assertEquals('llama', $result->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests filtering happens for string filter.
+ */
+ public function testInvertMultiString() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'exclude_specified_items',
+ 'weights' => [],
+ 'settings' => [
+ 'exclude' => 'alpaca',
+ 'regex' => 0,
+ 'invert' => 1,
+ ],
+ ]);
+ $this->processor->setConfiguration([
+ 'exclude' => 'llama,badger',
+ 'regex' => 0,
+ 'invert' => 1,
+ ]);
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(2, $filtered_results);
+
+ $filtered_results_values = [];
+ foreach ($filtered_results as $result) {
+ $filtered_results_values[] = $result->getDisplayValue();
+ }
+ $this->assertContains('llama', $filtered_results_values);
+ $this->assertContains('badger', $filtered_results_values);
+ }
+
+ /**
+ * Tests filtering happens for regex filter.
+ *
+ * @dataProvider provideRegexTests
+ */
+ public function testRegexFilter($regex, $expected_results) {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->addProcessor([
+ 'processor_id' => 'exclude_specified_items',
+ 'weights' => [],
+ 'settings' => [
+ 'exclude' => 'alpaca',
+ 'regex' => 0,
+ ],
+ ]);
+ $this->processor->setConfiguration([
+ 'exclude' => $regex,
+ 'regex' => 1,
+ ]);
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(count($expected_results), $filtered_results);
+
+ foreach ($filtered_results as $res) {
+ $this->assertContains($res->getDisplayValue(), $expected_results);
+ }
+ }
+
+ /**
+ * Provides multiple data sets for ::testRegexFilter.
+ */
+ public function provideRegexTests() {
+ return [
+ [
+ 'test',
+ [
+ 'llama',
+ 'duck',
+ 'badger',
+ 'snake',
+ 'snaake',
+ 'snaaake',
+ 'snaaaake',
+ 'snaaaaake',
+ 'snaaaaaake',
+ 'snbke',
+ ],
+ ],
+ [
+ 'llama',
+ [
+ 'badger',
+ 'duck',
+ 'snake',
+ 'snaake',
+ 'snaaake',
+ 'snaaaake',
+ 'snaaaaake',
+ 'snaaaaaake',
+ 'snbke',
+ ],
+ ],
+ [
+ 'duck',
+ [
+ 'llama',
+ 'badger',
+ 'snake',
+ 'snaake',
+ 'snaaake',
+ 'snaaaake',
+ 'snaaaaake',
+ 'snaaaaaake',
+ 'snbke',
+ ],
+ ],
+ [
+ 'sn(.*)ke',
+ [
+ 'llama',
+ 'duck',
+ 'badger',
+ ],
+ ],
+ [
+ 'sn(a*)ke',
+ [
+ 'llama',
+ 'duck',
+ 'badger',
+ 'snbke',
+ ],
+ ],
+ [
+ 'sn(a+)ke',
+ [
+ 'llama',
+ 'duck',
+ 'badger',
+ 'snbke',
+ ],
+ ],
+ [
+ 'sn(a{3,5})ke',
+ [
+ 'llama',
+ 'duck',
+ 'badger',
+ 'snake',
+ 'snaake',
+ 'snaaaaaake',
+ 'snbke',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals(['exclude' => '', 'regex' => 0, 'invert' => 0], $config);
+ }
+
+ /**
+ * Tests testDescription().
+ */
+ public function testDescription() {
+ $this->assertEquals('Sorts the widget results by display value.', $this->processor->getDescription());
+ }
+
+ /**
+ * Tests isHidden().
+ */
+ public function testIsHidden() {
+ $this->assertEquals(FALSE, $this->processor->isHidden());
+ }
+
+ /**
+ * Tests isLocked().
+ */
+ public function testIsLocked() {
+ $this->assertEquals(FALSE, $this->processor->isLocked());
+ }
+
+ /**
+ * Tests supportsStage().
+ */
+ public function testSupportsStage() {
+ $this->assertTrue($this->processor->supportsStage('build'));
+ $this->assertFalse($this->processor->supportsStage('sort'));
+ }
+
+ /**
+ * Tests getDefaultWeight().
+ */
+ public function testGetDefaultWeight() {
+ $this->assertEquals(50, $this->processor->getDefaultWeight('build'));
+ $this->assertEquals(0, $this->processor->getDefaultWeight('sort'));
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideActiveItemsProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideActiveItemsProcessorTest.php
new file mode 100644
index 000000000..c8050cd74
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideActiveItemsProcessorTest.php
@@ -0,0 +1,115 @@
+originalResults = [
+ new Result($facet, 'llama', 'llama', 10),
+ new Result($facet, 'badger', 'badger', 15),
+ new Result($facet, 'duck', 'duck', 15),
+ ];
+
+ $this->processor = new HideActiveItemsProcessor([], 'hide_non_narrowing_result_processor', []);
+ }
+
+ /**
+ * Tests filtering of results.
+ */
+ public function testNoFilterResults() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(3, $filtered_results);
+
+ $this->assertEquals(10, $filtered_results[0]->getCount());
+ $this->assertEquals('llama', $filtered_results[0]->getDisplayValue());
+ $this->assertEquals(15, $filtered_results[1]->getCount());
+ $this->assertEquals('badger', $filtered_results[1]->getDisplayValue());
+ $this->assertEquals(15, $filtered_results[2]->getCount());
+ $this->assertEquals('duck', $filtered_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests filtering of results.
+ */
+ public function testFilterResults() {
+ $results = $this->originalResults;
+ $results[2]->setActiveState(TRUE);
+
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($results);
+
+ $filtered_results = $this->processor->build($facet, $results);
+
+ $this->assertCount(2, $filtered_results);
+
+ $this->assertEquals(10, $filtered_results[0]->getCount());
+ $this->assertEquals('llama', $filtered_results[0]->getDisplayValue());
+ $this->assertEquals(15, $filtered_results[1]->getCount());
+ $this->assertEquals('badger', $filtered_results[1]->getDisplayValue());
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals([], $config);
+ }
+
+ /**
+ * Tests testDescription().
+ */
+ public function testDescription() {
+ $this->assertEquals('', $this->processor->getDescription());
+ }
+
+ /**
+ * Tests isHidden().
+ */
+ public function testIsHidden() {
+ $this->assertEquals(FALSE, $this->processor->isHidden());
+ }
+
+ /**
+ * Tests isLocked().
+ */
+ public function testIsLocked() {
+ $this->assertEquals(FALSE, $this->processor->isLocked());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideNonNarrowingResultProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideNonNarrowingResultProcessorTest.php
new file mode 100644
index 000000000..c679d7850
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideNonNarrowingResultProcessorTest.php
@@ -0,0 +1,120 @@
+originalResults = [
+ new Result($facet, 'llama', 'llama', 10),
+ new Result($facet, 'badger', 'badger', 15),
+ new Result($facet, 'duck', 'duck', 15),
+ ];
+
+ $this->processor = new HideNonNarrowingResultProcessor([], 'hide_non_narrowing_result_processor', []);
+ }
+
+ /**
+ * Tests filtering of results.
+ */
+ public function testNoFilterResults() {
+
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+
+ $filtered_results = $this->processor->build($facet, $this->originalResults);
+
+ $this->assertCount(3, $filtered_results);
+
+ $this->assertEquals(10, $filtered_results[0]->getCount());
+ $this->assertEquals('llama', $filtered_results[0]->getDisplayValue());
+ $this->assertEquals(15, $filtered_results[1]->getCount());
+ $this->assertEquals('badger', $filtered_results[1]->getDisplayValue());
+ $this->assertEquals(15, $filtered_results[2]->getCount());
+ $this->assertEquals('duck', $filtered_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests filtering of results.
+ */
+ public function testFilterResults() {
+
+ $results = $this->originalResults;
+ $results[2]->setActiveState(TRUE);
+
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($results);
+
+ $filtered_results = $this->processor->build($facet, $results);
+
+ $this->assertCount(2, $filtered_results);
+
+ // Llama is shown because it narrows results.
+ $this->assertEquals(10, $filtered_results[0]->getCount());
+ $this->assertEquals('llama', $filtered_results[0]->getDisplayValue());
+
+ // Duck is shown because it's already active.
+ $this->assertEquals(15, $filtered_results[2]->getCount());
+ $this->assertEquals('duck', $filtered_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals([], $config);
+ }
+
+ /**
+ * Tests testDescription().
+ */
+ public function testDescription() {
+ $this->assertEquals('', $this->processor->getDescription());
+ }
+
+ /**
+ * Tests isHidden().
+ */
+ public function testIsHidden() {
+ $this->assertEquals(FALSE, $this->processor->isHidden());
+ }
+
+ /**
+ * Tests isLocked().
+ */
+ public function testIsLocked() {
+ $this->assertEquals(FALSE, $this->processor->isLocked());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideOnlyOneItemProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideOnlyOneItemProcessorTest.php
new file mode 100644
index 000000000..951dabbdf
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/HideOnlyOneItemProcessorTest.php
@@ -0,0 +1,74 @@
+getMockBuilder(Facet::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $processed_results = $processor->build($facet, $results);
+ $this->assertCount(0, $processed_results);
+ }
+
+ /**
+ * Tests with one result that is already active.
+ *
+ * @covers ::build
+ */
+ public function testWithOneActiveResult() {
+ $processor = new HideOnlyOneItemProcessor([], 'hide_only_one_item', []);
+ $facet = new Facet([], 'facets_facet');
+ $results = [
+ new Result($facet, '1', 1, 1),
+ ];
+ $results[0]->setActiveState(TRUE);
+ $facet = $this->getMockBuilder(Facet::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $processed_results = $processor->build($facet, $results);
+ $this->assertCount(1, $processed_results);
+ }
+
+ /**
+ * Tests with one result.
+ *
+ * @covers ::build
+ */
+ public function testWithMoreResults() {
+ $processor = new HideOnlyOneItemProcessor([], 'hide_only_one_item', []);
+ $facet = new Facet([], 'facets_facet');
+ $results = [
+ new Result($facet, '1', 1, 1),
+ new Result($facet, '2', 2, 2),
+ ];
+ $facet = $this->getMockBuilder(Facet::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $processed_results = $processor->build($facet, $results);
+ $this->assertCount(2, $processed_results);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ListItemProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ListItemProcessorTest.php
new file mode 100644
index 000000000..46d95746b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ListItemProcessorTest.php
@@ -0,0 +1,281 @@
+results = [
+ new Result($facet, 1, 1, 10),
+ new Result($facet, 2, 2, 5),
+ new Result($facet, 3, 3, 15),
+ ];
+
+ $config_manager = $this->getMockBuilder(ConfigManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $entity_field_manager = $this->getMockBuilder(EntityFieldManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $entity_type_bundle_info = $this->getMockBuilder(EntityTypeBundleInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ // Create a search api based facet source and make the property definition
+ // return null.
+ $data_definition = $this->createMock(ComplexDataDefinitionInterface::class);
+ $data_definition->expects($this->any())
+ ->method('getPropertyDefinition')
+ ->willReturn(NULL);
+ $facet_source = $this->getMockBuilder(FacetSourcePluginInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $facet_source->expects($this->any())
+ ->method('getDataDefinition')
+ ->willReturn($data_definition);
+
+ // Add the plugin manager.
+ $pluginManager = $this->getMockBuilder(FacetSourcePluginManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $pluginManager->expects($this->any())
+ ->method('hasDefinition')
+ ->willReturn(TRUE);
+ $pluginManager->expects($this->any())
+ ->method('createInstance')
+ ->willReturn($facet_source);
+
+ $this->processor = new ListItemProcessor([], 'list_item', [], $config_manager, $entity_field_manager, $entity_type_bundle_info);
+
+ $container = new ContainerBuilder();
+ $container->set('plugin.manager.facets.facet_source', $pluginManager);
+ \Drupal::setContainer($container);
+ }
+
+ /**
+ * Tests facet build with field.module field.
+ */
+ public function testBuildConfigurableField() {
+ $module_field = $this->getMockBuilder(FieldStorageConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ // Make sure that when the processor calls loadConfigEntityByName the field
+ // we created here is called.
+ $config_manager = $this->getMockBuilder(ConfigManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $config_manager->expects($this->exactly(2))
+ ->method('loadConfigEntityByName')
+ ->willReturn($module_field);
+
+ $entity_field_manager = $this->getMockBuilder(EntityFieldManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $entity_type_bundle_info = $this->getMockBuilder(EntityTypeBundleInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $processor = new ListItemProcessor([], 'list_item', [], $config_manager, $entity_field_manager, $entity_type_bundle_info);
+
+ // Config entity field facet.
+ $module_field_facet = new Facet([], 'facets_facet');
+ $module_field_facet->setFieldIdentifier('test_facet');
+ $module_field_facet->setFacetSourceId('llama_source');
+ $module_field_facet->setResults($this->results);
+ $module_field_facet->addProcessor([
+ 'processor_id' => 'list_item',
+ 'weights' => [],
+ 'settings' => [],
+ ]);
+
+ /** @var \Drupal\facets\Result\Result[] $module_field_facet- */
+ $module_field_results = $processor->build($module_field_facet, $this->results);
+
+ $this->assertCount(3, $module_field_results);
+ $this->assertEquals('llama', $module_field_results[0]->getDisplayValue());
+ $this->assertEquals('badger', $module_field_results[1]->getDisplayValue());
+ $this->assertEquals('kitten', $module_field_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests facet build with field.module field.
+ */
+ public function testBuildBundle() {
+ $module_field = $this->getMockBuilder(FieldStorageConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $config_manager = $this->getMockBuilder(ConfigManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $config_manager->expects($this->exactly(2))
+ ->method('loadConfigEntityByName')
+ ->willReturn($module_field);
+
+ $entity_field_manager = $this->getMockBuilder(EntityFieldManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $entity_type_bundle_info = $this->getMockBuilder(EntityTypeBundleInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $processor = new ListItemProcessor([], 'list_item', [], $config_manager, $entity_field_manager, $entity_type_bundle_info);
+
+ // Config entity field facet.
+ $module_field_facet = new Facet([], 'facets_facet');
+ $module_field_facet->setFieldIdentifier('test_facet');
+ $module_field_facet->setFacetSourceId('llama_source');
+ $module_field_facet->setResults($this->results);
+ $module_field_facet->addProcessor([
+ 'processor_id' => 'list_item',
+ 'weights' => [],
+ 'settings' => [],
+ ]);
+ /** @var \Drupal\facets\Result\Result[] $module_field_facet- */
+ $module_field_results = $processor->build($module_field_facet, $this->results);
+
+ $this->assertCount(3, $module_field_results);
+ $this->assertEquals('llama', $module_field_results[0]->getDisplayValue());
+ $this->assertEquals('badger', $module_field_results[1]->getDisplayValue());
+ $this->assertEquals('kitten', $module_field_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests facet build with base props.
+ */
+ public function testBuildBaseField() {
+ $config_manager = $this->getMockBuilder(ConfigManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $base_field = $this->getMockBuilder(BaseFieldDefinition::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $entity_field_manager = $this->getMockBuilder(EntityFieldManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $entity_field_manager->expects($this->any())
+ ->method('getFieldDefinitions')
+ ->with('node', '')
+ ->willReturn([
+ 'test_facet_baseprop' => $base_field,
+ ]);
+
+ $entity_type_bundle_info = $this->getMockBuilder(EntityTypeBundleInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $processor = new ListItemProcessor([], 'list_item', [], $config_manager, $entity_field_manager, $entity_type_bundle_info);
+
+ // Base prop facet.
+ $base_prop_facet = new Facet([], 'facets_facet');
+ $base_prop_facet->setFieldIdentifier('test_facet_baseprop');
+ $base_prop_facet->setFacetSourceId('llama_source');
+ $base_prop_facet->setResults($this->results);
+ $base_prop_facet->addProcessor([
+ 'processor_id' => 'list_item',
+ 'weights' => [],
+ 'settings' => [],
+ ]);
+
+ /** @var \Drupal\facets\Result\Result[] $base_prop_results */
+ $base_prop_results = $processor->build($base_prop_facet, $this->results);
+
+ $this->assertCount(3, $base_prop_results);
+ $this->assertEquals('llama', $base_prop_results[0]->getDisplayValue());
+ $this->assertEquals('badger', $base_prop_results[1]->getDisplayValue());
+ $this->assertEquals('kitten', $base_prop_results[2]->getDisplayValue());
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals([], $config);
+ }
+
+ /**
+ * Tests testDescription().
+ */
+ public function testDescription() {
+ $this->assertEquals('', $this->processor->getDescription());
+ }
+
+ /**
+ * Tests isHidden().
+ */
+ public function testIsHidden() {
+ $this->assertEquals(FALSE, $this->processor->isHidden());
+ }
+
+ /**
+ * Tests isLocked().
+ */
+ public function testIsLocked() {
+ $this->assertEquals(FALSE, $this->processor->isLocked());
+ }
+
+}
+
+namespace Drupal\facets\Plugin\facets\processor;
+
+if (!function_exists('options_allowed_values')) {
+
+ /**
+ * Overwrite the global function with a version that returns the test values.
+ */
+ function options_allowed_values() {
+ return [
+ 1 => 'llama',
+ 2 => 'badger',
+ 3 => 'kitten',
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/RawValueWidgetOrderProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/RawValueWidgetOrderProcessorTest.php
new file mode 100644
index 000000000..e4094236a
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/RawValueWidgetOrderProcessorTest.php
@@ -0,0 +1,85 @@
+originalResults = [
+ new Result($facet, 'C', 'thetans', 10),
+ new Result($facet, 'B', 'xenu', 5),
+ new Result($facet, 'A', 'Tom', 15),
+ new Result($facet, 'D', 'Hubbard', 666),
+ new Result($facet, 'E', 'FALSE', 1),
+ new Result($facet, 'G', '1977', 20),
+ new Result($facet, 'F', '2', 22),
+ ];
+
+ $this->processor = new RawValueWidgetOrderProcessor([], 'raw_value_widget_order', []);
+ }
+
+ /**
+ * Tests sorting.
+ */
+ public function testSorting() {
+ $sort_value = $this->processor->sortResults($this->originalResults[0], $this->originalResults[1]);
+ $this->assertEquals(1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[1], $this->originalResults[2]);
+ $this->assertEquals(1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[2], $this->originalResults[3]);
+ $this->assertEquals(-1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[3], $this->originalResults[4]);
+ $this->assertEquals(-1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[4], $this->originalResults[5]);
+ $this->assertEquals(-1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[5], $this->originalResults[6]);
+ $this->assertEquals(1, $sort_value);
+
+ $sort_value = $this->processor->sortResults($this->originalResults[3], $this->originalResults[3]);
+ $this->assertEquals(0, $sort_value);
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testDefaultConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals(['sort' => 'ASC'], $config);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ShowOnlyDeepestLevelItemsProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ShowOnlyDeepestLevelItemsProcessorTest.php
new file mode 100644
index 000000000..24cf1ac5e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/ShowOnlyDeepestLevelItemsProcessorTest.php
@@ -0,0 +1,61 @@
+processor = new ShowOnlyDeepestLevelItemsProcessor([], 'test', []);
+ }
+
+ /**
+ * Tests that only items without children survive.
+ *
+ * @covers ::build
+ */
+ public function testRemoveItemsWithoutChildren() {
+ $facet = new Facet(['id' => 'llama'], 'facets_facet');
+ // Setup results.
+ $results = [
+ new Result($facet, 'a', 'A', 5),
+ new Result($facet, 'b', 'B', 2),
+ new Result($facet, 'c', 'C', 4),
+ ];
+ $child = new Result($facet, 'a_1', 'A 1', 3);
+ $results[0]->setChildren([$child]);
+
+ // Execute the build method, so we can test the behavior.
+ $built_results = $this->processor->build($facet, $results);
+
+ // Sort to have a 0-indexed array.
+ sort($built_results);
+
+ // Check the output.
+ $this->assertCount(2, $built_results);
+ $this->assertSame('b', $built_results[0]->getRawValue());
+ $this->assertSame('c', $built_results[1]->getRawValue());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/TermWeightWidgetOrderProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/TermWeightWidgetOrderProcessorTest.php
new file mode 100644
index 000000000..425e5a05b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/TermWeightWidgetOrderProcessorTest.php
@@ -0,0 +1,166 @@
+termStorage = $this->getMockBuilder(EntityStorageInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->entityTypeManager = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->entityTypeManager->expects($this->any())
+ ->method('getStorage')
+ ->willReturn($this->termStorage);
+
+ // Instantiate the processor and load it up with our mock chain.
+ $this->processor = new TermWeightWidgetOrderProcessor([], 'term_weight_widget_order', [], $this->entityTypeManager);
+
+ // Setup two mock terms that will be set up to have specific weights before
+ // the processor is used to compare them.
+ // The mocks are used in the individual tests.
+ $this->termA = $this->getMockBuilder(Term::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->termB = $this->getMockBuilder(Term::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ // Prepare the terms that will be returned when the processor loads its list
+ // of term-ids from the Results raw values.
+ $terms = [
+ 1 => $this->termA,
+ 2 => $this->termB,
+ ];
+
+ // Setup the termStorage mock to return our terms. As we keep a reference to
+ // the terms via $this the individual tests can set up the weights later.
+ $this->termStorage->expects($this->any())
+ ->method('loadMultiple')
+ ->willReturn($terms);
+
+ // Prepare the results that we use the processor to sort, the raw_value has
+ // to match the term_id keys used above in $terms. Display_value and count
+ // is not used.
+ $facet = new Facet([], 'facets_facet');
+ $this->originalResults = [
+ new Result($facet, 1, 10, 100),
+ new Result($facet, 2, 20, 200),
+ ];
+
+ }
+
+ /**
+ * Tests that sorting two terms of equal weight yields 0.
+ */
+ public function testEqual() {
+ $this->termA->expects($this->any())
+ ->method('getWeight')
+ ->willReturn('1');
+
+ $this->termB->expects($this->any())
+ ->method('getWeight')
+ ->willReturn('1');
+
+ $sort_value = $this->processor->sortResults($this->originalResults[0], $this->originalResults[1]);
+ $this->assertEquals(0, $sort_value);
+ }
+
+ /**
+ * Compare a term with a high weight with a term with a low.
+ */
+ public function testHigher() {
+ $this->termA->expects($this->any())
+ ->method('getWeight')
+ ->willReturn('10');
+
+ $this->termB->expects($this->any())
+ ->method('getWeight')
+ ->willReturn('-10');
+
+ $sort_value = $this->processor->sortResults($this->originalResults[0], $this->originalResults[1]);
+ $this->assertGreaterThan(0, $sort_value);
+ }
+
+ /**
+ * Compare a term with a low weight with a term with a high.
+ */
+ public function testLow() {
+ $this->termA->expects($this->any())
+ ->method('getWeight')
+ ->willReturn('-10');
+
+ $this->termB->expects($this->any())
+ ->method('getWeight')
+ ->willReturn('10');
+
+ // Compare the two values and check the result with an assertion.
+ $sort_value = $this->processor->sortResults($this->originalResults[0], $this->originalResults[1]);
+ $this->assertLessThan(0, $sort_value);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/TranslateEntityProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/TranslateEntityProcessorTest.php
new file mode 100644
index 000000000..7030d2b5b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/TranslateEntityProcessorTest.php
@@ -0,0 +1,253 @@
+languageManager = $this->getMockBuilder(LanguageManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $language = new Language(['langcode' => 'en']);
+ $this->languageManager->expects($this->any())
+ ->method('getCurrentLanguage')
+ ->will($this->returnValue($language));
+
+ // Mock entity type manager.
+ $this->entityTypeManager = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ // Create and set a global container with the language manager and entity
+ // type manager.
+ $container = new ContainerBuilder();
+ $container->set('language_manager', $this->languageManager);
+ $container->set('entity_type.manager', $this->entityTypeManager);
+ \Drupal::setContainer($container);
+ }
+
+ /**
+ * Provides mock data for the tests in this class.
+ *
+ * We create a data definition for both entity reference and entity reference
+ * revision field types so we can test with both the label tranformation.
+ *
+ * @return array
+ * The facet and results test data.
+ */
+ public function facetDataProvider() {
+ $data = [];
+ foreach (['entity_reference', 'entity_reference_revision'] as $field_type) {
+ // Mock the typed data chain.
+ $target_field_definition = $this->createMock(EntityDataDefinition::class);
+ $target_field_definition->expects($this->once())
+ ->method('getEntityTypeId')
+ ->willReturn('entity_type');
+ $property_definition = $this->createMock(DataReferenceDefinitionInterface::class);
+ $property_definition->expects($this->any())
+ ->method('getTargetDefinition')
+ ->willReturn($target_field_definition);
+ $property_definition->expects($this->any())
+ ->method('getDataType')
+ ->willReturn($field_type);
+ $data_definition = $this->createMock(ComplexDataDefinitionInterface::class);
+ $data_definition->expects($this->any())
+ ->method('getPropertyDefinition')
+ ->willReturn($property_definition);
+ $data_definition->expects($this->any())
+ ->method('getPropertyDefinitions')
+ ->willReturn([$property_definition]);
+
+ // Create the actual facet.
+ $facet = $this->getMockBuilder(Facet::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $facet->expects($this->any())
+ ->method('getDataDefinition')
+ ->willReturn($data_definition);
+
+ // Add a field identifier.
+ $facet->expects($this->any())
+ ->method('getFieldIdentifier')
+ ->willReturn('testfield');
+
+ $results = [new Result($facet, 2, 2, 5)];
+ $facet->setResults($results);
+
+ $data[$field_type][] = $facet;
+ $data[$field_type][] = $results;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Tests that node results were correctly changed.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * A facet mock.
+ * @param array $results
+ * The facet original results mock.
+ *
+ * @dataProvider facetDataProvider
+ */
+ public function testNodeResultsChanged(FacetInterface $facet, array $results) {
+ // Mock a node and add the label to it.
+ $node = $this->getMockBuilder(Node::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node->expects($this->any())
+ ->method('label')
+ ->willReturn('shaken not stirred');
+ $nodes = [
+ 2 => $node,
+ ];
+ $node_storage = $this->createMock(EntityStorageInterface::class);
+ $node_storage->expects($this->any())
+ ->method('loadMultiple')
+ ->willReturn($nodes);
+ $this->entityTypeManager->expects($this->exactly(1))
+ ->method('getStorage')
+ ->willReturn($node_storage);
+
+ // Set expected results.
+ $expected_results = [
+ ['nid' => 2, 'title' => 'shaken not stirred'],
+ ];
+
+ // Without the processor we expect the id to display.
+ foreach ($expected_results as $key => $expected) {
+ $this->assertEquals($expected['nid'], $results[$key]->getRawValue());
+ $this->assertEquals($expected['nid'], $results[$key]->getDisplayValue());
+ }
+
+ // With the processor we expect the title to display.
+ /** @var \Drupal\facets\Result\ResultInterface[] $filtered_results */
+ $processor = new TranslateEntityProcessor([], 'translate_entity', [], $this->languageManager, $this->entityTypeManager);
+ $filtered_results = $processor->build($facet, $results);
+ foreach ($expected_results as $key => $expected) {
+ $this->assertEquals($expected['nid'], $filtered_results[$key]->getRawValue());
+ $this->assertEquals($expected['title'], $filtered_results[$key]->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests that term results were correctly changed.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * A facet mock.
+ * @param array $results
+ * The facet original results mock.
+ *
+ * @dataProvider facetDataProvider
+ */
+ public function testTermResultsChanged(FacetInterface $facet, array $results) {
+ // Mock term.
+ $term = $this->getMockBuilder(Term::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $term->expects($this->once())
+ ->method('label')
+ ->willReturn('Burrowing owl');
+ $terms = [
+ 2 => $term,
+ ];
+ $term_storage = $this->createMock(EntityStorageInterface::class);
+ $term_storage->expects($this->any())
+ ->method('loadMultiple')
+ ->willReturn($terms);
+ $this->entityTypeManager->expects($this->exactly(1))
+ ->method('getStorage')
+ ->willReturn($term_storage);
+
+ // Set expected results.
+ $expected_results = [
+ ['tid' => 2, 'name' => 'Burrowing owl'],
+ ];
+
+ // Without the processor we expect the id to display.
+ foreach ($expected_results as $key => $expected) {
+ $this->assertEquals($expected['tid'], $results[$key]->getRawValue());
+ $this->assertEquals($expected['tid'], $results[$key]->getDisplayValue());
+ }
+
+ /** @var \Drupal\facets\Result\ResultInterface[] $filtered_results */
+ $processor = new TranslateEntityProcessor([], 'translate_entity', [], $this->languageManager, $this->entityTypeManager);
+ $filtered_results = $processor->build($facet, $results);
+
+ // With the processor we expect the title to display.
+ foreach ($expected_results as $key => $expected) {
+ $this->assertEquals($expected['tid'], $filtered_results[$key]->getRawValue());
+ $this->assertEquals($expected['name'], $filtered_results[$key]->getDisplayValue());
+ }
+ }
+
+ /**
+ * Test that deleted entities still in index results doesn't display.
+ *
+ * @param \Drupal\facets\FacetInterface $facet
+ * A facet mock.
+ * @param array $results
+ * The facet original results mock.
+ *
+ * @dataProvider facetDataProvider
+ */
+ public function testDeletedEntityResults(FacetInterface $facet, array $results) {
+ // Set original results.
+ $term_storage = $this->createMock(EntityStorageInterface::class);
+ $term_storage->expects($this->any())
+ ->method('loadMultiple')
+ ->willReturn([]);
+ $this->entityTypeManager->expects($this->exactly(1))
+ ->method('getStorage')
+ ->willReturn($term_storage);
+
+ // Processor should return nothing (and not throw an exception).
+ /** @var \Drupal\facets\Result\ResultInterface[] $filtered_results */
+ $processor = new TranslateEntityProcessor([], 'translate_entity', [], $this->languageManager, $this->entityTypeManager);
+ $filtered_results = $processor->build($facet, $results);
+ $this->assertEmpty($filtered_results);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/UidToUserNameCallbackProcessorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/UidToUserNameCallbackProcessorTest.php
new file mode 100644
index 000000000..e6f0503f8
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/UidToUserNameCallbackProcessorTest.php
@@ -0,0 +1,130 @@
+processor = new UidToUserNameCallbackProcessor([], 'uid_to_username_callback', []);
+ }
+
+ /**
+ * Tests that results were correctly changed.
+ */
+ public function testResultsChanged() {
+ $user_storage = $this->createMock(EntityStorageInterface::class);
+ $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
+ $entity_repository = $this->createMock(EntityTypeRepositoryInterface::class);
+ $entity_repository->expects($this->any())
+ ->method('getEntityTypeFromClass')
+ ->willReturn('user');
+ $entity_type_manager->expects($this->any())
+ ->method('getStorage')
+ ->willReturn($user_storage);
+
+ $user1 = $this->createMock(AccountInterface::class);
+ $user1->method('getDisplayName')
+ ->willReturn('Admin');
+
+ $user_storage->method('load')
+ ->willReturn($user1);
+
+ $container = new ContainerBuilder();
+ $container->set('entity_type.repository', $entity_repository);
+ $container->set('entity_type.manager', $entity_type_manager);
+ \Drupal::setContainer($container);
+
+ $facet = new Facet([], 'facets_facet');
+ $original_results = [
+ new Result($facet, 1, 1, 5),
+ ];
+
+ $facet->setResults($original_results);
+
+ $expected_results = [
+ ['uid' => 1, 'name' => 'Admin'],
+ ];
+
+ foreach ($expected_results as $key => $expected) {
+ $this->assertEquals($expected['uid'], $original_results[$key]->getRawValue());
+ $this->assertEquals($expected['uid'], $original_results[$key]->getDisplayValue());
+ }
+
+ $filtered_results = $this->processor->build($facet, $original_results);
+
+ foreach ($expected_results as $key => $expected) {
+ $this->assertEquals($expected['uid'], $filtered_results[$key]->getRawValue());
+ $this->assertEquals($expected['name'], $filtered_results[$key]->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests that deleted entity results were correctly handled.
+ */
+ public function testDeletedEntityResults() {
+ $user_storage = $this->createMock(EntityStorageInterface::class);
+ $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
+ $entity_repository = $this->createMock(EntityTypeRepositoryInterface::class);
+ $entity_repository->expects($this->any())
+ ->method('getEntityTypeFromClass')
+ ->willReturn('user');
+ $entity_type_manager->expects($this->any())
+ ->method('getStorage')
+ ->willReturn($user_storage);
+
+ $user_storage->method('load')
+ ->willReturn(NULL);
+
+ $container = new ContainerBuilder();
+ $container->set('entity_type.repository', $entity_repository);
+ $container->set('entity_type.manager', $entity_type_manager);
+ \Drupal::setContainer($container);
+
+ $facet = new Facet([], 'facets_facet');
+ $original_results = [
+ new Result($facet, 1, 1, 5),
+ ];
+
+ $facet->setResults($original_results);
+
+ $filtered_results = $this->processor->build($facet, $original_results);
+
+ $this->assertEmpty($filtered_results);
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testConfiguration() {
+ $config = $this->processor->defaultConfiguration();
+ $this->assertEquals([], $config);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/UrlProcessorHandlerTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/UrlProcessorHandlerTest.php
new file mode 100644
index 000000000..76ff67c53
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/processor/UrlProcessorHandlerTest.php
@@ -0,0 +1,126 @@
+expectException(InvalidProcessorException::class);
+ $this->expectExceptionMessage("The UrlProcessorHandler doesn't have the required 'facet' in the configuration array.");
+ new UrlProcessorHandler([], 'test', []);
+ }
+
+ /**
+ * Tests that the processor correctly throws an exception.
+ */
+ public function testInvalidProcessorConfiguration() {
+ $this->expectException(InvalidProcessorException::class);
+ $this->expectExceptionMessage("The UrlProcessorHandler doesn't have the required 'facet' in the configuration array.");
+ new UrlProcessorHandler(['facet' => new \stdClass()], 'test', []);
+ }
+
+ /**
+ * Tests that the build method is correctly called.
+ */
+ public function testBuild() {
+ $facet = new Facet(['id' => '_test'], 'facets_facet');
+ $this->createContainer();
+
+ $processor = new UrlProcessorHandler(['facet' => $facet], 'url_processor_handler', []);
+ // The actual results of this should be tested in the actual processor.
+ $processor->build($facet, []);
+ }
+
+ /**
+ * Tests configuration.
+ */
+ public function testConfiguration() {
+ $facet = new Facet([], 'facets_facet');
+ $this->createContainer();
+ $processor = new UrlProcessorHandler(['facet' => $facet], 'url_processor_handler', []);
+
+ $config = $processor->defaultConfiguration();
+ $this->assertEquals([], $config);
+ }
+
+ /**
+ * Tests testDescription().
+ */
+ public function testDescription() {
+ $facet = new Facet([], 'facets_facet');
+ $this->createContainer();
+ $processor = new UrlProcessorHandler(['facet' => $facet], 'url_processor_handler', []);
+
+ $this->assertEquals('', $processor->getDescription());
+ }
+
+ /**
+ * Tests isHidden().
+ */
+ public function testIsHidden() {
+ $facet = new Facet([], 'facets_facet');
+ $this->createContainer();
+ $processor = new UrlProcessorHandler(['facet' => $facet], 'url_processor_handler', []);
+
+ $this->assertEquals(FALSE, $processor->isHidden());
+ }
+
+ /**
+ * Tests isLocked().
+ */
+ public function testIsLocked() {
+ $facet = new Facet([], 'facets_facet');
+ $this->createContainer();
+ $processor = new UrlProcessorHandler(['facet' => $facet], 'url_processor_handler', []);
+
+ $this->assertEquals(FALSE, $processor->isLocked());
+ }
+
+ /**
+ * Sets up a container.
+ */
+ protected function createContainer() {
+ $url_processor = $this->getMockBuilder(UrlProcessorInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $manager = $this->getMockBuilder(FacetSourcePluginManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $manager->expects($this->exactly(1))
+ ->method('createInstance')
+ ->willReturn($url_processor);
+
+ $storage = $this->createMock(EntityStorageInterface::class);
+ $em = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $em->expects($this->exactly(1))
+ ->method('getStorage')
+ ->willReturn($storage);
+
+ $container = new ContainerBuilder();
+ $container->set('entity_type.manager', $em);
+ $container->set('plugin.manager.facets.url_processor', $manager);
+ \Drupal::setContainer($container);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/query_type/SearchApiGranularTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/query_type/SearchApiGranularTest.php
new file mode 100644
index 000000000..b15b2267b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/query_type/SearchApiGranularTest.php
@@ -0,0 +1,185 @@
+ [
+ 'id' => $processor_id,
+ 'class' => GranularItemProcessor::class,
+ ],
+ ];
+
+ $granularityProcessor = new GranularItemProcessor(['granularity' => 10], 'granularity_item', []);
+
+ $processor_manager = $this->prophesize(ProcessorPluginManager::class);
+ $processor_manager->getDefinitions()->willReturn($processor_definitions);
+ $processor_manager->createInstance('granularity_item', Argument::any())
+ ->willReturn($granularityProcessor);
+
+ $container = new ContainerBuilder();
+ $container->set('plugin.manager.facets.processor', $processor_manager->reveal());
+ \Drupal::setContainer($container);
+ }
+
+ /**
+ * Tests string query type without executing the query with an "AND" operator.
+ */
+ public function testQueryTypeAnd() {
+ $backend = $this->prophesize(BackendInterface::class);
+ $backend->getSupportedFeatures()->willReturn([]);
+ $server = $this->prophesize(ServerInterface::class);
+ $server->getBackend()->willReturn($backend);
+ $index = $this->prophesize(IndexInterface::class);
+ $index->getServerInstance()->willReturn($server);
+ $query = $this->prophesize(SearchApiQuery::class);
+ $query->getIndex()->willReturn($index);
+
+ $facet = new Facet(
+ ['query_operator' => 'AND', 'widget' => 'links'],
+ 'facets_facet'
+ );
+ $facet->addProcessor([
+ 'processor_id' => 'granularity_item',
+ 'weights' => [],
+ 'settings' => ['granularity' => 10],
+ ]);
+
+ // Results for the widget.
+ $original_results = [
+ ['count' => 3, 'filter' => '2'],
+ ['count' => 5, 'filter' => '4'],
+ ['count' => 7, 'filter' => '9'],
+ ['count' => 9, 'filter' => '11'],
+ ];
+
+ // Facets the widget should produce.
+ $grouped_results = [
+ 0 => ['count' => 15, 'filter' => '0'],
+ 10 => ['count' => 9, 'filter' => 10],
+ ];
+
+ $query_type = new SearchApiGranular(
+ [
+ 'facet' => $facet,
+ 'query' => $query->reveal(),
+ 'results' => $original_results,
+ ],
+ 'search_api_string',
+ []
+ );
+
+ $built_facet = $query_type->build();
+ $this->assertInstanceOf(FacetInterface::class, $built_facet);
+
+ $results = $built_facet->getResults();
+ $this->assertSame('array', gettype($results));
+
+ foreach ($grouped_results as $k => $result) {
+ $this->assertInstanceOf(ResultInterface::class, $results[$k]);
+ $this->assertEquals($result['count'], $results[$k]->getCount());
+ $this->assertEquals($result['filter'], $results[$k]->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests string query type without results.
+ */
+ public function testEmptyResults() {
+ $query = new SearchApiQuery([], 'search_api_query', []);
+ $facet = new Facet([], 'facets_facet');
+
+ $query_type = new SearchApiGranular(
+ [
+ 'facet' => $facet,
+ 'query' => $query,
+ ],
+ 'search_api_string',
+ []
+ );
+
+ $built_facet = $query_type->build();
+ $this->assertInstanceOf(FacetInterface::class, $built_facet);
+
+ $results = $built_facet->getResults();
+ $this->assertSame('array', gettype($results));
+ $this->assertEmpty($results);
+ }
+
+ /**
+ * Tests the calculateResultFilter method.
+ *
+ * @dataProvider provideDataForCalculateResultFilter
+ */
+ public function testCalculateResultFilter($input, $expected_result) {
+ $query = new SearchApiQuery([], 'search_api_query', []);
+ $facet = new Facet(
+ ['query_operator' => 'AND', 'widget' => 'links'],
+ 'facets_facet'
+ );
+ $facet->addProcessor([
+ 'processor_id' => 'granularity_item',
+ 'weights' => [],
+ 'settings' => [],
+ ]);
+ $facet->getProcessors()['granularity_item']->setConfiguration([
+ 'granularity' => 3,
+ 'min_value' => 5,
+ 'max_value' => 15,
+ ]);
+
+ $query_type = new SearchApiGranular(
+ [
+ 'facet' => $facet,
+ 'query' => $query,
+ ],
+ 'search_api_string',
+ []
+ );
+
+ $result = $query_type->calculateResultFilter($input);
+ $this->assertSame($expected_result, $result);
+ }
+
+ /**
+ * Provides testdata.
+ *
+ * @return array
+ * Test data.
+ */
+ public function provideDataForCalculateResultFilter() {
+ return [
+ 'normal' => [10, ['display' => 8.0, 'raw' => 8.0]],
+ 'under_min' => [4, FALSE],
+ 'over_max' => [20, FALSE],
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/query_type/SearchApiStringTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/query_type/SearchApiStringTest.php
new file mode 100644
index 000000000..3c62b3883
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/query_type/SearchApiStringTest.php
@@ -0,0 +1,190 @@
+ 'and'],
+ 'facets_facet'
+ );
+
+ $original_results = [
+ ['count' => 3, 'filter' => 'badger'],
+ ['count' => 5, 'filter' => 'mushroom'],
+ ['count' => 7, 'filter' => 'narwhal'],
+ ['count' => 9, 'filter' => 'unicorn'],
+ ];
+
+ $query_type = new SearchApiString(
+ [
+ 'facet' => $facet,
+ 'query' => $query,
+ 'results' => $original_results,
+ ],
+ 'search_api_string',
+ []
+ );
+
+ $built_facet = $query_type->build();
+ $this->assertInstanceOf(FacetInterface::class, $built_facet);
+
+ $results = $built_facet->getResults();
+ $this->assertSame('array', gettype($results));
+
+ foreach ($original_results as $k => $result) {
+ $this->assertInstanceOf(ResultInterface::class, $results[$k]);
+ $this->assertEquals($result['count'], $results[$k]->getCount());
+ $this->assertEquals($result['filter'], $results[$k]->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests string query type without executing the query with an "OR" operator.
+ */
+ public function testQueryTypeOr() {
+ $query = new SearchApiQuery([], 'search_api_query', []);
+ $facet = new Facet(
+ ['query_operator' => 'or'],
+ 'facets_facet'
+ );
+ $facet->setFieldIdentifier('field_animal');
+
+ $original_results = [
+ ['count' => 3, 'filter' => 'badger'],
+ ['count' => 5, 'filter' => 'mushroom'],
+ ['count' => 7, 'filter' => 'narwhal'],
+ ['count' => 9, 'filter' => 'unicorn'],
+ ];
+
+ $query_type = new SearchApiString(
+ [
+ 'facet' => $facet,
+ 'query' => $query,
+ 'results' => $original_results,
+ ],
+ 'search_api_string',
+ []
+ );
+
+ $built_facet = $query_type->build();
+ $this->assertInstanceOf(FacetInterface::class, $built_facet);
+
+ $results = $built_facet->getResults();
+ $this->assertSame('array', gettype($results));
+
+ foreach ($original_results as $k => $result) {
+ $this->assertInstanceOf(ResultInterface::class, $results[$k]);
+ $this->assertEquals($result['count'], $results[$k]->getCount());
+ $this->assertEquals($result['filter'], $results[$k]->getDisplayValue());
+ }
+ }
+
+ /**
+ * Tests string query type without results.
+ */
+ public function testEmptyResults() {
+ $query = new SearchApiQuery([], 'search_api_query', []);
+ $facet = new Facet([], 'facets_facet');
+
+ $query_type = new SearchApiString(
+ [
+ 'facet' => $facet,
+ 'query' => $query,
+ ],
+ 'search_api_string',
+ []
+ );
+
+ $built_facet = $query_type->build();
+ $this->assertInstanceOf(FacetInterface::class, $built_facet);
+
+ $results = $built_facet->getResults();
+ $this->assertSame('array', gettype($results));
+ $this->assertEmpty($results);
+ }
+
+ /**
+ * Tests string query type without results.
+ */
+ public function testConfiguration() {
+ $query = new SearchApiQuery([], 'search_api_query', []);
+ $facet = new Facet([], 'facets_facet');
+
+ $default_config = ['facet' => $facet, 'query' => $query];
+ $query_type = new SearchApiString($default_config, 'search_api_string', []);
+
+ $this->assertEquals([], $query_type->defaultConfiguration());
+ $this->assertEquals($default_config, $query_type->getConfiguration());
+
+ $query_type->setConfiguration(['owl' => 'Long-eared owl']);
+ $this->assertEquals(['owl' => 'Long-eared owl'], $query_type->getConfiguration());
+ }
+
+ /**
+ * Tests trimming in ::build.
+ *
+ * @dataProvider provideTrimValues
+ */
+ public function testTrim($expected_value, $input_value) {
+ $query = new SearchApiQuery([], 'search_api_query', []);
+ $facet = new Facet([], 'facets_facet');
+
+ $original_results = [['count' => 1, 'filter' => $input_value]];
+
+ $query_type = new SearchApiString(
+ [
+ 'facet' => $facet,
+ 'query' => $query,
+ 'results' => $original_results,
+ ],
+ 'search_api_string',
+ []
+ );
+
+ $built_facet = $query_type->build();
+ $this->assertInstanceOf(FacetInterface::class, $built_facet);
+
+ $results = $built_facet->getResults();
+ $this->assertSame('array', gettype($results));
+
+ $this->assertInstanceOf(ResultInterface::class, $results[0]);
+ $this->assertEquals(1, $results[0]->getCount());
+ $this->assertEquals($expected_value, $results[0]->getDisplayValue());
+ }
+
+ /**
+ * Data provider for ::provideTrimValues.
+ *
+ * @return array
+ * An array of expected and input values.
+ */
+ public function provideTrimValues() {
+ return [
+ ['owl', '"owl"'],
+ ['owl', 'owl'],
+ ['owl', '"owl'],
+ ['owl', 'owl"'],
+ ['"owl', '""owl"'],
+ ['owl"', '"owl""'],
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/url_processor/QueryStringTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/url_processor/QueryStringTest.php
new file mode 100644
index 000000000..6e5ecdef5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/url_processor/QueryStringTest.php
@@ -0,0 +1,472 @@
+eventDispatcher = $this->createMock(EventDispatcherInterface::class);
+
+ $facet = new Facet([], 'facets_facet');
+ $this->originalResults = [
+ new Result($facet, 'llama', 'Llama', 15),
+ new Result($facet, 'badger', 'Badger', 5),
+ new Result($facet, 'mushroom', 'Mushroom', 5),
+ new Result($facet, 'duck', 'Duck', 15),
+ new Result($facet, 'alpaca', 'Alpaca', 25),
+ ];
+
+ $this->setContainer();
+ }
+
+ /**
+ * Tests that the processor correctly throws an exception.
+ */
+ public function testEmptyProcessorConfiguration() {
+ $this->expectException(InvalidProcessorException::class);
+ $this->expectExceptionMessage("The url processor doesn't have the required 'facet' in the configuration array.");
+ new QueryString([], 'test', [], new Request(), $this->entityManager, $this->eventDispatcher);
+ }
+
+ /**
+ * Tests with one active item.
+ */
+ public function testSetSingleActiveItem() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->setUrlAlias('test');
+ $facet->setFieldIdentifier('test');
+
+ $discovery_property = new \ReflectionProperty($facet, 'id');
+ $discovery_property->setAccessible(TRUE);
+ $discovery_property->setValue($facet, 'test');
+
+ $storage = $this->createMock(EntityStorageInterface::class);
+ $storage->expects($this->any())
+ ->method('loadByProperties')
+ ->willReturn([$facet]);
+ $entityTypeManager = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $entityTypeManager->expects($this->any())
+ ->method('getStorage')
+ ->willReturn($storage);
+
+ $container = \Drupal::getContainer();
+ $container->set('entity_type.manager', $entityTypeManager);
+ \Drupal::setContainer($container);
+
+ $request = new Request();
+ $request->query->set('f', ['test:badger']);
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], $request, $entityTypeManager, $this->eventDispatcher);
+ $this->processor->setActiveItems($facet);
+
+ $this->assertEquals(['badger'], $facet->getActiveItems());
+ }
+
+ /**
+ * Tests with multiple active items.
+ */
+ public function testSetMultipleActiveItems() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->setUrlAlias('test');
+ $facet->setFieldIdentifier('test');
+
+ $discovery_property = new \ReflectionProperty($facet, 'id');
+ $discovery_property->setAccessible(TRUE);
+ $discovery_property->setValue($facet, 'test');
+
+ $storage = $this->createMock(EntityStorageInterface::class);
+ $storage->expects($this->atLeastOnce())
+ ->method('loadByProperties')
+ ->willReturnOnConsecutiveCalls([$facet], [$facet], []);
+ $entityTypeManager = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $entityTypeManager->expects($this->any())
+ ->method('getStorage')
+ ->willReturn($storage);
+
+ $container = \Drupal::getContainer();
+ $container->set('entity_type.manager', $entityTypeManager);
+ \Drupal::setContainer($container);
+
+ $request = new Request();
+ $request->query->set('f', ['test:badger', 'test:mushroom', 'donkey:kong']);
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], $request, $entityTypeManager, $this->eventDispatcher);
+ $this->processor->setActiveItems($facet);
+
+ $this->assertEquals(['badger', 'mushroom'], $facet->getActiveItems());
+ }
+
+ /**
+ * Tests with an empty build.
+ */
+ public function testEmptyBuild() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setUrlAlias('test');
+ $facet->setFacetSourceId('facet_source__dummy');
+
+ $request = new Request();
+ $request->query->set('f', []);
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], $request, $this->entityManager, $this->eventDispatcher);
+ $results = $this->processor->buildUrls($facet, []);
+ $this->assertEmpty($results);
+ }
+
+ /**
+ * Tests with default build.
+ */
+ public function testBuild() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setFieldIdentifier('test');
+ $facet->setUrlAlias('test');
+ $facet->setFacetSourceId('facet_source__dummy');
+
+ $request = new Request();
+ $request->query->set('f', []);
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], $request, $this->entityManager, $this->eventDispatcher);
+ $results = $this->processor->buildUrls($facet, $this->originalResults);
+
+ $this->assertEquals('f', $this->processor->getFilterKey());
+
+ /** @var \Drupal\facets\Result\ResultInterface $r */
+ foreach ($results as $r) {
+ $this->assertInstanceOf(ResultInterface::class, $r);
+ $this->assertEquals('route:test?f%5B0%5D=test%3A' . $r->getRawValue(), $r->getUrl()->toUriString());
+ }
+ }
+
+ /**
+ * Tests with an active item already from url.
+ */
+ public function testBuildWithActiveItem() {
+ $facet = new Facet(['id' => 'facet_1'], 'facets_facet');
+ $facet->setFieldIdentifier('test');
+ $facet->setUrlAlias('test');
+ $facet->setFacetSourceId('facet_source__dummy');
+ $facet2 = new Facet(['id' => 'facet_2'], 'facets_facet');
+ $facet2->setFieldIdentifier('king');
+ $facet2->setUrlAlias('king');
+ $facet2->setFacetSourceId('facet_source__dummy');
+
+ $discovery_property = new \ReflectionProperty($facet, 'id');
+ $discovery_property->setAccessible(TRUE);
+ $discovery_property->setValue($facet, 'test');
+
+ $storage = $this->createMock(EntityStorageInterface::class);
+ $storage->expects($this->atLeastOnce())
+ ->method('loadByProperties')
+ ->willReturnOnConsecutiveCalls([$facet2], [$facet2], [$facet2], [$facet2], [$facet2], [$facet2]);
+ $entityTypeManager = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $entityTypeManager->expects($this->any())
+ ->method('getStorage')
+ ->willReturn($storage);
+
+ $container = \Drupal::getContainer();
+ $container->set('entity_type.manager', $entityTypeManager);
+ \Drupal::setContainer($container);
+
+ $original_results = $this->originalResults;
+ $original_results[2]->setActiveState(TRUE);
+
+ $request = new Request();
+ $request->query->set('f', ['king:kong']);
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], $request, $entityTypeManager, $this->eventDispatcher);
+ $results = $this->processor->buildUrls($facet, $original_results);
+
+ /** @var \Drupal\facets\Result\ResultInterface $r */
+ foreach ($results as $k => $r) {
+ $this->assertInstanceOf(ResultInterface::class, $r);
+ if ($k === 2) {
+ $this->assertEquals('route:test?f%5B0%5D=king%3Akong', $r->getUrl()->toUriString());
+ }
+ else {
+ $this->assertEquals('route:test?f%5B0%5D=king%3Akong&f%5B1%5D=test%3A' . $r->getRawValue(), $r->getUrl()->toUriString());
+ }
+ }
+ }
+
+ /**
+ * Tests with only one result.
+ */
+ public function testWithOnlyOneResult() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setFieldIdentifier('test');
+ $facet->setUrlAlias('test');
+ $facet->setFacetSourceId('facet_source__dummy');
+ $facet->setShowOnlyOneResult(TRUE);
+
+ $this->originalResults[1]->setActiveState(TRUE);
+ $this->originalResults[2]->setActiveState(TRUE);
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], new Request(), $this->entityManager, $this->eventDispatcher);
+ $results = $this->processor->buildUrls($facet, $this->originalResults);
+
+ $this->assertEquals('route:test?f%5B0%5D=test%3A' . $results[0]->getRawValue(), $results[0]->getUrl()->toUriString());
+ $this->assertEquals('route:test?f%5B0%5D=test%3A' . $results[3]->getRawValue(), $results[3]->getUrl()->toUriString());
+ $this->assertEquals('route:test?f%5B0%5D=test%3A' . $results[4]->getRawValue(), $results[4]->getUrl()->toUriString());
+ $this->assertEquals('route:test', $results[1]->getUrl()->toUriString());
+ $this->assertEquals('route:test', $results[2]->getUrl()->toUriString());
+ }
+
+ /**
+ * Tests that the facet source configuration filter key override works.
+ */
+ public function testFacetSourceFilterKeyOverride() {
+ $facet_source = new FacetSource(['filter_key' => 'ab'], 'facets_facet_source');
+
+ // Override the container with the new facet source.
+ $storage = $this->createMock(EntityStorageInterface::class);
+ $storage->expects($this->once())
+ ->method('load')
+ ->willReturn($facet_source);
+ $em = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $em->expects($this->any())
+ ->method('getStorage')
+ ->willReturn($storage);
+
+ $container = \Drupal::getContainer();
+ $container->set('entity_type.manager', $em);
+ \Drupal::setContainer($container);
+
+ $facet = new Facet([], 'facets_facet');
+ $facet->setFieldIdentifier('test');
+ $facet->setFacetSourceId('facet_source__dummy');
+ $facet->setUrlAlias('test');
+
+ $request = new Request();
+ $request->query->set('ab', []);
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], $request, $this->entityManager, $this->eventDispatcher);
+ $results = $this->processor->buildUrls($facet, $this->originalResults);
+
+ /** @var \Drupal\facets\Result\ResultInterface $r */
+ foreach ($results as $r) {
+ $this->assertInstanceOf(ResultInterface::class, $r);
+ $this->assertEquals('route:test?ab%5B0%5D=test%3A' . $r->getRawValue(), $r->getUrl()->toUriString());
+ }
+ }
+
+ /**
+ * Tests that the separator works as expected.
+ */
+ public function testSeparator() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setFieldIdentifier('test');
+ $facet->setUrlAlias('test');
+ $facet->setFacetSourceId('facet_source__dummy');
+
+ $this->processor = new QueryString(['facet' => $facet, 'separator' => '__'], 'query_string', [], new Request(), $this->entityManager, $this->eventDispatcher);
+ $results = $this->processor->buildUrls($facet, $this->originalResults);
+
+ foreach ($results as $result) {
+ $this->assertEquals('route:test?f%5B0%5D=test__' . $result->getRawValue(), $result->getUrl()->toUriString());
+ }
+ }
+
+ /**
+ * Tests that contextual filter get's re-added.
+ */
+ public function testContextualFilters() {
+ // Override router.
+ $router = $this->getMockBuilder(TestRouterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $router->expects($this->any())
+ ->method('matchRequest')
+ ->willReturn([
+ '_raw_variables' => new ParameterBag(['node' => '1']),
+ '_route' => 'node_view',
+ ]);
+
+ // Get the container from the setUp method and change it with the
+ // implementation created here, that has the route parameters.
+ $container = \Drupal::getContainer();
+ $container->set('router.no_access_checks', $router);
+ \Drupal::setContainer($container);
+
+ // Create facet.
+ $facet = new Facet([], 'facets_facet');
+ $facet->setFieldIdentifier('test');
+ $facet->setUrlAlias('test');
+ $facet->setFacetSourceId('facet_source__dummy');
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], new Request(), $this->entityManager, $this->eventDispatcher);
+ $results = $this->processor->buildUrls($facet, $this->originalResults);
+
+ foreach ($results as $result) {
+ $this->assertEquals(['node' => 1], $result->getUrl()->getRouteParameters());
+ }
+ }
+
+ /**
+ * Tests that unrouted paths can be handled properly.
+ */
+ public function testUnroutedPath() {
+ // Override router.
+ $router = $this->getMockBuilder(TestRouterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $router->expects($this->any())
+ ->method('matchRequest')
+ ->willThrowException(new ResourceNotFoundException());
+
+ // Get the container from the setUp method and change it with the
+ // implementation created here, that has the route parameters.
+ $container = \Drupal::getContainer();
+ $container->set('router.no_access_checks', $router);
+ \Drupal::setContainer($container);
+
+ // Create facet.
+ $facet = new Facet([], 'facets_facet');
+ $facet->setFieldIdentifier('test');
+ $facet->setUrlAlias('test');
+ $facet->setFacetSourceId('facet_source__dummy');
+
+ $this->processor = new QueryString(['facet' => $facet], 'query_string', [], new Request(), $this->entityManager, $this->eventDispatcher);
+
+ $results = $this->processor->buildUrls($facet, $this->originalResults);
+
+ foreach ($results as $result) {
+ $this->assertEquals('base:test', $result->getUrl()->getUri());
+ }
+ }
+
+ /**
+ * Sets up a container.
+ */
+ protected function setContainer() {
+ $router = $this->getMockBuilder(TestRouterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $router->expects($this->any())
+ ->method('matchRequest')
+ ->willReturn([
+ '_raw_variables' => new ParameterBag([]),
+ '_route' => 'test',
+ ]);
+
+ $validator = $this->createMock(PathValidatorInterface::class);
+
+ $fsi = $this->getMockBuilder(FacetSourcePluginInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $fsi->method('getPath')
+ ->willReturn('/test');
+
+ $manager = $this->getMockBuilder(FacetSourcePluginManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $manager->method('createInstance')
+ ->willReturn($fsi);
+ $manager->method('hasDefinition')
+ ->with('facet_source__dummy')
+ ->willReturn(TRUE);
+
+ $facetentity = $this->getMockBuilder(Facet::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $facetentity->method('id')
+ ->willReturn('king');
+
+ $storage = $this->createMock(EntityStorageInterface::class);
+ $storage->expects($this->any())
+ ->method('loadByProperties')
+ ->willReturn([$facetentity]);
+ $em = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $em->expects($this->any())
+ ->method('getStorage')
+ ->willReturn($storage);
+ $this->entityManager = $em;
+
+ $container = new ContainerBuilder();
+ $container->set('router.no_access_checks', $router);
+ $container->set('plugin.manager.facets.facet_source', $manager);
+ $container->set('entity_type.manager', $em);
+ $container->set('path.validator', $validator);
+ \Drupal::setContainer($container);
+ }
+
+}
+
+namespace Drupal\facets\Plugin\facets\url_processor;
+
+/**
+ * Mocks the usage of drupal static.
+ *
+ * @see \drupal_static
+ */
+function &drupal_static($name, $default_value = NULL, $reset = FALSE) {
+ $data = [];
+ return $data;
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/ArrayWidgetTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/ArrayWidgetTest.php
new file mode 100644
index 000000000..d45deb8d8
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/ArrayWidgetTest.php
@@ -0,0 +1,299 @@
+widget = new ArrayWidget(['show_numbers' => 1], 'array_widget', []);
+ }
+
+ /**
+ * Tests widget without filters.
+ */
+ public function testNoFilterResults() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+ $facet->setFieldIdentifier('tag');
+
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['tag']);
+
+ $expected_links = [
+ [
+ 'url' => NULL,
+ 'raw_value' => 'llama',
+ 'values' => ['value' => 'Llama', 'count' => 10],
+ ],
+ [
+ 'url' => NULL,
+ 'raw_value' => 'badger',
+ 'values' => ['value' => 'Badger', 'count' => 20],
+ ],
+ [
+ 'url' => NULL,
+ 'raw_value' => 'duck',
+ 'values' => ['value' => 'Duck', 'count' => 15],
+ ],
+ [
+ 'url' => NULL,
+ 'raw_value' => 'alpaca',
+ 'values' => ['value' => 'Alpaca', 'count' => 9],
+ ],
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['tag'][$index]));
+ $this->assertSame($value, $output['tag'][$index]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+ $expected = [
+ 'show_numbers' => FALSE,
+ ];
+ $this->assertEquals($expected, $default_config);
+ }
+
+ /**
+ * Tests ArrayWidget build with deep nested results.
+ */
+ public function testNesting(): void {
+ $results_data = [
+ '1' => [
+ '1.1' => [
+ '1.1.1' => [
+ '1.1.1.1' => [
+ '1.1.1.1.1' => [],
+ '1.1.1.1.2' => [],
+ ],
+ ],
+ ],
+ '1.2' => [],
+ '1.3' => [
+ '1.3.1' => [],
+ ],
+ ],
+ '2' => [],
+ '3' => [
+ '3.1' => [],
+ ],
+ ];
+
+ $expected_build = [
+ [
+ 'url' => 'http://example.com/1',
+ 'raw_value' => '1',
+ 'values' => [
+ 'value' => 'One',
+ 'count' => 1,
+ ],
+ 'children' => [
+ [
+ [
+ 'url' => 'http://example.com/1.1',
+ 'raw_value' => '1.1',
+ 'values' => [
+ 'value' => 'One.One',
+ 'count' => 11,
+ ],
+ 'children' => [
+ [
+ [
+ 'url' => 'http://example.com/1.1.1',
+ 'raw_value' => '1.1.1',
+ 'values' => [
+ 'value' => 'One.One.One',
+ 'count' => 111,
+ ],
+ 'children' => [
+ [
+ [
+ 'url' => 'http://example.com/1.1.1.1',
+ 'raw_value' => '1.1.1.1',
+ 'values' => [
+ 'value' => 'One.One.One.One',
+ 'count' => 1111,
+ ],
+ 'children' => [
+ [
+ [
+ 'url' => 'http://example.com/1.1.1.1.1',
+ 'raw_value' => '1.1.1.1.1',
+ 'values' => [
+ 'value' => 'One.One.One.One.One',
+ 'count' => 11111,
+ ],
+ ],
+ [
+ 'url' => 'http://example.com/1.1.1.1.2',
+ 'raw_value' => '1.1.1.1.2',
+ 'values' => [
+ 'value' => 'One.One.One.One.Two',
+ 'count' => 11112,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'url' => 'http://example.com/1.2',
+ 'raw_value' => '1.2',
+ 'values' => [
+ 'value' => 'One.Two',
+ 'count' => 12,
+ ],
+ ],
+ [
+ 'url' => 'http://example.com/1.3',
+ 'raw_value' => '1.3',
+ 'values' => [
+ 'value' => 'One.Three',
+ 'count' => 13,
+ ],
+ 'children' => [
+ [
+ [
+ 'url' => 'http://example.com/1.3.1',
+ 'raw_value' => '1.3.1',
+ 'values' => [
+ 'value' => 'One.Three.One',
+ 'count' => 131,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'url' => 'http://example.com/2',
+ 'raw_value' => '2',
+ 'values' => [
+ 'value' => 'Two',
+ 'count' => 2,
+ ],
+ ],
+ [
+ 'url' => 'http://example.com/3',
+ 'raw_value' => '3',
+ 'values' => [
+ 'value' => 'Three',
+ 'count' => 3,
+ ],
+ 'children' => [
+ [
+ [
+ 'url' => 'http://example.com/3.1',
+ 'raw_value' => '3.1',
+ 'values' => [
+ 'value' => 'Three.One',
+ 'count' => 31,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ $this->facet->setResults($this->buildResults($results_data));
+ $this->facet->setFieldIdentifier('tag');
+
+ $this->assertSame($expected_build, $this->widget->build($this->facet)['tag']);
+ }
+
+ /**
+ * Builds a list of deep nested results.
+ *
+ * @param array $children
+ * Result data.
+ *
+ * @return \Drupal\facets\Result\ResultInterface[]
+ * A list of nested results.
+ */
+ protected function buildResults(array $children): array {
+ $results = [];
+ foreach ($children as $value => $child) {
+ $has_children = !empty($child);
+ $value = (string) $value;
+ $display_value = str_replace(['1', '2', '3'], ['One', 'Two', 'Three'], $value);
+ $count = (int) str_replace('.', '', $value);
+ $result = new Result($this->facet, $value, $display_value, $count);
+ $result->setUrl(TestUrl::fromUri("http://example.com/{$value}"));
+ if ($has_children) {
+ $result->setChildren($this->buildResults($child));
+ }
+ $results[] = $result;
+ }
+ return $results;
+ }
+
+}
+
+/**
+ * Mocks \Drupal\Core\Url.
+ */
+class TestUrl extends Url {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $uri;
+
+ /**
+ * Constructs a new URL instance.
+ *
+ * @param string $uri
+ * The URI.
+ */
+ public function __construct(string $uri) {
+ $this->uri = $uri;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function fromUri($uri, $options = []) {
+ return new static($uri);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toString($collect_bubbleable_metadata = FALSE) {
+ return $this->uri;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/CheckboxWidgetTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/CheckboxWidgetTest.php
new file mode 100644
index 000000000..157fd19b3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/CheckboxWidgetTest.php
@@ -0,0 +1,70 @@
+widget = new CheckboxWidget(['show_numbers' => TRUE], 'checkbox_widget', []);
+ }
+
+ /**
+ * Tests widget without filters.
+ */
+ public function testNoFilterResults() {
+ $facet = $this->facet;
+ $facet->setResults($this->originalResults);
+
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $this->assertEquals(['facet-inactive', 'js-facets-checkbox-links'], $output['#attributes']['class']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertSame('array', gettype($output['#items'][$index]['#title']));
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+ $this->assertArrayHasKey('show_numbers', $default_config);
+ $this->assertArrayHasKey('soft_limit', $default_config);
+ $this->assertArrayHasKey('show_reset_link', $default_config);
+ $this->assertArrayHasKey('reset_text', $default_config);
+ $this->assertArrayHasKey('soft_limit_settings', $default_config);
+ $this->assertArrayHasKey('show_less_label', $default_config['soft_limit_settings']);
+ $this->assertArrayHasKey('show_more_label', $default_config['soft_limit_settings']);
+
+ $this->assertEquals(FALSE, $default_config['show_numbers']);
+ $this->assertEquals(0, $default_config['soft_limit']);
+ $this->assertEquals(FALSE, $default_config['show_reset_link']);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/DropdownWidgetTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/DropdownWidgetTest.php
new file mode 100644
index 000000000..ea54ffad5
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/DropdownWidgetTest.php
@@ -0,0 +1,65 @@
+widget = new DropdownWidget(['show_numbers' => TRUE], 'dropdown_widget', []);
+ }
+
+ /**
+ * Tests widget without filters.
+ */
+ public function testNoFilterResults() {
+ $facet = $this->facet;
+ $facet->setResults($this->originalResults);
+
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $this->assertEquals(['facet-inactive', 'js-facets-dropdown-links'], $output['#attributes']['class']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertSame('array', gettype($output['#items'][$index]['#title']));
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+
+ // We can't use $this->assertEquals() because that makes mocking here too
+ // hard, that way we'd need to also mock the translation interface. That's
+ // not needed.
+ $this->assertArrayHasKey('show_numbers', $default_config);
+ $this->assertArrayHasKey('default_option_label', $default_config);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/LinksWidgetTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/LinksWidgetTest.php
new file mode 100644
index 000000000..e344e282d
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/LinksWidgetTest.php
@@ -0,0 +1,283 @@
+widget = new LinksWidget([], 'links_widget', []);
+ }
+
+ /**
+ * Tests widget without filters.
+ */
+ public function testNoFilterResults() {
+ $facet = $this->facet;
+ $facet->setResults($this->originalResults);
+
+ $this->widget->setConfiguration(['show_numbers' => TRUE]);
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertSame('array', gettype($output['#items'][$index]['#title']));
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * Test widget with 2 active items.
+ */
+ public function testActiveItems() {
+ $original_results = $this->originalResults;
+ $original_results[0]->setActiveState(TRUE);
+ $original_results[3]->setActiveState(TRUE);
+
+ $facet = $this->facet;
+ $facet->setResults($original_results);
+
+ $this->widget->setConfiguration(['show_numbers' => TRUE]);
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10, TRUE),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9, TRUE),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ if ($index === 0 || $index === 3) {
+ $this->assertEquals(['is-active'], $output['#items'][$index]['#attributes']['class']);
+ }
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * Tests widget, make sure hiding and showing numbers works.
+ */
+ public function testHideNumbers() {
+ $original_results = $this->originalResults;
+ $original_results[1]->setActiveState(TRUE);
+
+ $facet = $this->facet;
+ $facet->setResults($original_results);
+
+ $this->widget->setConfiguration(['show_numbers' => FALSE]);
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10, FALSE, FALSE),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20, TRUE, FALSE),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15, FALSE, FALSE),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9, FALSE, FALSE),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ if ($index === 1) {
+ $this->assertEquals(['is-active'], $output['#items'][$index]['#attributes']['class']);
+ }
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+
+ // Enable the 'show_numbers' setting again to make sure that the switch
+ // between those settings works.
+ $this->widget->setConfiguration(['show_numbers' => TRUE]);
+
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20, TRUE),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ if ($index === 1) {
+ $this->assertEquals(['is-active'], $output['#items'][$index]['#attributes']['class']);
+ }
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+
+ /**
+ * Tests for links widget with children.
+ */
+ public function testChildren() {
+ $original_results = $this->originalResults;
+
+ $facet = $this->facet;
+ $child = new Result($facet, 'snake', 'Snake', 5);
+ $original_results[1]->setActiveState(TRUE);
+ $original_results[1]->setChildren([$child]);
+
+ $facet->setResults($original_results);
+
+ $this->widget->setConfiguration(['show_numbers' => TRUE]);
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $expected_links = [
+ $this->buildLinkAssertion('Llama', 'llama', $facet, 10),
+ $this->buildLinkAssertion('Badger', 'badger', $facet, 20, TRUE),
+ $this->buildLinkAssertion('Duck', 'duck', $facet, 15),
+ $this->buildLinkAssertion('Alpaca', 'alpaca', $facet, 9),
+ ];
+ foreach ($expected_links as $index => $value) {
+ $this->assertSame('array', gettype($output['#items'][$index]));
+ $this->assertEquals($value, $output['#items'][$index]['#title']);
+ $this->assertEquals('link', $output['#items'][$index]['#type']);
+ if ($index === 1) {
+ $this->assertEquals(['is-active'], $output['#items'][$index]['#attributes']['class']);
+ $this->assertEquals(['facet-item', 'facet-item--expanded'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ else {
+ $this->assertEquals(['facet-item'], $output['#items'][$index]['#wrapper_attributes']['class']);
+ }
+ }
+ }
+
+ /**
+ * Tests the rest link.
+ */
+ public function testResetLink() {
+ $facet = new Facet([], 'facets_facet');
+ $facet->setResults($this->originalResults);
+
+ $output = $this->widget->build($facet);
+
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(4, $output['#items']);
+
+ $request = new Request();
+ $request->query->set('f', []);
+
+ $request_stack = new RequestStack();
+ $request_stack->push($request);
+
+ $this->createContainer();
+ $container = \Drupal::getContainer();
+ $container->set('request_stack', $request_stack);
+ \Drupal::setContainer($container);
+
+ // Enable the show reset link.
+ $this->widget->setConfiguration(['show_reset_link' => TRUE]);
+ $output = $this->widget->build($facet);
+
+ // Check that we now have more results.
+ $this->assertSame('array', gettype($output));
+ $this->assertCount(5, $output['#items']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+ $this->assertArrayHasKey('show_numbers', $default_config);
+ $this->assertArrayHasKey('soft_limit', $default_config);
+ $this->assertArrayHasKey('show_reset_link', $default_config);
+ $this->assertArrayHasKey('reset_text', $default_config);
+ $this->assertArrayHasKey('soft_limit_settings', $default_config);
+ $this->assertArrayHasKey('show_less_label', $default_config['soft_limit_settings']);
+ $this->assertArrayHasKey('show_more_label', $default_config['soft_limit_settings']);
+
+ $this->assertEquals(FALSE, $default_config['show_numbers']);
+ $this->assertEquals(0, $default_config['soft_limit']);
+ $this->assertEquals(FALSE, $default_config['show_reset_link']);
+ }
+
+ /**
+ * Sets up a container.
+ */
+ protected function createContainer() {
+ $router = $this->getMockBuilder(TestRouterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $router->expects($this->any())
+ ->method('matchRequest')
+ ->willReturn([
+ '_raw_variables' => new ParameterBag([]),
+ '_route' => 'test',
+ ]);
+
+ $url_processor = $this->getMockBuilder(UrlProcessorInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $manager = $this->getMockBuilder(FacetSourcePluginManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $manager->expects($this->exactly(1))
+ ->method('createInstance')
+ ->willReturn($url_processor);
+
+ $storage = $this->createMock(EntityStorageInterface::class);
+ $em = $this->getMockBuilder(EntityTypeManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $em->expects($this->exactly(1))
+ ->method('getStorage')
+ ->willReturn($storage);
+
+ $container = new ContainerBuilder();
+ $container->set('router.no_access_checks', $router);
+ $container->set('entity_type.manager', $em);
+ $container->set('plugin.manager.facets.url_processor', $manager);
+ \Drupal::setContainer($container);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/WidgetTestBase.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/WidgetTestBase.php
new file mode 100644
index 000000000..927785d14
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Plugin/widget/WidgetTestBase.php
@@ -0,0 +1,142 @@
+facet = $facet;
+ /** @var \Drupal\facets\Result\Result[] $original_results */
+ $original_results = [
+ new Result($facet, 'llama', 'Llama', 10),
+ new Result($facet, 'badger', 'Badger', 20),
+ new Result($facet, 'duck', 'Duck', 15),
+ new Result($facet, 'alpaca', 'Alpaca', 9),
+ ];
+
+ foreach ($original_results as $original_result) {
+ $original_result->setUrl(new Url('test'));
+ }
+ $this->originalResults = $original_results;
+
+ // Create a container, so we can access string translation.
+ $string_translation = $this->prophesize(TranslationInterface::class);
+ $url_generator = $this->prophesize(UrlGeneratorInterface::class);
+ $widget_manager = $this->prophesize(WidgetPluginManager::class);
+
+ $container = new ContainerBuilder();
+ $container->set('plugin.manager.facets.widget', $widget_manager->reveal());
+ $container->set('string_translation', $string_translation->reveal());
+ $container->set('url_generator', $url_generator->reveal());
+ \Drupal::setContainer($container);
+
+ $this->queryTypes = [
+ 'date' => 'date',
+ 'string' => 'string',
+ 'numeric' => 'numeric',
+ 'range' => 'range',
+ ];
+ }
+
+ /**
+ * Tests default configuration.
+ */
+ public function testDefaultConfiguration() {
+ $default_config = $this->widget->defaultConfiguration();
+ $this->assertEquals(['show_numbers' => FALSE, 'soft_limit' => 0], $default_config);
+ }
+
+ /**
+ * Tests get query type.
+ */
+ public function testGetQueryType() {
+ $result = $this->widget->getQueryType($this->queryTypes);
+ $this->assertEquals(NULL, $result);
+ }
+
+ /**
+ * Tests default for required properties.
+ */
+ public function testIsPropertyRequired() {
+ $this->assertFalse($this->widget->isPropertyRequired('llama', 'owl'));
+ }
+
+ /**
+ * Build a formattable markup object to use as assertion.
+ *
+ * @param string $text
+ * Text to display.
+ * @param string $raw_value
+ * Raw value of the result.
+ * @param \Drupal\facets\FacetInterface $facet
+ * The facet.
+ * @param int $count
+ * Number of results.
+ * @param bool $active
+ * Link is active.
+ * @param bool $show_numbers
+ * Numbers are displayed.
+ *
+ * @return array
+ * A render array.
+ */
+ protected function buildLinkAssertion($text, $raw_value, FacetInterface $facet, $count = 0, $active = FALSE, $show_numbers = TRUE) {
+ return [
+ '#theme' => 'facets_result_item',
+ '#raw_value' => $raw_value,
+ '#facet' => $facet,
+ '#value' => $text,
+ '#show_count' => $show_numbers && ($count !== NULL),
+ '#count' => $count,
+ '#is_active' => $active,
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Processor/ProcessorPluginManagerTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Processor/ProcessorPluginManagerTest.php
new file mode 100644
index 000000000..3b2838883
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Processor/ProcessorPluginManagerTest.php
@@ -0,0 +1,131 @@
+discovery = $this->createMock(DiscoveryInterface::class);
+
+ $this->factory = $this->getMockBuilder(DefaultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->moduleHandler = $this->createMock(ModuleHandlerInterface::class);
+
+ $this->cache = $this->createMock(CacheBackendInterface::class);
+
+ $this->translator = $this->createMock(TranslationInterface::class);
+
+ $namespaces = new \ArrayObject();
+
+ $this->sut = new ProcessorPluginManager($namespaces, $this->cache, $this->moduleHandler, $this->translator);
+ $discovery_property = new \ReflectionProperty($this->sut, 'discovery');
+ $discovery_property->setAccessible(TRUE);
+ $discovery_property->setValue($this->sut, $this->discovery);
+ $factory_property = new \ReflectionProperty($this->sut, 'factory');
+ $factory_property->setAccessible(TRUE);
+ $factory_property->setValue($this->sut, $this->factory);
+ }
+
+ /**
+ * Tests plugin manager constructor.
+ */
+ public function testConstruct() {
+ $namespaces = new \ArrayObject();
+ $sut = new ProcessorPluginManager($namespaces, $this->cache, $this->moduleHandler, $this->translator);
+ $this->assertInstanceOf(ProcessorPluginManager::class, $sut);
+ }
+
+ /**
+ * Tests plugin manager's getDefinitions method.
+ */
+ public function testGetDefinitions() {
+ $definitions = [
+ 'foo' => [
+ 'label' => $this->randomMachineName(),
+ ],
+ ];
+ $this->discovery->expects($this->once())
+ ->method('getDefinitions')
+ ->willReturn($definitions);
+ $this->assertSame($definitions, $this->sut->getDefinitions());
+ }
+
+ /**
+ * Tests processing stages.
+ */
+ public function testGetProcessingStages() {
+ $namespaces = new \ArrayObject();
+ $sut = new ProcessorPluginManager($namespaces, $this->cache, $this->moduleHandler, $this->translator);
+
+ $stages = [
+ ProcessorInterface::STAGE_PRE_QUERY,
+ ProcessorInterface::STAGE_POST_QUERY,
+ ProcessorInterface::STAGE_BUILD,
+ ProcessorInterface::STAGE_SORT,
+ ];
+
+ $this->assertEquals($stages, array_keys($sut->getProcessingStages()));
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/QueryType/QueryTypePluginManagerTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/QueryType/QueryTypePluginManagerTest.php
new file mode 100644
index 000000000..7a4842f21
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/QueryType/QueryTypePluginManagerTest.php
@@ -0,0 +1,103 @@
+discovery = $this->createMock(DiscoveryInterface::class);
+
+ $this->factory = $this->getMockBuilder(DefaultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->moduleHandler = $this->createMock(ModuleHandlerInterface::class);
+
+ $this->cache = $this->createMock(CacheBackendInterface::class);
+
+ $namespaces = new \ArrayObject();
+
+ $this->sut = new QueryTypePluginManager($namespaces, $this->cache, $this->moduleHandler);
+ $discovery_property = new \ReflectionProperty($this->sut, 'discovery');
+ $discovery_property->setAccessible(TRUE);
+ $discovery_property->setValue($this->sut, $this->discovery);
+ $factory_property = new \ReflectionProperty($this->sut, 'factory');
+ $factory_property->setAccessible(TRUE);
+ $factory_property->setValue($this->sut, $this->factory);
+ }
+
+ /**
+ * Tests plugin manager constructor.
+ */
+ public function testConstruct() {
+ $namespaces = new \ArrayObject();
+ $sut = new QueryTypePluginManager($namespaces, $this->cache, $this->moduleHandler);
+ $this->assertInstanceOf(QueryTypePluginManager::class, $sut);
+ }
+
+ /**
+ * Tests plugin manager's getDefinitions method.
+ */
+ public function testGetDefinitions() {
+ $definitions = [
+ 'foo' => [
+ 'label' => $this->randomMachineName(),
+ ],
+ ];
+ $this->discovery->expects($this->once())
+ ->method('getDefinitions')
+ ->willReturn($definitions);
+ $this->assertSame($definitions, $this->sut->getDefinitions());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Result/ResultTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Result/ResultTest.php
new file mode 100644
index 000000000..b70c8449b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Result/ResultTest.php
@@ -0,0 +1,48 @@
+ 'foo'], 'facets_facet');
+
+ $result = new Result($facet, 11, 'Eleven', '3.11');
+ $this->assertInstanceOf(Result::class, $result);
+ $this->assertSame(11, $result->getRawValue());
+ $this->assertSame('Eleven', $result->getDisplayValue());
+ $this->assertSame(3, $result->getCount());
+ $this->assertSame($facet, $result->getFacet());
+ }
+
+ /**
+ * Tests getters.
+ */
+ public function testGetters() {
+ $facet = new Facet(['id' => 'foo'], 'facets_facet');
+
+ $result = new Result($facet, 11, 'Eleven', 3);
+ $result->setCount(11.2);
+ $this->assertSame(11, $result->getCount());
+ $result->setDisplayValue('Foo');
+ $this->assertSame('Foo', $result->getDisplayValue());
+
+ $url = new Url('foo');
+ $result->setUrl($url);
+ $this->assertSame($url, $result->getUrl());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/UrlProcessor/UrlProcessorPluginManagerTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/UrlProcessor/UrlProcessorPluginManagerTest.php
new file mode 100644
index 000000000..93eb4ac0e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/UrlProcessor/UrlProcessorPluginManagerTest.php
@@ -0,0 +1,113 @@
+discovery = $this->createMock(DiscoveryInterface::class);
+
+ $this->factory = $this->getMockBuilder(DefaultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->moduleHandler = $this->createMock(ModuleHandlerInterface::class);
+
+ $this->cache = $this->createMock(CacheBackendInterface::class);
+
+ $this->translator = $this->createMock(TranslationInterface::class);
+
+ $namespaces = new \ArrayObject();
+
+ $this->sut = new UrlProcessorPluginManager($namespaces, $this->cache, $this->moduleHandler, $this->translator);
+ $discovery_property = new \ReflectionProperty($this->sut, 'discovery');
+ $discovery_property->setAccessible(TRUE);
+ $discovery_property->setValue($this->sut, $this->discovery);
+ $factory_property = new \ReflectionProperty($this->sut, 'factory');
+ $factory_property->setAccessible(TRUE);
+ $factory_property->setValue($this->sut, $this->factory);
+ }
+
+ /**
+ * Tests plugin manager constructor.
+ */
+ public function testConstruct() {
+ $namespaces = new \ArrayObject();
+ $sut = new UrlProcessorPluginManager($namespaces, $this->cache, $this->moduleHandler, $this->translator);
+ $this->assertInstanceOf(UrlProcessorPluginManager::class, $sut);
+ }
+
+ /**
+ * Tests plugin manager's getDefinitions method.
+ */
+ public function testGetDefinitions() {
+ $definitions = [
+ 'foo' => [
+ 'label' => $this->randomMachineName(),
+ ],
+ ];
+ $this->discovery->expects($this->once())
+ ->method('getDefinitions')
+ ->willReturn($definitions);
+ $this->assertSame($definitions, $this->sut->getDefinitions());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Utility/FacetsDateHandlerTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Utility/FacetsDateHandlerTest.php
new file mode 100644
index 000000000..f49a3f3b3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Utility/FacetsDateHandlerTest.php
@@ -0,0 +1,279 @@
+createMock(EntityStorageInterface::class);
+
+ $em = $this->createMock(EntityTypeManagerInterface::class);
+ $em->expects($this->any())
+ ->method('getStorage')
+ ->with('date_format')
+ ->willReturn($entity_storage);
+
+ $language = new Language(['id' => 'en']);
+
+ $lm = $this->createMock(LanguageManagerInterface::class);
+ $lm->method('getCurrentLanguage')
+ ->willReturn($language);
+ $st = $this->createMock(TranslationInterface::class);
+ $rs = $this->createMock(RequestStack::class);
+ $cf = $this->getConfigFactoryStub();
+
+ $config_factory = $this->getConfigFactoryStub([
+ 'system.date' => ['country' => ['default' => 'GB']],
+ ]);
+ $container = new ContainerBuilder();
+ $container->set('config.factory', $config_factory);
+ \Drupal::setContainer($container);
+
+ $date_formatter = new DateFormatter($em, $lm, $st, $cf, $rs);
+
+ $this->handler = new FacetsDateHandler($date_formatter);
+ }
+
+ /**
+ * Tests the isoDate method.
+ *
+ * @dataProvider provideIsoDates
+ */
+ public function testIsoDate($iso_date, $gap) {
+ $fd = $this->handler;
+ $this->assertEquals($iso_date, $fd->isoDate(static::TIMESTAMP, $gap));
+ }
+
+ /**
+ * Tests for ::getNextDateGap.
+ */
+ public function testGetNextDateGap() {
+ $fd = $this->handler;
+
+ $gap = $fd->getNextDateGap($fd::FACETS_DATE_SECOND);
+ $this->assertEquals($fd::FACETS_DATE_SECOND, $gap);
+
+ $gap = $fd->getNextDateGap($fd::FACETS_DATE_MINUTE);
+ $this->assertEquals($fd::FACETS_DATE_SECOND, $gap);
+
+ $gap = $fd->getNextDateGap($fd::FACETS_DATE_SECOND, $fd::FACETS_DATE_MINUTE);
+ $this->assertEquals($fd::FACETS_DATE_MINUTE, $gap);
+
+ $gap = $fd->getNextDateGap($fd::FACETS_DATE_MINUTE, $fd::FACETS_DATE_MINUTE);
+ $this->assertEquals($fd::FACETS_DATE_MINUTE, $gap);
+
+ $gap = $fd->getNextDateGap($fd::FACETS_DATE_SECOND, $fd::FACETS_DATE_HOUR);
+ $this->assertEquals($fd::FACETS_DATE_HOUR, $gap);
+
+ $gap = $fd->getNextDateGap($fd::FACETS_DATE_MINUTE, $fd::FACETS_DATE_HOUR);
+ $this->assertEquals($fd::FACETS_DATE_HOUR, $gap);
+
+ $gap = $fd->getNextDateGap($fd::FACETS_DATE_HOUR, $fd::FACETS_DATE_HOUR);
+ $this->assertEquals($fd::FACETS_DATE_HOUR, $gap);
+ }
+
+ /**
+ * Tests for ::getTimestampGap.
+ */
+ public function testGetTimestampGap() {
+ $fd = $this->handler;
+
+ // The best search gap between two dates must be a year.
+ $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 31536000);
+ $this->assertEquals($fd::FACETS_DATE_YEAR, $date_gap);
+
+ // The best search gap between two dates must be a month.
+ $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 86400 * 60);
+ $this->assertEquals($fd::FACETS_DATE_MONTH, $date_gap);
+
+ // The best search gap between two dates must be a day.
+ $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 86400);
+ $this->assertEquals($fd::FACETS_DATE_DAY, $date_gap);
+
+ // The best search gap between two dates must be an hour.
+ $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 3600);
+ $this->assertEquals($fd::FACETS_DATE_HOUR, $date_gap);
+
+ // The best search gap between two dates must be a minute.
+ $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 60);
+ $this->assertEquals($fd::FACETS_DATE_MINUTE, $date_gap);
+
+ // The best search gap between two dates must be a second.
+ $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 59);
+ $this->assertEquals($fd::FACETS_DATE_SECOND, $date_gap);
+
+ // When passing in a minimum gap it should be respected.
+ $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 3600, $fd::FACETS_DATE_DAY);
+ $this->assertEquals($fd::FACETS_DATE_DAY, $date_gap);
+ }
+
+ /**
+ * Tests for ::getDateGap method.
+ */
+ public function testGetDateGap() {
+ $fd = $this->handler;
+
+ // Cannot convert to timestamp.
+ $this->assertFalse($fd->getDateGap(static::TIMESTAMP, static::TIMESTAMP));
+
+ // The min. gap is MONTH but the result is larger.
+ $this->assertEquals($fd::FACETS_DATE_YEAR, $fd->getDateGap('1983-03-03T20:43:04Z', '1987-11-26T20:43:04Z', $fd::FACETS_DATE_MONTH));
+
+ // The gap is YEAR.
+ $this->assertEquals($fd::FACETS_DATE_YEAR, $fd->getDateGap('1983-03-03T20:43:04Z', '1987-11-26T20:43:04Z'));
+
+ // The gap is MONTH.
+ $this->assertEquals($fd::FACETS_DATE_MONTH, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-11-26T20:43:04Z'));
+
+ // The gap is DAY.
+ $this->assertEquals($fd::FACETS_DATE_DAY, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-03-26T20:43:04Z'));
+
+ // The gap is HOUR.
+ $this->assertEquals($fd::FACETS_DATE_HOUR, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-03-03T21:44:04Z'));
+
+ // The gap is MINUTE.
+ $this->assertEquals($fd::FACETS_DATE_MINUTE, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-03-03T20:44:04Z'));
+
+ // The gap is SECOND.
+ $this->assertEquals($fd::FACETS_DATE_SECOND, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-03-03T20:43:55Z'));
+ }
+
+ /**
+ * Tests for ::nextDateIncrement method.
+ *
+ * @dataProvider provideNextDateIncrementData
+ */
+ public function testNextDateIncrement($incremented_iso_date, $gap) {
+ $this->assertEquals($incremented_iso_date, $this->handler->getNextDateIncrement(static::ISO_DATE, $gap));
+ }
+
+ /**
+ * Tests for ::nextDateIncrement method.
+ */
+ public function testInvalidNextDateIncrement() {
+ $this->assertFalse($this->handler->getNextDateIncrement('foo', FacetsDateHandler::FACETS_DATE_SECOND));
+ }
+
+ /**
+ * Tests for ::gapCompare method.
+ */
+ public function testGapCompare() {
+ $fd = $this->handler;
+
+ // Timestamps are equals.
+ $this->assertEquals(0, $fd->gapCompare(static::TIMESTAMP, static::TIMESTAMP));
+
+ // Timestamps are equals.
+ $this->assertEquals(0, $fd->gapCompare($fd::FACETS_DATE_YEAR, $fd::FACETS_DATE_YEAR));
+
+ // gap1 is less than gap2.
+ $this->assertEquals(-1, $fd->gapCompare($fd::FACETS_DATE_MONTH, $fd::FACETS_DATE_YEAR));
+
+ // gap1 is less than gap2.
+ $this->assertEquals(1, $fd->gapCompare($fd::FACETS_DATE_MONTH, $fd::FACETS_DATE_DAY));
+ }
+
+ /**
+ * Tests for ::formatTimestamp method.
+ */
+ public function testFormatTimestamp() {
+ $fd = $this->handler;
+
+ $formatted = $fd->formatTimestamp(static::TIMESTAMP);
+ $this->assertEquals('1987', $formatted);
+
+ $formatted = $fd->formatTimestamp(static::TIMESTAMP, 'llama');
+ $this->assertEquals('1987', $formatted);
+
+ $formatted = $fd->formatTimestamp(static::TIMESTAMP, $fd::FACETS_DATE_YEAR);
+ $this->assertEquals('1987', $formatted);
+ }
+
+ /**
+ * Test extract items.
+ */
+ public function testExtractActiveItems() {
+ $this->assertFalse($this->handler->extractActiveItems('foo'));
+
+ $range = '[2016-03-01T00:00:00Z TO 2016-04-01T00:00:00Z]';
+ $extracted = $this->handler->extractActiveItems($range);
+
+ $this->assertSame('array', gettype($extracted));
+ $this->assertEquals('1456790400', $extracted['start']['timestamp']);
+ $this->assertEquals('2016-03-01T00:00:00Z', $extracted['start']['iso']);
+ }
+
+ /**
+ * Returns a data provider for the ::testIsoDate().
+ *
+ * @return array
+ * Arrays with data for the test data.
+ */
+ public function provideIsoDates() {
+ return [
+ ['1987-11-26T20:43:04Z', FacetsDateHandler::FACETS_DATE_SECOND],
+ ['1987-11-26T20:43:00Z', FacetsDateHandler::FACETS_DATE_MINUTE],
+ ['1987-11-26T20:00:00Z', FacetsDateHandler::FACETS_DATE_HOUR],
+ ['1987-11-26T00:00:00Z', FacetsDateHandler::FACETS_DATE_DAY],
+ ['1987-11-01T00:00:00Z', FacetsDateHandler::FACETS_DATE_MONTH],
+ ['1987-01-01T00:00:00Z', FacetsDateHandler::FACETS_DATE_YEAR],
+ ['1987-11-26T20:43:04Z', FacetsDateHandler::FACETS_DATE_ISO8601],
+ ];
+ }
+
+ /**
+ * Returns a data provider for the ::testNextDateIncrement().
+ *
+ * @return array
+ * Arrays with data for the test data.
+ */
+ public function provideNextDateIncrementData() {
+ return [
+ ['1987-11-26T20:43:05Z', FacetsDateHandler::FACETS_DATE_SECOND],
+ ['1987-11-26T20:44:04Z', FacetsDateHandler::FACETS_DATE_MINUTE],
+ ['1987-11-26T21:43:04Z', FacetsDateHandler::FACETS_DATE_HOUR],
+ ['1987-11-27T20:43:04Z', FacetsDateHandler::FACETS_DATE_DAY],
+ ['1987-12-26T20:43:04Z', FacetsDateHandler::FACETS_DATE_MONTH],
+ ['1988-11-26T20:43:04Z', FacetsDateHandler::FACETS_DATE_YEAR],
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Utility/FacetsUrlGeneratorTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Utility/FacetsUrlGeneratorTest.php
new file mode 100644
index 000000000..16a98c6fb
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Utility/FacetsUrlGeneratorTest.php
@@ -0,0 +1,78 @@
+prophesize(UrlProcessorPluginManager::class)->reveal();
+
+ $storage = $this->prophesize(EntityStorageInterface::class);
+ $etm = $this->prophesize(EntityTypeManagerInterface::class);
+ $etm->getStorage('facets_facet')->willReturn($storage->reveal());
+
+ $url_generator = new FacetsUrlGenerator($url_processor_plugin_manager, $etm->reveal());
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("The active filters passed in are invalid. They should look like: ['facet_id' => ['value1', 'value2']]");
+ $url_generator->getUrl([]);
+ }
+
+ /**
+ * Tests that passing an invalid facet ID throws an InvalidArgumentException.
+ *
+ * @covers ::getUrl
+ */
+ public function testInvalidArray() {
+ $url_processor_plugin_manager = $this->prophesize(UrlProcessorPluginManager::class)->reveal();
+
+ $storage = $this->prophesize(EntityStorageInterface::class);
+ $etm = $this->prophesize(EntityTypeManagerInterface::class);
+ $etm->getStorage('facets_facet')->willReturn($storage->reveal());
+
+ $url_generator = new FacetsUrlGenerator($url_processor_plugin_manager, $etm->reveal());
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("The active filters passed in are invalid. They should look like: [imaginary => ['value1', 'value2']]");
+ $url_generator->getUrl(['imaginary' => 'unicorn']);
+ }
+
+ /**
+ * Tests that passing an invalid facet ID throws an InvalidArgumentException.
+ *
+ * @covers ::getUrl
+ */
+ public function testInvalidFacet() {
+ $url_processor_plugin_manager = $this->prophesize(UrlProcessorPluginManager::class)->reveal();
+
+ $storage = $this->prophesize(EntityStorageInterface::class);
+ $storage->load(Argument::type('string'))->willReturn(NULL);
+ $etm = $this->prophesize(EntityTypeManagerInterface::class);
+ $etm->getStorage('facets_facet')->willReturn($storage->reveal());
+
+ $url_generator = new FacetsUrlGenerator($url_processor_plugin_manager, $etm->reveal());
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The Facet imaginary could not be loaded.');
+ $url_generator->getUrl(['imaginary' => ['unicorn']]);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Widget/WidgetPluginManagerTest.php b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Widget/WidgetPluginManagerTest.php
new file mode 100644
index 000000000..fc3a563ab
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/facets/tests/src/Unit/Widget/WidgetPluginManagerTest.php
@@ -0,0 +1,113 @@
+discovery = $this->createMock(DiscoveryInterface::class);
+
+ $this->factory = $this->getMockBuilder(DefaultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->moduleHandler = $this->createMock(ModuleHandlerInterface::class);
+
+ $this->cache = $this->createMock(CacheBackendInterface::class);
+
+ $this->translator = $this->createMock(TranslationInterface::class);
+
+ $namespaces = new \ArrayObject();
+
+ $this->sut = new WidgetPluginManager($namespaces, $this->cache, $this->moduleHandler, $this->translator);
+ $discovery_property = new \ReflectionProperty($this->sut, 'discovery');
+ $discovery_property->setAccessible(TRUE);
+ $discovery_property->setValue($this->sut, $this->discovery);
+ $factory_property = new \ReflectionProperty($this->sut, 'factory');
+ $factory_property->setAccessible(TRUE);
+ $factory_property->setValue($this->sut, $this->factory);
+ }
+
+ /**
+ * Tests plugin manager constructor.
+ */
+ public function testConstruct() {
+ $namespaces = new \ArrayObject();
+ $sut = new WidgetPluginManager($namespaces, $this->cache, $this->moduleHandler, $this->translator);
+ $this->assertInstanceOf(WidgetPluginManager::class, $sut);
+ }
+
+ /**
+ * Tests plugin manager's getDefinitions method.
+ */
+ public function testGetDefinitions() {
+ $definitions = [
+ 'foo' => [
+ 'label' => $this->randomMachineName(),
+ ],
+ ];
+ $this->discovery->expects($this->once())
+ ->method('getDefinitions')
+ ->willReturn($definitions);
+ $this->assertSame($definitions, $this->sut->getDefinitions());
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/LICENSE.txt b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/LICENSE.txt
new file mode 100644
index 000000000..d159169d1
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/ReadMe.txt b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/ReadMe.txt
new file mode 100644
index 000000000..94eb32034
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/ReadMe.txt
@@ -0,0 +1,13 @@
+This Module Provides Custom Breadcrumb support. for Drupal 8 and it provides many features like Views, Taxonomy, nodes etc.
+
+How it Works!
+
+Got to this route: /admin/config/route_specific_breadcrumb/routespecific
+Add route like route_specific_breadcrumb.route_specific_form
+
+And below that Add breadcrumb as text and url in a add more form for above route.
+
+This will generate breadcrumb for entered route.
+
+note: If changes are not getting reflected please clear your cache.
+
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/composer.json b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/composer.json
new file mode 100644
index 000000000..6ea67ef3f
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/composer.json
@@ -0,0 +1,16 @@
+{
+ "name": "drupal/route_specific_breadcrumb",
+ "type": "drupal-module",
+ "description": "Route Specific Breadcrumb",
+ "keywords": [
+ "Drupal"
+ ],
+ "license": "GPL-2.0+",
+ "homepage": "https://www.drupal.org/project/route_specific_breadcrumb",
+ "minimum-stability": "dev",
+ "support": {
+ "issues": "https://www.drupal.org/project/issues/route_specific_breadcrumb",
+ "source": "http://cgit.drupalcode.org/route_specific_breadcrumb"
+ },
+ "require": {}
+}
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.info.yml b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.info.yml
new file mode 100644
index 000000000..dfc3c1c0e
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.info.yml
@@ -0,0 +1,11 @@
+name: Route Specific Breadcrumb
+type: module
+description: Route Specific Breadcrumb
+core_version_requirement: ^8 || ^9
+package: Custom
+configure: route_specific_breadcrumb.route_specific_form
+
+# Information added by Drupal.org packaging script on 2021-10-15
+version: '2.0.0'
+project: 'route_specific_breadcrumb'
+datestamp: 1634279299
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.install b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.install
new file mode 100644
index 000000000..54b90d02b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.install
@@ -0,0 +1,54 @@
+ 'Breadcrumb data for custom route',
+ 'fields' => [
+ 'rid' => [
+ 'description' => 'Unique identifier',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ],
+ 'uid' => [
+ 'description' => 'Current User Id',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ ],
+ 'route' => [
+ 'description' => 'Route to which Data is attached',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ ],
+ 'description' => [
+ 'description' => 'Data',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ ],
+ 'created' => [
+ 'description' => 'Time Meta Tag created',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ ],
+ 'updated' => [
+ 'description' => 'Time Meta Tag Updated',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ ],
+ ],
+ 'primary key' => ['rid'],
+ 'unique keys' => [
+ 'rid' => ['rid'],
+ ],
+ ];
+
+ return $schema;
+}
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.links.action.yml b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.links.action.yml
new file mode 100644
index 000000000..7fb7d2345
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.links.action.yml
@@ -0,0 +1,19 @@
+# All action links for this module
+
+route_specific_breadcrumb.route_specific_form:
+ # Which route will be called by the link
+ route_name: route_specific_breadcrumb.list_records_controller_update
+ title: 'Show Tables'
+
+ # Where will the link appear, defined by route name.
+ appears_on:
+ - route_specific_breadcrumb.route_specific_form
+
+route_specific_breadcrumb.list_records_controller_update:
+ # Which route will be called by the link
+ route_name: route_specific_breadcrumb.route_specific_form
+ title: 'Back to form'
+
+ # Where will the link appear, defined by route name.
+ appears_on:
+ - route_specific_breadcrumb.list_records_controller_update
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.links.menu.yml b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.links.menu.yml
new file mode 100644
index 000000000..468835247
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.links.menu.yml
@@ -0,0 +1,13 @@
+route_specific_breadcrumb.route_specific_form:
+ title: 'RouteSpecificForm'
+ route_name: route_specific_breadcrumb.route_specific_form
+ description: 'A description for the menu entry'
+ parent: system.admin_config_system
+ weight: 99
+
+route_specific_breadcrumb.list_records_controller_update:
+ title: Show Tables
+ description: 'Show Tables'
+ route_name: route_specific_breadcrumb.list_records_controller_update
+ parent: route_specific_breadcrumb.route_specific_form
+ weight: 10
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.module b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.module
new file mode 100644
index 000000000..35298dfb8
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.module
@@ -0,0 +1,35 @@
+' . t('About') . '';
+ $output .= '' . t('Route Specific Breadcrumb') . '
';
+ return $output;
+
+ default:
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function route_specific_breadcrumb_theme() {
+ return [
+ 'route_specific_breadcrumb' => [
+ 'render element' => 'children',
+ ],
+ ];
+}
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.routing.yml b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.routing.yml
new file mode 100644
index 000000000..052289ef3
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.routing.yml
@@ -0,0 +1,29 @@
+route_specific_breadcrumb.route_specific_form:
+ path: '/admin/config/route_specific_breadcrumb/routespecific'
+ defaults:
+ _form: '\Drupal\route_specific_breadcrumb\Form\RouteSpecificForm'
+ _title: 'RouteSpecificForm'
+ requirements:
+ _permission: 'administer site configuration'
+ options:
+ _admin_route: TRUE
+
+route_specific_breadcrumb.list_records_controller_update:
+ path: '/route_specific_breadcrumb/showroutes'
+ defaults:
+ _controller: '\Drupal\route_specific_breadcrumb\Controller\ListRecordsController::getRoute'
+ _title: 'Data Table'
+ requirements:
+ _permission: 'administer site configuration'
+ options:
+ _admin_route: TRUE
+
+route_specific_breadcrumb.route_specific_breadcrumb_delete_form:
+ path: '/admin/config/system/route_specific_breadcrumb/{rid}/delete'
+ defaults:
+ _form: '\Drupal\route_specific_breadcrumb\Form\RouteSpecificBreadcrumbDeleteForm'
+ _title: 'Route Specific Breadcrumb Delete Form'
+ requirements:
+ _permission: 'administer site configuration'
+ options:
+ _admin_route: TRUE
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.services.yml b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.services.yml
new file mode 100644
index 000000000..1179774f4
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/route_specific_breadcrumb.services.yml
@@ -0,0 +1,6 @@
+services:
+ route_specific_breadcrumb.breadcrumb:
+ class: Drupal\route_specific_breadcrumb\Breadcrumb\RouteSpecificBreadcrumbBuilder
+ arguments: ['@database']
+ tags:
+ - { name: breadcrumb_builder, priority: 100 }
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Breadcrumb/RouteSpecificBreadcrumbBuilder.php b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Breadcrumb/RouteSpecificBreadcrumbBuilder.php
new file mode 100644
index 000000000..57a23a200
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Breadcrumb/RouteSpecificBreadcrumbBuilder.php
@@ -0,0 +1,67 @@
+database = $database;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applies(RouteMatchInterface $attributes) {
+ $routeName = $attributes->getRouteName();
+ $data = ListRecordsController::routeCheck($this->database, $routeName);
+ if (isset($data->route) && ($data->route == $routeName)) {
+ return $routeName;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(RouteMatchInterface $route_match) {
+ $data = ListRecordsController::routeCheck($this->database, $route_match->getRouteName());
+ $routeName = $route_match->getRouteName();
+ if ($data->route == $routeName) {
+ $breadcrumb = new Breadcrumb();
+ $description = unserialize($data->description);
+ foreach ($description as $value) {
+ if ($value['link'] === '') {
+ $value['link'] = '/';
+ }
+ if (!UrlHelper::isExternal($value['link'])) {
+ $url = Url::fromUri('internal:' . $value['link']);
+ $project_link = Link::fromTextAndUrl($value['name'], $url);
+ $breadcrumb->addLink($project_link);
+ }
+ }
+ }
+ return $breadcrumb;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Controller/ListRecordsController.php b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Controller/ListRecordsController.php
new file mode 100644
index 000000000..4b543c89c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Controller/ListRecordsController.php
@@ -0,0 +1,158 @@
+database = $database;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('database')
+ );
+ }
+
+ /**
+ * Function routeCheck.
+ *
+ * @return string
+ * Return check.
+ */
+ static public function routeCheck($obj, $route) {
+ // Return TRUE if $sendData is FALSE
+ // Else Return $value object.
+ $data = $obj->select('route_specific_breadcrumb', 'r')
+ ->fields('r', ['route', 'description'])
+ ->condition('r.route', $route, '=')
+ ->execute();
+ $data->allowRowCount = TRUE;
+ if ($data->rowCount() > 0) {
+ foreach ($data as $value) {
+ return $value;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * routeData.
+ *
+ * @return string
+ * Return check.
+ */
+ static public function routeData($obj, $rid) {
+ // Return TRUE if $sendData is FALSE
+ // Else Return $value object.
+ $data = $obj->select('route_specific_breadcrumb', 'r')
+ ->fields('r', ['route', 'description'])
+ ->condition('r.rid', $rid, '=')
+ ->execute();
+ $data->allowRowCount = TRUE;
+ if ($data->rowCount() > 0) {
+ foreach ($data as $value) {
+ return $value;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Routedelete.
+ *
+ * @return bool
+ * Return TRUE if data is deleted or FALSE otherwise.
+ */
+ static public function routeDelete($obj, $rid) {
+ return $obj->delete('route_specific_breadcrumb')
+ ->condition('rid', $rid, '=')
+ ->execute() === NULL ? FALSE : TRUE;
+ }
+
+ /**
+ * Get Tables.
+ *
+ * @return string
+ * Return array.
+ */
+ public function getRoute() {
+ $data = $this->database->select('route_specific_breadcrumb', 'r');
+ $data->fields('r', [
+ 'uid',
+ 'route',
+ 'description',
+ 'created',
+ 'updated',
+ 'rid',
+ ]
+ );
+ $rows = [];
+ $header = [
+ 'ID',
+ 'Route',
+ 'Description',
+ 'Created',
+ 'Updated',
+ 'Edit',
+ ];
+ $table_sort = $data->extend('Drupal\Core\Database\Query\TableSortExtender')
+ ->orderByHeader($header);
+ $pager = $table_sort->extend('Drupal\Core\Database\Query\PagerSelectExtender')
+ ->limit(10);
+ $result = $pager->execute();
+ $result->allowRowCount = TRUE;
+ if ($result->rowCount() > 0) {
+ foreach ($result as $row) {
+ $row->created = date('d-m-Y H:i:s', $row->created);
+ $row->updated = date('d-m-Y H:i:s', $row->updated);
+ // Internal path (defined by a route in Drupal 8).
+ $internal_link = Link::createFromRoute('edit', 'route_specific_breadcrumb.route_specific_form', [
+ 'rid' => $row->rid,
+ ]
+ );
+ $row = (array) $row;
+ $row['rid'] = $internal_link;
+ $rows[] = ['data' => (array) $row, 'style' => 'word-break:break-all;'];
+ }
+ }
+ $build = [
+ '#markup' => 'List of all data',
+ ];
+
+ $build['location_table'] = [
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#empty' => 'No items available',
+ ];
+ $build['pager'] = [
+ '#type' => 'pager',
+ ];
+ return $build;
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Form/RouteSpecificBreadcrumbDeleteForm.php b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Form/RouteSpecificBreadcrumbDeleteForm.php
new file mode 100644
index 000000000..ecebc5c65
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Form/RouteSpecificBreadcrumbDeleteForm.php
@@ -0,0 +1,124 @@
+database = $database;
+ $this->messenger = $messenger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('database'),
+ $container->get('messenger')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'route_specific_breadcrumb_delete_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return t('Do you want to delete %id?', ['%id' => $this->id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('route_specific_breadcrumb.list_records_controller_update');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return t('This action can not be undone.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return t('Delete it!');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelText() {
+ return t('Cancel');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param int $rid
+ * (optional) The ID of the item to be deleted.
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, $rid = NULL) {
+ $this->id = $rid;
+ $result = ListRecordsController::routeData($this->database, $rid);
+ if ($result === FALSE) {
+ return $this->redirect('route_specific_breadcrumb.list_records_controller_update');
+ }
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $delete = ListRecordsController::routeDelete($this->database, $this->id);
+ if ($delete === TRUE) {
+ $this->messenger->addMessage($this->t('Data Deleted Successfully'));
+ $url = Url::fromRoute('route_specific_breadcrumb.list_records_controller_update');
+ $form_state->setRedirectUrl($url);
+ }
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Form/RouteSpecificForm.php b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Form/RouteSpecificForm.php
new file mode 100644
index 000000000..aced95089
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/route_specific_breadcrumb/src/Form/RouteSpecificForm.php
@@ -0,0 +1,319 @@
+database = $database;
+ $this->currenUser = $currentUser;
+ $this->route = $route;
+ $this->serialize = $serialize;
+ $this->pathValidator = $path_validator;
+ $this->messenger = $messenger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('database'),
+ $container->get('current_user'),
+ $container->get('router.route_provider'),
+ $container->get('serialization.phpserialize'),
+ $container->get('path.validator'),
+ $container->get('messenger')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'route_specific_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $route = NULL;
+ $description = NULL;
+ if (!empty($this->getRequest()->query)) {
+ $rid = $this->getRequest()->query->get('rid');
+ if (isset($rid)) {
+ $result = ListRecordsController::routeData($this->database, $rid, TRUE);
+ if (is_object($result)) {
+ $route = $result->route;
+ $description = $this->serialize->decode($result->description);
+ }
+ $form['rid'] = [
+ '#type' => 'hidden',
+ '#default_value' => $rid,
+ ];
+ $form['delete'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Delete'),
+ '#url' => Url::fromRoute('route_specific_breadcrumb.route_specific_breadcrumb_delete_form', [
+ 'rid' => $rid,
+ ]
+ ),
+ '#weight' => 10,
+ ];
+ }
+ }
+ $form['route_name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Route Name'),
+ '#description' => $this->t('Enter Route Name'),
+ '#maxlength' => 255,
+ '#size' => 64,
+ '#default_value' => $route,
+ '#required' => TRUE,
+ ];
+ if ($description !== NULL) {
+ $num_names = $form_state->get('num_names');
+ if (empty($num_names)) {
+ $num_names = count($description);
+ }
+ $form_state->set('num_names', $num_names);
+
+ }
+ else {
+ // Gather the number of names in the form already.
+ $num_names = $form_state->get('num_names');
+ }
+ // We have to ensure that there is at least one name field.
+ if ($num_names === NULL) {
+ $form_state->set('num_names', 1);
+ $num_names = 1;
+ }
+ $form['#tree'] = TRUE;
+ $form['breadcrumb_fieldset'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Breadcrumb Level'),
+ '#prefix' => '',
+ '#suffix' => '
',
+ ];
+
+ for ($i = 0; $i < $num_names; $i++) {
+ $form['breadcrumb_fieldset'][$i]['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Name'),
+ '#default_value' => $description[$i]['name'],
+ ];
+ $form['breadcrumb_fieldset'][$i]['link'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Link'),
+ '#default_value' => $description[$i]['link'],
+ ];
+ }
+
+ $form['breadcrumb_fieldset']['actions'] = [
+ '#type' => 'actions',
+ ];
+ $form['breadcrumb_fieldset']['actions']['add_name'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Add one more'),
+ '#submit' => ['::addOne'],
+ '#ajax' => [
+ 'callback' => '::addmoreCallback',
+ 'wrapper' => 'breadcrumb-fieldset-wrapper',
+ ],
+ ];
+ // If there is more than one name, add the remove button.
+ if ($num_names > 1) {
+ $form['breadcrumb_fieldset']['actions']['remove_name'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Remove one'),
+ '#submit' => ['::removeCallback'],
+ '#ajax' => [
+ 'callback' => '::addmoreCallback',
+ 'wrapper' => 'breadcrumb-fieldset-wrapper',
+ ],
+ ];
+ }
+ $form_state->setCached(FALSE);
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $breadCrumb = $form_state->getValue('breadcrumb_fieldset');
+ $route = $form_state->getValue('route_name');
+ if (count($this->route->getRoutesByNames([$route])) !== 1) {
+ $form_state->setErrorByName('route_name', 'The route you entered does not exists.');
+ }
+ foreach ($breadCrumb as $key => $value) {
+ if (is_numeric($key) && !empty($value['link'])) {
+ $url = $this->pathValidator->getUrlIfValid($value['link']);
+ if ($url === FALSE && $value['link'] !== '') {
+ $form_state->setErrorByName('breadcrumb_fieldset][' . $key . '][link', 'The link is not valid.');
+ }
+ if (UrlHelper::isExternal($value['link'])) {
+ $form_state->setErrorByName('breadcrumb_fieldset][' . $key . '][link', 'Only internal paths are allowed.');
+ }
+ }
+ }
+ parent::validateForm($form, $form_state);
+ }
+
+ /**
+ * Callback for both ajax-enabled buttons.
+ *
+ * Selects and returns the fieldset with the names in it.
+ */
+ public function addmoreCallback(array &$form, FormStateInterface $form_state) {
+ $name_field = $form_state->get('num_names');
+ return $form['breadcrumb_fieldset'];
+ }
+
+ /**
+ * Submit handler for the "add-one-more" button.
+ *
+ * Increments the max counter and causes a rebuild.
+ */
+ public function addOne(array &$form, FormStateInterface $form_state) {
+ $name_field = $form_state->get('num_names');
+ $add_button = $name_field + 1;
+ $form_state->set('num_names', $add_button);
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Submit handler for the "remove one" button.
+ *
+ * Decrements the max counter and causes a form rebuild.
+ */
+ public function removeCallback(array &$form, FormStateInterface $form_state) {
+ $name_field = $form_state->get('num_names');
+ if ($name_field > 1) {
+ $remove_button = $name_field - 1;
+ $form_state->set('num_names', $remove_button);
+ }
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Final submit handler.
+ *
+ * Reports what values were finally set.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $data = [];
+ foreach ($form_state->getValue('breadcrumb_fieldset') as $key => $value) {
+ if (is_numeric($key)) {
+ $data[$key] = $value;
+ }
+ }
+ $query = $this->database->select('route_specific_breadcrumb', 'rsb');
+ $query->fields('rsb',['rid']);
+ $query->condition('rsb.route', $form_state->getValue('route_name'));
+ $result = $query->execute()->fetchField();
+ $arr = [
+ 'uid' => $this->currenUser->id(),
+ 'route' => $form_state->getValue('route_name'),
+ 'description' => $this->serialize->encode($data),
+ 'created' => \Drupal::time()->getRequestTime(),
+ 'updated' => \Drupal::time()->getRequestTime(),
+ ];
+ if ($result) {
+ $insert = $this->routeInsert($this->database, $arr, TRUE);
+ }
+ else {
+ $insert = $this->routeInsert($this->database, $arr);
+ }
+ if ($insert) {
+ $this->messenger->addMessage($this->t('Submitted Successfully.'));
+ }
+ parent::submitForm($form, $form_state);
+
+ }
+
+ /**
+ * Metalinkcheck.
+ *
+ * @return bool
+ * Return TRUE or FALSE on data operations.
+ */
+ public function routeInsert($obj, $fields, $update = FALSE) {
+ if ($update) {
+ return $obj->update('route_specific_breadcrumb')
+ ->fields($fields)
+ ->condition('route', $fields['route'])
+ ->execute() === NULL ? FALSE : TRUE;
+ }
+ return $obj->insert('route_specific_breadcrumb')
+ ->fields($fields)
+ ->execute() === NULL ? FALSE : TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEditableConfigNames() {
+ return [
+ 'route_specific_breadcrumb.routespecific',
+ ];
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/CHANGELOG.txt b/frontend/drupal9/web/modules/contrib/search_api/CHANGELOG.txt
index 4e77b2f14..7d9d30eda 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/CHANGELOG.txt
+++ b/frontend/drupal9/web/modules/contrib/search_api/CHANGELOG.txt
@@ -1,3 +1,49 @@
+Search API 1.24 (2022-07-07):
+-----------------------------
+- #3292241 by drunken monkey: Fixed test fails.
+- #3243313 by Steven Jones, drunken monkey: Removed the "search_api_base_path"
+ query option for views.
+- #3179643 by drunken monkey: Fixed Views field extraction bevahior when fields
+ returned from server don't have values.
+- #3253986 by drunken monkey, gaddman: Fixed empty "ignore characters" setting
+ for the Tokenizer processor.
+- #3197050 by BAHbKA, drunken monkey: Fixed caching issue for facets on AJAX
+ views.
+- #3246615 by drunken monkey: Fixed error when saving an unindexed translation
+ of an entity.
+- #3258375 by Eugene Bocharov, wells, drunken monkey, joshuami, MrDaleSmith,
+ Grayle: Fixed out-of-memory error when used with Views, Metatag and caching.
+- #3270324 by drunken monkey, kimberleycgm: Fixed incorrect storing of
+ "Rendered item" processor setting "roles".
+- #3266715 by kimberleycgm, drunken monkey: Fixed "Rendered item" processor not
+ adding the "Authenticated" role when another is selected.
+- #3270695 by mkalkbrenner, drunken monkey: Improved status messages during
+ indexing.
+- #3268953 by drunken monkey: Changed indents in composer.json to 4 spaces.
+- #3168162 by gabesullice, drunken monkey, mkalkbrenner: Updated to latest
+ changes in Event system.
+- #3248262 by Robert_T, drunken monkey: Fixed problems with recent database
+ update functions.
+- #3239649 by drunken monkey, phma: Fixed missing config schemas for various
+ Views plugins.
+- #3262771 by drunken monkey, marciaibanez: Moved test modules to test/modules.
+- #3256028 by drunken monkey, mkalkbrenner: Increased Core version requirement
+ to 9.2.
+- #3247914 by drunken monkey: Fixed config form of the "Type-specific boosting"
+ processor.
+- #3262702 by drunken monkey: Fixed test failures on Drupal 9.2.
+- #3227659 by drunken monkey, daften: Fixed double-escaped query parameters
+ when using the Views "Preserve facets" option.
+- #3227268 by drunken monkey, Driskell: Fixed inconsistent collation in
+ Database backend.
+- #3196990 by nguerrier, ekes, drunken monkey, hswong3i: Fixed wrong query and
+ cache type on Search API views.
+- #3258802 by drunken monkey: Fixed errors when indexing fields with unknown
+ types.
+- #3232736 by GuyPaddock, idebr, drunken monkey: Added query tags as an option
+ to Views.
+- #3188138 by drunken monkey, devad: Added an "Item URL" field to Views.
+
Search API 1.23 (2022-01-21):
-----------------------------
- #3247781 (follow-up) by drunken monkey: Fixed missing update for Task entity
diff --git a/frontend/drupal9/web/modules/contrib/search_api/composer.json b/frontend/drupal9/web/modules/contrib/search_api/composer.json
index b53b97426..a0fd8d549 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/composer.json
+++ b/frontend/drupal9/web/modules/contrib/search_api/composer.json
@@ -1,45 +1,45 @@
{
- "name": "drupal/search_api",
- "description": "Provides a generic framework for modules offering search capabilities.",
- "type": "drupal-module",
- "homepage": "https://www.drupal.org/project/search_api",
- "authors": [
- {
- "name": "Thomas Seidl",
- "homepage": "https://www.drupal.org/u/drunken-monkey"
+ "name": "drupal/search_api",
+ "description": "Provides a generic framework for modules offering search capabilities.",
+ "type": "drupal-module",
+ "homepage": "https://www.drupal.org/project/search_api",
+ "authors": [
+ {
+ "name": "Thomas Seidl",
+ "homepage": "https://www.drupal.org/u/drunken-monkey"
+ },
+ {
+ "name": "Nick Veenhof",
+ "homepage": "https://www.drupal.org/u/nick_vh"
+ },
+ {
+ "name": "See other contributors",
+ "homepage": "https://www.drupal.org/node/790418/committers"
+ }
+ ],
+ "support": {
+ "issues": "https://www.drupal.org/project/issues/search_api",
+ "irc": "irc://irc.freenode.org/drupal-search-api",
+ "source": "https://git.drupalcode.org/project/search_api"
},
- {
- "name": "Nick Veenhof",
- "homepage": "https://www.drupal.org/u/nick_vh"
+ "license": "GPL-2.0-or-later",
+ "require-dev": {
+ "drupal/language_fallback_fix": "@dev",
+ "drupal/search_api_autocomplete": "@dev"
},
- {
- "name": "See other contributors",
- "homepage":"https://www.drupal.org/node/790418/committers"
+ "suggest": {
+ "drupal/facets": "Adds the ability to create faceted searches.",
+ "drupal/search_api_autocomplete": "Allows adding autocomplete suggestions to search fields.",
+ "drupal/search_api_solr": "Adds support for using Apache Solr as a backend."
+ },
+ "extra": {
+ "drush": {
+ "services": {
+ "drush.services.yml": "^9 || ^10"
+ }
+ }
+ },
+ "conflict": {
+ "drupal/search_api_solr": "2.* || 3.0 || 3.1"
}
- ],
- "support": {
- "issues": "https://www.drupal.org/project/issues/search_api",
- "irc": "irc://irc.freenode.org/drupal-search-api",
- "source": "https://git.drupalcode.org/project/search_api"
- },
- "license": "GPL-2.0-or-later",
- "require-dev": {
- "drupal/language_fallback_fix": "@dev",
- "drupal/search_api_autocomplete": "@dev"
- },
- "suggest": {
- "drupal/facets": "Adds the ability to create faceted searches.",
- "drupal/search_api_autocomplete": "Allows adding autocomplete suggestions to search fields.",
- "drupal/search_api_solr": "Adds support for using Apache Solr as a backend."
- },
- "extra": {
- "drush": {
- "services": {
- "drush.services.yml": "^9 || ^10"
- }
- }
- },
- "conflict": {
- "drupal/search_api_solr": "2.* || 3.0 || 3.1"
- }
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/config/schema/search_api.processor.schema.yml b/frontend/drupal9/web/modules/contrib/search_api/config/schema/search_api.processor.schema.yml
index 50b7305cf..f3d37b878 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/config/schema/search_api.processor.schema.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/config/schema/search_api.processor.schema.yml
@@ -246,7 +246,7 @@ search_api.property_configuration.rendered_item:
roles:
type: sequence
label: 'The selected roles'
- orderby: key
+ orderby: value
sequence:
type: string
label: 'The user roles which will be active when the entity is rendered'
diff --git a/frontend/drupal9/web/modules/contrib/search_api/config/schema/search_api.views.schema.yml b/frontend/drupal9/web/modules/contrib/search_api/config/schema/search_api.views.schema.yml
index 94511cb85..af7dd3ec1 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/config/schema/search_api.views.schema.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/config/schema/search_api.views.schema.yml
@@ -11,6 +11,12 @@ views.query.search_api_query:
preserve_facet_query_args:
type: boolean
label: By default, changing an exposed filter would reset all selected facets. This option allows you to prevent this behavior.
+ query_tags:
+ type: sequence
+ label: 'Query Tags'
+ sequence:
+ type: string
+ label: 'Tag'
views.row.search_api:
type: views_row
@@ -28,6 +34,10 @@ views.row.search_api:
type: string
label: View mode for the specific bundle
+views.row.search_api_data:
+ type: views.row.data_field
+ label: 'Entity (Search API)'
+
views.cache.search_api_time:
type: views_cache
label: 'Time-based caching (Search API)'
@@ -62,6 +72,14 @@ views.argument.search_api:
type: views.argument.numeric
label: 'Search API'
+views.argument.search_api_all_terms:
+ type: views.argument.search_api
+ label: 'Search API all taxonomy terms'
+
+views.argument.search_api_date:
+ type: views.argument.search_api
+ label: 'Search API date'
+
views.argument.search_api_fulltext:
type: views.argument.search_api
label: 'Search API more like this'
@@ -170,6 +188,10 @@ views.field.search_api_entity:
type: string
label: 'View mode'
+views.field.search_api_entity_operations:
+ type: views.field.entity_operations
+ label: 'Search API operations links'
+
views.field.search_api_field:
type: views.field.field
label: 'Search API entity field'
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db.info.yml b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db.info.yml
index aa2018761..014d79055 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db.info.yml
@@ -2,11 +2,11 @@ type: module
name: Database Search
description: Offers an implementation of the Search API that uses database tables for indexing content.
package: Search
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
dependencies:
- search_api:search_api
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml
index b8eebc911..5d7350704 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml
@@ -36,7 +36,7 @@ field_settings:
property_path: rendered_item
configuration:
roles:
- anonymous: anonymous
+ - anonymous
view_mode:
'entity:node':
article: search_index
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml
index 3f0d31f62..6d39e9b59 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml
@@ -2,7 +2,7 @@ type: module
name: Database Search Defaults
description: Enable this module for a best-practice default setup of Search API with the Database backend. After installation it is recommended to uninstall this module again for performance reasons. The provided configuration will not be removed.
package: Search
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
dependencies:
- drupal:comment
- drupal:field
@@ -13,7 +13,7 @@ dependencies:
- drupal:views
- search_api:search_api_db
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php
index 485b55032..1da19bceb 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php
@@ -61,6 +61,14 @@ class IntegrationTest extends BrowserTestBase {
// Installation invokes a batch and this breaks it.
\Drupal::state()->set('search_api_use_tracking_batch', FALSE);
+ // Uninstall the Core search module.
+ $edit_enable = [
+ 'uninstall[search]' => TRUE,
+ ];
+ $this->drupalGet('admin/modules/uninstall');
+ $this->submitForm($edit_enable, 'Uninstall');
+ $this->submitForm([], 'Uninstall');
+
// Install the search_api_db_defaults module.
$edit_enable = [
'modules[search_api_db_defaults][enable]' => TRUE,
@@ -110,6 +118,8 @@ class IntegrationTest extends BrowserTestBase {
$this->submitForm(['keys' => 'test'], 'Search');
$this->assertSession()->pageTextContains($title);
$this->assertSession()->responseNotContains('Error message');
+ $this->assertSession()->pageTextNotContains('Please enter some keywords.');
+ $this->assertSession()->pageTextNotContains('Your search yielded no results.');
// Uninstall the module.
$this->drupalLogin($this->adminUser);
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php
index c8062210c..2ccca60d6 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php
@@ -24,6 +24,12 @@ class MySql extends GenericDatabase {
$collation = $type === 'text' ? 'utf8mb4_bin' : 'utf8_general_ci';
try {
$this->database->query("ALTER TABLE {{$table}} CONVERT TO CHARACTER SET '$charset' COLLATE '$collation'");
+ // Even for text tables, we need the "item_id" column to have the same
+ // collation as everywhere else. Otherwise, this can slow down search
+ // queries significantly.
+ if ($type === 'text') {
+ $this->database->query("ALTER TABLE {{$table}} MODIFY item_id VARCHAR(150) CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'");
+ }
}
catch (\PDOException $e) {
$class = get_class($e);
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/Event/QueryPreExecuteEvent.php b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/Event/QueryPreExecuteEvent.php
index 894f77868..1e1ff6982 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/Event/QueryPreExecuteEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/Event/QueryPreExecuteEvent.php
@@ -4,7 +4,7 @@ namespace Drupal\search_api_db\Event;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\search_api\Query\QueryInterface;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a query pre-execute event.
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php
index 4e5605bc7..0c2611a87 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php
@@ -2,6 +2,7 @@
namespace Drupal\search_api_db\Plugin\search_api\backend;
+use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
@@ -36,7 +37,7 @@ use Drupal\search_api_db\DatabaseCompatibility\GenericDatabase;
use Drupal\search_api_db\Event\QueryPreExecuteEvent;
use Drupal\search_api_db\Event\SearchApiDbEvents;
use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Indexes and searches items using the database.
@@ -138,7 +139,7 @@ class Database extends BackendPluginBase implements AutocompleteBackendInterface
/**
* The event dispatcher.
*
- * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|null
+ * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface|null
*/
protected $eventDispatcher;
@@ -362,7 +363,7 @@ class Database extends BackendPluginBase implements AutocompleteBackendInterface
/**
* Retrieves the event dispatcher.
*
- * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ * @return \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
* The event dispatcher.
*/
public function getEventDispatcher() {
@@ -372,7 +373,7 @@ class Database extends BackendPluginBase implements AutocompleteBackendInterface
/**
* Sets the event dispatcher.
*
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The new event dispatcher.
*
* @return $this
@@ -920,6 +921,18 @@ class Database extends BackendPluginBase implements AutocompleteBackendInterface
return ['type' => 'int', 'size' => 'tiny'];
default:
+ try {
+ $data_type = $this->getDataTypePluginManager()->createInstance($type);
+ if ($data_type && !$data_type->isDefault()) {
+ $fallback_type = $data_type->getFallbackType();
+ if ($fallback_type != $type) {
+ return $this->sqlType($fallback_type);
+ }
+ }
+ }
+ catch (PluginException $e) {
+ // Ignore.
+ }
throw new SearchApiException("Unknown field type '$type'.");
}
}
@@ -1540,6 +1553,18 @@ class Database extends BackendPluginBase implements AutocompleteBackendInterface
return $value ? 1 : 0;
default:
+ try {
+ $data_type = $this->getDataTypePluginManager()->createInstance($type);
+ if ($data_type && !$data_type->isDefault()) {
+ $fallback_type = $data_type->getFallbackType();
+ if ($fallback_type != $type) {
+ return $this->convert($value, $fallback_type, $original_type, $index);
+ }
+ }
+ }
+ catch (PluginException $e) {
+ // Ignore.
+ }
throw new SearchApiException("Unknown field type '$type'.");
}
}
@@ -1799,7 +1824,7 @@ class Database extends BackendPluginBase implements AutocompleteBackendInterface
// query is constructed from it).
$event_base_name = SearchApiDbEvents::QUERY_PRE_EXECUTE;
$event = new QueryPreExecuteEvent($db_query, $query);
- $this->getEventDispatcher()->dispatch($event_base_name, $event);
+ $this->getEventDispatcher()->dispatch($event, $event_base_name);
$db_query = $event->getDbQuery();
$description = 'This hook is deprecated in search_api:8.x-1.16 and is removed from search_api:2.0.0. Please use the "search_api_db.query_pre_execute" event instead. See https://www.drupal.org/node/3103591';
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml
index cb6c48220..11d66311a 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml
@@ -5,10 +5,10 @@ package: Testing
dependencies:
- search_api:search_api_test_views
- search_api_autocomplete:search_api_autocomplete
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php
index f08340a11..68234e2cf 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php
@@ -4,6 +4,7 @@ namespace Drupal\Tests\search_api_db\Kernel;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\Database as CoreDatabase;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Entity\Server;
@@ -20,6 +21,7 @@ use Drupal\search_api_db\DatabaseCompatibility\GenericDatabase;
use Drupal\search_api_db\Plugin\search_api\backend\Database;
use Drupal\search_api_db\Tests\DatabaseTestsTrait;
use Drupal\Tests\search_api\Kernel\BackendTestBase;
+use Drupal\Tests\search_api\Kernel\TestLogger;
/**
* Tests index and search capabilities using the Database search backend.
@@ -50,6 +52,15 @@ class BackendTest extends BackendTestBase {
*/
protected $indexId = 'database_search_index';
+ /**
+ * The test logger installed in the container.
+ *
+ * Will throw expections whenever a warning or error is logged.
+ *
+ * @var \Drupal\Tests\search_api\Kernel\TestLogger
+ */
+ protected $logger;
+
/**
* {@inheritdoc}
*/
@@ -94,6 +105,19 @@ class BackendTest extends BackendTestBase {
$index->save();
}
+ /**
+ * {@inheritdoc}
+ */
+ public function register(ContainerBuilder $container): void {
+ parent::register($container);
+
+ // Set a logger that will throw exceptions when warnings/errors are logged.
+ $this->logger = new TestLogger('');
+ $container->set('logger.factory', $this->logger);
+ $container->set('logger.channel.search_api', $this->logger);
+ $container->set('logger.channel.search_api_db', $this->logger);
+ }
+
/**
* {@inheritdoc}
*/
@@ -126,6 +150,8 @@ class BackendTest extends BackendTestBase {
$this->regressionTest2873023();
$this->regressionTest3199355();
$this->regressionTest3225675();
+ $this->regressionTest3258802();
+ $this->regressionTest3227268();
}
/**
@@ -699,9 +725,17 @@ class BackendTest extends BackendTestBase {
protected function regressionTest2925464() {
$index = $this->getIndex();
+ // Changing the field type and, thus, column type, will cause a database
+ // error on MySQL and Postgres due to illegal integer values.
+ if (in_array(\Drupal::database()->driver(), ['mysql', 'pgsql'])) {
+ $this->logger->setExpectedErrors(1);
+ }
+
$index->getField('category')->setType('integer');
$index->save();
+ $this->logger->assertAllExpectedErrorsEncountered();
+
$index->getField('category')->setType('string');
$index->save();
@@ -886,6 +920,70 @@ class BackendTest extends BackendTestBase {
$this->indexItems($this->indexId);
}
+ /**
+ * Tests whether unknown field types are handled correctly.
+ *
+ * @see https://www.drupal.org/node/3258802
+ */
+ protected function regressionTest3258802(): void {
+ $this->enableModules(['search_api_test']);
+
+ $index = $this->getIndex();
+ $type_field = $index->getField('type');
+ $this->assertEquals('string', $type_field->getType());
+ $type_field->setType('search_api_test_unsupported');
+ $index->save();
+ // No tasks should have been created.
+ $task_manager = \Drupal::getContainer()->get('search_api.task_manager');
+ $this->assertEquals(0, $task_manager->getTasksCount());
+ // Reindexing should work fine.
+ $index->clear();
+ $this->assertEquals(5, $this->indexItems($this->indexId));
+
+ $results = $index->query()->addCondition('type', 'article')->execute();
+ $this->assertResults([4, 5], $results, 'Search with filter on field with unknown type');
+
+ $index = $this->getIndex();
+ $type_field = $index->getField('type');
+ $this->assertEquals('search_api_test_unsupported', $type_field->getType());
+ $type_field->setType('string');
+ $index->save();
+ // No tasks should have been created.
+ $tasks_count = $task_manager->getTasksCount();
+ $this->assertEquals(0, $tasks_count);
+ $this->indexItems($this->indexId);
+
+ $this->disableModules(['search_api_test']);
+ }
+
+ /**
+ * Tests whether the text table's "item_id" column has the correct collation.
+ *
+ * This check is only active on MySQL.
+ *
+ * @see https://www.drupal.org/node/3227268
+ *
+ * @see \Drupal\search_api_db\DatabaseCompatibility\MySql::alterNewTable()
+ */
+ protected function regressionTest3227268(): void {
+ $database = \Drupal::database();
+ if ($database->driver() !== 'mysql') {
+ return;
+ }
+ $db_info = $this->getIndexDbInfo();
+ $text_table = $db_info['field_tables']['body']['table'];
+ $this->assertTrue(\Drupal::database()->schema()->tableExists($text_table));
+ $sql = "SHOW FULL COLUMNS FROM {{$text_table}}";
+ $collations = [];
+ foreach ($database->query($sql) as $row) {
+ $collations[$row->Field] = $row->Collation;
+ }
+ // Unfortunately, it's not consistent whether the database will report the
+ // collation as "utf8_general_ci" or "utf8mb3_general_ci".
+ $this->assertContains($collations['item_id'], ['utf8mb3_general_ci', 'utf8_general_ci']);
+ $this->assertEquals('utf8mb4_bin', $collations['word']);
+ }
+
/**
* {@inheritdoc}
*/
@@ -904,6 +1002,16 @@ class BackendTest extends BackendTestBase {
return $index;
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function regressionTest2471509(): void {
+ // As this test will log an exception, we need to take that into account.
+ $this->logger->setExpectedErrors(2);
+ parent::regressionTest2471509();
+ $this->logger->assertAllExpectedErrorsEncountered();
+ }
+
/**
* {@inheritdoc}
*/
@@ -1014,7 +1122,7 @@ class BackendTest extends BackendTestBase {
\Drupal::moduleHandler()->alter('search_api_index_items', $index, $items);
$event = new IndexingItemsEvent($index, $items);
\Drupal::getContainer()->get('event_dispatcher')
- ->dispatch(SearchApiEvents::INDEXING_ITEMS, $event);
+ ->dispatch($event, SearchApiEvents::INDEXING_ITEMS);
foreach ($items as $item) {
// This will cache the extracted fields so processors, etc., can retrieve
// them directly.
diff --git a/frontend/drupal9/web/modules/contrib/search_api/search_api.info.yml b/frontend/drupal9/web/modules/contrib/search_api/search_api.info.yml
index da80545f2..2cc0a891d 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/search_api.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/search_api.info.yml
@@ -2,11 +2,11 @@ type: module
name: 'Search API'
description: 'Provides a generic framework for modules offering search capabilities.'
package: Search
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
lifecycle: stable
configure: search_api.overview
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/search_api.install b/frontend/drupal9/web/modules/contrib/search_api/search_api.install
index d89929b03..6eae352d1 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/search_api.install
+++ b/frontend/drupal9/web/modules/contrib/search_api/search_api.install
@@ -12,7 +12,6 @@ use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\search_api\Entity\Server;
-use Drupal\search_api\Entity\TaskStorageSchema;
/**
* Implements hook_schema().
@@ -338,10 +337,8 @@ function search_api_update_8106() {
* Add a unique index to the task entity type's storage.
*/
function search_api_update_8107() {
- $manager = \Drupal::entityDefinitionUpdateManager();
- $entity_type = $manager->getEntityType('search_api_task');
- $entity_type->setHandlerClass('storage_schema', TaskStorageSchema::class);
- $manager->updateEntityType($entity_type);
+ // This function body was removed since it was out-dated.
+ // See search_api_update_8110().
}
/**
@@ -406,6 +403,16 @@ function search_api_update_8109(): MarkupInterface {
function search_api_update_8110() {
$manager = \Drupal::entityDefinitionUpdateManager();
$entity_type = $manager->getEntityType('search_api_task');
+ // Apparently, getEntityType() can return NULL under some circumstances.
+ if (!$entity_type) {
+ return;
+ }
+ // Do not bother resetting the storage schema handler in case it was not set
+ // in the first place.
+ $handler = $entity_type->getHandlerClass('storage_schema');
+ if (in_array($handler, [SqlContentEntityStorageSchema::class, NULL], TRUE)) {
+ return;
+ }
$entity_type->setHandlerClass('storage_schema', SqlContentEntityStorageSchema::class);
$manager->updateEntityType($entity_type);
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/search_api.module b/frontend/drupal9/web/modules/contrib/search_api/search_api.module
index 904248478..ca17f6878 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/search_api.module
+++ b/frontend/drupal9/web/modules/contrib/search_api/search_api.module
@@ -420,23 +420,31 @@ function search_api_views_plugins_filter_alter(array &$plugins) {
*/
function search_api_view_insert(ViewEntityInterface $view) {
_search_api_view_crud_event($view);
+}
- // Disable Views' default caching mechanisms on Search API views.
- $displays = $view->get('display');
- if ($displays['default']['display_options']['query']['type'] === 'search_api_query') {
- $change = FALSE;
+/**
+ * Implements hook_ENTITY_TYPE_presave() for type "view".
+ */
+function search_api_view_presave(ViewEntityInterface $view) {
+ // Set query type to "search_api_query" and disable Views' default caching
+ // mechanisms on Search API views.
+ if (SearchApiQuery::getIndexFromTable($view->get('base_table'))) {
+ $displays = $view->get('display');
+ $changed_cache = FALSE;
foreach ($displays as $id => $display) {
+ if (($display['display_options']['query']['type'] ?? '') === 'views_query') {
+ $displays[$id]['display_options']['query']['type'] = 'search_api_query';
+ }
if (in_array($display['display_options']['cache']['type'] ?? '', ['tag', 'time'])) {
$displays[$id]['display_options']['cache']['type'] = 'none';
- $change = TRUE;
+ $changed_cache = TRUE;
}
}
+ $view->set('display', $displays);
- if ($change) {
+ if ($changed_cache) {
$warning = t('The selected caching mechanism does not work with views on Search API indexes. Please either use one of the Search API-specific caching options or "None". Caching was turned off for this view.');
\Drupal::messenger()->addWarning($warning);
- $view->set('display', $displays);
- $view->save();
}
}
}
@@ -607,9 +615,10 @@ function search_api_form_views_exposed_form_alter(&$form, FormStateInterface $fo
continue;
}
// Add a hidden form field for the facet parameter.
- $form["{$filter_key}[$key]"] = [
+ $form[$filter_key][$key] = [
'#type' => 'hidden',
'#value' => $value,
+ '#name' => "{$filter_key}[$key]",
];
}
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/search_api.post_update.php b/frontend/drupal9/web/modules/contrib/search_api/search_api.post_update.php
index 2419308b9..096fc1d0d 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/search_api.post_update.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/search_api.post_update.php
@@ -17,3 +17,50 @@ function search_api_post_update_fix_index_dependencies(&$sandbox = NULL) {
return TRUE;
});
}
+
+/**
+ * Update Search API views to use the correct query type.
+ *
+ * In some cases, Views creates Search API views with the default "views_query"
+ * query type instead of "search_api_query".
+ */
+function search_api_post_update_views_query_type() {
+ $config_factory = \Drupal::configFactory();
+ $changed_cache = [];
+
+ foreach ($config_factory->listAll('views.view.') as $view_config_name) {
+ $view = $config_factory->getEditable($view_config_name);
+ if (substr($view->get('base_table'), 0, 17) == 'search_api_index_') {
+ $displays = $view->get('display');
+
+ $update_query = $update_cache = FALSE;
+ foreach ($displays as $id => $display) {
+ if (($display['display_options']['query']['type'] ?? '') === 'views_query') {
+ $displays[$id]['display_options']['query']['type'] = 'search_api_query';
+ $update_query = TRUE;
+ }
+ if (in_array($display['display_options']['cache']['type'] ?? '', ['tag', 'time'])) {
+ $displays[$id]['display_options']['cache']['type'] = 'none';
+ $update_cache = TRUE;
+ }
+ }
+
+ if ($update_query || $update_cache) {
+ $view->set('display', $displays);
+ // Mark the resulting configuration as trusted data. This avoids issues
+ // with future schema changes.
+ $view->save(TRUE);
+ if ($update_cache) {
+ $changed_cache[] = $view->get('id');
+ }
+ }
+ }
+ }
+
+ if ($changed_cache) {
+ $vars = ['@ids' => implode(', ', array_unique($changed_cache))];
+ return t('The following views have had caching switched off. The selected caching mechanism does not work with views on Search API indexes. Please either use one of the Search API-specific caching options or "None": @ids.', $vars);
+ }
+
+ return NULL;
+}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/search_api.views.inc b/frontend/drupal9/web/modules/contrib/search_api/search_api.views.inc
index 04b601150..4714a9998 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/search_api.views.inc
+++ b/frontend/drupal9/web/modules/contrib/search_api/search_api.views.inc
@@ -407,7 +407,7 @@ function _search_api_views_handler_mapping() {
$event = new MappingViewsHandlersEvent($mapping);
\Drupal::getContainer()->get('event_dispatcher')
- ->dispatch(SearchApiEvents::MAPPING_VIEWS_HANDLERS, $event);
+ ->dispatch($event, SearchApiEvents::MAPPING_VIEWS_HANDLERS);
}
return $mapping;
@@ -515,6 +515,14 @@ function _search_api_views_data_special_fields(array &$table, IndexInterface $in
$table[$language_field]['real field'] = 'search_api_language';
}
+ $url_field = _search_api_views_find_field_alias('search_api_url', $table);
+ $table[$url_field]['title'] = t('Item URL');
+ $table[$url_field]['help'] = t("The item's URL");
+ $table[$url_field]['field']['id'] = 'search_api';
+ if ($url_field != 'search_api_url') {
+ $table[$url_field]['real field'] = 'search_api_url';
+ }
+
$relevance_field = _search_api_views_find_field_alias('search_api_relevance', $table);
$table[$relevance_field]['group'] = t('Search');
$table[$relevance_field]['title'] = t('Relevance');
@@ -990,7 +998,7 @@ function _search_api_views_get_field_handler_mapping() {
$event = new MappingViewsFieldHandlersEvent($plain_mapping);
\Drupal::getContainer()->get('event_dispatcher')
- ->dispatch(SearchApiEvents::MAPPING_VIEWS_FIELD_HANDLERS, $event);
+ ->dispatch($event, SearchApiEvents::MAPPING_VIEWS_FIELD_HANDLERS);
// Then create a new, more practical structure, with the mappings grouped by
// mapping type.
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Backend/BackendPluginManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/Backend/BackendPluginManager.php
index 4b5d82701..135e42b48 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Backend/BackendPluginManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Backend/BackendPluginManager.php
@@ -7,7 +7,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\search_api\Annotation\SearchApiBackend;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\SearchApiPluginManager;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Manages search backend plugins.
@@ -29,7 +29,7 @@ class BackendPluginManager extends SearchApiPluginManager {
* The cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EventDispatcherInterface $eventDispatcher) {
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Commands/SearchApiCommands.php b/frontend/drupal9/web/modules/contrib/search_api/src/Commands/SearchApiCommands.php
index cfe152959..978042cce 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Commands/SearchApiCommands.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Commands/SearchApiCommands.php
@@ -9,7 +9,7 @@ use Drupal\search_api\Contrib\RowsOfMultiValueFields;
use Drupal\search_api\Utility\CommandHelper;
use Drush\Commands\DrushCommands;
use Psr\Log\LoggerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Defines Drush commands for the Search API.
@@ -30,7 +30,7 @@ class SearchApiCommands extends DrushCommands {
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/DataType/DataTypePluginManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/DataType/DataTypePluginManager.php
index 7ebefcc24..f4de2b38c 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/DataType/DataTypePluginManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/DataType/DataTypePluginManager.php
@@ -6,7 +6,7 @@ use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\SearchApiPluginManager;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Manages data type plugins.
@@ -47,7 +47,7 @@ class DataTypePluginManager extends SearchApiPluginManager {
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EventDispatcherInterface $eventDispatcher) {
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Datasource/DatasourcePluginManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/Datasource/DatasourcePluginManager.php
index 7c8243c7b..49641fde8 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Datasource/DatasourcePluginManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Datasource/DatasourcePluginManager.php
@@ -6,7 +6,7 @@ use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\SearchApiPluginManager;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Manages datasource plugins.
@@ -28,7 +28,7 @@ class DatasourcePluginManager extends SearchApiPluginManager {
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EventDispatcherInterface $eventDispatcher) {
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Display/DisplayPluginManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/Display/DisplayPluginManager.php
index b15772e82..f87a6c520 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Display/DisplayPluginManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Display/DisplayPluginManager.php
@@ -6,7 +6,7 @@ use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\SearchApiPluginManager;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Manages display plugins.
@@ -37,7 +37,7 @@ class DisplayPluginManager extends SearchApiPluginManager implements DisplayPlug
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EventDispatcherInterface $eventDispatcher) {
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Entity/Index.php b/frontend/drupal9/web/modules/contrib/search_api/src/Entity/Index.php
index ae0473903..500135473 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Entity/Index.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Entity/Index.php
@@ -968,7 +968,7 @@ class Index extends ConfigEntityBase implements IndexInterface {
\Drupal::moduleHandler()->alterDeprecated($description, 'search_api_index_items', $this, $items);
$event = new IndexingItemsEvent($this, $items);
\Drupal::getContainer()->get('event_dispatcher')
- ->dispatch(SearchApiEvents::INDEXING_ITEMS, $event);
+ ->dispatch($event, SearchApiEvents::INDEXING_ITEMS);
$items = $event->getItems();
foreach ($items as $item) {
// This will cache the extracted fields so processors, etc., can retrieve
@@ -1010,7 +1010,7 @@ class Index extends ConfigEntityBase implements IndexInterface {
\Drupal::moduleHandler()->invokeAllDeprecated($description, 'search_api_items_indexed', [$this, $processed_ids]);
$dispatcher = \Drupal::getContainer()->get('event_dispatcher');
- $dispatcher->dispatch(SearchApiEvents::ITEMS_INDEXED, new ItemsIndexedEvent($this, $processed_ids));
+ $dispatcher->dispatch(new ItemsIndexedEvent($this, $processed_ids), SearchApiEvents::ITEMS_INDEXED);
// Clear search api list caches.
Cache::invalidateTags(['search_api_list:' . $this->id]);
@@ -1125,9 +1125,9 @@ class Index extends ConfigEntityBase implements IndexInterface {
$this->getTrackerInstance()->trackAllItemsUpdated();
$description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866';
\Drupal::moduleHandler()->invokeAllDeprecated($description, 'search_api_index_reindex', [$this, FALSE]);
- /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */
+ /** @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $dispatcher */
$dispatcher = \Drupal::getContainer()->get('event_dispatcher');
- $dispatcher->dispatch(SearchApiEvents::REINDEX_SCHEDULED, new ReindexScheduledEvent($this, FALSE));
+ $dispatcher->dispatch(new ReindexScheduledEvent($this, FALSE), SearchApiEvents::REINDEX_SCHEDULED);
}
}
@@ -1154,9 +1154,9 @@ class Index extends ConfigEntityBase implements IndexInterface {
$description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866';
\Drupal::moduleHandler()->invokeAllDeprecated($description, 'search_api_index_reindex', [$this, !$this->isReadOnly()]);
- /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */
+ /** @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $dispatcher */
$dispatcher = \Drupal::getContainer()->get('event_dispatcher');
- $dispatcher->dispatch(SearchApiEvents::REINDEX_SCHEDULED, new ReindexScheduledEvent($this, !$this->isReadOnly()));
+ $dispatcher->dispatch(new ReindexScheduledEvent($this, !$this->isReadOnly()), SearchApiEvents::REINDEX_SCHEDULED);
}
}
@@ -1176,9 +1176,9 @@ class Index extends ConfigEntityBase implements IndexInterface {
$description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.reindex_scheduled" event instead. See https://www.drupal.org/node/3059866';
\Drupal::moduleHandler()
->invokeAllDeprecated($description, 'search_api_index_reindex', [$this, FALSE]);
- /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */
+ /** @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $dispatcher */
$dispatcher = \Drupal::getContainer()->get('event_dispatcher');
- $dispatcher->dispatch(SearchApiEvents::REINDEX_SCHEDULED, new ReindexScheduledEvent($this, FALSE));
+ $dispatcher->dispatch(new ReindexScheduledEvent($this, FALSE), SearchApiEvents::REINDEX_SCHEDULED);
$index_task_manager->addItemsBatch($this);
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Entity/Server.php b/frontend/drupal9/web/modules/contrib/search_api/src/Entity/Server.php
index 3f2b06d2a..1cc5820da 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Entity/Server.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Entity/Server.php
@@ -217,9 +217,9 @@ class Server extends ConfigEntityBase implements ServerInterface {
$description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.determining_server_features" event instead. See https://www.drupal.org/node/3059866';
\Drupal::moduleHandler()
->alterDeprecated($description, 'search_api_server_features', $this->features, $this);
- /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher */
+ /** @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher */
$eventDispatcher = \Drupal::getContainer()->get('event_dispatcher');
- $eventDispatcher->dispatch(SearchApiEvents::DETERMINING_SERVER_FEATURES, new DeterminingServerFeaturesEvent($this->features, $this));
+ $eventDispatcher->dispatch(new DeterminingServerFeaturesEvent($this->features, $this), SearchApiEvents::DETERMINING_SERVER_FEATURES);
}
return $this->features;
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Entity/TaskStorageSchema.php b/frontend/drupal9/web/modules/contrib/search_api/src/Entity/TaskStorageSchema.php
index 72f809d73..c09e72a3a 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Entity/TaskStorageSchema.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Entity/TaskStorageSchema.php
@@ -4,7 +4,6 @@ namespace Drupal\search_api\Entity;
@trigger_error('\Drupal\search_api\Entity\TaskStorageSchema is deprecated in search_api:8.x-1.23 and is removed from search_api:2.0.0. There is no replacement. See https://www.drupal.org/node/3247781.', E_USER_DEPRECATED);
-use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
/**
@@ -15,38 +14,4 @@ use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
*
* @see https://www.drupal.org/node/3247781
*/
-class TaskStorageSchema extends SqlContentEntityStorageSchema {
-
- /**
- * {@inheritdoc}
- */
- protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE): array {
- $schema = parent::getEntitySchema($entity_type, $reset);
-
- $data_table = $this->storage->getBaseTable();
- if ($data_table) {
- $column = 'data';
- // MySQL cannot handle UNIQUE indices on TEXT/BLOB fields without a prefix
- // length.
- if ($this->database->driver() === 'mysql') {
- // From the MySQL documentation:
- // https://dev.mysql.com/doc/refman/8.0/en/innodb-limits.html
- //
- // The index key prefix length limit is 767 bytes for InnoDB tables that
- // use the REDUNDANT or COMPACT row format. For example, you might hit
- // this limit with a column prefix index of more than 191 characters on
- // a TEXT or VARCHAR column, assuming a utf8mb4 character set and the
- // maximum of 4 bytes for each character.
- //
- // To be on the safe side let's assume utf8mb4 character set.
- $column = ['data', 191];
- }
- $schema[$data_table]['unique keys'] += [
- 'task__unique' => ['type', 'server_id', 'index_id', $column],
- ];
- }
-
- return $schema;
- }
-
-}
+class TaskStorageSchema extends SqlContentEntityStorageSchema {}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/DeterminingServerFeaturesEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/DeterminingServerFeaturesEvent.php
index 5ec6917ce..223b6a428 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/DeterminingServerFeaturesEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/DeterminingServerFeaturesEvent.php
@@ -3,7 +3,7 @@
namespace Drupal\search_api\Event;
use Drupal\search_api\ServerInterface;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a determining server features event.
@@ -43,7 +43,7 @@ final class DeterminingServerFeaturesEvent extends Event {
* @return array
* Reference to the features supported by the server's backend.
*/
- public function &getFeatures() {
+ public function &getFeatures(): array {
return $this->features;
}
@@ -53,7 +53,7 @@ final class DeterminingServerFeaturesEvent extends Event {
* @return \Drupal\search_api\ServerInterface
* The search server in question.
*/
- public function getServer() {
+ public function getServer(): ServerInterface {
return $this->server;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/GatheringPluginInfoEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/GatheringPluginInfoEvent.php
index d2768819a..3f36ab57e 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/GatheringPluginInfoEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/GatheringPluginInfoEvent.php
@@ -2,7 +2,7 @@
namespace Drupal\search_api\Event;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a gathering of plugin information event.
@@ -32,7 +32,7 @@ final class GatheringPluginInfoEvent extends Event {
* @return array[]
* The plugin definitions collected so far, keyed by plugin ID.
*/
- public function &getDefinitions() {
+ public function &getDefinitions(): array {
return $this->definitions;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/IndexingItemsEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/IndexingItemsEvent.php
index 56f1bf84a..2418fa874 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/IndexingItemsEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/IndexingItemsEvent.php
@@ -3,7 +3,7 @@
namespace Drupal\search_api\Event;
use Drupal\search_api\IndexInterface;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps an indexing items event.
@@ -43,7 +43,7 @@ final class IndexingItemsEvent extends Event {
* @return \Drupal\search_api\IndexInterface
* The index on which items will be indexed.
*/
- public function getIndex() {
+ public function getIndex(): IndexInterface {
return $this->index;
}
@@ -53,7 +53,7 @@ final class IndexingItemsEvent extends Event {
* @return \Drupal\search_api\Item\ItemInterface[]
* The items that will be indexed.
*/
- public function getItems() {
+ public function getItems(): array {
return $this->items;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/ItemsIndexedEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/ItemsIndexedEvent.php
index 70afe5c1e..77244f1c0 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/ItemsIndexedEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/ItemsIndexedEvent.php
@@ -3,7 +3,7 @@
namespace Drupal\search_api\Event;
use Drupal\search_api\IndexInterface;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps an items indexed event.
@@ -43,7 +43,7 @@ final class ItemsIndexedEvent extends Event {
* @return \Drupal\search_api\IndexInterface
* The used index.
*/
- public function getIndex() {
+ public function getIndex(): IndexInterface {
return $this->index;
}
@@ -53,7 +53,7 @@ final class ItemsIndexedEvent extends Event {
* @return int[]
* An array containing the successfully indexed items' IDs.
*/
- public function getProcessedIds() {
+ public function getProcessedIds(): array {
return $this->processedIds;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingFieldTypesEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingFieldTypesEvent.php
index bcb66ce70..676c9028a 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingFieldTypesEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingFieldTypesEvent.php
@@ -2,7 +2,7 @@
namespace Drupal\search_api\Event;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a field types mapped event.
@@ -34,7 +34,7 @@ final class MappingFieldTypesEvent extends Event {
* corresponding Search API data types. A value of FALSE means that fields
* of that type should be ignored by the Search API.
*/
- public function &getFieldTypeMapping() {
+ public function &getFieldTypeMapping(): array {
return $this->fieldTypeMapping;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingForeignRelationshipsEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingForeignRelationshipsEvent.php
index 17216973a..4f6464c9d 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingForeignRelationshipsEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingForeignRelationshipsEvent.php
@@ -6,7 +6,7 @@ namespace Drupal\search_api\Event;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\search_api\IndexInterface;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a foreign relationships mapping event.
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingViewsFieldHandlersEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingViewsFieldHandlersEvent.php
index 8262d62ba..894acc20d 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingViewsFieldHandlersEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingViewsFieldHandlersEvent.php
@@ -2,7 +2,7 @@
namespace Drupal\search_api\Event;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a mapping Views field handlers event.
@@ -40,7 +40,7 @@ final class MappingViewsFieldHandlersEvent extends Event {
* tried before shorter ones. The "*" mapping therefore is the default if no
* other match could be found.
*/
- public function &getFieldHandlerMapping() {
+ public function &getFieldHandlerMapping(): array {
return $this->fieldHandlerMapping;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingViewsHandlersEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingViewsHandlersEvent.php
index bca06f05f..1a35b8764 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingViewsHandlersEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/MappingViewsHandlersEvent.php
@@ -2,7 +2,7 @@
namespace Drupal\search_api\Event;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a mapping Views handlers event.
@@ -37,7 +37,7 @@ final class MappingViewsHandlersEvent extends Event {
* "entity:ENTITY_TYPE" (with "ENTITY_TYPE" being the machine name of an
* entity type) for entities of that type.
*/
- public function &getHandlerMapping() {
+ public function &getHandlerMapping(): array {
return $this->handlerMapping;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/ProcessingResultsEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/ProcessingResultsEvent.php
index 9386d8755..31ca9b318 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/ProcessingResultsEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/ProcessingResultsEvent.php
@@ -3,7 +3,7 @@
namespace Drupal\search_api\Event;
use Drupal\search_api\Query\ResultSetInterface;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a processing results event.
@@ -33,7 +33,7 @@ final class ProcessingResultsEvent extends Event {
* @return \Drupal\search_api\Query\ResultSetInterface
* The search results to alter.
*/
- public function getResults() {
+ public function getResults(): ResultSetInterface {
return $this->results;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/QueryPreExecuteEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/QueryPreExecuteEvent.php
index c146053f6..fc9a4106a 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/QueryPreExecuteEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/QueryPreExecuteEvent.php
@@ -3,7 +3,7 @@
namespace Drupal\search_api\Event;
use Drupal\search_api\Query\QueryInterface;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a query pre-execute event.
@@ -33,7 +33,7 @@ final class QueryPreExecuteEvent extends Event {
* @return \Drupal\search_api\Query\QueryInterface
* The created query.
*/
- public function getQuery() {
+ public function getQuery(): QueryInterface {
return $this->query;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/ReindexScheduledEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/ReindexScheduledEvent.php
index 55c046e08..d4a1fe635 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/ReindexScheduledEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/ReindexScheduledEvent.php
@@ -3,7 +3,7 @@
namespace Drupal\search_api\Event;
use Drupal\search_api\IndexInterface;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Wraps a reindex scheduled event.
@@ -32,7 +32,7 @@ final class ReindexScheduledEvent extends Event {
* @param bool $clear
* Boolean indicating whether the index was also cleared.
*/
- public function __construct(IndexInterface $index, $clear) {
+ public function __construct(IndexInterface $index, bool $clear) {
$this->index = $index;
$this->clear = $clear;
}
@@ -43,7 +43,7 @@ final class ReindexScheduledEvent extends Event {
* @return \Drupal\search_api\IndexInterface
* The index scheduled for reindexing.
*/
- public function getIndex() {
+ public function getIndex(): IndexInterface {
return $this->index;
}
@@ -54,7 +54,7 @@ final class ReindexScheduledEvent extends Event {
* TRUE if the index was also cleared as part of the reindexing, FALSE
* otherwise.
*/
- public function isClear() {
+ public function isClear(): bool {
return $this->clear;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Event/SearchApiEvents.php b/frontend/drupal9/web/modules/contrib/search_api/src/Event/SearchApiEvents.php
index 9a13c1839..4049e483b 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Event/SearchApiEvents.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Event/SearchApiEvents.php
@@ -19,7 +19,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\DeterminingServerFeaturesEvent
*/
- const DETERMINING_SERVER_FEATURES = 'search_api.determining_server_features';
+ public const DETERMINING_SERVER_FEATURES = 'search_api.determining_server_features';
/**
* The name of the event fired when gathering backend plugins.
@@ -28,7 +28,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\GatheringPluginInfoEvent
*/
- const GATHERING_BACKENDS = 'search_api.gathering_backends';
+ public const GATHERING_BACKENDS = 'search_api.gathering_backends';
/**
* The name of the event fired when gathering datasource plugins.
@@ -37,7 +37,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\GatheringPluginInfoEvent
*/
- const GATHERING_DATA_SOURCES = 'search_api.gathering_data_sources';
+ public const GATHERING_DATA_SOURCES = 'search_api.gathering_data_sources';
/**
* The name of the event fired when gathering data type plugins.
@@ -46,7 +46,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\GatheringPluginInfoEvent
*/
- const GATHERING_DATA_TYPES = 'search_api.gathering_data_types';
+ public const GATHERING_DATA_TYPES = 'search_api.gathering_data_types';
/**
* The name of the event fired when gathering display plugins.
@@ -55,7 +55,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\GatheringPluginInfoEvent
*/
- const GATHERING_DISPLAYS = 'search_api.gathering_displays';
+ public const GATHERING_DISPLAYS = 'search_api.gathering_displays';
/**
* The name of the event fired when gathering parse mode plugins.
@@ -64,7 +64,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\GatheringPluginInfoEvent
*/
- const GATHERING_PARSE_MODES = 'search_api.gathering_parse_modes';
+ public const GATHERING_PARSE_MODES = 'search_api.gathering_parse_modes';
/**
* The name of the event fired when gathering processor plugins.
@@ -73,7 +73,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\GatheringPluginInfoEvent
*/
- const GATHERING_PROCESSORS = 'search_api.gathering_processors';
+ public const GATHERING_PROCESSORS = 'search_api.gathering_processors';
/**
* The name of the event fired when gathering tracker plugins.
@@ -82,7 +82,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\GatheringPluginInfoEvent
*/
- const GATHERING_TRACKERS = 'search_api.gathering_trackers';
+ public const GATHERING_TRACKERS = 'search_api.gathering_trackers';
/**
* The name of the event fired when preparing items for indexing.
@@ -100,7 +100,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\IndexingItemsEvent
*/
- const INDEXING_ITEMS = 'search_api.indexing_items';
+ public const INDEXING_ITEMS = 'search_api.indexing_items';
/**
* The name of the event fired when items have been successfully indexed.
@@ -109,7 +109,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\ItemsIndexedEvent
*/
- const ITEMS_INDEXED = 'search_api.items_indexed';
+ public const ITEMS_INDEXED = 'search_api.items_indexed';
/**
* The name of the event fired when mapping data types.
@@ -122,7 +122,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\MappingFieldTypesEvent
*/
- const MAPPING_FIELD_TYPES = 'search_api.mapping_field_types';
+ public const MAPPING_FIELD_TYPES = 'search_api.mapping_field_types';
/**
* The name of the event fired when mapping foreign relationships of an index.
@@ -137,7 +137,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\MappingForeignRelationshipsEvent
*/
- const MAPPING_FOREIGN_RELATIONSHIPS = 'search_api.mapping_foreign_relationships';
+ public const MAPPING_FOREIGN_RELATIONSHIPS = 'search_api.mapping_foreign_relationships';
/**
* The name of the event fired when building a map of Views field handlers.
@@ -154,7 +154,7 @@ final class SearchApiEvents {
* @see \Drupal\search_api\Event\MappingViewsFieldHandlersEvent
* @see _search_api_views_get_field_handler_mapping()
*/
- const MAPPING_VIEWS_FIELD_HANDLERS = 'search_api.mapping_views_field_handlers';
+ public const MAPPING_VIEWS_FIELD_HANDLERS = 'search_api.mapping_views_field_handlers';
/**
* The name of the event fired when building a map of Views handlers.
@@ -171,7 +171,7 @@ final class SearchApiEvents {
* @see \Drupal\search_api\Event\MappingViewsFieldHandlersEvent
* @see _search_api_views_handler_mapping()
*/
- const MAPPING_VIEWS_HANDLERS = 'search_api.mapping_views_handlers';
+ public const MAPPING_VIEWS_HANDLERS = 'search_api.mapping_views_handlers';
/**
* The name of the event fired after a search has been executed on the server.
@@ -182,7 +182,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\ProcessingResultsEvent
*/
- const PROCESSING_RESULTS = 'search_api.processing_results';
+ public const PROCESSING_RESULTS = 'search_api.processing_results';
/**
* The name of the event fired before executing a search query.
@@ -194,7 +194,7 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\QueryPreExecuteEvent
*/
- const QUERY_PRE_EXECUTE = 'search_api.query_pre_execute';
+ public const QUERY_PRE_EXECUTE = 'search_api.query_pre_execute';
/**
* The name of the event fired when scheduling an index for re-indexing.
@@ -207,6 +207,6 @@ final class SearchApiEvents {
*
* @see \Drupal\search_api\Event\ReindexScheduledEvent
*/
- const REINDEX_SCHEDULED = 'search_api.reindex_scheduled';
+ public const REINDEX_SCHEDULED = 'search_api.reindex_scheduled';
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/IndexBatchHelper.php b/frontend/drupal9/web/modules/contrib/search_api/src/IndexBatchHelper.php
index f531e2a88..28e447392 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/IndexBatchHelper.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/IndexBatchHelper.php
@@ -156,7 +156,7 @@ class IndexBatchHelper {
$context['results']['indexed'] += $indexed;
// Display progress message.
if ($indexed > 0) {
- $context['message'] = static::formatPlural($context['results']['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.');
+ $context['message'] = static::formatPlural($context['results']['indexed'], 'Successfully indexed 1 item on @index.', 'Successfully indexed @count items on @index.', ['@index' => $index->label()]);
}
// Everything has been indexed?
if ($indexed === 0 || $context['results']['indexed'] >= $context['sandbox']['original_item_count']) {
@@ -170,7 +170,7 @@ class IndexBatchHelper {
catch (\Exception $e) {
// Log exception to watchdog and abort the batch job.
watchdog_exception('search_api', $e);
- $context['message'] = static::t('An error occurred during indexing: @message', ['@message' => $e->getMessage()]);
+ $context['message'] = static::t('An error occurred during indexing on @index: @message', ['@index' => $index->label(), '@message' => $e->getMessage()]);
$context['finished'] = 1;
$context['results']['not indexed'] = $context['sandbox']['original_item_count'] - $context['results']['indexed'];
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/ParseMode/ParseModePluginManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/ParseMode/ParseModePluginManager.php
index 5f4b6ff4e..35151ef18 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/ParseMode/ParseModePluginManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/ParseMode/ParseModePluginManager.php
@@ -6,7 +6,7 @@ use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\SearchApiPluginManager;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Manages parse mode plugins.
@@ -28,7 +28,7 @@ class ParseModePluginManager extends SearchApiPluginManager {
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EventDispatcherInterface $eventDispatcher) {
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/datasource/ContentEntity.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/datasource/ContentEntity.php
index 283e20ac4..d401da395 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/datasource/ContentEntity.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/datasource/ContentEntity.php
@@ -672,9 +672,10 @@ class ContentEntity extends DatasourcePluginBase implements PluginFormInterface
*/
public function getItemId(ComplexDataInterface $item) {
if ($entity = $this->getEntity($item)) {
- $enabled_bundles = $this->getBundles();
- if (isset($enabled_bundles[$entity->bundle()])) {
- return $entity->id() . ':' . $entity->language()->getId();
+ $langcode = $entity->language()->getId();
+ if (isset($this->getBundles()[$entity->bundle()])
+ && isset($this->getLanguages()[$langcode])) {
+ return $entity->id() . ':' . $langcode;
}
}
return NULL;
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/datasource/ContentEntityTrackingManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/datasource/ContentEntityTrackingManager.php
index 5828285b5..957aaa6da 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/datasource/ContentEntityTrackingManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/datasource/ContentEntityTrackingManager.php
@@ -158,13 +158,21 @@ class ContentEntityTrackingManager {
foreach ($indexes as $index) {
if ($inserted_ids) {
$filtered_item_ids = static::filterValidItemIds($index, $datasource_id, $inserted_ids);
- $index->trackItemsInserted($datasource_id, $filtered_item_ids);
+ if ($filtered_item_ids) {
+ $index->trackItemsInserted($datasource_id, $filtered_item_ids);
+ }
}
if ($updated_ids) {
- $index->trackItemsUpdated($datasource_id, $updated_ids);
+ $filtered_item_ids = static::filterValidItemIds($index, $datasource_id, $updated_ids);
+ if ($filtered_item_ids) {
+ $index->trackItemsUpdated($datasource_id, $filtered_item_ids);
+ }
}
if ($deleted_ids) {
- $index->trackItemsDeleted($datasource_id, $deleted_ids);
+ $filtered_item_ids = static::filterValidItemIds($index, $datasource_id, $deleted_ids);
+ if ($filtered_item_ids) {
+ $index->trackItemsDeleted($datasource_id, $filtered_item_ids);
+ }
}
}
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/RenderedItem.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/RenderedItem.php
index 0a5859ae6..3ca7c56bf 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/RenderedItem.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/RenderedItem.php
@@ -16,6 +16,7 @@ use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\LoggerTrait;
use Drupal\search_api\Plugin\search_api\processor\Property\RenderedItemProperty;
use Drupal\search_api\Processor\ProcessorPluginBase;
+use Drupal\user\RoleInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -256,10 +257,18 @@ class RenderedItem extends ProcessorPluginBase {
foreach ($fields as $field) {
$configuration = $field->getConfiguration();
+ // If a (non-anonymous) role is selected, then also add the authenticated
+ // user role.
+ $roles = $configuration['roles'];
+ $authenticated = RoleInterface::AUTHENTICATED_ID;
+ if (array_diff($roles, [$authenticated, RoleInterface::ANONYMOUS_ID])) {
+ $roles[$authenticated] = $authenticated;
+ }
+
// Change the current user to our dummy implementation to ensure we are
// using the configured roles.
$this->getAccountSwitcher()
- ->switchTo(new UserSession(['roles' => $configuration['roles']]));
+ ->switchTo(new UserSession(['roles' => array_values($roles)]));
$datasource_id = $item->getDatasourceId();
$datasource = $item->getDatasource();
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/Tokenizer.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/Tokenizer.php
index ba557d542..0949f221b 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/Tokenizer.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/Tokenizer.php
@@ -78,7 +78,7 @@ class Tokenizer extends FieldsProcessorPluginBase {
$form['ignored'] = [
'#type' => 'textfield',
'#title' => $this->t('Ignored characters'),
- '#description' => $this->t('Specify the characters that should be removed prior to processing. Dots, dashes, and underscores are ignored by default to allow meaningful search behavior with acronyms and URLs. Specify the characters as the inside of a PCRE character class .', $args),
+ '#description' => $this->t('Specify the characters that should be removed prior to processing, as the inside of a PCRE character class .', $args),
'#default_value' => $this->configuration['ignored'],
];
@@ -256,11 +256,13 @@ class Tokenizer extends FieldsProcessorPluginBase {
// Readable regular expression: "([number]+)[punctuation]+(?=[number])".
$text = preg_replace('/([' . $this->getPregClassNumbers() . ']+)[' . $this->getPregClassPunctuation() . ']+(?=[' . $this->getPregClassNumbers() . '])/u', '\1', $text);
- // A group of multiple ignored characters is still treated as whitespace.
- $text = preg_replace('/[' . $this->ignored . ']{2,}/u', ' ', $text);
+ if ($this->ignored !== '') {
+ // A group of multiple ignored characters is still treated as whitespace.
+ $text = preg_replace('/[' . $this->ignored . ']{2,}/u', ' ', $text);
- // Remove all other instances of ignored characters.
- $text = preg_replace('/[' . $this->ignored . ']+/u', '', $text);
+ // Remove all other instances of ignored characters.
+ $text = preg_replace('/[' . $this->ignored . ']+/u', '', $text);
+ }
// Finally, convert all characters we want to treat as word boundaries to
// plain spaces.
@@ -337,7 +339,7 @@ class Tokenizer extends FieldsProcessorPluginBase {
}
/**
- * Prepares the processor by setting the $spaces property.
+ * Prepares the processor by setting the $spaces and $ignored properties.
*/
protected function prepare() {
if (!isset($this->spaces)) {
@@ -349,12 +351,7 @@ class Tokenizer extends FieldsProcessorPluginBase {
}
}
if (!isset($this->ignored)) {
- if ($this->configuration['ignored'] !== '') {
- $this->ignored = str_replace('/', '\/', $this->configuration['ignored']);
- }
- else {
- $this->ignored = '._-';
- }
+ $this->ignored = str_replace('/', '\/', $this->configuration['ignored']);
}
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/TypeBoost.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/TypeBoost.php
index 37966f083..c911e4e25 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/TypeBoost.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/search_api/processor/TypeBoost.php
@@ -58,6 +58,7 @@ class TypeBoost extends ProcessorPluginBase implements PluginFormInterface {
'' => $this->t('Use datasource default'),
] + $boost_factors;
foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+ $datasource_config = $datasource_configurations[$datasource_id];
$form['boosts'][$datasource_id] = [
'#type' => 'details',
'#title' => $this->t('Boost settings for %datasource', ['%datasource' => $datasource->label()]),
@@ -67,7 +68,7 @@ class TypeBoost extends ProcessorPluginBase implements PluginFormInterface {
'#title' => $this->t('Default boost for items from this datasource'),
'#options' => $boost_factors,
'#description' => $this->t('A boost of 1.00 is the default. Assign a boost of 0.00 to not score the item at all.'),
- '#default_value' => $datasource_configurations[$datasource_id]['datasource_boost'],
+ '#default_value' => Utility::formatBoostFactor($datasource_config['datasource_boost']),
],
];
@@ -80,9 +81,12 @@ class TypeBoost extends ProcessorPluginBase implements PluginFormInterface {
unset($bundles[$datasource_id], $bundles[$datasource->getEntityTypeId()]);
}
- $bundle_boosts = $datasource_configurations[$datasource_id]['bundle_boosts'];
+ $bundle_boosts = $datasource_config['bundle_boosts'];
foreach ($bundles as $bundle => $bundle_label) {
- $bundle_boost = Utility::formatBoostFactor($bundle_boosts[$bundle] ?? 0);
+ $bundle_boost = $bundle_boosts[$bundle] ?? '';
+ if ($bundle_boost !== '') {
+ $bundle_boost = Utility::formatBoostFactor($bundle_boost);
+ }
$form['boosts'][$datasource_id]['bundle_boosts'][$bundle] = [
'#type' => 'select',
'#title' => $this->t('Boost for the %bundle bundle', ['%bundle' => $bundle_label]),
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/ResultRow.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/ResultRow.php
index 186e2dbe6..615ed6a52 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/ResultRow.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/ResultRow.php
@@ -3,6 +3,7 @@
namespace Drupal\search_api\Plugin\views;
use Drupal\views\ResultRow as ViewsResultRow;
+use Drupal\views\ViewExecutable;
/**
* A class representing a result row of a Search API-based view.
@@ -72,7 +73,7 @@ class ResultRow extends ViewsResultRow {
}
/**
- * Implements the magic __wakeup() method to lazy-load certain properties.
+ * Implements the magic __get() method to lazy-load certain properties.
*/
public function __get($name) {
$properties = get_object_vars($this);
@@ -90,4 +91,26 @@ class ResultRow extends ViewsResultRow {
return NULL;
}
+ /**
+ * Implements the magic __sleep() method to remove the view from the entity.
+ *
+ * The "view" property is added by the "Rendered entity" row plugin in
+ * \Drupal\search_api\Plugin\views\row\SearchApiRow::preRender() to make it
+ * available to entity viewing code. It should not be needed elsewhere, but
+ * can cause problems (including infinite loops) when Views results are
+ * serialized (as done, for instance, by the Metatag module in some setups).
+ *
+ * @return string[]
+ * An array with the names of all object properties.
+ */
+ public function __sleep() {
+ if (!empty($this->_object)) {
+ $entity = $this->_object->getValue();
+ if (($entity->view ?? NULL) instanceof ViewExecutable) {
+ unset($entity->view);
+ }
+ }
+ return array_keys(get_object_vars($this));
+ }
+
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/cache/SearchApiCachePluginTrait.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/cache/SearchApiCachePluginTrait.php
index 8f677ac76..9d1a2e36a 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/cache/SearchApiCachePluginTrait.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/cache/SearchApiCachePluginTrait.php
@@ -4,6 +4,7 @@ namespace Drupal\search_api\Plugin\views\cache;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\search_api\Plugin\views\query\SearchApiQuery;
@@ -115,19 +116,22 @@ trait SearchApiCachePluginTrait {
}
$view = $this->getView();
+ $query = $this->getQuery();
$data = [
'result' => $view->result,
'total_rows' => $view->total_rows ?? 0,
'current_page' => $view->getCurrentPage(),
- 'search_api results' => $this->getQuery()->getSearchApiResults(),
+ 'search_api results' => $query->getSearchApiResults(),
];
$expire = $this->cacheSetMaxAge($type);
if ($expire !== Cache::PERMANENT) {
$expire += (int) $view->getRequest()->server->get('REQUEST_TIME');
}
+ $tags = Cache::mergeTags($this->getCacheTags(), $query->getCacheTags());
+
$this->getCacheBackend()
- ->set($this->generateResultsKey(), $data, $expire, $this->getCacheTags());
+ ->set($this->generateResultsKey(), $data, $expire, $tags);
}
/**
@@ -216,18 +220,48 @@ trait SearchApiCachePluginTrait {
/**
* Retrieves the Search API Views query for the current view.
*
- * @return \Drupal\search_api\Plugin\views\query\SearchApiQuery|null
+ * @param bool $reset
+ * (optional) If TRUE, reset the query to its initial/unprocessed state.
+ * Should only be used in the context of a view being saved, never when the
+ * view is actually being executed.
+ *
+ * @return \Drupal\search_api\Plugin\views\query\SearchApiQuery
* The Search API Views query associated with the current view.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if there is no current Views query, or it is no Search API query.
*/
- protected function getQuery() {
- $query = $this->getView()->getQuery();
+ protected function getQuery(bool $reset = FALSE): SearchApiQuery {
+ if ($reset) {
+ $view = $this->getView();
+ $view_display = $view->getDisplay();
+ $query = $view_display->getPlugin('query');
+ $query->init($view, $view_display);
+ }
+ else {
+ $query = $this->getView()->getQuery();
+ }
+
if ($query instanceof SearchApiQuery) {
return $query;
}
throw new SearchApiException('No matching Search API Views query found in view.');
}
+ /**
+ * {@inheritdoc}
+ */
+ public function alterCacheMetadata(CacheableMetadata $cache_metadata) {
+ // A view can have multiple displays, but when information is gathered about
+ // all the displays' metadata, it initializes the query plugin only once for
+ // the first display. However, we need to collect cacheability metadata for
+ // every single cacheable display in the view, thus we are resetting the
+ // query to its original unprocessed state.
+ $query = $this->getQuery(TRUE)->getSearchApiQuery();
+ $query->preExecute();
+ // Allow modules that alter the query to add their cache metadata to the
+ // view.
+ $cache_metadata->addCacheableDependency($query);
+ }
+
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/cache/SearchApiTagCache.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/cache/SearchApiTagCache.php
index 906d6c86c..9527c82a1 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/cache/SearchApiTagCache.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/cache/SearchApiTagCache.php
@@ -3,7 +3,6 @@
namespace Drupal\search_api\Plugin\views\cache;
use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\views\Plugin\views\cache\Tag;
@@ -116,15 +115,4 @@ class SearchApiTagCache extends Tag {
return $tags;
}
- /**
- * {@inheritdoc}
- */
- public function alterCacheMetadata(CacheableMetadata $cache_metadata) {
- // Allow modules that alter the query to add their cache metadata to the
- // view.
- $query = $this->getQuery()->getSearchApiQuery();
- $query->preExecute();
- $cache_metadata->addCacheableDependency($query);
- }
-
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/query/SearchApiQuery.php b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/query/SearchApiQuery.php
index 94c9af510..0b9455401 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/query/SearchApiQuery.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Plugin/views/query/SearchApiQuery.php
@@ -12,7 +12,6 @@ use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
-use Drupal\Core\Url;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\LoggerTrait;
use Drupal\search_api\ParseMode\ParseModeInterface;
@@ -369,6 +368,9 @@ class SearchApiQuery extends QueryPluginBase {
'preserve_facet_query_args' => [
'default' => FALSE,
],
+ 'query_tags' => [
+ 'default' => [],
+ ],
];
}
@@ -408,6 +410,29 @@ class SearchApiQuery extends QueryPluginBase {
'#value' => FALSE,
];
}
+
+ $form['query_tags'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Query Tags'),
+ '#description' => $this->t('If set, these tags will be appended to the query and can be used to identify the query in a module. This can be helpful for altering queries.'),
+ '#default_value' => implode(', ', $this->options['query_tags']),
+ '#element_validate' => ['views_element_validate_tags'],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitOptionsForm(&$form, FormStateInterface $form_state) {
+ $value = &$form_state->getValue(['query', 'options', 'query_tags']);
+ if (is_array($value)) {
+ // We already ran on this form state. This happens when the user toggles a
+ // display to override defaults or vice-versa – the submit handler gets
+ // invoked twice, and we don't want to bash the values from the original
+ // call.
+ return;
+ }
+ $value = array_filter(array_map('trim', explode(',', $value)));
}
/**
@@ -516,10 +541,11 @@ class SearchApiQuery extends QueryPluginBase {
$this->query->setOption('search_api_bypass_access', TRUE);
}
- // If the View and the Panel conspire to provide an overridden path then
- // pass that through as the base path.
- if (($path = $this->view->getPath()) && strpos(Url::fromRoute('')->toString(), $path) !== 0) {
- $this->query->setOption('search_api_base_path', $path);
+ // Add the query tags.
+ if (!empty($this->options['query_tags'])) {
+ foreach ($this->options['query_tags'] as $tag) {
+ $this->query->addTag($tag);
+ }
}
// Save query information for Views UI.
@@ -685,25 +711,23 @@ class SearchApiQuery extends QueryPluginBase {
// Gather any properties from the search results.
foreach ($result->getFields(FALSE) as $field_id => $field) {
- if ($field->getValues()) {
- $path = $field->getCombinedPropertyPath();
- try {
- $property = $field->getDataDefinition();
- // For configurable processor-defined properties, our Views field
- // handlers use a special property path to distinguish multiple
- // fields with the same property path. Therefore, we here also set
- // the values using that special property path so this will work
- // correctly.
- if ($property instanceof ConfigurablePropertyInterface) {
- $path .= '|' . $field_id;
- }
+ $path = $field->getCombinedPropertyPath();
+ try {
+ $property = $field->getDataDefinition();
+ // For configurable processor-defined properties, our Views field
+ // handlers use a special property path to distinguish multiple
+ // fields with the same property path. Therefore, we here also set
+ // the values using that special property path so this will work
+ // correctly.
+ if ($property instanceof ConfigurablePropertyInterface) {
+ $path .= '|' . $field_id;
}
- catch (SearchApiException $e) {
- // If we're not able to retrieve the data definition at this point,
- // it doesn't really matter.
- }
- $values[$path] = $field->getValues();
}
+ catch (SearchApiException $e) {
+ // If we're not able to retrieve the data definition at this point,
+ // it doesn't really matter.
+ }
+ $values[$path] = $field->getValues();
}
$values['index'] = $count++;
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Processor/ProcessorPluginManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/Processor/ProcessorPluginManager.php
index 1c665e415..56573f56e 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Processor/ProcessorPluginManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Processor/ProcessorPluginManager.php
@@ -8,7 +8,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\SearchApiPluginManager;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Manages processor plugins.
@@ -32,7 +32,7 @@ class ProcessorPluginManager extends SearchApiPluginManager {
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The string translation manager.
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Query/Query.php b/frontend/drupal9/web/modules/contrib/search_api/src/Query/Query.php
index 3049a1a05..77eca0539 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Query/Query.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Query/Query.php
@@ -584,13 +584,13 @@ class Query implements QueryInterface, RefinableCacheableDependencyInterface {
// Let modules alter the query.
$event_base_name = SearchApiEvents::QUERY_PRE_EXECUTE;
$event = new QueryPreExecuteEvent($this);
- $this->getEventDispatcher()->dispatch($event_base_name, $event);
+ $this->getEventDispatcher()->dispatch($event, $event_base_name);
$hooks = ['search_api_query'];
foreach ($this->tags as $tag) {
$hooks[] = "search_api_query_$tag";
$event_name = "$event_base_name.$tag";
$event = new QueryPreExecuteEvent($this);
- $this->getEventDispatcher()->dispatch($event_name, $event);
+ $this->getEventDispatcher()->dispatch($event, $event_name);
}
$description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.query_pre_execute" event instead. See https://www.drupal.org/node/3059866';
@@ -612,7 +612,7 @@ class Query implements QueryInterface, RefinableCacheableDependencyInterface {
// Let modules alter the results.
$event_base_name = SearchApiEvents::PROCESSING_RESULTS;
$event = new ProcessingResultsEvent($this->results);
- $this->getEventDispatcher()->dispatch($event_base_name, $event);
+ $this->getEventDispatcher()->dispatch($event, $event_base_name);
$this->results = $event->getResults();
$hooks = ['search_api_results'];
@@ -620,7 +620,7 @@ class Query implements QueryInterface, RefinableCacheableDependencyInterface {
$hooks[] = "search_api_results_$tag";
$event = new ProcessingResultsEvent($this->results);
- $this->getEventDispatcher()->dispatch("$event_base_name.$tag", $event);
+ $this->getEventDispatcher()->dispatch($event, "$event_base_name.$tag");
$this->results = $event->getResults();
}
$description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.processing_results" event instead. See https://www.drupal.org/node/3059866';
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/SearchApiPluginManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/SearchApiPluginManager.php
index aa767a7bd..c18b49721 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/SearchApiPluginManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/SearchApiPluginManager.php
@@ -5,7 +5,7 @@ namespace Drupal\search_api;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\search_api\Event\GatheringPluginInfoEvent;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Extends the default plugin manager to add support for alter events.
@@ -15,7 +15,7 @@ class SearchApiPluginManager extends DefaultPluginManager {
/**
* The event dispatcher.
*
- * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
@@ -36,7 +36,7 @@ class SearchApiPluginManager extends DefaultPluginManager {
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
* @param string|null $plugin_interface
* (optional) The interface each plugin should implement.
@@ -77,7 +77,7 @@ class SearchApiPluginManager extends DefaultPluginManager {
if ($this->alterEventName) {
$event = new GatheringPluginInfoEvent($definitions);
- $this->eventDispatcher->dispatch($this->alterEventName, $event);
+ $this->eventDispatcher->dispatch($event, $this->alterEventName);
}
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Task/TaskEvent.php b/frontend/drupal9/web/modules/contrib/search_api/src/Task/TaskEvent.php
index 5d5509144..5109aefdf 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Task/TaskEvent.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Task/TaskEvent.php
@@ -3,7 +3,7 @@
namespace Drupal\search_api\Task;
use Drupal\search_api\SearchApiException;
-use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\EventDispatcher\Event;
/**
* Represents an event that was fired to execute a pending task.
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Task/TaskManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/Task/TaskManager.php
index 60f2abb1e..9347b27a9 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Task/TaskManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Task/TaskManager.php
@@ -11,7 +11,7 @@ use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\ServerInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Provides a service for managing pending tasks.
@@ -55,7 +55,7 @@ class TaskManager implements TaskManagerInterface {
/**
* The event dispatcher.
*
- * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
@@ -71,7 +71,7 @@ class TaskManager implements TaskManagerInterface {
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The string translation service.
@@ -209,7 +209,7 @@ class TaskManager implements TaskManagerInterface {
*/
public function executeSpecificTask(TaskInterface $task) {
$event = new TaskEvent($task);
- $this->eventDispatcher->dispatch('search_api.task.' . $task->getType(), $event);
+ $this->eventDispatcher->dispatch($event, 'search_api.task.' . $task->getType());
if (!$event->isPropagationStopped()) {
$id = $task->id();
$type = $task->getType();
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Tracker/TrackerPluginManager.php b/frontend/drupal9/web/modules/contrib/search_api/src/Tracker/TrackerPluginManager.php
index 5dd69a20f..6b5171b3e 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Tracker/TrackerPluginManager.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Tracker/TrackerPluginManager.php
@@ -7,7 +7,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\SearchApiPluginManager;
use Drupal\search_api\Utility\Utility;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Manages tracker plugins.
@@ -30,7 +30,7 @@ class TrackerPluginManager extends SearchApiPluginManager {
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EventDispatcherInterface $eventDispatcher) {
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Utility/CommandHelper.php b/frontend/drupal9/web/modules/contrib/search_api/src/Utility/CommandHelper.php
index 2e443df4f..d284b64fa 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Utility/CommandHelper.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Utility/CommandHelper.php
@@ -15,7 +15,7 @@ use Drupal\search_api\IndexInterface;
use Drupal\search_api\SearchApiException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
// phpcs:disable DrupalPractice.General.ExceptionT.ExceptionT
@@ -75,7 +75,7 @@ class CommandHelper implements LoggerAwareInterface {
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param string|callable $translation_function
* (optional) A callable for translating strings.
@@ -379,7 +379,7 @@ class CommandHelper implements LoggerAwareInterface {
$this->moduleHandler->invokeAllDeprecated($description, 'search_api_index_reindex', [$index, FALSE]);
$event_name = SearchApiEvents::REINDEX_SCHEDULED;
$event = new ReindexScheduledEvent($index, FALSE);
- $this->eventDispatcher->dispatch($event_name, $event);
+ $this->eventDispatcher->dispatch($event, $event_name);
$arguments = [
'!index' => $index->label(),
'!datasources' => implode(', ', $reindexed_datasources),
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Utility/DataTypeHelper.php b/frontend/drupal9/web/modules/contrib/search_api/src/Utility/DataTypeHelper.php
index b6e7a0446..f2f8fbb83 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Utility/DataTypeHelper.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Utility/DataTypeHelper.php
@@ -8,7 +8,7 @@ use Drupal\search_api\Event\MappingFieldTypesEvent;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\SearchApiException;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Provides helper methods for dealing with Search API data types.
@@ -32,7 +32,7 @@ class DataTypeHelper implements DataTypeHelperInterface {
/**
* The event dispatcher.
*
- * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
@@ -59,7 +59,7 @@ class DataTypeHelper implements DataTypeHelperInterface {
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
* @param \Drupal\search_api\DataType\DataTypePluginManager $dataTypeManager
* The data type plugin manager.
@@ -143,7 +143,7 @@ class DataTypeHelper implements DataTypeHelperInterface {
$this->moduleHandler->alterDeprecated($description, $hook, $mapping);
$eventName = SearchApiEvents::MAPPING_FIELD_TYPES;
$event = new MappingFieldTypesEvent($mapping);
- $this->eventDispatcher->dispatch($eventName, $event);
+ $this->eventDispatcher->dispatch($event, $eventName);
$this->fieldTypeMapping = $mapping;
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/src/Utility/TrackingHelper.php b/frontend/drupal9/web/modules/contrib/search_api/src/Utility/TrackingHelper.php
index d11761874..93512eca6 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/src/Utility/TrackingHelper.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/src/Utility/TrackingHelper.php
@@ -20,7 +20,7 @@ use Drupal\search_api\Event\MappingForeignRelationshipsEvent;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\SearchApiException;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Provides datasource-independent item change tracking functionality.
@@ -44,7 +44,7 @@ class TrackingHelper implements TrackingHelperInterface {
/**
* The event dispatcher.
*
- * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
@@ -69,7 +69,7 @@ class TrackingHelper implements TrackingHelperInterface {
* The entity type manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* The language manager.
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
* @param \Drupal\search_api\Utility\FieldsHelperInterface $fieldsHelper
* The fields helper.
@@ -253,7 +253,7 @@ class TrackingHelper implements TrackingHelperInterface {
// Let other modules alter this information, potentially adding more
// relationships.
$event = new MappingForeignRelationshipsEvent($index, $data, $cacheability);
- $this->eventDispatcher->dispatch(SearchApiEvents::MAPPING_FOREIGN_RELATIONSHIPS, $event);
+ $this->eventDispatcher->dispatch($event, SearchApiEvents::MAPPING_FOREIGN_RELATIONSHIPS);
$this->cache->set($cid, $data, $cacheability->getCacheMaxAge(), $cacheability->getCacheTags());
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/fixtures/views.view.search_api_query_type_test.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/fixtures/views.view.search_api_query_type_test.yml
new file mode 100644
index 000000000..dc8643c8c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/fixtures/views.view.search_api_query_type_test.yml
@@ -0,0 +1,167 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - search_api.index.test_node_index
+ module:
+ - search_api
+id: search_api_query_type_test
+label: search_api_query_type_test
+module: views
+description: ''
+tag: ''
+base_table: search_api_index_test_node_index
+base_field: nid
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Default
+ position: 0
+ display_options:
+ access:
+ type: none
+ options: { }
+ cache:
+ type: tag
+ options: { }
+ query:
+ type: views_query
+ options:
+ bypass_access: false
+ skip_access: false
+ preserve_facet_query_args: false
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Apply
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: mini
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: ‹‹
+ next: ››
+ style:
+ type: default
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ uses_fields: false
+ row:
+ type: fields
+ options:
+ inline: { }
+ separator: ''
+ hide_empty: false
+ default_field_elements: true
+ fields:
+ status:
+ table: search_api_index_test_node_index
+ field: status
+ id: status
+ entity_type: null
+ entity_field: null
+ plugin_id: search_api_field
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: ''
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: boolean
+ settings: { }
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ field_rendering: true
+ fallback_handler: search_api_boolean
+ fallback_options:
+ type: yes-no
+ type_custom_true: ''
+ type_custom_false: ''
+ not: false
+ link_to_item: false
+ use_highlighting: false
+ multi_type: separator
+ multi_separator: ', '
+ filters: { }
+ sorts: { }
+ header: { }
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - 'user.node_grants:view'
+ tags:
+ - 'config:search_api.index.test_node_index'
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/config/schema/search_api_test.schema.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/config/schema/search_api_test.schema.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/config/schema/search_api_test.schema.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/config/schema/search_api_test.schema.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/search_api_test.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/search_api_test.info.yml
similarity index 55%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/search_api_test.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/search_api_test.info.yml
index 439afb067..e770091c0 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/search_api_test.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/search_api_test.info.yml
@@ -4,10 +4,10 @@ description: 'Support module for Search API tests'
package: Testing
dependencies:
- search_api:search_api
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/search_api_test.module b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/search_api_test.module
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/search_api_test.module
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/search_api_test.module
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/MethodOverrides.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/MethodOverrides.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/MethodOverrides.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/MethodOverrides.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/backend/TestBackend.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/backend/TestBackend.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/data_type/AlteringValueTestDataType.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/data_type/AlteringValueTestDataType.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/data_type/AlteringValueTestDataType.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/data_type/AlteringValueTestDataType.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/data_type/TestDataType.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/data_type/TestDataType.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/data_type/TestDataType.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/data_type/TestDataType.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/data_type/UnsupportedTestDataType.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/data_type/UnsupportedTestDataType.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/data_type/UnsupportedTestDataType.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/data_type/UnsupportedTestDataType.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/datasource/TestDatasource.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/datasource/TestDatasource.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/datasource/TestDatasource.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/datasource/TestDatasource.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/display/TestDisplay.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/display/TestDisplay.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/display/TestDisplay.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/display/TestDisplay.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/processor/TestProcessor.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/processor/TestProcessor.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/processor/TestProcessor.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/processor/TestProcessor.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/tracker/TestTracker.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/tracker/TestTracker.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/tracker/TestTracker.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/tracker/TestTracker.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/tracker/TestTrackerStringLabel.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/tracker/TestTrackerStringLabel.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/Plugin/search_api/tracker/TestTrackerStringLabel.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/Plugin/search_api/tracker/TestTrackerStringLabel.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/PluginTestTrait.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/PluginTestTrait.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/PluginTestTrait.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/PluginTestTrait.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/TestPluginTrait.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/TestPluginTrait.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test/src/TestPluginTrait.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test/src/TestPluginTrait.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/search_api.index.test_index.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/search_api.index.test_index.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/search_api.index.test_index.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/search_api.index.test_index.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/search_api.server.test_server.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/search_api.server.test_server.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/search_api.server.test_server.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/search_api.server.test_server.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/system.action.search_api_test_bulk_form_entity_test.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/system.action.search_api_test_bulk_form_entity_test.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/system.action.search_api_test_bulk_form_entity_test.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/system.action.search_api_test_bulk_form_entity_test.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/system.action.search_api_test_bulk_form_entity_test_string_id.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/system.action.search_api_test_bulk_form_entity_test_string_id.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/system.action.search_api_test_bulk_form_entity_test_string_id.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/system.action.search_api_test_bulk_form_entity_test_string_id.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/views.view.search_api_test_bulk_form.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/views.view.search_api_test_bulk_form.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/install/views.view.search_api_test_bulk_form.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/install/views.view.search_api_test_bulk_form.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/schema/search_api_test_bulk_form.schema.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/schema/search_api_test_bulk_form.schema.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/config/schema/search_api_test_bulk_form.schema.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/config/schema/search_api_test_bulk_form.schema.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/search_api_test_bulk_form.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/search_api_test_bulk_form.info.yml
similarity index 65%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/search_api_test_bulk_form.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/search_api_test_bulk_form.info.yml
index 96e12d221..3c8a20a3f 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/search_api_test_bulk_form.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/search_api_test_bulk_form.info.yml
@@ -7,10 +7,10 @@ dependencies:
- drupal:views
- search_api:search_api_db
- search_api:search_api_test
-core_version_requirement: ^8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/src/Plugin/Action/EntityTestAction.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/src/Plugin/Action/EntityTestAction.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/src/Plugin/Action/EntityTestAction.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/src/Plugin/Action/EntityTestAction.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/src/Plugin/Action/EntityTestStringIdAction.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/src/Plugin/Action/EntityTestStringIdAction.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/src/Plugin/Action/EntityTestStringIdAction.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/src/Plugin/Action/EntityTestStringIdAction.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/src/Plugin/Action/TestActionTrait.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/src/Plugin/Action/TestActionTrait.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/src/Plugin/Action/TestActionTrait.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/src/Plugin/Action/TestActionTrait.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/src/TypedData/FooDataDefinition.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/src/TypedData/FooDataDefinition.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_bulk_form/src/TypedData/FooDataDefinition.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_bulk_form/src/TypedData/FooDataDefinition.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/config/install/search_api.index.database_search_index.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/config/install/search_api.index.database_search_index.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/config/install/search_api.index.database_search_index.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/config/install/search_api.index.database_search_index.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/config/install/search_api.server.database_search_server.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/config/install/search_api.server.database_search_server.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/config/install/search_api.server.database_search_server.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/config/install/search_api.server.database_search_server.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/search_api_test_db.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/search_api_test_db.info.yml
similarity index 62%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/search_api_test_db.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/search_api_test_db.info.yml
index ebfcb05bd..9bd4f206c 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/search_api_test_db.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/search_api_test_db.info.yml
@@ -5,10 +5,10 @@ package: Testing
dependencies:
- search_api:search_api_db
- search_api:search_api_test_example_content
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/search_api_test_db.search_api.inc b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/search_api_test_db.search_api.inc
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/search_api_test_db.search_api.inc
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/search_api_test_db.search_api.inc
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/search_api_test_db.services.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/search_api_test_db.services.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/search_api_test_db.services.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/search_api_test_db.services.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/src/EventListener.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/src/EventListener.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_db/src/EventListener.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_db/src/EventListener.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_events/search_api_test_events.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_events/search_api_test_events.info.yml
similarity index 58%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_events/search_api_test_events.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_events/search_api_test_events.info.yml
index 72d99e4ff..256ef1a33 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_events/search_api_test_events.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_events/search_api_test_events.info.yml
@@ -4,10 +4,10 @@ description: 'Support module for Search API tests, tests all events.'
package: Testing
dependencies:
- search_api:search_api
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_events/search_api_test_events.services.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_events/search_api_test_events.services.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_events/search_api_test_events.services.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_events/search_api_test_events.services.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_events/src/EventListener.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_events/src/EventListener.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_events/src/EventListener.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_events/src/EventListener.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.body.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.body.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.body.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.body.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.category.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.category.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.category.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.category.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.keywords.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.keywords.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.keywords.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.keywords.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.width.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.width.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.width.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.width.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.body.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.body.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.body.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.body.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.category.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.category.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.category.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.category.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.keywords.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.keywords.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.keywords.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.keywords.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.width.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.width.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.width.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.width.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.body.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.body.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.body.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.body.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.category.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.category.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.category.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.category.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.keywords.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.keywords.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.keywords.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.keywords.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.width.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.width.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.width.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.width.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.body.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.body.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.body.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.body.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.category.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.category.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.category.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.category.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.keywords.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.keywords.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.keywords.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.keywords.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.width.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.width.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.width.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.width.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/search_api_test_example_content.info.yml
similarity index 56%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/search_api_test_example_content.info.yml
index 292327663..c7c57c564 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content/search_api_test_example_content.info.yml
@@ -4,10 +4,10 @@ description: 'Provides field definitions for example content.'
package: Testing
dependencies:
- drupal:entity_test
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.indexed.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.field.node.child.indexed.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.indexed.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.field.node.child.indexed.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.not_indexed.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.field.node.child.not_indexed.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.child.not_indexed.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.field.node.child.not_indexed.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.grandparent.parent_reference.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.field.node.grandparent.parent_reference.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.grandparent.parent_reference.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.field.node.grandparent.parent_reference.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.parent.entity_reference.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.field.node.parent.entity_reference.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.field.node.parent.entity_reference.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.field.node.parent.entity_reference.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.entity_reference.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.storage.node.entity_reference.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.entity_reference.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.storage.node.entity_reference.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.indexed.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.storage.node.indexed.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.indexed.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.storage.node.indexed.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.not_indexed.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.storage.node.not_indexed.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.not_indexed.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.storage.node.not_indexed.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.parent_reference.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.storage.node.parent_reference.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/field.storage.node.parent_reference.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/field.storage.node.parent_reference.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/node.type.child.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/node.type.child.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/node.type.child.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/node.type.child.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/node.type.grandparent.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/node.type.grandparent.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/node.type.grandparent.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/node.type.grandparent.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/node.type.parent.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/node.type.parent.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/config/install/node.type.parent.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/config/install/node.type.parent.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/search_api_test_example_content_references.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/search_api_test_example_content_references.info.yml
similarity index 61%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/search_api_test_example_content_references.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/search_api_test_example_content_references.info.yml
index 62f9491d2..03ccc0a25 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_example_content_references/search_api_test_example_content_references.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_example_content_references/search_api_test_example_content_references.info.yml
@@ -4,10 +4,10 @@ description: 'Provides field definitions for example content that include entity
package: Testing
dependencies:
- drupal:node
-core_version_requirement: ^8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt/config/install/views.view.search_api_test_search_excerpt.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt/config/install/views.view.search_api_test_search_excerpt.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt/config/install/views.view.search_api_test_search_excerpt.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt/config/install/views.view.search_api_test_search_excerpt.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt/search_api_test_excerpt.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt/search_api_test_excerpt.info.yml
similarity index 62%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt/search_api_test_excerpt.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt/search_api_test_excerpt.info.yml
index ef54a2aa1..3b7cd930c 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt/search_api_test_excerpt.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt/search_api_test_excerpt.info.yml
@@ -2,15 +2,14 @@ name: 'Search API Excerpt Test'
type: module
description: 'Support module for Search API Excerpt tests'
package: Search
-core_version_requirement: ^8 || ^9
+core_version_requirement: ^9.2 || ^10.0
dependencies:
- search_api:search_api
- search_api:search_api_test_db
- drupal:views
-core: 8.x
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.article.search_result.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.article.search_result.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.article.search_result.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.article.search_result.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.item.search_result.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.item.search_result.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.item.search_result.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/config/install/core.entity_view_display.entity_test_mulrev_changed.item.search_result.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/config/install/core.entity_view_mode.entity_test_mulrev_changed.search_result.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/config/install/core.entity_view_mode.entity_test_mulrev_changed.search_result.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/config/install/core.entity_view_mode.entity_test_mulrev_changed.search_result.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/config/install/core.entity_view_mode.entity_test_mulrev_changed.search_result.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/config/install/views.view.search_api_test_excerpt_field.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/config/install/views.view.search_api_test_excerpt_field.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/config/install/views.view.search_api_test_excerpt_field.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/config/install/views.view.search_api_test_excerpt_field.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml
similarity index 64%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml
index 3a950c679..fd2176b58 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/search_api_test_excerpt_field.info.yml
@@ -6,10 +6,10 @@ dependencies:
- search_api:search_api
- search_api:search_api_test_db
- drupal:views
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.module b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/search_api_test_excerpt_field.module
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_excerpt_field/search_api_test_excerpt_field.module
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_excerpt_field/search_api_test_excerpt_field.module
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/config/install/field.field.entity_test_mulrev_changed.article.links.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/config/install/field.field.entity_test_mulrev_changed.article.links.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/config/install/field.field.entity_test_mulrev_changed.article.links.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/config/install/field.field.entity_test_mulrev_changed.article.links.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/config/install/field.storage.entity_test_mulrev_changed.links.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/config/install/field.storage.entity_test_mulrev_changed.links.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/config/install/field.storage.entity_test_mulrev_changed.links.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/config/install/field.storage.entity_test_mulrev_changed.links.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/search_api_test_extraction.info.yml
similarity index 59%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/search_api_test_extraction.info.yml
index f463e9830..d4274aa9e 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/search_api_test_extraction.info.yml
@@ -4,10 +4,10 @@ description: 'Provides a setup for testing field values extraction.'
package: Testing
dependencies:
- drupal:entity_test
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/src/Plugin/search_api/processor/SoulMate.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/src/Plugin/search_api/processor/SoulMate.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_extraction/src/Plugin/search_api/processor/SoulMate.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_extraction/src/Plugin/search_api/processor/SoulMate.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_hooks/search_api_test_hooks.info.yml
similarity index 58%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_hooks/search_api_test_hooks.info.yml
index 84d8e6044..e1fef45ea 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_hooks/search_api_test_hooks.info.yml
@@ -4,10 +4,10 @@ description: 'Support module for Search API tests, tests all the hooks.'
package: Testing
dependencies:
- search_api:search_api
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_hooks/search_api_test_hooks.module b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_hooks/search_api_test_hooks.module
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_hooks/search_api_test_hooks.module
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_hooks/search_api_test_hooks.module
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_hooks/search_api_test_hooks.search_api.inc b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_hooks/search_api_test_hooks.search_api.inc
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_hooks/search_api_test_hooks.search_api.inc
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_hooks/search_api_test_hooks.search_api.inc
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_inconsistent_config/config/install/search_api.index.inconsistent_search_index.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_inconsistent_config/config/install/search_api.index.inconsistent_search_index.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_inconsistent_config/config/install/search_api.index.inconsistent_search_index.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_inconsistent_config/config/install/search_api.index.inconsistent_search_index.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_inconsistent_config/config/install/search_api.server.inconsistent_search_server.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_inconsistent_config/config/install/search_api.server.inconsistent_search_server.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_inconsistent_config/config/install/search_api.server.inconsistent_search_server.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_inconsistent_config/config/install/search_api.server.inconsistent_search_server.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml
similarity index 61%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml
index a395a616a..ab14286d0 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_inconsistent_config/search_api_test_inconsistent_config.info.yml
@@ -5,10 +5,10 @@ package: Testing
dependencies:
- search_api:search_api
- search_api:search_api_test
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_language_fallback/search_api_test_language_fallback.info.yml
similarity index 51%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_language_fallback/search_api_test_language_fallback.info.yml
index 77022499e..1af29ce70 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_language_fallback/search_api_test_language_fallback.info.yml
@@ -2,10 +2,10 @@ type: module
name: 'Language Fallback Test'
description: 'Provides a language fallback fr => es.'
package: 'Search API'
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.module b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_language_fallback/search_api_test_language_fallback.module
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_language_fallback/search_api_test_language_fallback.module
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_language_fallback/search_api_test_language_fallback.module
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/search_api_test_no_ui.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/search_api_test_no_ui.info.yml
similarity index 58%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/search_api_test_no_ui.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/search_api_test_no_ui.info.yml
index c0e791ab5..545203a79 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/search_api_test_no_ui.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/search_api_test_no_ui.info.yml
@@ -4,10 +4,10 @@ description: 'Support module for Search API tests ("No UI" plugins)'
package: Testing
dependencies:
- search_api:search_api
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/backend/NoUi.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/backend/NoUi.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/backend/NoUi.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/backend/NoUi.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/data_type/NoUi.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/data_type/NoUi.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/data_type/NoUi.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/data_type/NoUi.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/datasource/NoUi.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/datasource/NoUi.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/datasource/NoUi.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/datasource/NoUi.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/parse_mode/NoUi.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/parse_mode/NoUi.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/parse_mode/NoUi.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/parse_mode/NoUi.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/processor/NoUi.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/processor/NoUi.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/processor/NoUi.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/processor/NoUi.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/tracker/NoUi.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/tracker/NoUi.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_no_ui/src/Plugin/search_api/tracker/NoUi.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_no_ui/src/Plugin/search_api/tracker/NoUi.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/config/install/search_api.index.test_node_index.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/config/install/search_api.index.test_node_index.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/config/install/search_api.index.test_node_index.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/config/install/search_api.index.test_node_index.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/config/install/search_api.server.database_search_server.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/config/install/search_api.server.database_search_server.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/config/install/search_api.server.database_search_server.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/config/install/search_api.server.database_search_server.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml
similarity index 79%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml
index 0ee6bce97..dbcdf05b2 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml
@@ -105,3 +105,29 @@ display:
- 'user.node_grants:view'
tags:
- 'config:search_api.index.test_node_index'
+ page_2:
+ id: page_2
+ display_title: Page 2
+ display_plugin: page
+ position: 2
+ display_options:
+ cache:
+ type: search_api_time
+ options:
+ results_lifespan: 21600
+ results_lifespan_custom: 0
+ output_lifespan: 518400
+ output_lifespan_custom: 0
+ defaults:
+ cache: false
+ display_extenders: { }
+ path: test-index-content-time
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - 'user.node_grants:view'
+ tags:
+ - 'config:search_api.index.test_node_index'
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/search_api_test_node_indexing.info.yml
similarity index 60%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/search_api_test_node_indexing.info.yml
index fc02a97a6..f0721c919 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_node_indexing/search_api_test_node_indexing.info.yml
@@ -4,10 +4,10 @@ description: 'Test module for testing indexing of nodes in Search API.'
package: 'Search API'
dependencies:
- search_api:search_api_db
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_tasks/search_api_test_tasks.info.yml
similarity index 58%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_tasks/search_api_test_tasks.info.yml
index cabd931f9..231f9cb7e 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_tasks/search_api_test_tasks.info.yml
@@ -4,10 +4,10 @@ description: 'Support module for tests of the Search API tasks system.'
package: Testing
dependencies:
- search_api:search_api
-core_version_requirement: ^8.8 || ^9
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_tasks/search_api_test_tasks.services.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_tasks/search_api_test_tasks.services.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_tasks/search_api_test_tasks.services.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_tasks/search_api_test_tasks.services.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_tasks/src/TestTaskWorker.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_tasks/src/TestTaskWorker.php
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_tasks/src/TestTaskWorker.php
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_tasks/src/TestTaskWorker.php
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_block_view.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/config/install/views.view.search_api_test_block_view.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_block_view.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/config/install/views.view.search_api_test_block_view.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_cache.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/config/install/views.view.search_api_test_cache.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_cache.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/config/install/views.view.search_api_test_cache.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_sorts.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/config/install/views.view.search_api_test_sorts.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_sorts.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/config/install/views.view.search_api_test_sorts.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_view.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/config/install/views.view.search_api_test_view.yml
similarity index 100%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_view.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/config/install/views.view.search_api_test_view.yml
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/search_api_test_views.info.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.info.yml
similarity index 61%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/search_api_test_views.info.yml
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.info.yml
index 1b4dfa3d1..1f692bafb 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/search_api_test_views.info.yml
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.info.yml
@@ -8,10 +8,11 @@ dependencies:
- drupal:node
- drupal:rest
- drupal:views
-core_version_requirement: ^8.8 || ^9
+ - drupal:views_test_data
+core_version_requirement: ^9.2 || ^10.0
hidden: true
-# Information added by Drupal.org packaging script on 2022-01-21
-version: '8.x-1.23'
+# Information added by Drupal.org packaging script on 2022-07-07
+version: '8.x-1.24'
project: 'search_api'
-datestamp: 1642769875
+datestamp: 1657180588
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/search_api_test_views.module b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.module
similarity index 87%
rename from frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/search_api_test_views.module
rename to frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.module
index dc5a5ff62..ede90505a 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/search_api_test_views/search_api_test_views.module
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.module
@@ -26,13 +26,20 @@ function search_api_test_views_search_api_query_alter(QueryInterface $query) {
$alter_cache_metadata = \Drupal::state()
->get('search_api_test_views.alter_query_cacheability_metadata', FALSE);
+
if ($alter_cache_metadata
&& $query instanceof RefinableCacheableDependencyInterface) {
- // Alter in some imaginary cacheability metadata for testing.
- $query->addCacheContexts(['search_api_test_context']);
- $query->addCacheTags(['search_api:test_tag']);
+ // Alter in some imaginary cacheability metadata for testing, including a
+ // cache tag that depends on the search ID (to simulate caching information
+ // that depends on the specific search).
+ $query->addCacheContexts(['views_test_cache_context']);
+ $query->addCacheTags([
+ 'search_api:test_tag',
+ 'search_api:test_' . $query->getSearchId(),
+ ]);
$query->mergeCacheMaxAge(100);
}
+
}
/**
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.services.yml b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.services.yml
new file mode 100644
index 000000000..f567296ac
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/search_api_test_views.services.yml
@@ -0,0 +1,6 @@
+services:
+ search_api_test_views.event_listener:
+ class: Drupal\search_api_test_views\EventListener
+ arguments: ['@messenger']
+ tags:
+ - { name: event_subscriber }
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/src/EventListener.php b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/src/EventListener.php
new file mode 100644
index 000000000..5eff1dda0
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/modules/search_api_test_views/src/EventListener.php
@@ -0,0 +1,49 @@
+messenger = $messenger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents(): array {
+ return [
+ SearchApiEvents::QUERY_PRE_EXECUTE . '.weather' => 'queryTagAlter',
+ ];
+ }
+
+ /**
+ * Reacts to the query TAG alter event.
+ */
+ public function queryTagAlter(): void {
+ $this->messenger->addStatus('Sunshine');
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/FieldIntegrationTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/FieldIntegrationTest.php
index 33bae1c5d..f564951a3 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/FieldIntegrationTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/FieldIntegrationTest.php
@@ -29,7 +29,7 @@ class FieldIntegrationTest extends SearchApiBrowserTestBase {
$fields = $index->getFields();
// Load and parse the same configuration file.
- $yaml_file = __DIR__ . '/../../search_api_test_db/config/install/search_api.index.database_search_index.yml';
+ $yaml_file = __DIR__ . '/../../modules/search_api_test_db/config/install/search_api.index.database_search_index.yml';
$index_configuration = Yaml::decode(file_get_contents($yaml_file));
$field_settings = $index_configuration['field_settings'];
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/ProcessorIntegrationTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/ProcessorIntegrationTest.php
index 8c1206547..156931f80 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/ProcessorIntegrationTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/ProcessorIntegrationTest.php
@@ -456,6 +456,7 @@ class ProcessorIntegrationTest extends SearchApiBrowserTestBase {
$form_values['boosts']['entity:node']['bundle_boosts']['page'] = '';
$this->editSettingsForm($configuration, 'type_boost', $form_values);
+ $this->editSettingsForm($configuration, 'type_boost', []);
}
/**
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/ViewsTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/ViewsTest.php
index 656e666b7..e0aabb422 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/ViewsTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Functional/ViewsTest.php
@@ -64,6 +64,8 @@ class ViewsTest extends SearchApiBrowserTestBase {
if (!Utility::isRunningInCli()) {
\Drupal::state()->set('search_api_use_tracking_batch', FALSE);
}
+
+ $this->rebuildContainer();
}
/**
@@ -781,6 +783,7 @@ class ViewsTest extends SearchApiBrowserTestBase {
$this->assertSession()->pageTextContains('Language code');
$this->assertSession()->pageTextContains('The user language code.');
$this->assertSession()->pageTextContains('(No description available)');
+ $this->assertSession()->pageTextContains('Item URL');
$this->assertSession()->pageTextNotContains('Error: missing help');
// Then add some fields.
@@ -797,6 +800,7 @@ class ViewsTest extends SearchApiBrowserTestBase {
'search_api_entity_user.roles',
'search_api_index_database_search_index.rendered_item',
'search_api_index_database_search_index.search_api_rendered_item',
+ 'search_api_index_database_search_index.search_api_url',
];
$edit = [];
foreach ($fields as $field) {
@@ -879,6 +883,7 @@ class ViewsTest extends SearchApiBrowserTestBase {
'user_id:roles',
'rendered_item',
'search_api_rendered_item',
+ 'search_api_url',
];
$rendered_item_fields = ['rendered_item', 'search_api_rendered_item'];
foreach ($this->entities as $id => $entity) {
@@ -901,6 +906,9 @@ class ViewsTest extends SearchApiBrowserTestBase {
if ($field === 'search_api_datasource') {
$data = [$datasource_id];
}
+ elseif ($field === 'search_api_url') {
+ $data = [$field_entity->toUrl()->toString()];
+ }
elseif (in_array($field, $rendered_item_fields)) {
$view_mode = $field === 'rendered_item' ? 'full' : 'teaser';
$data = [$view_mode];
@@ -990,6 +998,19 @@ class ViewsTest extends SearchApiBrowserTestBase {
$this->submitForm([], 'Save');
$this->assertSession()->statusCodeEquals(200);
+ // Set query tags.
+ $this->drupalGet('admin/structure/views/nojs/display/search_api_test_view/page_1/query');
+ $this->submitForm(['query[options][query_tags]' => 'weather'], 'Apply');
+ $this->submitForm([], 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->drupalGet('search-api-test');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('Sunshine');
+ $this->drupalGet('admin/structure/views/nojs/display/search_api_test_view/page_1/query');
+ $this->submitForm(['query[options][query_tags]' => 'weather'], 'Apply');
+ $this->submitForm([], 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+
$this->drupalLogout();
$this->drupalGet('search-api-test');
$this->assertSession()->statusCodeEquals(200);
@@ -1018,6 +1039,7 @@ class ViewsTest extends SearchApiBrowserTestBase {
'search_api_datasource',
'rendered_item',
'search_api_rendered_item',
+ 'search_api_url',
];
// The "Fallback options" are only available for fields based on the Field
// API.
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/ConfigEntity/DefaultConfigEntityInstallationTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/ConfigEntity/DefaultConfigEntityInstallationTest.php
index 708651473..0f7d726da 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/ConfigEntity/DefaultConfigEntityInstallationTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/ConfigEntity/DefaultConfigEntityInstallationTest.php
@@ -62,7 +62,7 @@ class DefaultConfigEntityInstallationTest extends KernelTestBase {
* Tests that creating new config entities directly works correctly.
*/
public function testNormalEntityCreation() {
- $dir = __DIR__ . '/../../../search_api_test_inconsistent_config/config/install/';
+ $dir = __DIR__ . '/../../../modules/search_api_test_inconsistent_config/config/install/';
$yaml_file = $dir . 'search_api.server.inconsistent_search_server.yml';
$values = Yaml::decode(file_get_contents($yaml_file));
Server::create($values)->save();
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Datasource/NodeTrackingTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Datasource/NodeTrackingTest.php
new file mode 100644
index 000000000..e9bc15dbb
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Datasource/NodeTrackingTest.php
@@ -0,0 +1,149 @@
+installSchema('search_api', ['search_api_item']);
+ $this->installSchema('node', ['node_access']);
+ $this->installEntitySchema('search_api_task');
+ $this->installEntitySchema('user');
+ $this->installEntitySchema('node');
+ $this->installConfig(['language', 'search_api']);
+
+ // Create some languages.
+ for ($i = 0; $i < 2; ++$i) {
+ ConfigurableLanguage::create([
+ 'id' => 'l' . $i,
+ 'label' => 'language - ' . $i,
+ 'weight' => $i,
+ ])->save();
+ }
+ $this->container->get('language_manager')->reset();
+
+ // Create a test index.
+ Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'backend' => 'search_api_test',
+ ])->save();
+ $this->index = Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => TRUE,
+ 'server' => 'test_server',
+ 'datasource_settings' => [
+ 'entity:node' => [
+ 'languages' => [
+ 'default' => TRUE,
+ 'selected' => ['l0'],
+ ],
+ ],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'processor_settings' => [
+ 'content_access' => [],
+ ],
+ 'field_settings' => [
+ 'node_grants' => [
+ 'label' => 'Node access information',
+ 'type' => 'string',
+ 'property_path' => 'search_api_node_grants',
+ 'indexed_locked' => TRUE,
+ 'type_locked' => TRUE,
+ 'hidden' => TRUE,
+ ],
+ 'status' => [
+ 'label' => 'Publishing status',
+ 'type' => 'boolean',
+ 'datasource_id' => 'entity:node',
+ 'property_path' => 'status',
+ 'indexed_locked' => TRUE,
+ 'type_locked' => TRUE,
+ ],
+ 'uid' => [
+ 'label' => 'Author ID',
+ 'type' => 'integer',
+ 'datasource_id' => 'entity:node',
+ 'property_path' => 'uid',
+ 'indexed_locked' => TRUE,
+ 'type_locked' => TRUE,
+ ],
+ ],
+ 'options' => [
+ 'index_directly' => TRUE,
+ ],
+ ]);
+ $this->index->save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function register(ContainerBuilder $container): void {
+ parent::register($container);
+
+ // Set a logger that will throw exceptions when warnings/errors are logged.
+ $logger = new TestLogger('');
+ $container->set('logger.factory', $logger);
+ $container->set('logger.channel.search_api', $logger);
+ }
+
+ /**
+ * Tests that editing an entity of a disabled language produces no error.
+ *
+ * @covers ::trackEntityChange
+ */
+ public function testIgnoredLanguageEntityUpdate(): void {
+ $entity = Node::create([
+ 'nid' => 1,
+ 'type' => 'node',
+ 'langcode' => 'l0',
+ 'title' => 'Language 0 node',
+ ]);
+ $entity->save();
+ $entity->addTranslation('l1')->set('title', 'Language 1 node')->save();
+
+ $this->triggerPostRequestIndexing();
+ $this->assertTrue(TRUE);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Processor/TypeBoostTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Processor/TypeBoostTest.php
index 560de50d9..78d9a4749 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Processor/TypeBoostTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Processor/TypeBoostTest.php
@@ -133,13 +133,14 @@ class TypeBoostTest extends ProcessorTestBase {
}
/**
- * Tests that default values for individual bundles are correct in the form.
+ * Tests that default values are correct in the config form.
*/
- public function testConfigFormBundleBoostDefaults() {
+ public function testConfigFormDefaultValues() {
$form = $this->processor->buildConfigurationForm([], new FormState());
- $this->assertEquals(Utility::formatBoostFactor(0), $form['boosts']['entity:node']['bundle_boosts']['article']['#default_value']);
- $this->assertEquals(Utility::formatBoostFactor(0), $form['boosts']['entity:node']['bundle_boosts']['page']['#default_value']);
+ $this->assertEquals(Utility::formatBoostFactor(1), $form['boosts']['entity:node']['datasource_boost']['#default_value']);
+ $this->assertEquals('', $form['boosts']['entity:node']['bundle_boosts']['article']['#default_value']);
+ $this->assertEquals('', $form['boosts']['entity:node']['bundle_boosts']['page']['#default_value']);
$configuration = [
'boosts' => [
@@ -155,8 +156,28 @@ class TypeBoostTest extends ProcessorTestBase {
$form = $this->processor->buildConfigurationForm([], new FormState());
+ $this->assertEquals(Utility::formatBoostFactor(3), $form['boosts']['entity:node']['datasource_boost']['#default_value']);
$this->assertEquals(Utility::formatBoostFactor(0), $form['boosts']['entity:node']['bundle_boosts']['article']['#default_value']);
- $this->assertEquals(Utility::formatBoostFactor(0), $form['boosts']['entity:node']['bundle_boosts']['page']['#default_value']);
+ $this->assertEquals('', $form['boosts']['entity:node']['bundle_boosts']['page']['#default_value']);
+
+ $configuration = [
+ 'boosts' => [
+ 'entity:node' => [
+ 'datasource_boost' => Utility::formatBoostFactor(2),
+ 'bundle_boosts' => [
+ 'article' => Utility::formatBoostFactor(3),
+ 'page' => Utility::formatBoostFactor(1.5),
+ ],
+ ],
+ ],
+ ];
+ $this->processor->setConfiguration($configuration);
+
+ $form = $this->processor->buildConfigurationForm([], new FormState());
+
+ $this->assertEquals(Utility::formatBoostFactor(2), $form['boosts']['entity:node']['datasource_boost']['#default_value']);
+ $this->assertEquals(Utility::formatBoostFactor(3), $form['boosts']['entity:node']['bundle_boosts']['article']['#default_value']);
+ $this->assertEquals(Utility::formatBoostFactor(1.5), $form['boosts']['entity:node']['bundle_boosts']['page']['#default_value']);
}
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/TestLogger.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/TestLogger.php
new file mode 100644
index 000000000..28015a21b
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/TestLogger.php
@@ -0,0 +1,96 @@
+expectedErrors;
+ }
+
+ /**
+ * Sets an expectation for one or more errors to be logged.
+ *
+ * @param int $num_expected_errors
+ * The new expected number of errors.
+ * @param int|null $expected_previous_setting
+ * (optional) The expected previous setting of the number of expected errors
+ * in order to make an assertion on that. Or NULL to skip the assertion.
+ *
+ * @return $this
+ */
+ public function setExpectedErrors(int $num_expected_errors = 1, ?int $expected_previous_setting = 0): self {
+ if ($expected_previous_setting !== NULL) {
+ Assert::assertEquals($expected_previous_setting, $this->expectedErrors);
+ }
+ $this->expectedErrors = $num_expected_errors;
+ return $this;
+ }
+
+ /**
+ * Asserts that all expected errors were in fact encountered.
+ *
+ * In other words, asserts that the currently expected number of errors to be
+ * logged is 0.
+ *
+ * @return $this
+ */
+ public function assertAllExpectedErrorsEncountered(): self {
+ Assert::assertEquals(0, $this->expectedErrors);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function log($level, $message, array $context = []): void {
+ if ($level < RfcLogLevel::INFO) {
+ if ($this->expectedErrors > 0) {
+ --$this->expectedErrors;
+ return;
+ }
+ $message = strtr($message, $context);
+ throw new \Exception($message);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($channel): LoggerChannelInterface {
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addLogger(LoggerInterface $logger, $priority = 0): void {
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsCacheInvalidationTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsCacheInvalidationTest.php
index 5562f96be..1bd912a65 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsCacheInvalidationTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsCacheInvalidationTest.php
@@ -95,6 +95,13 @@ class ViewsCacheInvalidationTest extends KernelTestBase {
*/
protected $nodes;
+ /**
+ * The state service.
+ *
+ * @var \Drupal\Core\State\StateInterface
+ */
+ protected $state;
+
/**
* {@inheritdoc}
*/
@@ -112,6 +119,7 @@ class ViewsCacheInvalidationTest extends KernelTestBase {
'text',
'user',
'views',
+ 'views_test_data',
];
/**
@@ -140,6 +148,7 @@ class ViewsCacheInvalidationTest extends KernelTestBase {
$this->renderer = $this->container->get('renderer');
$this->cacheTagsInvalidator = $this->container->get('cache_tags.invalidator');
$this->currentUser = $this->container->get('current_user');
+ $this->state = $this->container->get('state');
// Use the test search index from the search_api_test_db module.
$this->index = Index::load('test_node_index');
@@ -326,6 +335,22 @@ class ViewsCacheInvalidationTest extends KernelTestBase {
$this->assertViewsResult('has-access', ['Angua', 'Cheery', 'Detritus']);
$this->assertCached('no-access');
$this->assertCached('has-access');
+
+ // Activate the alter hook and resave the view so it will recalculate the
+ // cacheability metadata.
+ $this->state->set('search_api_test_views.alter_query_cacheability_metadata', TRUE);
+ $view = $this->getView();
+ $view->save();
+ // Populate the Views results cache.
+ $this->assertViewsResult('no-access', ['Cheery']);
+ $this->assertViewsResult('has-access', ['Angua', 'Cheery', 'Detritus']);
+ $this->assertCached('no-access');
+ $this->assertCached('has-access');
+ // Make sure that the Views results cache is invalidated whenever the custom
+ // cache tag that was added to the query is invalidated.
+ $this->cacheTagsInvalidator->invalidateTags(['search_api:test_views_page:search_api_test_node_view__page_1']);
+ $this->assertNotCached('no-access');
+ $this->assertNotCached('has-access');
}
/**
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsCacheabilityMetadataExportTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsCacheabilityMetadataExportTest.php
index c6e79270a..03c293ccb 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsCacheabilityMetadataExportTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsCacheabilityMetadataExportTest.php
@@ -2,10 +2,7 @@
namespace Drupal\Tests\search_api\Kernel\Views;
-use Drupal\Core\Cache\Context\CacheContextsManager;
-use Drupal\Core\Cache\Context\ContextCacheKeys;
use Drupal\Core\Config\Config;
-use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Drupal\views\ViewExecutable;
@@ -19,14 +16,16 @@ class ViewsCacheabilityMetadataExportTest extends KernelTestBase {
/**
* The ID of the view used in the test.
*/
- const TEST_VIEW_ID = 'search_api_test_node_view';
+ protected const TEST_VIEW_ID = 'search_api_test_node_view';
/**
* The display IDs used in the test.
- *
- * @var string[]
*/
- protected static $testViewDisplayIds = ['default', 'page_1'];
+ protected const TEST_VIEW_DISPLAY_IDS = [
+ 'default',
+ 'page_1',
+ 'page_2',
+ ];
/**
* The entity type manager.
@@ -63,23 +62,9 @@ class ViewsCacheabilityMetadataExportTest extends KernelTestBase {
'text',
'user',
'views',
+ 'views_test_data',
];
- /**
- * {@inheritdoc}
- */
- public function register(ContainerBuilder $container) {
- parent::register($container);
-
- // Use a mocked version of the cache contexts manager so we can use a mocked
- // cache context "search_api_test_context" without triggering a validation
- // error.
- $cache_contexts_manager = $this->createMock(CacheContextsManager::class);
- $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
- $cache_contexts_manager->method('convertTokensToKeys')->willReturn(new ContextCacheKeys([]));
- $container->set('cache_contexts_manager', $cache_contexts_manager);
- }
-
/**
* {@inheritdoc}
*/
@@ -133,24 +118,32 @@ class ViewsCacheabilityMetadataExportTest extends KernelTestBase {
// By default the result is permanently cached.
'max-age' => -1,
];
+ $expected_view_metadata = [];
+ foreach (self::TEST_VIEW_DISPLAY_IDS as $display_id) {
+ $expected_view_metadata[$display_id] = $expected_cacheability_metadata;
+ }
// Check that our test view has the expected cacheability metadata.
$view = $this->getView();
- $this->assertViewCacheabilityMetadata($expected_cacheability_metadata, $view);
+ $this->assertViewCacheabilityMetadata($expected_view_metadata, $view);
// For efficiency Views calculates the cacheability metadata whenever a view
// is saved, and includes it in the exported configuration.
// @see \Drupal\views\Entity\View::addCacheMetadata()
// Check that the exported configuration contains the expected metadata.
$view_config = $this->config('views.view.' . self::TEST_VIEW_ID);
- $this->assertViewConfigCacheabilityMetadata($expected_cacheability_metadata, $view_config);
+ $this->assertViewConfigCacheabilityMetadata($expected_view_metadata, $view_config);
// Test that modules are able to alter the cacheability metadata. Our test
// hook implementation will alter all 3 types of metadata.
// @see search_api_test_views_search_api_query_alter()
- $expected_cacheability_metadata['contexts'][] = 'search_api_test_context';
- $expected_cacheability_metadata['tags'][] = 'search_api:test_tag';
- $expected_cacheability_metadata['max-age'] = 100;
+ foreach (self::TEST_VIEW_DISPLAY_IDS as $display_id) {
+ $expected_view_metadata[$display_id]['contexts'][] = 'views_test_cache_context';
+ $expected_view_metadata[$display_id]['tags'][] = 'search_api:test_tag';
+ [$plugin_id] = explode('_', $display_id, 2);
+ $expected_view_metadata[$display_id]['tags'][] = "search_api:test_views_$plugin_id:search_api_test_node_view__$display_id";
+ $expected_view_metadata[$display_id]['max-age'] = 100;
+ }
// Activate the alter hook and resave the view so it will recalculate the
// cacheability metadata.
@@ -161,18 +154,18 @@ class ViewsCacheabilityMetadataExportTest extends KernelTestBase {
// Check that the altered metadata is now present in the view and the
// configuration.
$view = $this->getView();
- $this->assertViewCacheabilityMetadata($expected_cacheability_metadata, $view);
+ $this->assertViewCacheabilityMetadata($expected_view_metadata, $view);
$view_config = $this->config('views.view.' . self::TEST_VIEW_ID);
- $this->assertViewConfigCacheabilityMetadata($expected_cacheability_metadata, $view_config);
+ $this->assertViewConfigCacheabilityMetadata($expected_view_metadata, $view_config);
}
/**
* Checks that the given view has the expected cacheability metadata.
*
- * @param array $expected_cacheability_metadata
- * An array of cacheability metadata that is expected to be present on the
- * view.
+ * @param array[] $expected_cacheability_metadata
+ * Arrays of cacheability metadata that are expected to be present on the
+ * various displays of the view, keyed by display ID.
* @param \Drupal\views\ViewExecutable $view
* The view.
*/
@@ -180,23 +173,23 @@ class ViewsCacheabilityMetadataExportTest extends KernelTestBase {
// Cacheability metadata is stored separately for each Views display since
// depending on how the display is configured it might have different
// caching needs. Ensure to check all displays.
- foreach (self::$testViewDisplayIds as $display_id) {
+ foreach (self::TEST_VIEW_DISPLAY_IDS as $display_id) {
$view->setDisplay($display_id);
$display = $view->getDisplay();
$actual_cacheability_metadata = $display->getCacheMetadata();
- $this->assertArrayEquals($expected_cacheability_metadata['contexts'], $actual_cacheability_metadata->getCacheContexts());
- $this->assertArrayEquals($expected_cacheability_metadata['tags'], $actual_cacheability_metadata->getCacheTags());
- $this->assertEquals($expected_cacheability_metadata['max-age'], $actual_cacheability_metadata->getCacheMaxAge());
+ $this->assertArrayEquals($expected_cacheability_metadata[$display_id]['contexts'], $actual_cacheability_metadata->getCacheContexts());
+ $this->assertArrayEquals($expected_cacheability_metadata[$display_id]['tags'], $actual_cacheability_metadata->getCacheTags());
+ $this->assertEquals($expected_cacheability_metadata[$display_id]['max-age'], $actual_cacheability_metadata->getCacheMaxAge());
}
}
/**
* Checks that the given view config has the expected cacheability metadata.
*
- * @param array $expected_cacheability_metadata
- * An array of cacheability metadata that is expected to be present on the
- * view configuration.
+ * @param array[] $expected_cacheability_metadata
+ * Arrays of cacheability metadata that are expected to be present in the
+ * configuration of the various displays of the view, keyed by display ID.
* @param \Drupal\Core\Config\Config $config
* The configuration to check.
*/
@@ -204,9 +197,9 @@ class ViewsCacheabilityMetadataExportTest extends KernelTestBase {
// Cacheability metadata is stored separately for each Views display since
// depending on how the display is configured it might have different
// caching needs. Ensure to check all displays.
- foreach (self::$testViewDisplayIds as $display_id) {
+ foreach (self::TEST_VIEW_DISPLAY_IDS as $display_id) {
$view_config_display = $config->get("display.$display_id");
- foreach ($expected_cacheability_metadata as $cache_key => $value) {
+ foreach ($expected_cacheability_metadata[$display_id] as $cache_key => $value) {
if (is_array($value)) {
$this->assertArrayEquals($value, $view_config_display['cache_metadata'][$cache_key]);
}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsQueryTypeTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsQueryTypeTest.php
new file mode 100644
index 000000000..d9fece3fa
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsQueryTypeTest.php
@@ -0,0 +1,77 @@
+installEntitySchema('node');
+ $this->installEntitySchema('search_api_task');
+
+ $this->installConfig([
+ 'search_api',
+ 'search_api_test_node_indexing',
+ ]);
+ }
+
+ /**
+ * Tests that a new view with default incorrect query gets corrected.
+ */
+ public function testViewInsert() {
+ $view_yml = file_get_contents(drupal_get_path('module', 'search_api') . '/tests/fixtures/views.view.search_api_query_type_test.yml');
+ $values = Yaml::decode($view_yml);
+ $view = View::create($values);
+ $this->assertTrue($view->isNew());
+ $view->save();
+
+ // Check that the altered metadata is now present in the view and the
+ // configuration.
+ $view = \Drupal::getContainer()
+ ->get('entity_type.manager')
+ ->getStorage('view')
+ ->load($values['id']);
+ assert($view instanceof ViewEntityInterface);
+ $executable = \Drupal::getContainer()->get('views.executable')->get($view);
+ $display = $executable->getDisplay();
+ $this->assertEquals('search_api_query', $display->getOption('query')['type']);
+ $this->assertEquals('none', $display->getOption('cache')['type']);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsQueryTypeUpdateTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsQueryTypeUpdateTest.php
new file mode 100644
index 000000000..ce6341a3c
--- /dev/null
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Kernel/Views/ViewsQueryTypeUpdateTest.php
@@ -0,0 +1,82 @@
+installEntitySchema('node');
+ $this->installEntitySchema('search_api_task');
+
+ $this->installConfig([
+ 'search_api',
+ 'search_api_test_node_indexing',
+ ]);
+ }
+
+ /**
+ * Tests that an existing view is updated with correct query and cache plugin.
+ */
+ public function testViewUpdate() {
+ // Create the view with faulty properties.
+ $module_path = drupal_get_path('module', 'search_api');
+ $view_yml = file_get_contents("$module_path/tests/fixtures/views.view.search_api_query_type_test.yml");
+ $values = Yaml::decode($view_yml);
+ $view_id = $values['id'];
+ $config = \Drupal::configFactory()->getEditable('views.view.' . $view_id);
+ $config->setData($values);
+ $config->save();
+
+ require "$module_path/search_api.post_update.php";
+ search_api_post_update_views_query_type();
+
+ // Check that the altered metadata is now present in the view and the
+ // configuration.
+ $view = \Drupal::getContainer()
+ ->get('entity_type.manager')
+ ->getStorage('view')
+ ->load($view_id);
+ assert($view instanceof ViewEntityInterface);
+ $executable = \Drupal::getContainer()->get('views.executable')->get($view);
+ $display = $executable->getDisplay();
+ $this->assertEquals('search_api_query', $display->getOption('query')['type']);
+ $this->assertEquals('none', $display->getOption('cache')['type']);
+ }
+
+}
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Unit/Processor/TestItemsTrait.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Unit/Processor/TestItemsTrait.php
index 6b787d0d2..45cfb69cd 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Unit/Processor/TestItemsTrait.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Unit/Processor/TestItemsTrait.php
@@ -13,7 +13,7 @@ use Drupal\search_api\Item\Item;
use Drupal\search_api\Query\Query;
use Drupal\search_api\Utility\QueryHelperInterface;
use Drupal\search_api\Utility\Utility;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Provides common methods for test cases that need to create search items.
diff --git a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Unit/Processor/TokenizerTest.php b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Unit/Processor/TokenizerTest.php
index 6b8a31f29..ad4d451a3 100644
--- a/frontend/drupal9/web/modules/contrib/search_api/tests/src/Unit/Processor/TokenizerTest.php
+++ b/frontend/drupal9/web/modules/contrib/search_api/tests/src/Unit/Processor/TokenizerTest.php
@@ -137,6 +137,14 @@ class TokenizerTest extends UnitTestCase {
[Utility::createTextToken('foobr')],
['ignored' => 'a'],
],
+ [
+ 'foo-bar',
+ [Utility::createTextToken('foo-bar')],
+ [
+ 'ignored' => '',
+ 'spaces' => ' ',
+ ],
+ ],
// Test multiple ignored characters are still treated as word boundary.
[
'foobar',
diff --git a/frontend/drupal9/web/modules/contrib/twig_vardumper/README.md b/frontend/drupal9/web/modules/contrib/twig_vardumper/README.md
deleted file mode 100644
index 74d2c4f4e..000000000
--- a/frontend/drupal9/web/modules/contrib/twig_vardumper/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Twig VarDumper for Drupal 9
-
-Provides a way to display Twig PHP variables in a pretty way.
-
-Twig VarDumper provides a better `{{ dump() }}` and `{{ vardumper() }}` function that can help you debug Twig variables.
-
-By default, the module display the var_dump output, just like the other common debugging mode.
-
-Make sure to have the required Symfony libraries to get this module working.
-
-See the examples below on how to use it, it's very easy to use.
-
-## Installation
-
-The module is relying on the VarDumper and http-foundation components of the Symfony project.
-There easiest way to install this module is with composer. Here are the commands to run:
-
-* `composer config repositories.drupal composer https://packages.drupal.org/9`
-* `composer require drupal/twig_vardumper`
-* `drush en twig_vardumper -y`
-* Once the module and/or the submodules are enabled, don't forget to check for the new user permissions.
-
-## How to use
-
-Enable the module twig_vardumper then (e.g., page.html.twig)...
-
-
-
- {{ dump(page.content) }}
- {{ vardumper(page.content) }}
-
- {{ page.content }}
-
-
-
-## Related modules
-
-* Twig Tweak: with drupal_dump() etc...
-
-## Related documentation
-
-* https://www.drupal.org/docs/8/theming/twig/debugging-twig-templates
-* https://front.id/en/articles/drupal-template-helper
-* https://www.keopx.net/blog/drupal-template-helper-para-drupal-8 (Spanish)
diff --git a/frontend/drupal9/web/modules/contrib/twig_vardumper/composer.json b/frontend/drupal9/web/modules/contrib/twig_vardumper/composer.json
deleted file mode 100644
index a1fb96c13..000000000
--- a/frontend/drupal9/web/modules/contrib/twig_vardumper/composer.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "keopx/twig_vardumper",
- "type": "drupal-module",
- "description": "Twig vardumper provides a better dump() and vardumper() function that can help you debug Twig variables.",
- "keywords": ["Drupal", "Twig", "Development"],
- "license": "GPL-2.0+",
- "homepage": "https://www.drupal.org/project/twig_vardumper",
- "minimum-stability": "dev",
- "prefer-stable": true,
- "authors": [
- { "name": "Ruben Egiguren a.k.a keopx", "email": "keopx@keopx.net" }
- ],
- "support": {
- "issues": "https://www.drupal.org/project/issues/twig_vardumper",
- "source": "https://git.drupalcode.org/project/twig_vardumper"
- },
- "require": {
- "symfony/var-dumper": "~5"
- }
-}
diff --git a/frontend/drupal9/web/modules/contrib/twig_vardumper/src/TwigExtension.php b/frontend/drupal9/web/modules/contrib/twig_vardumper/src/TwigExtension.php
deleted file mode 100644
index 74e0b1133..000000000
--- a/frontend/drupal9/web/modules/contrib/twig_vardumper/src/TwigExtension.php
+++ /dev/null
@@ -1,77 +0,0 @@
- ['html'],
- 'needs_context' => TRUE,
- 'needs_environment' => TRUE,
- 'is_variadic' => TRUE,
- ]),
- new TwigFunction('vardumper', [$this, 'drupalDump'], [
- 'is_safe' => ['html'],
- 'needs_context' => TRUE,
- 'needs_environment' => TRUE,
- 'is_variadic' => TRUE,
- ]),
- ];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getName() {
- return 'twig_vardumper';
- }
-
- /**
- * Dumps information about variables.
- *
- * @param \Twig\Environment $env
- * Enviroment values.
- * @param array $context
- * Context values.
- * @param array $args
- * Variables.
- *
- * @return false|string|void
- */
- public function drupalDump(Environment $env, array $context, array $args = []) {
-
- if (!$env->isDebug()) {
- return;
- }
-
- ob_start();
- $var_dumper = '\Symfony\Component\VarDumper\VarDumper';
- if (class_exists($var_dumper)) {
- if (!empty($args)) {
- foreach ($args as $arg) {
- call_user_func($var_dumper . '::dump', $arg);
- }
- }
- else {
- call_user_func($var_dumper . '::dump', $context);
- }
- return ob_get_clean();
- }
- else {
- trigger_error('Could not dump the variable because symfony/var-dumper component is not installed.', E_USER_WARNING);
- }
- }
-
-}
diff --git a/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.info.yml b/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.info.yml
deleted file mode 100644
index d36a42f5c..000000000
--- a/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.info.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-name: Twig VarDumper
-type: module
-description: 'Provides a way to display Twig PHP variables in a pretty way.'
-core_version_requirement: ^9
-package: Development
-
-# Information added by Drupal.org packaging script on 2020-06-11
-version: '3.0.2'
-project: 'twig_vardumper'
-datestamp: 1591885119
diff --git a/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.install b/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.install
deleted file mode 100644
index b8a2120f2..000000000
--- a/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.install
+++ /dev/null
@@ -1,23 +0,0 @@
- t('Twig Vardumper requires the symfony/var-dumper library.'),
- 'severity' => REQUIREMENT_ERROR,
- ];
- }
- }
-
- return $requirements;
-}
diff --git a/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.module b/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.module
deleted file mode 100644
index 0e1812b01..000000000
--- a/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.module
+++ /dev/null
@@ -1,24 +0,0 @@
-' . t('About') . '';
- $output .= '' . t('Twig vardumper provides a better {{ dump() }} and {{ vardumper() }} function that can help you debug Twig variables.') . '
';
- return $output;
-
- default:
- }
-}
diff --git a/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.services.yml b/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.services.yml
deleted file mode 100644
index 5cf5ddd07..000000000
--- a/frontend/drupal9/web/modules/contrib/twig_vardumper/twig_vardumper.services.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-services:
- twig_vardumper.twig_extension:
- class: Drupal\twig_vardumper\TwigExtension
- arguments: []
- tags:
- - { name: twig.extension }