diff --git a/core/modules/comment/config/optional/views.view.comment.yml b/core/modules/comment/config/optional/views.view.comment.yml index 36ab4560168b51b432376b7ff8c302ab8395fc46..45d566636fedca71bb3c8c3c5618c76b8b765668 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 0387a60737de5610a82b07d505a7449420f2a016..587040dad863a87311407c630ab8aac2c95f76d3 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 e6542aa9d44c246be2f65ea8da13ad4990030674..b3da477847b6530d8b260ba6b526be70b49c98d7 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 17518dc84a502b470c60450c5096457887521a0c..df4df46c72c970cc93abddd28e6d70e017015c0b 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 35fd413df8d05d70a8c3e91dee9a6a12c4e885f1..1bd04639ef33b879fde2b357ee8acf0d7c6b037c 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 b8e55476d14f0dfa69d4b3a0a17413d60458ac58..9ac4c063b2267e99042db20508b2f1e5a8158aad 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 44de40b828f6dd47ff9259c9840c5ce9375f02fd..ec80480d247cfe0752ab16db6ac6f039df672265 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 efdae12c1860677c584531165b4e1db98550f243..3b0bd89412971f1d6f123c9f2fa097bda6a9e27b 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 895019632e4e35a64dec87ea0a123e8731041027..0fa147dda59f1b1dcb37d61e3eaace5632c74ee8 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 46a22eba1f37b026c10327e7ed2c37ca0c784aeb..7a6de5a7f3a002b6ec941f125efa28c51a035718 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 2898850ce67a4e05b06551be201fc2e08bf9c7c2..1054052d3aa779a7dd0fb650873d6a84d8a23d84 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 2229485cd970b3e9ef0b56841801d40c4240c0b4..2b40ccf43d7eae225fd08e4d2667e0aee2ead06d 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 708e12f6d8ecb3ffa9e1957b151ba5948ebf51c2..f9f73973f2dab23a233ba7eab66fc71752a7f904 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 cfb593d6e663671f2299ad79b57373f511c850f3..75daffccb0541442709e54b5090202a230727067 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 cf036fc810f3b4addf1e179b3ae29145c45fb3f6..fcac77e013e3c4463cd9ed0e41a0a41079b50f71 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 88442d46ddc158f4ab730661bb92584c1a868c8b..dfb89e338adb863468a4d2cca032bc2ac6855786 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 676d976afbca44c81bca9a65263d2f38c4312707..42f5ae084caf6d049d66bde1c639a44f4a6b3134 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 0000000000000000000000000000000000000000..e7b0cbdcd30cc1b03abef73d1fc53f13c6619ae8 --- /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 a2eb682d4cddcaab8976ee1c1963861f8f23d68c..1fe28612385b4f4e392ec67dd3837f38ed8bdc5c 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 eab54c113620e6d52cd16e54b6383c90a3295374..cd925a9b845c2b9e50bacffb6d0f3278ab8c6110 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 060afc91eee3ba2bab40c627bf6b12a4d533bc46..565132721c6c818bab6c23ee169c56e7a3fb40a4 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 873125b952e9f588950f6ed85ff948370720f222..62b8ba89da13b12d3747d636fb77c7c742675d7c 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 7a9f37d8bb5e7b4796fb5c5d7adbc52cfcb85818..b1821bc6e239cabc6a72e996adcf9fc28aa12de8 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 e643082cd3a0234ce402b638dc013314f01440c8..a547a78769f919734a63ff14e8bf78c7abc7dc26 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 af7dd1167c029d336a0d17e5fb83d318f3c2a198..f614159078d9eed9ed116f6f47373acd0fd4e1b0 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 2dbaf939619ee3391542a4dde3684a826a3ecb67..7d1262f62c2a4dda88ec0b665fdc8127622096d1 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 1952fc3c4174a4727d67b0c59d01b785d4de26fc..9113af5ef79974fcd90968e9726ed271b12e0fa1 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 e3cdb98cb1c16123f84b420c97d4bcec728c88a5..80af602c9a05dae3e35c276b722475494b16243c 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 79b65de101d9e5b02432870cbe33c832d35cb618..7b8355922d3a4ad276e6463e0b57cc6d2564c132 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 17518dc84a502b470c60450c5096457887521a0c..df4df46c72c970cc93abddd28e6d70e017015c0b 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 56fda12d7f482037e325b1b4e8ff2077ac58146e..6fc7a17375873c5a6cbb4f39ae7478b510fd2044 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 84709902fe7be7325c569d41e3b453f7a0067b09..9e8a723ab059fb912be78277a2871e0618d302bd 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 b3f21bb4e187575de20adbf9d735c5d544e2d853..76fd7b7de6616455f865e617f672bfd8c193f281 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 3b25581b811c20336bee92cac4c93b4be2d4f2b5..3c0ff632501dd6e86332767b7e9423e7e50d4afa 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 bb79f4d1619b7a83f224fd0fdf54e06de90fe370..912c8257b4cdf3cb16ba3248deb471f67d92b3c4 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 32de3f97c5f7e191f79d7eef783bb0a94dfa7105..39fc644fea4385631bc5d61de3bed8523a60ce4e 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 dd988dfe99d22da98e85a5d00711b4e681e58ac6..344e974012c6e18006b2aea748d447a2b38cff2b 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;