diff --git a/2820347-44.patch b/2820347-44.patch new file mode 100644 index 0000000000..e970f9c11b --- /dev/null +++ b/2820347-44.patch @@ -0,0 +1,283 @@ +diff --git a/core/modules/views/src/Form/ViewsExposedForm.php b/core/modules/views/src/Form/ViewsExposedForm.php +index 44e7ec4b0f..0647d00563 100644 +--- a/core/modules/views/src/Form/ViewsExposedForm.php ++++ b/core/modules/views/src/Form/ViewsExposedForm.php +@@ -5,6 +5,7 @@ + use Drupal\Component\Utility\Html; + use Drupal\Core\Form\FormBase; + use Drupal\Core\Form\FormStateInterface; ++use Drupal\Core\Path\CurrentPathStack; + use Drupal\Core\Render\Element\Checkboxes; + use Drupal\Core\Url; + use Drupal\views\ExposedFormCache; +@@ -22,21 +23,35 @@ class ViewsExposedForm extends FormBase { + */ + protected $exposedFormCache; + ++ ++ /** ++ * The current path stack. ++ * ++ * @var \Drupal\Core\Path\CurrentPathStack ++ */ ++ protected $currentPathStack; ++ + /** + * Constructs a new ViewsExposedForm + * + * @param \Drupal\views\ExposedFormCache $exposed_form_cache + * The exposed form cache. ++ * @param \Drupal\Core\Path\CurrentPathStack $current_path_stack ++ * The current path stack. + */ +- public function __construct(ExposedFormCache $exposed_form_cache) { ++ public function __construct(ExposedFormCache $exposed_form_cache, CurrentPathStack $current_path_stack) { + $this->exposedFormCache = $exposed_form_cache; ++ $this->currentPathStack = $current_path_stack; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { +- return new static($container->get('views.exposed_form_cache')); ++ return new static( ++ $container->get('views.exposed_form_cache'), ++ $container->get('path.current') ++ ); + } + + /** +@@ -111,7 +126,23 @@ public function buildForm(array $form, FormStateInterface $form_state) { + '#id' => Html::getUniqueId('edit-submit-' . $view->storage->id()), + ]; + +- $form['#action'] = $view->hasUrl() ? $view->getUrl()->toString() : Url::fromRoute('')->toString(); ++ if (!$view->hasUrl()) { ++ // If we are building an ajax form, don't set the action to the views ++ // ajax route. ++ $current = Url::fromRoute(''); ++ if ($this->getRouteMatch()->getRouteName() !== 'views.ajax') { ++ $form_action = $current->toString(); ++ } ++ else { ++ // Instead set the action to the page we were on. ++ $form_action = $this->currentPathStack->getPath(); ++ } ++ } ++ else { ++ $form_action = $view->getUrl()->toString(); ++ } ++ ++ $form['#action'] = $form_action; + $form['#theme'] = $view->buildThemeFunctions('views_exposed_form'); + $form['#id'] = Html::cleanCssIdentifier('views_exposed_form-' . $view->storage->id() . '-' . $display['id']); + +diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_block_exposed_ajax.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_block_exposed_ajax.yml +new file mode 100644 +index 0000000000..0c3544621f +--- /dev/null ++++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_block_exposed_ajax.yml +@@ -0,0 +1,80 @@ ++langcode: en ++status: true ++dependencies: ++ config: ++ - core.entity_view_mode.node.teaser ++ module: ++ - node ++id: test_block_exposed_ajax ++label: '' ++module: views ++description: '' ++tag: '' ++base_table: node_field_data ++base_field: nid ++core: '8' ++display: ++ default: ++ display_options: ++ access: ++ type: none ++ cache: ++ type: tag ++ exposed_form: ++ options: ++ submit_button: Apply ++ reset_button: true ++ type: basic ++ filters: ++ type: ++ expose: ++ identifier: type ++ label: 'Content: Type' ++ operator_id: type_op ++ reduce: false ++ exposed: true ++ field: type ++ id: type ++ table: node_field_data ++ plugin_id: in_operator ++ entity_type: node ++ entity_field: type ++ pager: ++ type: full ++ query: ++ options: ++ query_comment: '' ++ type: views_query ++ style: ++ type: default ++ row: ++ type: 'entity:node' ++ display_extenders: { } ++ use_ajax: true ++ display_plugin: default ++ display_title: Master ++ id: default ++ position: 0 ++ cache_metadata: ++ max-age: -1 ++ contexts: ++ - 'languages:language_interface' ++ - url ++ - url.query_args ++ - 'user.node_grants:view' ++ tags: { } ++ block_1: ++ display_plugin: block ++ id: block_1 ++ display_title: Block ++ position: 2 ++ display_options: ++ display_extenders: { } ++ cache_metadata: ++ max-age: -1 ++ contexts: ++ - 'languages:language_interface' ++ - url ++ - url.query_args ++ - 'user.node_grants:view' ++ tags: { } +diff --git a/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php +new file mode 100644 +index 0000000000..09fe7e896a +--- /dev/null ++++ b/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php +@@ -0,0 +1,79 @@ ++drupalPlaceBlock('views_block:test_block_exposed_ajax-block_1'); ++ $this->createContentType(['type' => 'page']); ++ $this->createContentType(['type' => 'article']); ++ $this->createNode(['title' => 'Page A']); ++ $this->createNode(['title' => 'Page B']); ++ $this->createNode(['title' => 'Article A', 'type' => 'article']); ++ ++ $this->drupalLogin($this->drupalCreateUser([ ++ 'access content', ++ ])); ++ } ++ ++ /** ++ * Tests if exposed filtering and reset works with a views block and ajax. ++ */ ++ public function testExposedFilteringAndReset() { ++ $node = $this->createNode(); ++ $this->drupalGet($node->toUrl()); ++ ++ $page = $this->getSession()->getPage(); ++ ++ // Ensure that the Content we're testing for is present. ++ $html = $page->getHtml(); ++ $this->assertContains('Page A', $html); ++ $this->assertContains('Page B', $html); ++ $this->assertContains('Article A', $html); ++ ++ // Filter by page type. ++ $this->submitForm(['type' => 'page'], t('Apply')); ++ $this->assertSession()->assertWaitOnAjaxRequest(); ++ $this->assertSession()->waitForElementRemoved('xpath', "//text()[normalize-space() = 'Article A']"); ++ ++ // Verify that only the page nodes are present. ++ $html = $page->getHtml(); ++ $this->assertContains('Page A', $html); ++ $this->assertContains('Page B', $html); ++ $this->assertNotContains('Article A', $html); ++ ++ // Reset the form. ++ $this->submitForm([], t('Reset')); ++ // Assert we are still on the node page. ++ $html = $page->getHtml(); ++ $this->assertNotContains('The requested page could not be found.', $html); ++ $this->assertEquals($node->toUrl()->setAbsolute()->toString(), $this->getSession()->getCurrentUrl()); ++ } ++ ++} +diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php +index 81d379f915..3fd45e02c9 100644 +--- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php ++++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php +@@ -72,6 +72,32 @@ public function waitForElement($selector, $locator, $timeout = 10000) { + } + + /** ++ * Looks for the specified selector and returns TRUE when it is unavailable. ++ * ++ * @param string $selector ++ * The selector engine name. See ElementInterface::findAll() for the ++ * supported selectors. ++ * @param string|array $locator ++ * The selector locator. ++ * @param int $timeout ++ * (Optional) Timeout in milliseconds, defaults to 10000. ++ * ++ * @return bool ++ * TRUE if not found, FALSE if found. ++ * ++ * @see \Behat\Mink\Element\ElementInterface::findAll() ++ */ ++ public function waitForElementRemoved($selector, $locator, $timeout = 10000) { ++ $page = $this->session->getPage(); ++ ++ $result = $page->waitFor($timeout / 1000, function() use ($page, $selector, $locator) { ++ return !$page->find($selector, $locator); ++ }); ++ ++ return $result; ++ } ++ ++ /** + * Waits for the specified selector and returns it when available and visible. + * + * @param string $selector diff --git a/core/modules/views/src/Form/ViewsExposedForm.php b/core/modules/views/src/Form/ViewsExposedForm.php index 44e7ec4b0f..198f860b08 100644 --- a/core/modules/views/src/Form/ViewsExposedForm.php +++ b/core/modules/views/src/Form/ViewsExposedForm.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\Render\Element\Checkboxes; use Drupal\Core\Url; use Drupal\views\ExposedFormCache; @@ -22,21 +23,35 @@ class ViewsExposedForm extends FormBase { */ protected $exposedFormCache; + + /** + * The current path stack. + * + * @var \Drupal\Core\Path\CurrentPathStack + */ + protected $currentPathStack; + /** * Constructs a new ViewsExposedForm * * @param \Drupal\views\ExposedFormCache $exposed_form_cache * The exposed form cache. + * @param \Drupal\Core\Path\CurrentPathStack $current_path_stack + * The current path stack. */ - public function __construct(ExposedFormCache $exposed_form_cache) { + public function __construct(ExposedFormCache $exposed_form_cache, CurrentPathStack $current_path_stack) { $this->exposedFormCache = $exposed_form_cache; + $this->currentPathStack = $current_path_stack; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('views.exposed_form_cache')); + return new static( + $container->get('views.exposed_form_cache'), + $container->get('path.current') + ); } /** @@ -111,7 +126,23 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#id' => Html::getUniqueId('edit-submit-' . $view->storage->id()), ]; - $form['#action'] = $view->hasUrl() ? $view->getUrl()->toString() : Url::fromRoute('')->toString(); + if (!$view->hasUrl()) { + // If we are building an ajax form, don't set the action to the views + // ajax route. + if ($this->getRouteMatch()->getRouteName() !== 'views.ajax') { + $current = Url::fromRoute(''); + $form_action = $current->toString(); + } + else { + // Instead set the action to the page we were on. + $form_action = $this->currentPathStack->getPath(); + } + } + else { + $form_action = $view->getUrl()->toString(); + } + + $form['#action'] = $form_action; $form['#theme'] = $view->buildThemeFunctions('views_exposed_form'); $form['#id'] = Html::cleanCssIdentifier('views_exposed_form-' . $view->storage->id() . '-' . $display['id']); diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_block_exposed_ajax.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_block_exposed_ajax.yml new file mode 100644 index 0000000000..0c3544621f --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_block_exposed_ajax.yml @@ -0,0 +1,80 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.node.teaser + module: + - node +id: test_block_exposed_ajax +label: '' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: '8' +display: + default: + display_options: + access: + type: none + cache: + type: tag + exposed_form: + options: + submit_button: Apply + reset_button: true + type: basic + filters: + type: + expose: + identifier: type + label: 'Content: Type' + operator_id: type_op + reduce: false + exposed: true + field: type + id: type + table: node_field_data + plugin_id: in_operator + entity_type: node + entity_field: type + pager: + type: full + query: + options: + query_comment: '' + type: views_query + style: + type: default + row: + type: 'entity:node' + display_extenders: { } + use_ajax: true + display_plugin: default + display_title: Master + id: default + position: 0 + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + tags: { } + block_1: + display_plugin: block + id: block_1 + display_title: Block + position: 2 + display_options: + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + tags: { } diff --git a/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php new file mode 100644 index 0000000000..09fe7e896a --- /dev/null +++ b/core/modules/views/tests/src/FunctionalJavascript/BlockExposedFilterAJAXTest.php @@ -0,0 +1,79 @@ +drupalPlaceBlock('views_block:test_block_exposed_ajax-block_1'); + $this->createContentType(['type' => 'page']); + $this->createContentType(['type' => 'article']); + $this->createNode(['title' => 'Page A']); + $this->createNode(['title' => 'Page B']); + $this->createNode(['title' => 'Article A', 'type' => 'article']); + + $this->drupalLogin($this->drupalCreateUser([ + 'access content', + ])); + } + + /** + * Tests if exposed filtering and reset works with a views block and ajax. + */ + public function testExposedFilteringAndReset() { + $node = $this->createNode(); + $this->drupalGet($node->toUrl()); + + $page = $this->getSession()->getPage(); + + // Ensure that the Content we're testing for is present. + $html = $page->getHtml(); + $this->assertContains('Page A', $html); + $this->assertContains('Page B', $html); + $this->assertContains('Article A', $html); + + // Filter by page type. + $this->submitForm(['type' => 'page'], t('Apply')); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->waitForElementRemoved('xpath', "//text()[normalize-space() = 'Article A']"); + + // Verify that only the page nodes are present. + $html = $page->getHtml(); + $this->assertContains('Page A', $html); + $this->assertContains('Page B', $html); + $this->assertNotContains('Article A', $html); + + // Reset the form. + $this->submitForm([], t('Reset')); + // Assert we are still on the node page. + $html = $page->getHtml(); + $this->assertNotContains('The requested page could not be found.', $html); + $this->assertEquals($node->toUrl()->setAbsolute()->toString(), $this->getSession()->getCurrentUrl()); + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php index 81d379f915..3fd45e02c9 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php @@ -72,6 +72,32 @@ public function waitForElement($selector, $locator, $timeout = 10000) { } /** + * Looks for the specified selector and returns TRUE when it is unavailable. + * + * @param string $selector + * The selector engine name. See ElementInterface::findAll() for the + * supported selectors. + * @param string|array $locator + * The selector locator. + * @param int $timeout + * (Optional) Timeout in milliseconds, defaults to 10000. + * + * @return bool + * TRUE if not found, FALSE if found. + * + * @see \Behat\Mink\Element\ElementInterface::findAll() + */ + public function waitForElementRemoved($selector, $locator, $timeout = 10000) { + $page = $this->session->getPage(); + + $result = $page->waitFor($timeout / 1000, function() use ($page, $selector, $locator) { + return !$page->find($selector, $locator); + }); + + return $result; + } + + /** * Waits for the specified selector and returns it when available and visible. * * @param string $selector diff --git a/interdiff.txt b/interdiff.txt new file mode 100644 index 0000000000..95ec5873f8 --- /dev/null +++ b/interdiff.txt @@ -0,0 +1,14 @@ +diff --git a/core/modules/views/src/Form/ViewsExposedForm.php b/core/modules/views/src/Form/ViewsExposedForm.php +index 0647d00563..198f860b08 100644 +--- a/core/modules/views/src/Form/ViewsExposedForm.php ++++ b/core/modules/views/src/Form/ViewsExposedForm.php +@@ -129,8 +129,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { + if (!$view->hasUrl()) { + // If we are building an ajax form, don't set the action to the views + // ajax route. +- $current = Url::fromRoute(''); + if ($this->getRouteMatch()->getRouteName() !== 'views.ajax') { ++ $current = Url::fromRoute(''); + $form_action = $current->toString(); + } + else {