diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 160b526ebd527f4d9efa5eb5082a3f39a9760785..95311ada2bbff11f3f12977e3b2c4e6ea5e0bcf2 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -213,23 +213,6 @@ theme_settings: sequence: type: theme_settings.third_party.[%key] -views_field_bulk_form: - type: views_field - label: 'Bulk operation' - mapping: - action_title: - type: label - label: 'Action title' - include_exclude: - type: string - label: 'Available actions' - selected_actions: - type: sequence - label: 'Available actions' - sequence: - type: string - label: 'Action' - # Array of routes with route_name and route_params keys. route: type: mapping diff --git a/core/modules/comment/src/Plugin/views/field/CommentBulkForm.php b/core/modules/comment/src/Plugin/views/field/CommentBulkForm.php index d4283734f76cea93ce3bf61182c156e67411ed3e..b67cff227ec82e2cbacf1f55fe9fa7e3e8b0b994 100644 --- a/core/modules/comment/src/Plugin/views/field/CommentBulkForm.php +++ b/core/modules/comment/src/Plugin/views/field/CommentBulkForm.php @@ -2,7 +2,7 @@ namespace Drupal\comment\Plugin\views\field; -use Drupal\system\Plugin\views\field\BulkForm; +use Drupal\views\Plugin\views\field\BulkForm; /** * Defines a comment operations bulk form element. diff --git a/core/modules/node/src/Plugin/views/field/NodeBulkForm.php b/core/modules/node/src/Plugin/views/field/NodeBulkForm.php index e5aad5645dd65401caca9e5344a1d71fbeb3eee3..4a6e219cceb3c086cadd86286bacaf2a05a7ac2f 100644 --- a/core/modules/node/src/Plugin/views/field/NodeBulkForm.php +++ b/core/modules/node/src/Plugin/views/field/NodeBulkForm.php @@ -2,7 +2,7 @@ namespace Drupal\node\Plugin\views\field; -use Drupal\system\Plugin\views\field\BulkForm; +use Drupal\views\Plugin\views\field\BulkForm; /** * Defines a node operations bulk form element. diff --git a/core/modules/system/src/Plugin/views/field/BulkForm.php b/core/modules/system/src/Plugin/views/field/BulkForm.php index 65fdc51cc73388ae608ee32a8f2a30bc9eaf50c2..e34e6a551ab6f77fc964d50f638ee1f9ea9b9359 100644 --- a/core/modules/system/src/Plugin/views/field/BulkForm.php +++ b/core/modules/system/src/Plugin/views/field/BulkForm.php @@ -2,506 +2,20 @@ namespace Drupal\system\Plugin\views\field; -use Drupal\Core\Cache\CacheableDependencyInterface; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\RevisionableInterface; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\Routing\RedirectDestinationTrait; -use Drupal\Core\TypedData\TranslatableInterface; -use Drupal\views\Entity\Render\EntityTranslationRenderTrait; -use Drupal\views\Plugin\views\display\DisplayPluginBase; -use Drupal\views\Plugin\views\field\FieldPluginBase; -use Drupal\views\Plugin\views\field\UncacheableFieldHandlerTrait; -use Drupal\views\Plugin\views\style\Table; -use Drupal\views\ResultRow; -use Drupal\views\ViewExecutable; -use Symfony\Component\DependencyInjection\ContainerInterface; +@trigger_error(__NAMESPACE__ . '\BulkForm is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\views\Plugin\views\field\BulkForm instead. See https://www.drupal.org/node/2916716.', E_USER_DEPRECATED); + +use Drupal\views\Plugin\views\field\BulkForm as ViewsBulkForm; /** * Defines a actions-based bulk operation form element. * - * @ViewsField("bulk_form") + * @ViewsField("legacy_bulk_form") + * + * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use + * \Drupal\views\Plugin\views\field\BulkForm instead. + * + * @see https://www.drupal.org/node/2916716 */ -class BulkForm extends FieldPluginBase implements CacheableDependencyInterface { - - use RedirectDestinationTrait; - use UncacheableFieldHandlerTrait; - use EntityTranslationRenderTrait; - - /** - * The entity manager. - * - * @var \Drupal\Core\Entity\EntityManagerInterface - */ - protected $entityManager; - - /** - * The action storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $actionStorage; - - /** - * An array of actions that can be executed. - * - * @var \Drupal\system\ActionConfigEntityInterface[] - */ - protected $actions = []; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * Constructs a new BulkForm object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin ID for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager - * The entity manager. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - - $this->entityManager = $entity_manager; - $this->actionStorage = $entity_manager->getStorage('action'); - $this->languageManager = $language_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity.manager'), - $container->get('language_manager') - ); - } - - /** - * {@inheritdoc} - */ - public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { - parent::init($view, $display, $options); - - $entity_type = $this->getEntityType(); - // Filter the actions to only include those for this entity type. - $this->actions = array_filter($this->actionStorage->loadMultiple(), function ($action) use ($entity_type) { - return $action->getType() == $entity_type; - }); - } - - /** - * {@inheritdoc} - */ - public function getCacheMaxAge() { - // @todo Consider making the bulk operation form cacheable. See - // https://www.drupal.org/node/2503009. - return 0; - } - - /** - * {@inheritdoc} - */ - public function getCacheContexts() { - return $this->languageManager->isMultilingual() ? $this->getEntityTranslationRenderer()->getCacheContexts() : []; - } - - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return []; - } - - /** - * {@inheritdoc} - */ - public function getEntityTypeId() { - return $this->getEntityType(); - } - - /** - * {@inheritdoc} - */ - protected function getEntityManager() { - return $this->entityManager; - } - - /** - * {@inheritdoc} - */ - protected function getLanguageManager() { - return $this->languageManager; - } - - /** - * {@inheritdoc} - */ - protected function getView() { - return $this->view; - } - - /** - * {@inheritdoc} - */ - protected function defineOptions() { - $options = parent::defineOptions(); - $options['action_title'] = ['default' => $this->t('Action')]; - $options['include_exclude'] = [ - 'default' => 'exclude', - ]; - $options['selected_actions'] = [ - 'default' => [], - ]; - return $options; - } - - /** - * {@inheritdoc} - */ - public function buildOptionsForm(&$form, FormStateInterface $form_state) { - $form['action_title'] = [ - '#type' => 'textfield', - '#title' => $this->t('Action title'), - '#default_value' => $this->options['action_title'], - '#description' => $this->t('The title shown above the actions dropdown.'), - ]; - - $form['include_exclude'] = [ - '#type' => 'radios', - '#title' => $this->t('Available actions'), - '#options' => [ - 'exclude' => $this->t('All actions, except selected'), - 'include' => $this->t('Only selected actions'), - ], - '#default_value' => $this->options['include_exclude'], - ]; - $form['selected_actions'] = [ - '#type' => 'checkboxes', - '#title' => $this->t('Selected actions'), - '#options' => $this->getBulkOptions(FALSE), - '#default_value' => $this->options['selected_actions'], - ]; - - parent::buildOptionsForm($form, $form_state); - } - - /** - * {@inheritdoc} - */ - 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))); - } - - /** - * {@inheritdoc} - */ - public function preRender(&$values) { - parent::preRender($values); - - // If the view is using a table style, provide a placeholder for a - // "select all" checkbox. - if (!empty($this->view->style_plugin) && $this->view->style_plugin instanceof Table) { - // Add the tableselect css classes. - $this->options['element_label_class'] .= 'select-all'; - // Hide the actual label of the field on the table header. - $this->options['label'] = ''; - } - } - - /** - * {@inheritdoc} - */ - public function getValue(ResultRow $row, $field = NULL) { - return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->'; - } - - /** - * Form constructor for the bulk form. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function viewsForm(&$form, FormStateInterface $form_state) { - // Make sure we do not accidentally cache this form. - // @todo Evaluate this again in https://www.drupal.org/node/2503009. - $form['#cache']['max-age'] = 0; - - // Add the tableselect javascript. - $form['#attached']['library'][] = 'core/drupal.tableselect'; - $use_revision = array_key_exists('revision', $this->view->getQuery()->getEntityTableInfo()); - - // Only add the bulk form options and buttons if there are results. - if (!empty($this->view->result)) { - // Render checkboxes for all rows. - $form[$this->options['id']]['#tree'] = TRUE; - foreach ($this->view->result as $row_index => $row) { - $entity = $this->getEntityTranslation($this->getEntity($row), $row); - - $form[$this->options['id']][$row_index] = [ - '#type' => 'checkbox', - // We are not able to determine a main "title" for each row, so we can - // only output a generic label. - '#title' => $this->t('Update this item'), - '#title_display' => 'invisible', - '#default_value' => !empty($form_state->getValue($this->options['id'])[$row_index]) ? 1 : NULL, - '#return_value' => $this->calculateEntityBulkFormKey($entity, $use_revision), - ]; - } - - // Replace the form submit button label. - $form['actions']['submit']['#value'] = $this->t('Apply to selected items'); - - // Ensure a consistent container for filters/operations in the view header. - $form['header'] = [ - '#type' => 'container', - '#weight' => -100, - ]; - - // Build the bulk operations action widget for the header. - // Allow themes to apply .container-inline on this separate container. - $form['header'][$this->options['id']] = [ - '#type' => 'container', - ]; - $form['header'][$this->options['id']]['action'] = [ - '#type' => 'select', - '#title' => $this->options['action_title'], - '#options' => $this->getBulkOptions(), - ]; - - // Duplicate the form actions into the action container in the header. - $form['header'][$this->options['id']]['actions'] = $form['actions']; - } - else { - // Remove the default actions build array. - unset($form['actions']); - } - } - - /** - * Returns the available operations for this form. - * - * @param bool $filtered - * (optional) Whether to filter actions to selected actions. - * @return array - * An associative array of operations, suitable for a select element. - */ - protected function getBulkOptions($filtered = TRUE) { - $options = []; - // Filter the action list. - foreach ($this->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, - // skip actions that were not selected. - if (($this->options['include_exclude'] == 'include') && !$in_selected) { - continue; - } - // Otherwise, if the field is configured to exclude the selected - // actions, skip actions that were selected. - elseif (($this->options['include_exclude'] == 'exclude') && $in_selected) { - continue; - } - } - - $options[$id] = $action->label(); - } - - return $options; - } - - /** - * Submit handler for the bulk form. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - * Thrown when the user tried to access an action without access to it. - */ - public function viewsFormSubmit(&$form, FormStateInterface $form_state) { - if ($form_state->get('step') == 'views_form_views_form') { - // Filter only selected checkboxes. Use the actual user input rather than - // the raw form values array, since the site data may change before the - // bulk form is submitted, which can lead to data loss. - $user_input = $form_state->getUserInput(); - $selected = array_filter($user_input[$this->options['id']]); - $entities = []; - $action = $this->actions[$form_state->getValue('action')]; - $count = 0; - - foreach ($selected as $bulk_form_key) { - $entity = $this->loadEntityFromBulkFormKey($bulk_form_key); - - // Skip execution if the user did not have access. - if (!$action->getPlugin()->access($entity, $this->view->getUser())) { - $this->drupalSetMessage($this->t('No access to execute %action on the @entity_type_label %entity_label.', [ - '%action' => $action->label(), - '@entity_type_label' => $entity->getEntityType()->getLabel(), - '%entity_label' => $entity->label() - ]), 'error'); - continue; - } - - $count++; - - $entities[$bulk_form_key] = $entity; - } - - $action->execute($entities); - - $operation_definition = $action->getPluginDefinition(); - if (!empty($operation_definition['confirm_form_route_name'])) { - $options = [ - 'query' => $this->getDestinationArray(), - ]; - $form_state->setRedirect($operation_definition['confirm_form_route_name'], [], $options); - } - else { - // Don't display the message unless there are some elements affected and - // there is no confirmation form. - if ($count) { - drupal_set_message($this->formatPlural($count, '%action was applied to @count item.', '%action was applied to @count items.', [ - '%action' => $action->label(), - ])); - } - } - } - } - - /** - * Returns the message to be displayed when there are no selected items. - * - * @return string - * Message displayed when no items are selected. - */ - protected function emptySelectedMessage() { - return $this->t('No items selected.'); - } - - /** - * {@inheritdoc} - */ - public function viewsFormValidate(&$form, FormStateInterface $form_state) { - $selected = array_filter($form_state->getValue($this->options['id'])); - if (empty($selected)) { - $form_state->setErrorByName('', $this->emptySelectedMessage()); - } - } - - /** - * {@inheritdoc} - */ - public function query() { - if ($this->languageManager->isMultilingual()) { - $this->getEntityTranslationRenderer()->query($this->query, $this->relationship); - } - } - - /** - * {@inheritdoc} - */ - public function clickSortable() { - return FALSE; - } - - /** - * Wraps drupal_set_message(). - */ - protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) { - drupal_set_message($message, $type, $repeat); - } - - /** - * Calculates a bulk form key. - * - * This generates a key that is used as the checkbox return value when - * submitting a bulk form. This key allows the entity for the row to be loaded - * totally independently of the executed view row. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to calculate a bulk form key for. - * @param bool $use_revision - * Whether the revision id should be added to the bulk form key. This should - * be set to TRUE only if the view is listing entity revisions. - * - * @return string - * The bulk form key representing the entity's id, language and revision (if - * applicable) as one string. - * - * @see self::loadEntityFromBulkFormKey() - */ - protected function calculateEntityBulkFormKey(EntityInterface $entity, $use_revision) { - $key_parts = [$entity->language()->getId(), $entity->id()]; - - if ($entity instanceof RevisionableInterface && $use_revision) { - $key_parts[] = $entity->getRevisionId(); - } - - // An entity ID could be an arbitrary string (although they are typically - // numeric). JSON then Base64 encoding ensures the bulk_form_key is - // safe to use in HTML, and that the key parts can be retrieved. - $key = json_encode($key_parts); - return base64_encode($key); - } - - /** - * Loads an entity based on a bulk form key. - * - * @param string $bulk_form_key - * The bulk form key representing the entity's id, language and revision (if - * applicable) as one string. - * - * @return \Drupal\Core\Entity\EntityInterface - * The entity loaded in the state (language, optionally revision) specified - * as part of the bulk form key. - */ - protected function loadEntityFromBulkFormKey($bulk_form_key) { - $key = base64_decode($bulk_form_key); - $key_parts = json_decode($key); - $revision_id = NULL; - - // If there are 3 items, vid will be last. - if (count($key_parts) === 3) { - $revision_id = array_pop($key_parts); - } - - // The first two items will always be langcode and ID. - $id = array_pop($key_parts); - $langcode = array_pop($key_parts); - - // Load the entity or a specific revision depending on the given key. - $storage = $this->entityManager->getStorage($this->getEntityType()); - $entity = $revision_id ? $storage->loadRevision($revision_id) : $storage->load($id); - - if ($entity instanceof TranslatableInterface) { - $entity = $entity->getTranslation($langcode); - } - - return $entity; - } +class BulkForm extends ViewsBulkForm { } diff --git a/core/modules/user/src/Plugin/views/field/UserBulkForm.php b/core/modules/user/src/Plugin/views/field/UserBulkForm.php index 3d0196a962170ba1a5b1c3190ac4edb0f2993e53..805df2b9e432721a7113c58678ac281a9e1294b7 100644 --- a/core/modules/user/src/Plugin/views/field/UserBulkForm.php +++ b/core/modules/user/src/Plugin/views/field/UserBulkForm.php @@ -3,8 +3,8 @@ namespace Drupal\user\Plugin\views\field; use Drupal\Core\Form\FormStateInterface; -use Drupal\system\Plugin\views\field\BulkForm; use Drupal\user\UserInterface; +use Drupal\views\Plugin\views\field\BulkForm; /** * Defines a user operations bulk form element. 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 a3d1b3430d8dc49a94d25694ed139f0d5c4b5f38..7736a747d83559c8ee7982f8f1e6605f7c5b2b36 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -820,3 +820,20 @@ views_cache: views_display_extender: type: mapping label: 'Display extender settings' + +views_field_bulk_form: + type: views_field + label: 'Bulk operation' + mapping: + action_title: + type: label + label: 'Action title' + include_exclude: + type: string + label: 'Available actions' + selected_actions: + type: sequence + label: 'Available actions' + sequence: + type: string + label: 'Action' diff --git a/core/modules/views/src/Plugin/views/field/BulkForm.php b/core/modules/views/src/Plugin/views/field/BulkForm.php new file mode 100644 index 0000000000000000000000000000000000000000..a253fc3ad53894cf4b52da222e23cfcf1b4ef2f5 --- /dev/null +++ b/core/modules/views/src/Plugin/views/field/BulkForm.php @@ -0,0 +1,505 @@ +<?php + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\RevisionableInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Routing\RedirectDestinationTrait; +use Drupal\Core\TypedData\TranslatableInterface; +use Drupal\views\Entity\Render\EntityTranslationRenderTrait; +use Drupal\views\Plugin\views\display\DisplayPluginBase; +use Drupal\views\Plugin\views\style\Table; +use Drupal\views\ResultRow; +use Drupal\views\ViewExecutable; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Defines a actions-based bulk operation form element. + * + * @ViewsField("bulk_form") + */ +class BulkForm extends FieldPluginBase implements CacheableDependencyInterface { + + use RedirectDestinationTrait; + use UncacheableFieldHandlerTrait; + use EntityTranslationRenderTrait; + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * The action storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $actionStorage; + + /** + * An array of actions that can be executed. + * + * @var \Drupal\system\ActionConfigEntityInterface[] + */ + protected $actions = []; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * Constructs a new BulkForm object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->entityManager = $entity_manager; + $this->actionStorage = $entity_manager->getStorage('action'); + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager'), + $container->get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { + parent::init($view, $display, $options); + + $entity_type = $this->getEntityType(); + // Filter the actions to only include those for this entity type. + $this->actions = array_filter($this->actionStorage->loadMultiple(), function ($action) use ($entity_type) { + return $action->getType() == $entity_type; + }); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + // @todo Consider making the bulk operation form cacheable. See + // https://www.drupal.org/node/2503009. + return 0; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return $this->languageManager->isMultilingual() ? $this->getEntityTranslationRenderer()->getCacheContexts() : []; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return []; + } + + /** + * {@inheritdoc} + */ + public function getEntityTypeId() { + return $this->getEntityType(); + } + + /** + * {@inheritdoc} + */ + protected function getEntityManager() { + return $this->entityManager; + } + + /** + * {@inheritdoc} + */ + protected function getLanguageManager() { + return $this->languageManager; + } + + /** + * {@inheritdoc} + */ + protected function getView() { + return $this->view; + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['action_title'] = ['default' => $this->t('Action')]; + $options['include_exclude'] = [ + 'default' => 'exclude', + ]; + $options['selected_actions'] = [ + 'default' => [], + ]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $form['action_title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Action title'), + '#default_value' => $this->options['action_title'], + '#description' => $this->t('The title shown above the actions dropdown.'), + ]; + + $form['include_exclude'] = [ + '#type' => 'radios', + '#title' => $this->t('Available actions'), + '#options' => [ + 'exclude' => $this->t('All actions, except selected'), + 'include' => $this->t('Only selected actions'), + ], + '#default_value' => $this->options['include_exclude'], + ]; + $form['selected_actions'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Selected actions'), + '#options' => $this->getBulkOptions(FALSE), + '#default_value' => $this->options['selected_actions'], + ]; + + parent::buildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + 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))); + } + + /** + * {@inheritdoc} + */ + public function preRender(&$values) { + parent::preRender($values); + + // If the view is using a table style, provide a placeholder for a + // "select all" checkbox. + if (!empty($this->view->style_plugin) && $this->view->style_plugin instanceof Table) { + // Add the tableselect css classes. + $this->options['element_label_class'] .= 'select-all'; + // Hide the actual label of the field on the table header. + $this->options['label'] = ''; + } + } + + /** + * {@inheritdoc} + */ + public function getValue(ResultRow $row, $field = NULL) { + return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->'; + } + + /** + * Form constructor for the bulk form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function viewsForm(&$form, FormStateInterface $form_state) { + // Make sure we do not accidentally cache this form. + // @todo Evaluate this again in https://www.drupal.org/node/2503009. + $form['#cache']['max-age'] = 0; + + // Add the tableselect javascript. + $form['#attached']['library'][] = 'core/drupal.tableselect'; + $use_revision = array_key_exists('revision', $this->view->getQuery()->getEntityTableInfo()); + + // Only add the bulk form options and buttons if there are results. + if (!empty($this->view->result)) { + // Render checkboxes for all rows. + $form[$this->options['id']]['#tree'] = TRUE; + foreach ($this->view->result as $row_index => $row) { + $entity = $this->getEntityTranslation($this->getEntity($row), $row); + + $form[$this->options['id']][$row_index] = [ + '#type' => 'checkbox', + // We are not able to determine a main "title" for each row, so we can + // only output a generic label. + '#title' => $this->t('Update this item'), + '#title_display' => 'invisible', + '#default_value' => !empty($form_state->getValue($this->options['id'])[$row_index]) ? 1 : NULL, + '#return_value' => $this->calculateEntityBulkFormKey($entity, $use_revision), + ]; + } + + // Replace the form submit button label. + $form['actions']['submit']['#value'] = $this->t('Apply to selected items'); + + // Ensure a consistent container for filters/operations in the view header. + $form['header'] = [ + '#type' => 'container', + '#weight' => -100, + ]; + + // Build the bulk operations action widget for the header. + // Allow themes to apply .container-inline on this separate container. + $form['header'][$this->options['id']] = [ + '#type' => 'container', + ]; + $form['header'][$this->options['id']]['action'] = [ + '#type' => 'select', + '#title' => $this->options['action_title'], + '#options' => $this->getBulkOptions(), + ]; + + // Duplicate the form actions into the action container in the header. + $form['header'][$this->options['id']]['actions'] = $form['actions']; + } + else { + // Remove the default actions build array. + unset($form['actions']); + } + } + + /** + * Returns the available operations for this form. + * + * @param bool $filtered + * (optional) Whether to filter actions to selected actions. + * @return array + * An associative array of operations, suitable for a select element. + */ + protected function getBulkOptions($filtered = TRUE) { + $options = []; + // Filter the action list. + foreach ($this->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, + // skip actions that were not selected. + if (($this->options['include_exclude'] == 'include') && !$in_selected) { + continue; + } + // Otherwise, if the field is configured to exclude the selected + // actions, skip actions that were selected. + elseif (($this->options['include_exclude'] == 'exclude') && $in_selected) { + continue; + } + } + + $options[$id] = $action->label(); + } + + return $options; + } + + /** + * Submit handler for the bulk form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown when the user tried to access an action without access to it. + */ + public function viewsFormSubmit(&$form, FormStateInterface $form_state) { + if ($form_state->get('step') == 'views_form_views_form') { + // Filter only selected checkboxes. Use the actual user input rather than + // the raw form values array, since the site data may change before the + // bulk form is submitted, which can lead to data loss. + $user_input = $form_state->getUserInput(); + $selected = array_filter($user_input[$this->options['id']]); + $entities = []; + $action = $this->actions[$form_state->getValue('action')]; + $count = 0; + + foreach ($selected as $bulk_form_key) { + $entity = $this->loadEntityFromBulkFormKey($bulk_form_key); + + // Skip execution if the user did not have access. + if (!$action->getPlugin()->access($entity, $this->view->getUser())) { + $this->drupalSetMessage($this->t('No access to execute %action on the @entity_type_label %entity_label.', [ + '%action' => $action->label(), + '@entity_type_label' => $entity->getEntityType()->getLabel(), + '%entity_label' => $entity->label() + ]), 'error'); + continue; + } + + $count++; + + $entities[$bulk_form_key] = $entity; + } + + $action->execute($entities); + + $operation_definition = $action->getPluginDefinition(); + if (!empty($operation_definition['confirm_form_route_name'])) { + $options = [ + 'query' => $this->getDestinationArray(), + ]; + $form_state->setRedirect($operation_definition['confirm_form_route_name'], [], $options); + } + else { + // Don't display the message unless there are some elements affected and + // there is no confirmation form. + if ($count) { + drupal_set_message($this->formatPlural($count, '%action was applied to @count item.', '%action was applied to @count items.', [ + '%action' => $action->label(), + ])); + } + } + } + } + + /** + * Returns the message to be displayed when there are no selected items. + * + * @return string + * Message displayed when no items are selected. + */ + protected function emptySelectedMessage() { + return $this->t('No items selected.'); + } + + /** + * {@inheritdoc} + */ + public function viewsFormValidate(&$form, FormStateInterface $form_state) { + $selected = array_filter($form_state->getValue($this->options['id'])); + if (empty($selected)) { + $form_state->setErrorByName('', $this->emptySelectedMessage()); + } + } + + /** + * {@inheritdoc} + */ + public function query() { + if ($this->languageManager->isMultilingual()) { + $this->getEntityTranslationRenderer()->query($this->query, $this->relationship); + } + } + + /** + * {@inheritdoc} + */ + public function clickSortable() { + return FALSE; + } + + /** + * Wraps drupal_set_message(). + */ + protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) { + drupal_set_message($message, $type, $repeat); + } + + /** + * Calculates a bulk form key. + * + * This generates a key that is used as the checkbox return value when + * submitting a bulk form. This key allows the entity for the row to be loaded + * totally independently of the executed view row. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to calculate a bulk form key for. + * @param bool $use_revision + * Whether the revision id should be added to the bulk form key. This should + * be set to TRUE only if the view is listing entity revisions. + * + * @return string + * The bulk form key representing the entity's id, language and revision (if + * applicable) as one string. + * + * @see self::loadEntityFromBulkFormKey() + */ + protected function calculateEntityBulkFormKey(EntityInterface $entity, $use_revision) { + $key_parts = [$entity->language()->getId(), $entity->id()]; + + if ($entity instanceof RevisionableInterface && $use_revision) { + $key_parts[] = $entity->getRevisionId(); + } + + // An entity ID could be an arbitrary string (although they are typically + // numeric). JSON then Base64 encoding ensures the bulk_form_key is + // safe to use in HTML, and that the key parts can be retrieved. + $key = json_encode($key_parts); + return base64_encode($key); + } + + /** + * Loads an entity based on a bulk form key. + * + * @param string $bulk_form_key + * The bulk form key representing the entity's id, language and revision (if + * applicable) as one string. + * + * @return \Drupal\Core\Entity\EntityInterface + * The entity loaded in the state (language, optionally revision) specified + * as part of the bulk form key. + */ + protected function loadEntityFromBulkFormKey($bulk_form_key) { + $key = base64_decode($bulk_form_key); + $key_parts = json_decode($key); + $revision_id = NULL; + + // If there are 3 items, vid will be last. + if (count($key_parts) === 3) { + $revision_id = array_pop($key_parts); + } + + // The first two items will always be langcode and ID. + $id = array_pop($key_parts); + $langcode = array_pop($key_parts); + + // Load the entity or a specific revision depending on the given key. + $storage = $this->entityManager->getStorage($this->getEntityType()); + $entity = $revision_id ? $storage->loadRevision($revision_id) : $storage->load($id); + + if ($entity instanceof TranslatableInterface) { + $entity = $entity->getTranslation($langcode); + } + + return $entity; + } + +} diff --git a/core/modules/views/tests/fixtures/update/legacy-bulk-form-update.php b/core/modules/views/tests/fixtures/update/legacy-bulk-form-update.php new file mode 100644 index 0000000000000000000000000000000000000000..e3ec6a4b92ef82fda10c3953e5626e1265233cc6 --- /dev/null +++ b/core/modules/views/tests/fixtures/update/legacy-bulk-form-update.php @@ -0,0 +1,19 @@ +<?php + +/** + * @file + * Test fixture. + */ + +use Drupal\Core\Database\Database; +use Drupal\Core\Serialization\Yaml; + +$connection = Database::getConnection(); + +$connection->insert('config') + ->fields([ + 'collection' => '', + 'name' => 'views.view.legacy_bulk_form', + 'data' => serialize(Yaml::decode(file_get_contents(__DIR__ . '/views.view.legacy_bulk_form.yml'))), + ]) + ->execute(); diff --git a/core/modules/views/tests/fixtures/update/views.view.legacy_bulk_form.yml b/core/modules/views/tests/fixtures/update/views.view.legacy_bulk_form.yml new file mode 100644 index 0000000000000000000000000000000000000000..5c10b289816caa19883164ebf21de65b96b70d74 --- /dev/null +++ b/core/modules/views/tests/fixtures/update/views.view.legacy_bulk_form.yml @@ -0,0 +1,243 @@ +uuid: 67e001ab-bf26-4317-98a0-9ef7c8e6773a +langcode: en +status: true +dependencies: + module: + - node + - system + - user +id: legacy_bulk_form +label: 'legacy bulk form' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: nid + plugin_id: field + node_bulk_form: + id: node_bulk_form + table: node + field: node_bulk_form + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + action_title: Action + include_exclude: exclude + selected_actions: { } + entity_type: node + plugin_id: bulk_form + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/tests/src/Functional/Update/LegacyBulkFormUpdateTest.php b/core/modules/views/tests/src/Functional/Update/LegacyBulkFormUpdateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..75e2b54b65ec7028f16a8545f502eb76d4c89823 --- /dev/null +++ b/core/modules/views/tests/src/Functional/Update/LegacyBulkFormUpdateTest.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\Tests\views\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; +use Drupal\views\Entity\View; + +/** + * Tests Views image style dependencies update. + * + * @group views + */ +class LegacyBulkFormUpdateTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../fixtures/update/legacy-bulk-form-update.php' + ]; + } + + /** + * Tests the updating of dependencies for Views using the bulk_form plugin. + */ + public function testBulkFormDependencies() { + $module_dependencies = View::load('legacy_bulk_form')->getDependencies()['module']; + + $this->assertTrue(in_array('system', $module_dependencies)); + + $this->runUpdates(); + + $module_dependencies = View::load('legacy_bulk_form')->getDependencies()['module']; + + $this->assertFalse(in_array('system', $module_dependencies)); + } + +} diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index 28c0f5c7ad9b3f2e109cdbf86943c5464adf7885..c41ae7f67d226ad5af0ea9f9001a718062f4cb0f 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -256,3 +256,17 @@ function views_post_update_entity_link_url() { } } } + +/** + * Update dependencies for moved bulk field plugin. + */ +function views_post_update_bulk_field_moved() { + $views = View::loadMultiple(); + array_walk($views, function (View $view) { + $old_dependencies = $view->getDependencies(); + $new_dependencies = $view->calculateDependencies()->getDependencies(); + if ($old_dependencies !== $new_dependencies) { + $view->save(); + } + }); +}