From d3dabc460323d525ef711d3ba3e67f0bbd597ad7 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Mon, 28 Jun 2021 13:24:26 +0100 Subject: [PATCH] Issue #2897638 by Spokje, claudiu.cristea, istavros, PaulDinelle, drclaw, Giuseppe87, KapilV, Lendude: Views exposed sort identifiers are not configurable --- .../config/optional/views.view.comment.yml | 1 + .../optional/views.view.comments_recent.yml | 4 ++ .../config/optional/views.view.watchdog.yml | 1 + .../config/optional/views.view.media.yml | 1 + .../install/views.view.media_library.yml | 3 ++ .../config/optional/views.view.archive.yml | 1 + .../optional/views.view.content_recent.yml | 1 + .../config/optional/views.view.frontpage.yml | 2 + .../optional/views.view.taxonomy_term.yml | 2 + .../optional/views.view.user_admin_people.yml | 1 + .../config/optional/views.view.who_s_new.yml | 1 + .../optional/views.view.who_s_online.yml | 1 + .../config/schema/views.data_types.schema.yml | 3 ++ .../exposed_form/ExposedFormPluginBase.php | 34 +++++++------- .../src/Plugin/views/sort/SortPluginBase.php | 47 ++++++++++++++++++- core/modules/views/src/ViewsConfigUpdater.php | 37 +++++++++++++++ .../src/Functional/Plugin/ExposedFormTest.php | 17 ++++++- .../Update/ViewsSortIdentifiersUpdateTest.php | 42 +++++++++++++++++ .../src/Kernel/Plugin/DisplayKernelTest.php | 13 ++++- core/modules/views/views.post_update.php | 12 +++++ .../views_ui/css/views_ui.admin.theme.css | 2 + .../src/Functional/ExposedFormUITest.php | 37 +++++++++++++-- .../install/views.view.articles_aside.yml | 2 + .../install/views.view.featured_articles.yml | 2 + .../config/install/views.view.frontpage.yml | 3 ++ .../install/views.view.promoted_items.yml | 1 + .../install/views.view.recipe_collections.yml | 1 + .../config/install/views.view.recipes.yml | 2 + .../install/views.view.taxonomy_term.yml | 2 + .../config/optional/views.view.media.yml | 1 + core/themes/claro/css/components/views-ui.css | 3 ++ .../claro/css/components/views-ui.pcss.css | 3 ++ .../claro/css/theme/views_ui.admin.theme.css | 2 + .../css/theme/views_ui.admin.theme.pcss.css | 2 + core/themes/seven/css/components/views-ui.css | 3 ++ .../css/views_ui/views_ui.admin.theme.css | 2 + .../css/views_ui/views_ui.admin.theme.css | 2 + 37 files changed, 268 insertions(+), 26 deletions(-) create mode 100644 core/modules/views/tests/src/Functional/Update/ViewsSortIdentifiersUpdateTest.php diff --git a/core/modules/comment/config/optional/views.view.comment.yml b/core/modules/comment/config/optional/views.view.comment.yml index 36ab4560168b..45d566636fed 100644 --- a/core/modules/comment/config/optional/views.view.comment.yml +++ b/core/modules/comment/config/optional/views.view.comment.yml @@ -806,6 +806,7 @@ display: exposed: false expose: label: '' + field_identifier: changed granularity: second entity_type: comment entity_field: changed diff --git a/core/modules/comment/config/optional/views.view.comments_recent.yml b/core/modules/comment/config/optional/views.view.comments_recent.yml index 0387a60737de..587040dad863 100644 --- a/core/modules/comment/config/optional/views.view.comments_recent.yml +++ b/core/modules/comment/config/optional/views.view.comments_recent.yml @@ -206,6 +206,7 @@ display: exposed: false expose: label: '' + field_identifier: created plugin_id: date entity_type: comment entity_field: created @@ -218,6 +219,9 @@ display: admin_label: '' order: DESC exposed: false + expose: + label: '' + field_identifier: cid plugin_id: field entity_type: comment entity_field: cid diff --git a/core/modules/dblog/config/optional/views.view.watchdog.yml b/core/modules/dblog/config/optional/views.view.watchdog.yml index e6542aa9d44c..b3da477847b6 100644 --- a/core/modules/dblog/config/optional/views.view.watchdog.yml +++ b/core/modules/dblog/config/optional/views.view.watchdog.yml @@ -647,6 +647,7 @@ display: exposed: false expose: label: '' + field_identifier: wid plugin_id: standard title: 'Recent log messages' header: { } diff --git a/core/modules/media/config/optional/views.view.media.yml b/core/modules/media/config/optional/views.view.media.yml index 17518dc84a50..df4df46c72c9 100644 --- a/core/modules/media/config/optional/views.view.media.yml +++ b/core/modules/media/config/optional/views.view.media.yml @@ -845,6 +845,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second title: Media header: { } diff --git a/core/modules/media_library/config/install/views.view.media_library.yml b/core/modules/media_library/config/install/views.view.media_library.yml index 35fd413df8d0..1bd04639ef33 100644 --- a/core/modules/media_library/config/install/views.view.media_library.yml +++ b/core/modules/media_library/config/install/views.view.media_library.yml @@ -418,6 +418,7 @@ display: exposed: true expose: label: 'Newest first' + field_identifier: created granularity: second entity_type: media entity_field: created @@ -433,6 +434,7 @@ display: exposed: true expose: label: 'Name (A-Z)' + field_identifier: name entity_type: media entity_field: name plugin_id: standard @@ -447,6 +449,7 @@ display: exposed: true expose: label: 'Name (Z-A)' + field_identifier: name_1 entity_type: media entity_field: name plugin_id: standard diff --git a/core/modules/node/config/optional/views.view.archive.yml b/core/modules/node/config/optional/views.view.archive.yml index b8e55476d14f..9ac4c063b226 100644 --- a/core/modules/node/config/optional/views.view.archive.yml +++ b/core/modules/node/config/optional/views.view.archive.yml @@ -77,6 +77,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second entity_type: node entity_field: created diff --git a/core/modules/node/config/optional/views.view.content_recent.yml b/core/modules/node/config/optional/views.view.content_recent.yml index 44de40b828f6..ec80480d247c 100644 --- a/core/modules/node/config/optional/views.view.content_recent.yml +++ b/core/modules/node/config/optional/views.view.content_recent.yml @@ -254,6 +254,7 @@ display: exposed: false expose: label: '' + field_identifier: changed granularity: second entity_type: node entity_field: changed diff --git a/core/modules/node/config/optional/views.view.frontpage.yml b/core/modules/node/config/optional/views.view.frontpage.yml index efdae12c1860..3b0bd8941297 100644 --- a/core/modules/node/config/optional/views.view.frontpage.yml +++ b/core/modules/node/config/optional/views.view.frontpage.yml @@ -203,6 +203,7 @@ display: admin_label: '' expose: label: '' + field_identifier: sticky exposed: false field: sticky group_type: group @@ -225,6 +226,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second entity_type: node entity_field: created diff --git a/core/modules/taxonomy/config/optional/views.view.taxonomy_term.yml b/core/modules/taxonomy/config/optional/views.view.taxonomy_term.yml index 895019632e4e..0fa147dda59f 100644 --- a/core/modules/taxonomy/config/optional/views.view.taxonomy_term.yml +++ b/core/modules/taxonomy/config/optional/views.view.taxonomy_term.yml @@ -77,6 +77,7 @@ display: exposed: false expose: label: '' + field_identifier: sticky created: id: created table: taxonomy_index @@ -89,6 +90,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second arguments: tid: diff --git a/core/modules/user/config/optional/views.view.user_admin_people.yml b/core/modules/user/config/optional/views.view.user_admin_people.yml index 46a22eba1f37..7a6de5a7f3a0 100644 --- a/core/modules/user/config/optional/views.view.user_admin_people.yml +++ b/core/modules/user/config/optional/views.view.user_admin_people.yml @@ -846,6 +846,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second plugin_id: date entity_type: user diff --git a/core/modules/user/config/optional/views.view.who_s_new.yml b/core/modules/user/config/optional/views.view.who_s_new.yml index 2898850ce67a..1054052d3aa7 100644 --- a/core/modules/user/config/optional/views.view.who_s_new.yml +++ b/core/modules/user/config/optional/views.view.who_s_new.yml @@ -156,6 +156,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second plugin_id: date entity_type: user diff --git a/core/modules/user/config/optional/views.view.who_s_online.yml b/core/modules/user/config/optional/views.view.who_s_online.yml index 2229485cd970..2b40ccf43d7e 100644 --- a/core/modules/user/config/optional/views.view.who_s_online.yml +++ b/core/modules/user/config/optional/views.view.who_s_online.yml @@ -165,6 +165,7 @@ display: exposed: false expose: label: '' + field_identifier: access granularity: second plugin_id: date entity_type: user diff --git a/core/modules/views/config/schema/views.data_types.schema.yml b/core/modules/views/config/schema/views.data_types.schema.yml index 708e12f6d8ec..f9f73973f2da 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -281,6 +281,9 @@ views_sort_expose: label: type: label label: 'Label' + field_identifier: + type: string + label: 'Field identifier' views_area: type: views_handler 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 cfb593d6e663..75daffccb054 100644 --- a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php +++ b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php @@ -157,19 +157,17 @@ public function query() { if (!empty($sort_by)) { // Make sure the original order of sorts is preserved // (e.g. a sticky sort is often first) - if (isset($view->sort[$sort_by])) { - $view->query->orderby = []; - foreach ($view->sort as $key => $sort) { - if (!$sort->isExposed()) { - $sort->query(); - } - elseif ($key == $sort_by) { - if (isset($exposed_data['sort_order']) && in_array($exposed_data['sort_order'], ['ASC', 'DESC'])) { - $sort->options['order'] = $exposed_data['sort_order']; - } - $sort->setRelationship(); - $sort->query(); + $view->query->orderby = []; + foreach ($view->sort as $key => $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(); } } } @@ -205,16 +203,18 @@ public function exposedFormAlter(&$form, FormStateInterface $form_state) { // 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()) { - $exposed_sorts[$id] = $handler->options['expose']['label']; + 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' => $exposed_sorts_options, '#title' => $this->options['exposed_sorts_label'], ]; $sort_order = [ @@ -222,8 +222,8 @@ public function exposedFormAlter(&$form, FormStateInterface $form_state) { 'DESC' => $this->options['sort_desc_label'], ]; $user_input = $form_state->getUserInput(); - if (isset($user_input['sort_by']) && isset($this->view->sort[$user_input['sort_by']])) { - $default_sort_order = $this->view->sort[$user_input['sort_by']]->options['order']; + 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); diff --git a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php index cf036fc810f3..fcac77e013e3 100644 --- a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php +++ b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php @@ -49,6 +49,7 @@ protected function defineOptions() { $options['expose'] = [ 'contains' => [ 'label' => ['default' => ''], + 'field_identifier' => ['default' => ''], ], ]; return $options; @@ -208,7 +209,50 @@ public function buildExposeForm(&$form, FormStateInterface $form_state) { '#required' => TRUE, '#size' => 40, '#weight' => -1, - ]; + ]; + + $form['expose']['field_identifier'] = [ + '#type' => 'textfield', + '#default_value' => $this->options['expose']['field_identifier'], + '#title' => $this->t('Sort field identifier'), + '#required' => TRUE, + '#size' => 40, + '#description' => $this->t("This will appear in the URL after the ?, as value of 'sort_by' parameter, to identify this sort field. Cannot be blank. Only letters, digits and the dot ('.'), hyphen ('-'), underscore ('_'), and tilde ('~') characters are allowed."), + ]; + } + + /** + * Validate the options form. + */ + public function validateExposeForm($form, FormStateInterface $form_state) { + $field_identifier = $form_state->getValue([ + 'options', + 'expose', + 'field_identifier', + ]); + if (!preg_match('/^[a-zA-z][a-zA-Z0-9_~.\-]*$/', $field_identifier)) { + $form_state->setErrorByName('expose][field_identifier', $this->t('This identifier has illegal characters.')); + return; + } + + // Validate that the sort field identifier is unique within the sort + // handlers. Note that the sort field identifier is different that other + // identifiers because it is used as a query string value of the 'sort_by' + // parameter, while the others are used as query string parameter keys. + // Therefore we can have a sort field identifier be the same as an exposed + // filter identifier. This prevents us from using + // DisplayPluginInterface::isIdentifierUnique() to test for uniqueness. + // @see \Drupal\views\Plugin\views\display\DisplayPluginInterface::isIdentifierUnique() + foreach ($this->view->display_handler->getHandlers('sort') as $key => $handler) { + if ($handler->canExpose() && $handler->isExposed()) { + if ($form_state->get('id') !== $key && isset($handler->options['expose']['field_identifier']) && $field_identifier === $handler->options['expose']['field_identifier']) { + $form_state->setErrorByName('expose][field_identifier', $this->t('This identifier is already used by %label sort handler.', [ + '%label' => $handler->adminLabel(TRUE), + ])); + return; + } + } + } } /** @@ -226,6 +270,7 @@ public static function trustedCallbacks() { public function defaultExposeOptions() { $this->options['expose'] = [ 'label' => $this->definition['title'], + 'field_identifier' => $this->options['id'], ]; } diff --git a/core/modules/views/src/ViewsConfigUpdater.php b/core/modules/views/src/ViewsConfigUpdater.php index 88442d46ddc1..dfb89e338adb 100644 --- a/core/modules/views/src/ViewsConfigUpdater.php +++ b/core/modules/views/src/ViewsConfigUpdater.php @@ -138,6 +138,9 @@ public function updateAll(ViewEntityInterface $view) { if ($this->processMultivalueBaseFieldHandler($handler, $handler_type, $key, $display_id, $view)) { $changed = TRUE; } + if ($this->processSortFieldIdentifierUpdateHandler($handler, $handler_type)) { + $changed = TRUE; + } return $changed; }); } @@ -477,4 +480,38 @@ protected function mapOperatorFromSingleToMultiple($single_operator) { } } + /** + * Updates the sort handlers by adding default sort field identifiers. + * + * @param \Drupal\views\ViewEntityInterface $view + * The View to update. + * + * @return bool + * Whether the view was updated. + */ + public function needsSortFieldIdentifierUpdate(ViewEntityInterface $view): bool { + return $this->processDisplayHandlers($view, TRUE, function (array &$handler, string $handler_type): bool { + return $this->processSortFieldIdentifierUpdateHandler($handler, $handler_type); + }); + } + + /** + * Processes sort handlers by adding the sort identifier. + * + * @param array $handler + * A display handler. + * @param string $handler_type + * The handler type. + * + * @return bool + * Whether the handler was updated. + */ + protected function processSortFieldIdentifierUpdateHandler(array &$handler, string $handler_type): bool { + if ($handler_type === 'sort' && !isset($handler['expose']['field_identifier'])) { + $handler['expose']['field_identifier'] = $handler['id']; + return TRUE; + } + return FALSE; + } + } diff --git a/core/modules/views/tests/src/Functional/Plugin/ExposedFormTest.php b/core/modules/views/tests/src/Functional/Plugin/ExposedFormTest.php index 676d976afbca..42f5ae084caf 100644 --- a/core/modules/views/tests/src/Functional/Plugin/ExposedFormTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/ExposedFormTest.php @@ -368,18 +368,24 @@ public function testExposedSortAndItemsPerPage() { $this->assertCacheContexts($contexts); $this->assertIds(range(40, 16, 1)); - // Change the label to something with special characters. $view = Views::getView('test_exposed_form_sort_items_per_page'); $view->setDisplay(); $sorts = $view->display_handler->getOption('sorts'); + // Change the label to something with special characters. $sorts['id']['expose']['label'] = $expected_label = "<script>alert('unsafe&dangerous');</script>"; + // Use a custom sort field identifier. + $sorts['id']['expose']['field_identifier'] = $field_identifier = $this->randomMachineName() . '-_.~'; $view->display_handler->setOption('sorts', $sorts); $view->save(); + // Test label escaping. $this->drupalGet('test_exposed_form_sort_items_per_page'); $options = $this->assertSession()->selectExists('edit-sort-by')->findAll('css', 'option'); $this->assertCount(1, $options); - $this->assertSession()->optionExists('edit-sort-by', $expected_label); + // Check option existence by option label. + $this->assertSession()->optionExists('Sort by', $expected_label); + // Check option existence by option value. + $this->assertSession()->optionExists('Sort by', $field_identifier); $escape_1 = Html::escape($expected_label); $escape_2 = Html::escape($escape_1); // Make sure we see the single-escaped string in the raw output. @@ -388,6 +394,13 @@ public function testExposedSortAndItemsPerPage() { $this->assertNoRaw($escape_2); // And not the raw label, either. $this->assertNoRaw($expected_label); + + // Check that the custom field identifier is used in the URL query string. + $this->submitForm(['sort_order' => 'DESC'], 'Apply'); + $this->assertCacheContexts($contexts); + $this->assertIds(range(50, 41)); + $url = $this->getSession()->getCurrentUrl(); + $this->assertStringContainsString('sort_by=' . urlencode($field_identifier), $url); } /** diff --git a/core/modules/views/tests/src/Functional/Update/ViewsSortIdentifiersUpdateTest.php b/core/modules/views/tests/src/Functional/Update/ViewsSortIdentifiersUpdateTest.php new file mode 100644 index 000000000000..e7b0cbdcd30c --- /dev/null +++ b/core/modules/views/tests/src/Functional/Update/ViewsSortIdentifiersUpdateTest.php @@ -0,0 +1,42 @@ +<?php + +namespace Drupal\Tests\views\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +/** + * Tests the views_post_update_sort_identifier() post update. + * + * @group views + * @group legacy + */ +class ViewsSortIdentifiersUpdateTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz', + ]; + } + + /** + * Tests views_post_update_sort_identifier(). + * + * @see views_post_update_sort_identifier() + */ + public function testSortIdentifierPostUpdate(): void { + $config_factory = \Drupal::configFactory(); + $view = $config_factory->get('views.view.comments_recent'); + $trail = 'display.default.display_options.sorts.created'; + $this->assertArrayNotHasKey('field_identifier', $view->get("{$trail}.expose")); + + $this->runUpdates(); + + $view = $config_factory->get('views.view.comments_recent'); + $sort_handler = $view->get($trail); + $this->assertSame($sort_handler['id'], $sort_handler['expose']['field_identifier']); + } + +} diff --git a/core/modules/views/tests/src/Kernel/Plugin/DisplayKernelTest.php b/core/modules/views/tests/src/Kernel/Plugin/DisplayKernelTest.php index a2eb682d4cdd..1fe28612385b 100644 --- a/core/modules/views/tests/src/Kernel/Plugin/DisplayKernelTest.php +++ b/core/modules/views/tests/src/Kernel/Plugin/DisplayKernelTest.php @@ -130,7 +130,10 @@ public function testisIdentifierUnique() { 'table' => 'views_test_data', 'plugin_id' => 'standard', 'order' => 'asc', - 'expose' => ['label' => 'id'], + 'expose' => [ + 'label' => 'Id', + 'field_identifier' => 'name', + ], 'exposed' => TRUE, ], ]; @@ -156,10 +159,16 @@ public function testisIdentifierUnique() { ]; $view->display_handler->setOption('sorts', $sorts); $view->display_handler->setOption('filters', $filters); - $view->save(); $this->assertTrue($view->display_handler->isIdentifierUnique('some_id', 'some_id')); $this->assertFalse($view->display_handler->isIdentifierUnique('some_id', 'id')); + + // Check that an exposed filter is able to use the same identifier as an + // exposed sort. + $sorts['name']['expose']['field_identifier'] = 'id'; + $view->display_handler->handlers = []; + $view->display_handler->setOption('sorts', $sorts); + $this->assertTrue($view->display_handler->isIdentifierUnique('id', 'id')); } } diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index eab54c113620..cd925a9b845c 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -6,6 +6,7 @@ */ use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\views\ViewEntityInterface; use Drupal\views\ViewsConfigUpdater; /** @@ -75,3 +76,14 @@ function views_post_update_remove_sorting_global_text_field() { function views_post_update_title_translations() { \Drupal::service('router.builder')->setRebuildNeeded(); } + +/** + * Add the identifier option to all sort handler configurations. + */ +function views_post_update_sort_identifier(?array &$sandbox = NULL): void { + /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */ + $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool { + return $view_config_updater->needsSortFieldIdentifierUpdate($view); + }); +} diff --git a/core/modules/views_ui/css/views_ui.admin.theme.css b/core/modules/views_ui/css/views_ui.admin.theme.css index 060afc91eee3..565132721c6c 100644 --- a/core/modules/views_ui/css/views_ui.admin.theme.css +++ b/core/modules/views_ui/css/views_ui.admin.theme.css @@ -682,6 +682,7 @@ td.group-title { } .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-top: 6px; margin-bottom: 6px; @@ -689,6 +690,7 @@ td.group-title { } [dir="rtl"] .form-item-options-expose-required, [dir="rtl"] .form-item-options-expose-label, +[dir="rtl"] .form-item-options-expose-field-identifier, [dir="rtl"] .form-item-options-expose-description { margin-right: 18px; margin-left: 0; diff --git a/core/modules/views_ui/tests/src/Functional/ExposedFormUITest.php b/core/modules/views_ui/tests/src/Functional/ExposedFormUITest.php index 873125b952e9..62b8ba89da13 100644 --- a/core/modules/views_ui/tests/src/Functional/ExposedFormUITest.php +++ b/core/modules/views_ui/tests/src/Functional/ExposedFormUITest.php @@ -105,6 +105,7 @@ public function testExposedAdminUi() { $this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/sort/created'); $this->helperButtonHasLabel('edit-options-expose-button-button', 'Expose sort'); $this->assertSession()->fieldNotExists('edit-options-expose-label'); + $this->assertSession()->fieldNotExists('Sort field identifier'); // Un-expose the filter. $this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type'); @@ -123,6 +124,7 @@ public function testExposedAdminUi() { // Check the label of the expose button. $this->helperButtonHasLabel('edit-options-expose-button-button', 'Hide sort'); $this->assertSession()->fieldValueEquals('edit-options-expose-label', 'Authored on'); + $this->assertSession()->fieldValueEquals('Sort field identifier', 'created'); // Test adding a new exposed sort criteria. $view_id = $this->randomView()['id']; @@ -135,15 +137,42 @@ public function testExposedAdminUi() { $this->submitForm([], 'Expose sort'); $this->assertSession()->fieldValueEquals('options[order]', 'DESC'); $this->assertSession()->fieldValueEquals('options[expose][label]', 'Authored on'); - // Change the label and save the view. - $edit = ['options[expose][label]' => $this->randomString()]; + $this->assertSession()->fieldValueEquals('Sort field identifier', 'created'); + + // Change the label and try with an empty identifier. + $edit = [ + 'options[expose][label]' => $this->randomString(), + 'options[expose][field_identifier]' => '', + ]; + $this->submitForm($edit, 'Apply'); + $this->assertSession()->pageTextContains('Sort field identifier field is required.'); + + // Try with an invalid identifier. + $edit['options[expose][field_identifier]'] = 'abc&! ###08.'; + $this->submitForm($edit, 'Apply'); + $this->assertSession()->pageTextContains('This identifier has illegal characters.'); + + // Use a valid identifier. + $edit['options[expose][field_identifier]'] = $this->randomMachineName() . '_-~.'; $this->submitForm($edit, 'Apply'); $this->submitForm([], 'Save'); + // Check that the values were saved. $display = View::load($view_id)->getDisplay('default'); $this->assertTrue($display['display_options']['sorts']['created']['exposed']); - $this->assertEquals(['label' => $edit['options[expose][label]']], $display['display_options']['sorts']['created']['expose']); - $this->assertEquals('DESC', $display['display_options']['sorts']['created']['order']); + $this->assertSame([ + 'label' => $edit['options[expose][label]'], + 'field_identifier' => $edit['options[expose][field_identifier]'], + ], $display['display_options']['sorts']['created']['expose']); + $this->assertSame('DESC', $display['display_options']['sorts']['created']['order']); + + // Test the identifier uniqueness. + $this->drupalGet("admin/structure/views/nojs/handler/{$view_id}/default/sort/created_1"); + $this->submitForm([], 'Expose sort'); + $this->submitForm([ + 'options[expose][field_identifier]' => $edit['options[expose][field_identifier]'], + ], 'Apply'); + $this->assertSession()->pageTextContains('This identifier is already used by Content: Authored on sort handler.'); } /** diff --git a/core/profiles/demo_umami/config/install/views.view.articles_aside.yml b/core/profiles/demo_umami/config/install/views.view.articles_aside.yml index 7a9f37d8bb5e..b1821bc6e239 100644 --- a/core/profiles/demo_umami/config/install/views.view.articles_aside.yml +++ b/core/profiles/demo_umami/config/install/views.view.articles_aside.yml @@ -189,6 +189,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second nid: id: nid @@ -201,6 +202,7 @@ display: exposed: false expose: label: '' + field_identifier: nid entity_type: node entity_field: nid plugin_id: standard diff --git a/core/profiles/demo_umami/config/install/views.view.featured_articles.yml b/core/profiles/demo_umami/config/install/views.view.featured_articles.yml index e643082cd3a0..a547a78769f9 100644 --- a/core/profiles/demo_umami/config/install/views.view.featured_articles.yml +++ b/core/profiles/demo_umami/config/install/views.view.featured_articles.yml @@ -202,6 +202,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second nid: id: nid @@ -214,6 +215,7 @@ display: exposed: false expose: label: '' + field_identifier: nid entity_type: node entity_field: nid plugin_id: standard diff --git a/core/profiles/demo_umami/config/install/views.view.frontpage.yml b/core/profiles/demo_umami/config/install/views.view.frontpage.yml index af7dd1167c02..f614159078d9 100644 --- a/core/profiles/demo_umami/config/install/views.view.frontpage.yml +++ b/core/profiles/demo_umami/config/install/views.view.frontpage.yml @@ -229,6 +229,7 @@ display: admin_label: '' expose: label: '' + field_identifier: sticky exposed: false field: sticky group_type: group @@ -251,6 +252,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second entity_type: node entity_field: created @@ -265,6 +267,7 @@ display: exposed: false expose: label: '' + field_identifier: nid entity_type: node entity_field: nid plugin_id: standard diff --git a/core/profiles/demo_umami/config/install/views.view.promoted_items.yml b/core/profiles/demo_umami/config/install/views.view.promoted_items.yml index 2dbaf939619e..7d1262f62c2a 100644 --- a/core/profiles/demo_umami/config/install/views.view.promoted_items.yml +++ b/core/profiles/demo_umami/config/install/views.view.promoted_items.yml @@ -220,6 +220,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second title: 'Promoted Items Double' header: { } diff --git a/core/profiles/demo_umami/config/install/views.view.recipe_collections.yml b/core/profiles/demo_umami/config/install/views.view.recipe_collections.yml index 1952fc3c4174..9113af5ef799 100644 --- a/core/profiles/demo_umami/config/install/views.view.recipe_collections.yml +++ b/core/profiles/demo_umami/config/install/views.view.recipe_collections.yml @@ -178,6 +178,7 @@ display: exposed: false expose: label: '' + field_identifier: name entity_type: taxonomy_term entity_field: name plugin_id: standard diff --git a/core/profiles/demo_umami/config/install/views.view.recipes.yml b/core/profiles/demo_umami/config/install/views.view.recipes.yml index e3cdb98cb1c1..80af602c9a05 100644 --- a/core/profiles/demo_umami/config/install/views.view.recipes.yml +++ b/core/profiles/demo_umami/config/install/views.view.recipes.yml @@ -202,6 +202,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second nid: id: nid @@ -214,6 +215,7 @@ display: exposed: false expose: label: '' + field_identifier: nid entity_type: node entity_field: nid plugin_id: standard diff --git a/core/profiles/demo_umami/config/install/views.view.taxonomy_term.yml b/core/profiles/demo_umami/config/install/views.view.taxonomy_term.yml index 79b65de101d9..7b8355922d3a 100644 --- a/core/profiles/demo_umami/config/install/views.view.taxonomy_term.yml +++ b/core/profiles/demo_umami/config/install/views.view.taxonomy_term.yml @@ -77,6 +77,7 @@ display: exposed: false expose: label: '' + field_identifier: sticky created: id: created table: taxonomy_index @@ -89,6 +90,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second arguments: tid: diff --git a/core/profiles/demo_umami/config/optional/views.view.media.yml b/core/profiles/demo_umami/config/optional/views.view.media.yml index 17518dc84a50..df4df46c72c9 100644 --- a/core/profiles/demo_umami/config/optional/views.view.media.yml +++ b/core/profiles/demo_umami/config/optional/views.view.media.yml @@ -845,6 +845,7 @@ display: exposed: false expose: label: '' + field_identifier: created granularity: second title: Media header: { } diff --git a/core/themes/claro/css/components/views-ui.css b/core/themes/claro/css/components/views-ui.css index 56fda12d7f48..6fc7a1737587 100644 --- a/core/themes/claro/css/components/views-ui.css +++ b/core/themes/claro/css/components/views-ui.css @@ -127,12 +127,14 @@ details.fieldset-no-legend { .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-left: 1.5em; /* LTR */ } [dir="rtl"] .form-item-options-expose-required, [dir="rtl"] .form-item-options-expose-label, +[dir="rtl"] .form-item-options-expose-field-identifier, [dir="rtl"] .form-item-options-expose-description { margin-right: 1.5em; margin-left: 0; @@ -144,6 +146,7 @@ details.fieldset-no-legend { .views-admin-dependent .form-item .form-item, .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-top: 0.375rem; margin-bottom: 0.375rem; diff --git a/core/themes/claro/css/components/views-ui.pcss.css b/core/themes/claro/css/components/views-ui.pcss.css index 84709902fe7b..9e8a723ab059 100644 --- a/core/themes/claro/css/components/views-ui.pcss.css +++ b/core/themes/claro/css/components/views-ui.pcss.css @@ -110,11 +110,13 @@ details.fieldset-no-legend { */ .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-left: 1.5em; /* LTR */ } [dir="rtl"] .form-item-options-expose-required, [dir="rtl"] .form-item-options-expose-label, +[dir="rtl"] .form-item-options-expose-field-identifier, [dir="rtl"] .form-item-options-expose-description { margin-right: 1.5em; margin-left: 0; @@ -126,6 +128,7 @@ details.fieldset-no-legend { .views-admin-dependent .form-item .form-item, .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-top: 6px; margin-bottom: 6px; diff --git a/core/themes/claro/css/theme/views_ui.admin.theme.css b/core/themes/claro/css/theme/views_ui.admin.theme.css index b3f21bb4e187..76fd7b7de661 100644 --- a/core/themes/claro/css/theme/views_ui.admin.theme.css +++ b/core/themes/claro/css/theme/views_ui.admin.theme.css @@ -663,6 +663,7 @@ td.group-title { .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-top: 0.375rem; margin-bottom: 0.375rem; @@ -671,6 +672,7 @@ td.group-title { [dir="rtl"] .form-item-options-expose-required, [dir="rtl"] .form-item-options-expose-label, +[dir="rtl"] .form-item-options-expose-field-identifier, [dir="rtl"] .form-item-options-expose-description { margin-right: 1.125rem; margin-left: 0; diff --git a/core/themes/claro/css/theme/views_ui.admin.theme.pcss.css b/core/themes/claro/css/theme/views_ui.admin.theme.pcss.css index 3b25581b811c..3c0ff632501d 100644 --- a/core/themes/claro/css/theme/views_ui.admin.theme.pcss.css +++ b/core/themes/claro/css/theme/views_ui.admin.theme.pcss.css @@ -546,6 +546,7 @@ td.group-title { } .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-top: 6px; margin-bottom: 6px; @@ -553,6 +554,7 @@ td.group-title { } [dir="rtl"] .form-item-options-expose-required, [dir="rtl"] .form-item-options-expose-label, +[dir="rtl"] .form-item-options-expose-field-identifier, [dir="rtl"] .form-item-options-expose-description { margin-right: 18px; margin-left: 0; diff --git a/core/themes/seven/css/components/views-ui.css b/core/themes/seven/css/components/views-ui.css index bb79f4d1619b..912c8257b4cd 100644 --- a/core/themes/seven/css/components/views-ui.css +++ b/core/themes/seven/css/components/views-ui.css @@ -64,11 +64,13 @@ details.fieldset-no-legend { */ .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-left: 1.5em; /* LTR */ } [dir="rtl"] .form-item-options-expose-required, [dir="rtl"] .form-item-options-expose-label, +[dir="rtl"] .form-item-options-expose-field-identifier, [dir="rtl"] .form-item-options-expose-description { margin-right: 1.5em; margin-left: 0; @@ -80,6 +82,7 @@ details.fieldset-no-legend { .views-admin-dependent .form-item .form-item, .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-top: 6px; margin-bottom: 6px; diff --git a/core/themes/stable/css/views_ui/views_ui.admin.theme.css b/core/themes/stable/css/views_ui/views_ui.admin.theme.css index 32de3f97c5f7..39fc644fea43 100644 --- a/core/themes/stable/css/views_ui/views_ui.admin.theme.css +++ b/core/themes/stable/css/views_ui/views_ui.admin.theme.css @@ -682,6 +682,7 @@ td.group-title { } .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-top: 6px; margin-bottom: 6px; @@ -689,6 +690,7 @@ td.group-title { } [dir="rtl"] .form-item-options-expose-required, [dir="rtl"] .form-item-options-expose-label, +[dir="rtl"] .form-item-options-expose-field-identifier, [dir="rtl"] .form-item-options-expose-description { margin-right: 18px; margin-left: 0; diff --git a/core/themes/stable9/css/views_ui/views_ui.admin.theme.css b/core/themes/stable9/css/views_ui/views_ui.admin.theme.css index dd988dfe99d2..344e974012c6 100644 --- a/core/themes/stable9/css/views_ui/views_ui.admin.theme.css +++ b/core/themes/stable9/css/views_ui/views_ui.admin.theme.css @@ -682,6 +682,7 @@ td.group-title { } .form-item-options-expose-required, .form-item-options-expose-label, +.form-item-options-expose-field-identifier, .form-item-options-expose-description { margin-top: 6px; margin-bottom: 6px; @@ -689,6 +690,7 @@ td.group-title { } [dir="rtl"] .form-item-options-expose-required, [dir="rtl"] .form-item-options-expose-label, +[dir="rtl"] .form-item-options-expose-field-identifier, [dir="rtl"] .form-item-options-expose-description { margin-right: 18px; margin-left: 0; -- GitLab