diff --git a/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_user_name.yml b/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_user_name.yml index 9bcf36b3fb..b3ae725d96 100644 --- a/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_user_name.yml +++ b/core/modules/user/tests/modules/user_test_views/test_views/views.view.test_user_name.yml @@ -19,6 +19,14 @@ display: type: tag exposed_form: type: basic + options: + submit_button: Apply + reset_button: true + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc pager: type: full row: diff --git a/core/modules/user/tests/src/Functional/Views/HandlerFilterUserNameTest.php b/core/modules/user/tests/src/Functional/Views/HandlerFilterUserNameTest.php index 5fddb8a906..1c6f8c340f 100644 --- a/core/modules/user/tests/src/Functional/Views/HandlerFilterUserNameTest.php +++ b/core/modules/user/tests/src/Functional/Views/HandlerFilterUserNameTest.php @@ -189,6 +189,16 @@ public function testExposedFilter() { foreach ($this->accounts as $account) { $this->assertSession()->pageTextContains($account->id()); } + + // Pass in an invalid username, after pressing reset the error should be + // gone. + $users = [$this->randomMachineName()]; + $users = array_map('strtolower', $users); + $options['query']['uid'] = implode(', ', $users); + $this->drupalGet($path, $options); + $this->assertSession()->pageTextContains('There are no users matching "' . implode(', ', $users) . '".'); + $this->getSession()->getPage()->pressButton('Reset'); + $this->assertSession()->pageTextNotContains('There are no users matching'); } } diff --git a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php index d986a97d05..3c3e0f5c71 100644 --- a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php +++ b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php @@ -251,6 +251,7 @@ public function exposedFormAlter(&$form, FormStateInterface $form_state) { '#value' => $this->options['reset_button_label'], '#type' => 'submit', '#weight' => 10, + '#limit_validation_errors' => [], ]; // Get an array of exposed filters, keyed by identifier option. @@ -279,6 +280,9 @@ public function exposedFormAlter(&$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function exposedFormValidate(&$form, FormStateInterface $form_state) { + if (!$form_state->isValueEmpty('op') && $form_state->getValue('op') === $this->options['reset_button_label']) { + $form_state->clearErrors(); + } if ($pager_plugin = $form_state->get('pager_plugin')) { $pager_plugin->exposedFormValidate($form, $form_state); } diff --git a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php.orig b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php.orig new file mode 100644 index 0000000000..d986a97d05 --- /dev/null +++ b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php.orig @@ -0,0 +1,386 @@ + $this->t('Apply')]; + $options['reset_button'] = ['default' => FALSE]; + $options['reset_button_label'] = ['default' => $this->t('Reset')]; + $options['exposed_sorts_label'] = ['default' => $this->t('Sort by')]; + $options['expose_sort_order'] = ['default' => TRUE]; + $options['sort_asc_label'] = ['default' => $this->t('Asc')]; + $options['sort_desc_label'] = ['default' => $this->t('Desc')]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + $form['submit_button'] = [ + '#type' => 'textfield', + '#title' => $this->t('Submit button text'), + '#default_value' => $this->options['submit_button'], + '#required' => TRUE, + ]; + + $form['reset_button'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Include reset button (resets all applied exposed filters)'), + '#default_value' => $this->options['reset_button'], + ]; + + $form['reset_button_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Reset button label'), + '#description' => $this->t('Text to display in the reset button of the exposed form.'), + '#default_value' => $this->options['reset_button_label'], + '#required' => TRUE, + '#states' => [ + 'invisible' => [ + 'input[name="exposed_form_options[reset_button]"]' => ['checked' => FALSE], + ], + ], + ]; + + $form['exposed_sorts_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Exposed sorts label'), + '#default_value' => $this->options['exposed_sorts_label'], + '#required' => TRUE, + ]; + + $form['expose_sort_order'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Allow people to choose the sort order'), + '#description' => $this->t('If sort order is not exposed, the sort criteria settings for each sort will determine its order.'), + '#default_value' => $this->options['expose_sort_order'], + ]; + + $form['sort_asc_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label for ascending sort'), + '#default_value' => $this->options['sort_asc_label'], + '#required' => TRUE, + '#states' => [ + 'visible' => [ + 'input[name="exposed_form_options[expose_sort_order]"]' => ['checked' => TRUE], + ], + ], + ]; + + $form['sort_desc_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label for descending sort'), + '#default_value' => $this->options['sort_desc_label'], + '#required' => TRUE, + '#states' => [ + 'visible' => [ + 'input[name="exposed_form_options[expose_sort_order]"]' => ['checked' => TRUE], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function renderExposedForm($block = FALSE) { + // Deal with any exposed filters we may have, before building. + $form_state = (new FormState()) + ->setStorage([ + 'view' => $this->view, + 'display' => &$this->view->display_handler->display, + 'rerender' => TRUE, + ]) + ->setMethod('get') + ->setAlwaysProcess() + ->disableRedirect(); + + // Some types of displays (eg. attachments) may wish to use the exposed + // filters of their parent displays instead of showing an additional + // exposed filter form for the attachment as well as that for the parent. + if (!$this->view->display_handler->displaysExposed() || (!$block && $this->view->display_handler->getOption('exposed_block'))) { + $form_state->set('rerender', NULL); + } + + if (!empty($this->ajax)) { + $form_state->set('ajax', TRUE); + } + + $form = \Drupal::formBuilder()->buildForm('\Drupal\views\Form\ViewsExposedForm', $form_state); + $errors = $form_state->getErrors(); + + // If the exposed form had errors, do not build the view. + if (!empty($errors)) { + $this->view->build_info['abort'] = TRUE; + } + + if (!$this->view->display_handler->displaysExposed() || (!$block && $this->view->display_handler->getOption('exposed_block'))) { + return []; + } + else { + return $form; + } + } + + /** + * {@inheritdoc} + */ + public function query() { + $view = $this->view; + $exposed_data = $view->exposed_data ?? []; + $sort_by = $exposed_data['sort_by'] ?? NULL; + if (!empty($sort_by)) { + // Make sure the original order of sorts is preserved + // (e.g. a sticky sort is often first) + $view->query->orderby = []; + foreach ($view->sort as $sort) { + if (!$sort->isExposed()) { + $sort->query(); + } + elseif (!empty($sort->options['expose']['field_identifier']) && $sort->options['expose']['field_identifier'] === $sort_by) { + if (isset($exposed_data['sort_order']) && in_array($exposed_data['sort_order'], ['ASC', 'DESC'], TRUE)) { + $sort->options['order'] = $exposed_data['sort_order']; + } + $sort->setRelationship(); + $sort->query(); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function preRender($values) {} + + /** + * {@inheritdoc} + */ + public function postRender(&$output) {} + + /** + * {@inheritdoc} + */ + public function preExecute() {} + + /** + * {@inheritdoc} + */ + public function postExecute() {} + + /** + * {@inheritdoc} + */ + public function exposedFormAlter(&$form, FormStateInterface $form_state) { + if (!empty($this->options['submit_button'])) { + $form['actions']['submit']['#value'] = $this->options['submit_button']; + } + + // Check if there is exposed sorts for this view + $exposed_sorts = []; + $exposed_sorts_options = []; + foreach ($this->view->sort as $id => $handler) { + if ($handler->canExpose() && $handler->isExposed() && !empty($handler->options['expose']['field_identifier'])) { + $exposed_sorts[$handler->options['expose']['field_identifier']] = $id; + $exposed_sorts_options[$handler->options['expose']['field_identifier']] = $handler->options['expose']['label']; + } + } + + if (count($exposed_sorts)) { + $form['sort_by'] = [ + '#type' => 'select', + '#options' => $exposed_sorts_options, + '#title' => $this->options['exposed_sorts_label'], + ]; + $sort_order = [ + 'ASC' => $this->options['sort_asc_label'], + 'DESC' => $this->options['sort_desc_label'], + ]; + $user_input = $form_state->getUserInput(); + if (isset($user_input['sort_by']) && isset($exposed_sorts[$user_input['sort_by']]) && isset($this->view->sort[$exposed_sorts[$user_input['sort_by']]])) { + $default_sort_order = $this->view->sort[$exposed_sorts[$user_input['sort_by']]]->options['order']; + } + else { + $first_sort = reset($this->view->sort); + $default_sort_order = $first_sort->options['order']; + } + + if (!isset($user_input['sort_by'])) { + $keys = array_keys($exposed_sorts); + $user_input['sort_by'] = array_shift($keys); + $form_state->setUserInput($user_input); + } + + if ($this->options['expose_sort_order']) { + $form['sort_order'] = [ + '#type' => 'select', + '#options' => $sort_order, + '#title' => $this->t('Order', [], ['context' => 'Sort order']), + '#default_value' => $default_sort_order, + ]; + } + } + + if (!empty($this->options['reset_button'])) { + $form['actions']['reset'] = [ + '#value' => $this->options['reset_button_label'], + '#type' => 'submit', + '#weight' => 10, + ]; + + // Get an array of exposed filters, keyed by identifier option. + $exposed_filters = []; + foreach ($this->view->filter as $id => $handler) { + if ($handler->canExpose() && $handler->isExposed() && !empty($handler->options['expose']['identifier'])) { + $exposed_filters[$handler->options['expose']['identifier']] = $id; + } + } + $all_exposed = array_merge($exposed_sorts, $exposed_filters); + + // Set the access to FALSE if there is no exposed input. + if (!array_intersect_key($all_exposed, $this->view->getExposedInput())) { + $form['actions']['reset']['#access'] = FALSE; + } + } + + $pager = $this->view->display_handler->getPlugin('pager'); + if ($pager) { + $pager->exposedFormAlter($form, $form_state); + $form_state->set('pager_plugin', $pager); + } + } + + /** + * {@inheritdoc} + */ + public function exposedFormValidate(&$form, FormStateInterface $form_state) { + if ($pager_plugin = $form_state->get('pager_plugin')) { + $pager_plugin->exposedFormValidate($form, $form_state); + } + } + + /** + * {@inheritdoc} + */ + public function exposedFormSubmit(&$form, FormStateInterface $form_state, &$exclude) { + if (!$form_state->isValueEmpty('op') && $form_state->getValue('op') == $this->options['reset_button_label']) { + $this->resetForm($form, $form_state); + } + if ($pager_plugin = $form_state->get('pager_plugin')) { + $pager_plugin->exposedFormSubmit($form, $form_state, $exclude); + $exclude[] = 'pager_plugin'; + } + } + + /** + * Resets all the states of the exposed form. + * + * This method is called when the "Reset" button is triggered. Clears + * user inputs, stored session, and the form state. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function resetForm(&$form, FormStateInterface $form_state) { + // _SESSION is not defined for users who are not logged in. + + // If filters are not overridden, store the 'remember' settings on the + // default display. If they are, store them on this display. This way, + // multiple displays in the same view can share the same filters and + // remember settings. + $display_id = ($this->view->display_handler->isDefaulted('filters')) ? 'default' : $this->view->current_display; + + $session = $this->view->getRequest()->getSession(); + $views_session = $session->get('views', []); + if (isset($views_session[$this->view->storage->id()][$display_id])) { + unset($views_session[$this->view->storage->id()][$display_id]); + } + $session->set('views', $views_session); + + // Set the form to allow redirect. + if (empty($this->view->live_preview) && !\Drupal::request()->isXmlHttpRequest()) { + $form_state->disableRedirect(FALSE); + } + else { + $form_state->setRebuild(); + $this->view->exposed_data = []; + } + + $form_state->setRedirect(''); + $form_state->setValues([]); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = []; + if ($this->options['expose_sort_order']) { + // The sort order query arg is just important in case there is an exposed + // sort order. + $has_exposed_sort_handler = FALSE; + /** @var \Drupal\views\Plugin\views\sort\SortPluginBase $sort_handler */ + foreach ($this->displayHandler->getHandlers('sort') as $sort_handler) { + if ($sort_handler->isExposed()) { + $has_exposed_sort_handler = TRUE; + } + } + + if ($has_exposed_sort_handler) { + $contexts[] = 'url.query_args:sort_order'; + } + } + + // Merge in cache contexts for all exposed filters to prevent display of + // cached forms. + foreach ($this->displayHandler->getHandlers('filter') as $filter_handler) { + if ($filter_handler->isExposed()) { + $contexts = Cache::mergeContexts($contexts, $filter_handler->getCacheContexts()); + } + } + + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return []; + } + +} diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php index 1d24826b75..4424078b64 100644 --- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php +++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php @@ -1495,7 +1495,7 @@ public function acceptExposedInput($input) { $value = $input[$this->options['group_info']['identifier']]; } else { - $value = $input[$this->options['expose']['identifier']]; + $value = $input[$this->options['expose']['identifier']] ?? NULL; } // Various ways to check for the absence of non-required input.