Commit 706ce181 authored by dragos-dumi's avatar dragos-dumi Committed by borisson_

Issue #2717537 by dragos-dumi, borisson_: Breadcrumbs support

parent ca4c9ccf
......@@ -17,4 +17,14 @@ facets.facet_source.*:
url_processor:
type: string
label: 'Url processor'
breadcrumb:
type: mapping
labal: 'Breadcrumb'
mapping:
active:
type: boolean
label: 'Append active facets to breadcrumb'
group:
type: boolean
label: 'Group active items under same crumb'
third_party_settings: {}
......@@ -6,9 +6,12 @@
*/
use Drupal\Component\Utility\Html;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\facets\Entity\Facet;
use Drupal\facets\Entity\FacetSource;
use Drupal\facets\FacetInterface;
use Drupal\search_api\Query\QueryInterface;
use Drupal\views\Entity\View;
use Drupal\Core\Entity\EntityInterface;
......@@ -211,3 +214,127 @@ function facets_entity_predelete(EntityInterface $entity) {
function facets_preprocess_facets_item_list(&$variables) {
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();
// 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 */
$facet_source_plugin = $facet_source_manager->createInstance($definition['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(':', '__', $definition['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 && $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();
$facets = $facet_manager->getFacetsByFacetSourceId($definition['id']);
// Sort facets by weight.
uasort($facets, function (FacetInterface $a, FacetInterface $b) {
if ($a->getWeight() == $b->getWeight()) {
return 0;
}
return ($a->getWeight() < $b->getWeight()) ? -1 : 1;
});
/** @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 ($active_items = $facet->getActiveItems()) {
// 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()) {
// 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->getUrlAlias()][] = $cloned_result;
}
}
$url_processor->buildUrls($facet, $active_results[$facet->getUrlAlias()]);
$active_facets[$facet->getUrlAlias()] = $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.
foreach ($active_results as $facet_alias => $facet_results) {
$facet_used_result[] = $facet_alias;
$facet_crumb_items = [];
reset($facet_results);
$facet_url = clone current($facet_results)->getUrl();
// Because we can't get the desired url 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_url = $res->getUrl();
/** @var \Drupal\Core\Url $facet_url */
$query = $facet_url->getOption('query');
$source_filter = $query[$facet_source->getFilterKey()];
$source_filter = array_unique($source_filter);
$source_filter = array_filter($source_filter);
$matches = preg_grep('/^' . implode('|^', $facet_used_result) . '/', $source_filter);
$query[$facet_source->getFilterKey()] = $matches;
$facet_crumb_items[] = $res->getDisplayValue();
}
// Set the new query and create the link.
$facet_url->setOption('query', $query);
$crumb_text = $active_facets[$facet_alias]->label() . ': ' . implode(', ', $facet_crumb_items);
$link = Link::fromTextAndUrl($crumb_text, $facet_url);
$breadcrumb->addLink($link);
}
}
}
......@@ -32,7 +32,8 @@ use Drupal\facets\FacetSourceInterface;
* "id",
* "name",
* "filter_key",
* "url_processor"
* "url_processor",
* "breadcrumb"
* },
* links = {
* "canonical" = "/admin/config/search/facets/facet-sources/",
......@@ -70,6 +71,13 @@ class FacetSource extends ConfigEntityBase implements FacetSourceInterface {
*/
protected $url_processor = 'query_string';
/**
* The breadcrumb settings.
*
* @var array
*/
protected $breadcrumb = [];
/**
* {@inheritdoc}
*/
......@@ -105,4 +113,17 @@ class FacetSource extends ConfigEntityBase implements FacetSourceInterface {
return $this->url_processor;
}
/**
* {@inheritdoc}
*/
public function getBreadcrumbSettings() {
return $this->breadcrumb;
}
/**
* {@inheritdoc}
*/
public function setBreadcrumbSettings(array $settings) {
$this->breadcrumb = $settings;
}
}
......@@ -49,4 +49,20 @@ interface FacetSourceInterface extends ConfigEntityInterface {
*/
public function getUrlProcessorName();
/**
* Returns an array with breadcrumb settings.
*
* @return array
* The breadcrumb settings.
*/
public function getBreadcrumbSettings();
/**
* Sets breadcrumb settings.
*
* @param array $settings
* The breadcrumb settings.
*/
public function setBreadcrumbSettings(array $settings);
}
......@@ -89,6 +89,7 @@ class FacetSourceEditForm extends EntityForm {
/** @var \Drupal\facets\FacetSourceInterface $facet_source */
$facet_source = $this->getEntity();
$form['#tree'] = TRUE;
$form['filter_key'] = [
'#type' => 'textfield',
'#title' => $this->t('Filter key'),
......@@ -116,6 +117,27 @@ class FacetSourceEditForm extends EntityForm {
'The URL Processor defines the url structure used for this facet source.') . '<br />- ' . implode('<br>- ', $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']['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);
}
......
<?php
namespace Drupal\Tests\facets\Functional;
use Drupal\Component\Utility\UrlHelper;
/**
* Tests the overall functionality of the Facets admin UI.
*
* @group facets
*/
class BreadcrumbIntegrationTest extends FacetsTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'views',
'node',
'search_api',
'facets',
'block',
'facets_search_api_dependency',
];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->setUpExampleStructure();
$this->insertExampleContent();
self::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->drupalGet('admin/config/search/facets');
$this->clickLink('Configure', 1);
$edit = [
'filter_key' => 'f',
'url_processor' => 'query_string',
'breadcrumb[active]' => TRUE,
'breadcrumb[group]' => TRUE,
];
$this->drupalPostForm(NULL, $edit, 'Save');
$id = 'keywords';
$name = '#Keywords';
$this->createFacet($name, $id, 'keywords');
$this->resetAll();
$this->drupalGet('admin/config/search/facets/' . $id . '/edit');
$id = 'type';
$name = '#Type';
$this->createFacet($name, $id);
$this->resetAll();
$this->drupalGet('admin/config/search/facets/' . $id . '/edit');
$this->drupalPostForm(NULL, ['facet_settings[weight]' => '1'], 'Save');
// 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->clickLink('article');
$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());
$this->assertArraySubset($initial_query, $curr_url['query']);
}
/**
* Tests Breadcrumb integration without grouping.
*/
// public function testNonGroupingIntegration() {
// TODO test it after we implement non grouping functionality.
// }
}
......@@ -83,4 +83,28 @@ class FacetSourceTest extends FacetsTestBase {
$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 = array(
'breadcrumb[active]' => TRUE,
'breadcrumb[group]' => TRUE,
);
$this->drupalPostForm(NULL, $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]');
}
}
......@@ -60,6 +60,8 @@ class FacetSourceTest extends KernelTestBase {
* @covers ::getFilterKey
* @covers ::setUrlProcessor
* @covers ::getUrlProcessorName
* @covers ::setBreadcrumbSettings
* @covers ::getBreadcrumbSettings
*/
public function testGetterSetters() {
$fs = new FacetSource(['id' => 'llama'], 'facets_facet_source');
......@@ -67,12 +69,17 @@ class FacetSourceTest extends KernelTestBase {
$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());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment