diff --git a/core/modules/comment/config/optional/views.view.comment.yml b/core/modules/comment/config/optional/views.view.comment.yml index a54fb25883ddf326c54b3b4dadce1f88ec477ef1..1fb1ca0a48229fa94dfc198136b8a44347674e79 100644 --- a/core/modules/comment/config/optional/views.view.comment.yml +++ b/core/modules/comment/config/optional/views.view.comment.yml @@ -75,6 +75,9 @@ display: selected_actions: - comment_delete_action - comment_unpublish_action + actions_order: + - comment_unpublish_action + - comment_delete_action subject: id: subject table: comment_field_data @@ -957,6 +960,9 @@ display: selected_actions: - comment_delete_action - comment_publish_action + actions_order: + - comment_publish_action + - comment_delete_action subject: id: subject table: comment_field_data diff --git a/core/modules/comment/tests/src/Kernel/Views/CommentAdminViewTest.php b/core/modules/comment/tests/src/Kernel/Views/CommentAdminViewTest.php index 177a25335bb20a8d2461498fd8c969c192f3592f..83c34172624706ec91f074fbc6d620eeece516be 100644 --- a/core/modules/comment/tests/src/Kernel/Views/CommentAdminViewTest.php +++ b/core/modules/comment/tests/src/Kernel/Views/CommentAdminViewTest.php @@ -37,11 +37,13 @@ class CommentAdminViewTest extends ViewsKernelTestBase { * {@inheritdoc} */ protected static $modules = [ - 'user', 'comment', 'entity_test', + 'field', 'language', 'locale', + 'text', + 'user', ]; /** @@ -54,7 +56,7 @@ protected function setUp($import_test_views = TRUE): void { $this->installEntitySchema('comment'); $this->installEntitySchema('entity_test'); // Create the anonymous role. - $this->installConfig(['user']); + $this->installConfig(['comment', 'user']); // Create user 1 so that the user created later in the test has a different // user ID. diff --git a/core/modules/media/config/optional/views.view.media.yml b/core/modules/media/config/optional/views.view.media.yml index c7a32129b33e023e5f95384e79d17e1436d3d29d..03a5d9f6fbdb5bada8c887938fd936507f10d872 100644 --- a/core/modules/media/config/optional/views.view.media.yml +++ b/core/modules/media/config/optional/views.view.media.yml @@ -76,6 +76,11 @@ display: action_title: Action include_exclude: exclude selected_actions: { } + actions_order: + - media_publish_action + - media_unpublish_action + - media_save_action + - media_delete_action thumbnail__target_id: id: thumbnail__target_id table: media_field_data diff --git a/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml b/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml index 8f7f0c8493df848b68dbb53321024455dbd59dc0..5e3339402fe1d52829fdafe335a8058df44a7501 100644 --- a/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml +++ b/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml @@ -41,6 +41,11 @@ display: action_title: 'With selection' include_exclude: exclude selected_actions: { } + actions_order: + - media_publish_action + - media_unpublish_action + - media_save_action + - media_delete_action name: id: name table: media_field_data diff --git a/core/modules/media/tests/src/Functional/MediaBulkFormTest.php b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php index 85c1d59eb15d8cb925ea5152c50e94adba59f523..a9bc842cc991d3b3ee5a5ac9b44c7110f308a4b9 100644 --- a/core/modules/media/tests/src/Functional/MediaBulkFormTest.php +++ b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\media\Functional; +use Behat\Mink\Element\NodeElement; use Drupal\media\Entity\Media; use Drupal\views\Views; @@ -74,16 +75,18 @@ public function testBulkForm(): void { // Check the operations are accessible to the logged in user. $this->drupalGet('test-media-bulk-form'); - // Current available actions: Delete, Save, Publish, Unpublish. - $available_actions = [ - 'media_delete_action', + + // Tests that actions are sorted according to the view configured order. + $actual_actions = $this->xpath('//select[@id="edit-action"]//option'); + $expected_actions = [ 'media_publish_action', - 'media_save_action', 'media_unpublish_action', + 'media_save_action', + 'media_delete_action', ]; - foreach ($available_actions as $action_name) { - $assert_session->optionExists('action', $action_name); - } + $this->assertSame($expected_actions, array_values(array_filter(array_map(function (NodeElement $action): string { + return $action->getValue(); + }, $actual_actions)))); // Test unpublishing in bulk. $page->checkField('media_bulk_form[0]'); 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 5450c17c9379fad2f10f36f5032d86e2f30419bf..1ead90981a008842be561f70c3a6f126f9650f43 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 @@ -81,6 +81,11 @@ display: action_title: Action include_exclude: exclude selected_actions: { } + actions_order: + - media_publish_action + - media_unpublish_action + - media_save_action + - media_delete_action rendered_entity: id: rendered_entity table: media @@ -537,6 +542,11 @@ display: action_title: Action include_exclude: exclude selected_actions: { } + actions_order: + - media_publish_action + - media_save_action + - media_unpublish_action + - media_delete_action name: id: name table: media_field_data diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaOverviewTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaOverviewTest.php index a88502c65961e610c943a8bc52be59e2ab630987..beb86d9a6860a355c907623ea7c2c814052485f0 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaOverviewTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaOverviewTest.php @@ -4,6 +4,8 @@ namespace Drupal\Tests\media_library\FunctionalJavascript; +use Behat\Mink\Element\NodeElement; + /** * Tests the grid-style media overview page. * @@ -66,6 +68,18 @@ public function testAdministrationPage(): void { // Visit the administration page. $this->drupalGet('admin/content/media'); + // Tests that actions are sorted according to the view configured order. + $actual_actions = $this->xpath('//select[@id="edit-action"]//option'); + $expected_actions = [ + 'media_publish_action', + 'media_unpublish_action', + 'media_save_action', + 'media_delete_action', + ]; + $this->assertSame($expected_actions, array_values(array_filter(array_map(function (NodeElement $action): string { + return $action->getValue(); + }, $actual_actions)))); + // There should be links to both the grid and table displays. $assert_session->linkExists('Grid'); $assert_session->linkExists('Table'); @@ -116,6 +130,7 @@ public function testAdministrationPage(): void { // This tests that anchor tags clicked inside the preview are suppressed. $this->getSession()->executeScript('jQuery(".js-click-to-select-trigger a")[4].click()'); + $this->getSession()->getPage()->selectFieldOption('Action', 'Delete media'); $this->submitForm([], 'Apply to selected items'); $assert_session->pageTextContains('Dog'); $assert_session->pageTextNotContains('Cat'); diff --git a/core/modules/node/config/optional/views.view.content.yml b/core/modules/node/config/optional/views.view.content.yml index d0bbb09cf5a0963ab532b4b28b78e3f9e92095e8..8e12ec15e8b8995f6379c7598e344b1f88c608af 100644 --- a/core/modules/node/config/optional/views.view.content.yml +++ b/core/modules/node/config/optional/views.view.content.yml @@ -36,6 +36,18 @@ display: hide_empty: false empty_zero: false hide_alter_empty: true + action_title: Action + include_exclude: exclude + selected_actions: { } + actions_order: + - node_publish_action + - node_unpublish_action + - node_promote_action + - node_unpromote_action + - node_make_sticky_action + - node_make_unsticky_action + - node_save_action + - node_delete_action title: id: title table: node_field_data diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml index d7a3fa080fed21b4077504d9f3eb15e5556fe36f..0f67e50640ea5b8a136389bf549f851e3a80b6e5 100644 --- a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml @@ -28,6 +28,18 @@ display: field: node_bulk_form plugin_id: node_bulk_form entity_type: node + action_title: Action + include_exclude: exclude + selected_actions: { } + actions_order: + - node_publish_action + - node_unpublish_action + - node_promote_action + - node_unpromote_action + - node_make_sticky_action + - node_make_unsticky_action + - node_save_action + - node_delete_action title: id: title table: node_field_data diff --git a/core/modules/node/tests/src/Functional/Views/BulkFormTest.php b/core/modules/node/tests/src/Functional/Views/BulkFormTest.php index 8514b3cee00d6298ea88c1c1fb7b5c8fa65ce4a8..d04dad1070bd2bd4cc8839ddbfd32811e9ca9d85 100644 --- a/core/modules/node/tests/src/Functional/Views/BulkFormTest.php +++ b/core/modules/node/tests/src/Functional/Views/BulkFormTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\node\Functional\Views; +use Behat\Mink\Element\NodeElement; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\views\Views; @@ -103,6 +104,30 @@ protected function setUp($import_test_views = TRUE, $modules = ['node_test_views * Tests the node bulk form. */ public function testBulkForm(): void { + // Tests that actions are sorted according to the view configured order. + $actual_actions = $this->xpath('//select[@id="edit-action"]//option'); + $expected_actions = [ + 'node_publish_action', + 'node_unpublish_action', + 'node_promote_action', + 'node_unpromote_action', + 'node_make_sticky_action', + 'node_make_unsticky_action', + 'node_save_action', + 'node_delete_action', + ]; + $this->assertSame( + $expected_actions, + array_values( + array_filter( + array_map( + fn(NodeElement $action): string => $action->getValue(), + $actual_actions, + ), + ), + ), + ); + // Unpublish a node using the bulk form. $node = reset($this->nodes); $this->assertTrue($node->isPublished(), 'Node is initially published'); 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 5f9c0667cace5787eb2ebb7096dda25e001866f7..0eeb33a43d5865212799295fdc145218f0af39d2 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 @@ -69,6 +69,10 @@ display: hide_empty: false empty_zero: false hide_alter_empty: true + action_title: Action + include_exclude: exclude + selected_actions: { } + actions_order: [] name: id: name table: users_field_data diff --git a/core/modules/user/tests/src/Functional/Views/BulkFormTest.php b/core/modules/user/tests/src/Functional/Views/BulkFormTest.php index 4047df4eadbbe1095f19f899b247a9c01f0594f6..9c1704f74cc82b46ffe9aa761a79e5b1c2864e4f 100644 --- a/core/modules/user/tests/src/Functional/Views/BulkFormTest.php +++ b/core/modules/user/tests/src/Functional/Views/BulkFormTest.php @@ -121,7 +121,7 @@ public function testBulkForm(): void { $action_id = 'user_add_role_action.' . $role; $edit = [ 'options[include_exclude]' => 'exclude', - "options[selected_actions][$action_id]" => $action_id, + "options[selected_actions][$action_id][selected]" => TRUE, ]; $this->drupalGet('admin/structure/views/nojs/handler/test_user_bulk_form/default/field/user_bulk_form'); $this->submitForm($edit, 'Apply'); 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 0e011d73dc15f54451fbd29ea4f28ca02369877e..ef213cf4bf628ad7823435bc5cd42f2df659d523 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -921,3 +921,9 @@ views_field_bulk_form: sequence: type: string label: 'Action' + actions_order: + type: sequence + label: 'Actions order' + sequence: + type: string + label: 'Action' diff --git a/core/modules/views/css/views.bulk_form.css b/core/modules/views/css/views.bulk_form.css new file mode 100644 index 0000000000000000000000000000000000000000..488c792d63f6793219e75efb43fef08b0fd78d4b --- /dev/null +++ b/core/modules/views/css/views.bulk_form.css @@ -0,0 +1,12 @@ +tr.draggable td { + width: 100%; +} +tr.draggable td:first-child { + display: flex; + align-items: center; + width: 2.5rem; +} +/* Ensure a space between checkbox and the 'Changed' asterisk */ +tr.draggable td:first-child input[type="checkbox"] { + margin-right: 3px; +} diff --git a/core/modules/views/src/Plugin/views/field/BulkForm.php b/core/modules/views/src/Plugin/views/field/BulkForm.php index 2fcf8588bb78aac7e93f42242a353a1a902d082d..ce20dee14672d6a470f96a06ddbf034a9c99a15d 100644 --- a/core/modules/views/src/Plugin/views/field/BulkForm.php +++ b/core/modules/views/src/Plugin/views/field/BulkForm.php @@ -217,6 +217,9 @@ protected function defineOptions() { $options['selected_actions'] = [ 'default' => [], ]; + $options['actions_order'] = [ + 'default' => [], + ]; return $options; } @@ -240,13 +243,52 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { ], '#default_value' => $this->options['include_exclude'], ]; + $form['selected_actions'] = [ - '#type' => 'checkboxes', + '#type' => 'table', '#title' => $this->t('Selected actions'), - '#options' => $this->getBulkOptions(FALSE), - '#default_value' => $this->options['selected_actions'], + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'action-order-weight', + ], + ], + '#tree' => FALSE, + '#input' => FALSE, + '#theme_wrappers' => ['form_element'], + '#attached' => ['library' => ['views/views.bulk_form']], ]; + $weight = 0; + foreach ($this->getBulkOptions(FALSE) as $action_id => $action_label) { + $form['selected_actions'][$action_id]['#attributes']['class'][] = 'draggable'; + $form['selected_actions'][$action_id]['#weight'] = $weight++; + + $arguments = ['@title' => $action_label]; + $form['selected_actions'][$action_id]['selected'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Select @title', $arguments), + '#title_display' => 'attribute', + '#default_value' => in_array($action_id, $this->options['selected_actions'], TRUE), + '#parents' => ['options', 'selected_actions', $action_id, 'selected'], + ]; + + $form['selected_actions'][$action_id]['action'] = [ + '#markup' => $action_label, + ]; + + $form['selected_actions'][$action_id]['weight'] = [ + '#type' => 'weight', + '#title' => $this->t('Weight for @title', $arguments), + '#title_display' => 'invisible', + '#delta' => 50, + '#default_value' => $weight, + '#parents' => ['options', 'selected_actions', $action_id, 'weight'], + '#attributes' => ['class' => ['action-order-weight']], + ]; + + } parent::buildOptionsForm($form, $form_state); } @@ -256,8 +298,16 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { public function validateOptionsForm(&$form, FormStateInterface $form_state) { parent::validateOptionsForm($form, $form_state); - $selected_actions = $form_state->getValue(['options', 'selected_actions']); - $form_state->setValue(['options', 'selected_actions'], array_values(array_filter($selected_actions))); + $selected_actions = $actions_order = []; + foreach ($form_state->getValue(['options', 'selected_actions']) as $action_id => $action_data) { + if ($action_data['selected']) { + $selected_actions[] = $action_id; + } + $actions_order[$action_id] = (int) $action_data['weight']; + } + $form_state->setValue(['options', 'selected_actions'], $selected_actions); + asort($actions_order); + $form_state->setValue(['options', 'actions_order'], array_keys($actions_order)); } /** @@ -327,6 +377,11 @@ public function viewsForm(&$form, FormStateInterface $form_state) { // Replace the form submit button label. $form['actions']['submit']['#value'] = $this->t('Apply to selected items'); + $form['actions']['submit']['#states'] = [ + 'disabled' => [ + 'select[name="action"]' => ['value' => ''], + ], + ]; // Ensure a consistent container for filters/operations in the view // header. @@ -345,6 +400,7 @@ public function viewsForm(&$form, FormStateInterface $form_state) { '#title' => $this->options['action_title'], '#options' => $this->getBulkOptions(), '#empty_option' => $this->t('- Select -'), + '#required' => TRUE, ]; // Duplicate the form actions into the action container in the header. @@ -367,8 +423,18 @@ public function viewsForm(&$form, FormStateInterface $form_state) { */ protected function getBulkOptions($filtered = TRUE) { $options = []; + + $actions = $this->actions; + + // Only order if there is a user specified order. + if ($actions && $this->options['actions_order']) { + // Some actions might have been removed. Keep only existing actions. + $actions_order = array_intersect($this->options['actions_order'], array_keys($actions)); + $actions = array_merge(array_flip($actions_order), $actions); + } + // Filter the action list. - foreach ($this->actions as $id => $action) { + foreach ($actions as $id => $action) { if ($filtered) { $in_selected = in_array($id, $this->options['selected_actions']); // If the field is configured to include only the selected actions, diff --git a/core/modules/views/src/ViewsConfigUpdater.php b/core/modules/views/src/ViewsConfigUpdater.php index 3b2709cc60c2ac7d43d8279bc8bb30c61d5220e4..b2f4add5155bd190639057bcb172ebd1c9d59d71 100644 --- a/core/modules/views/src/ViewsConfigUpdater.php +++ b/core/modules/views/src/ViewsConfigUpdater.php @@ -7,6 +7,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\views\Plugin\views\field\BulkForm; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -131,6 +132,9 @@ public function updateAll(ViewEntityInterface $view) { if ($this->processEntityArgumentUpdate($view)) { $changed = TRUE; } + if ($this->processBulkFormActionsOrderUpdate($handler, $handler_type)) { + $changed = TRUE; + } if ($this->processRememberRolesUpdate($handler, $handler_type)) { $changed = TRUE; } @@ -349,4 +353,45 @@ public function processTableCssClassUpdate(ViewEntityInterface $view): bool { return $changed; } + /** + * Updates the bulk form fields by adding the actions order configuration. + * + * @param \Drupal\views\ViewEntityInterface $view + * The View to update. + * + * @return bool + * Whether the view was updated. + */ + public function needsBulkFormActionsOrderUpdate(ViewEntityInterface $view): bool { + return $this->processDisplayHandlers($view, FALSE, function (array &$handler, string $handler_type): bool { + return $this->processBulkFormActionsOrderUpdate($handler, $handler_type); + }); + } + + /** + * Processes the bulk form fields by adding the actions order configuration. + * + * @param array $handler + * A display handler. + * @param string $handler_type + * The handler type. + * + * @return bool + * Whether the handler was updated. + */ + protected function processBulkFormActionsOrderUpdate(array &$handler, string $handler_type): bool { + if ($handler_type === 'field' && !isset($handler['actions_order']) && isset($handler['plugin_id'])) { + /** @var \Drupal\views\Plugin\ViewsPluginManager $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.views.field'); + if ($plugin_manager->hasDefinition($handler['plugin_id'])) { + $definition = $plugin_manager->getDefinition($handler['plugin_id']); + if (is_subclass_of($definition['class'], BulkForm::class)) { + $handler['actions_order'] = []; + return TRUE; + } + } + } + return FALSE; + } + } diff --git a/core/modules/views/tests/modules/action_bulk_test/config/install/views.view.test_bulk_form.yml b/core/modules/views/tests/modules/action_bulk_test/config/install/views.view.test_bulk_form.yml index cba11476ae8b12c99bfc03d4c460012128ace83d..7ce2b00b0c3b4859155fc25965a7f0bc28295f3a 100644 --- a/core/modules/views/tests/modules/action_bulk_test/config/install/views.view.test_bulk_form.yml +++ b/core/modules/views/tests/modules/action_bulk_test/config/install/views.view.test_bulk_form.yml @@ -89,6 +89,18 @@ display: hide_empty: false empty_zero: false hide_alter_empty: true + action_title: Action + include_exclude: exclude + selected_actions: { } + actions_order: + - node_publish_action + - node_unpublish_action + - node_promote_action + - node_unpromote_action + - node_make_sticky_action + - node_make_unsticky_action + - node_save_action + - node_delete_action pager: type: full options: diff --git a/core/modules/views/tests/src/Functional/BulkFormTest.php b/core/modules/views/tests/src/Functional/BulkFormTest.php index 842b9ff642bc67b8cc0cd22fa04badf4f6512e26..6badbed7b25ce8a7503954c33d00dd499cb56ac5 100644 --- a/core/modules/views/tests/src/Functional/BulkFormTest.php +++ b/core/modules/views/tests/src/Functional/BulkFormTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\views\Functional; +use Behat\Mink\Element\NodeElement; use Drupal\Tests\BrowserTestBase; use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\views\Views; @@ -71,6 +72,55 @@ public function testBulkForm(): void { $this->drupalGet('test_bulk_form'); + // Tests that actions are sorted according to the view configured order. + $actual_actions = $this->xpath('//select[@id="edit-action"]//option'); + $expected_actions = [ + 'node_publish_action', + 'node_unpublish_action', + 'node_promote_action', + 'node_unpromote_action', + 'node_make_sticky_action', + 'node_make_unsticky_action', + 'node_save_action', + 'node_delete_action', + 'test_action', + ]; + $this->assertSame($expected_actions, array_values(array_filter(array_map(function (NodeElement $action): string { + return $action->getValue(); + }, $actual_actions)))); + + // Test changing the order of the actions. + $view = Views::getView('test_bulk_form'); + $display = &$view->storage->getDisplay('default'); + $display['display_options']['fields']['node_bulk_form']['actions_order'] = [ + 'node_make_unsticky_action', + 'node_publish_action', + 'node_make_sticky_action', + 'node_save_action', + 'test_action', + 'node_unpromote_action', + 'node_delete_action', + 'node_unpublish_action', + 'node_promote_action', + ]; + $view->save(); + $this->getSession()->reload(); + $actual_actions = $this->xpath('//select[@id="edit-action"]//option'); + $expected_actions = [ + 'node_make_unsticky_action', + 'node_publish_action', + 'node_make_sticky_action', + 'node_save_action', + 'test_action', + 'node_unpromote_action', + 'node_delete_action', + 'node_unpublish_action', + 'node_promote_action', + ]; + $this->assertSame($expected_actions, array_values(array_filter(array_map(function (NodeElement $action): string { + return $action->getValue(); + }, $actual_actions)))); + // Test that the views edit header appears first. $this->assertSession()->elementExists('xpath', '//form/div[1][@id = "edit-header"]'); diff --git a/core/modules/views/tests/src/Functional/Plugin/ViewsBulkTest.php b/core/modules/views/tests/src/Functional/Plugin/ViewsBulkTest.php index 5c33bcb27a7df07d6d3b97b7bc968ad11f743561..3e7a049f387aab12e9ee2e30207da8dab0f3d11b 100644 --- a/core/modules/views/tests/src/Functional/Plugin/ViewsBulkTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/ViewsBulkTest.php @@ -71,7 +71,8 @@ public function testBulkSelection(): void { // Now click 'Apply to selected items' and assert the first node is selected // on the confirm form. - $this->submitForm(['node_bulk_form[0]' => TRUE], 'Apply to selected items'); + $edit = ['node_bulk_form[0]' => TRUE, 'action' => 'node_delete_action']; + $this->submitForm($edit, 'Apply to selected items'); $this->assertSession()->pageTextContains($node_1->getTitle()); $this->assertSession()->pageTextNotContains($node_2->getTitle()); @@ -92,7 +93,8 @@ public function testBulkSelection(): void { // Now click 'Apply to selected items' and assert the second node is // selected on the confirm form. - $this->submitForm(['node_bulk_form[1]' => TRUE], 'Apply to selected items'); + $edit = ['node_bulk_form[1]' => TRUE, 'action' => 'node_delete_action']; + $this->submitForm($edit, 'Apply to selected items'); $this->assertSession()->pageTextContains($node_1->getTitle()); $this->assertSession()->pageTextNotContains($node_3->getTitle()); } diff --git a/core/modules/views/tests/src/Functional/Update/ViewsBulkFormActionsOrderUpdateTest.php b/core/modules/views/tests/src/Functional/Update/ViewsBulkFormActionsOrderUpdateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..5e127e76d639260859e800cd50258c76893c1c6d --- /dev/null +++ b/core/modules/views/tests/src/Functional/Update/ViewsBulkFormActionsOrderUpdateTest.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\views\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +/** + * Tests views_post_update_bulk_form_action_order(). + * + * @group views + * @group legacy + */ +class ViewsBulkFormActionsOrderUpdateTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles(): void { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz', + ]; + } + + /** + * @covers \views_post_update_bulk_form_action_order + */ + public function testBulkFormActionsOrderPostUpdate(): void { + $config_factory = \Drupal::configFactory(); + $path = 'display.default.display_options.fields.user_bulk_form'; + $config = $config_factory->get('views.view.user_admin_people'); + $this->assertArrayNotHasKey('actions_order', $config->get($path)); + $this->runUpdates(); + $config = $config_factory->get('views.view.user_admin_people'); + $this->assertArrayHasKey('actions_order', $config->get($path)); + $this->assertSame([], $config->get("$path.actions_order")); + } + +} diff --git a/core/modules/views/views.libraries.yml b/core/modules/views/views.libraries.yml index 08ff2d1640798b7f9e862b32074392f2f392d7ae..89cc2558c707951a84aed80b417fb40e03f0f5f3 100644 --- a/core/modules/views/views.libraries.yml +++ b/core/modules/views/views.libraries.yml @@ -22,3 +22,9 @@ views.responsive-grid: css: layout: css/views-responsive-grid.css: {} + +views.bulk_form: + version: VERSION + css: + component: + css/views.bulk_form.css: {} diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index c3f352e99a4203c026860c66a84ef43f5d535d4b..018b3098f821803d747dda01a519e76dc95d47af 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -87,3 +87,14 @@ function views_post_update_table_css_class(?array &$sandbox = NULL): void { return $view_config_updater->needsTableCssClassUpdate($view); }); } + +/** + * Add the actions order option to all bulk form field configurations. + */ +function views_post_update_bulk_form_action_order(?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->needsBulkFormActionsOrderUpdate($view); + }); +} 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 98c7e3ee23763e735f0508e593c5af5504ea10b9..74bc3d55725ceef9dc485d3625c88fa55c49c466 100644 --- a/core/profiles/demo_umami/config/optional/views.view.media.yml +++ b/core/profiles/demo_umami/config/optional/views.view.media.yml @@ -76,6 +76,11 @@ display: action_title: Action include_exclude: exclude selected_actions: { } + actions_order: + - media_publish_action + - media_unpublish_action + - media_save_action + - media_delete_action thumbnail__target_id: id: thumbnail__target_id table: media_field_data diff --git a/core/tests/fixtures/config_install/multilingual/views.view.content.yml b/core/tests/fixtures/config_install/multilingual/views.view.content.yml index 20afac251c06cf65cc3877120c09f63c56ee202a..125070eec7b0489524dd83ac3904ca3e12bbc896 100644 --- a/core/tests/fixtures/config_install/multilingual/views.view.content.yml +++ b/core/tests/fixtures/config_install/multilingual/views.view.content.yml @@ -39,6 +39,18 @@ display: hide_empty: false empty_zero: false hide_alter_empty: true + action_title: Action + include_exclude: exclude + selected_actions: { } + actions_order: + - node_publish_action + - node_unpublish_action + - node_promote_action + - node_unpromote_action + - node_make_sticky_action + - node_make_unsticky_action + - node_save_action + - node_delete_action title: id: title table: node_field_data diff --git a/core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml b/core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml index 4c48d1332245021d3d6b536247e167bbc46b9e26..c2910f7d08b3ad560d5f1d6ba840c689edec6503 100644 --- a/core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml +++ b/core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml @@ -72,6 +72,10 @@ display: hide_empty: false empty_zero: false hide_alter_empty: true + action_title: Action + include_exclude: exclude + selected_actions: { } + actions_order: [] name: id: name table: users_field_data diff --git a/core/themes/claro/claro.info.yml b/core/themes/claro/claro.info.yml index 87156826550e0c66f732b70c1e454c2c2119c108..4fde9d2bb3ddaae699af639aa4fd223f49e88bc3 100644 --- a/core/themes/claro/claro.info.yml +++ b/core/themes/claro/claro.info.yml @@ -97,6 +97,11 @@ libraries-override: state: css/toolbar.menu.css: css/state/toolbar.menu.css + views/views.bulk_form: + css: + component: + css/views.bulk_form.css: css/components/views.bulk_form.css + views_ui/admin.styling: css: component: diff --git a/core/themes/claro/css/components/views.bulk_form.css b/core/themes/claro/css/components/views.bulk_form.css new file mode 100644 index 0000000000000000000000000000000000000000..2cae782ff1a3259519d488ef08d68fa45a0cda6f --- /dev/null +++ b/core/themes/claro/css/components/views.bulk_form.css @@ -0,0 +1,12 @@ +tr.draggable td { + width: 100%; +} +tr.draggable td:first-child { + display: flex; + align-items: center; + width: 5rem; +} +/* Ensure a space between checkbox and the 'Changed' asterisk */ +tr.draggable td:first-child input[type="checkbox"] { + margin-right: 3px; +} diff --git a/core/themes/stable9/css/views/views.bulk_form.css b/core/themes/stable9/css/views/views.bulk_form.css new file mode 100644 index 0000000000000000000000000000000000000000..488c792d63f6793219e75efb43fef08b0fd78d4b --- /dev/null +++ b/core/themes/stable9/css/views/views.bulk_form.css @@ -0,0 +1,12 @@ +tr.draggable td { + width: 100%; +} +tr.draggable td:first-child { + display: flex; + align-items: center; + width: 2.5rem; +} +/* Ensure a space between checkbox and the 'Changed' asterisk */ +tr.draggable td:first-child input[type="checkbox"] { + margin-right: 3px; +} diff --git a/core/themes/stable9/stable9.info.yml b/core/themes/stable9/stable9.info.yml index 71dc76f30b618ec3b37169f57ab83d449b75ce47..8ac2d7b1abbfdd6154ec707dc01cbe30182ea2ee 100644 --- a/core/themes/stable9/stable9.info.yml +++ b/core/themes/stable9/stable9.info.yml @@ -302,6 +302,11 @@ libraries-override: component: css/views.module.css: css/views/views.module.css + views/views.bulk_form: + css: + component: + css/views.bulk_form.css: css/views/views.bulk_form.css + views_ui/admin.styling: css: component: