3 namespace Drupal\system\Plugin\views\field;
5 use Drupal\Core\Cache\CacheableDependencyInterface;
6 use Drupal\Core\Entity\EntityInterface;
7 use Drupal\Core\Entity\EntityManagerInterface;
8 use Drupal\Core\Entity\RevisionableInterface;
9 use Drupal\Core\Form\FormStateInterface;
10 use Drupal\Core\Language\LanguageManagerInterface;
11 use Drupal\Core\Routing\RedirectDestinationTrait;
12 use Drupal\Core\TypedData\TranslatableInterface;
13 use Drupal\views\Entity\Render\EntityTranslationRenderTrait;
14 use Drupal\views\Plugin\views\display\DisplayPluginBase;
15 use Drupal\views\Plugin\views\field\FieldPluginBase;
16 use Drupal\views\Plugin\views\field\UncacheableFieldHandlerTrait;
17 use Drupal\views\Plugin\views\style\Table;
18 use Drupal\views\ResultRow;
19 use Drupal\views\ViewExecutable;
20 use Symfony\Component\DependencyInjection\ContainerInterface;
23 * Defines a actions-based bulk operation form element.
25 * @ViewsField("bulk_form")
27 class BulkForm extends FieldPluginBase implements CacheableDependencyInterface {
29 use RedirectDestinationTrait;
30 use UncacheableFieldHandlerTrait;
31 use EntityTranslationRenderTrait;
36 * @var \Drupal\Core\Entity\EntityManagerInterface
38 protected $entityManager;
43 * @var \Drupal\Core\Entity\EntityStorageInterface
45 protected $actionStorage;
48 * An array of actions that can be executed.
50 * @var \Drupal\system\ActionConfigEntityInterface[]
52 protected $actions = [];
55 * The language manager.
57 * @var \Drupal\Core\Language\LanguageManagerInterface
59 protected $languageManager;
62 * Constructs a new BulkForm object.
64 * @param array $configuration
65 * A configuration array containing information about the plugin instance.
66 * @param string $plugin_id
67 * The plugin ID for the plugin instance.
68 * @param mixed $plugin_definition
69 * The plugin implementation definition.
70 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
72 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
73 * The language manager.
75 public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
76 parent::__construct($configuration, $plugin_id, $plugin_definition);
78 $this->entityManager = $entity_manager;
79 $this->actionStorage = $entity_manager->getStorage('action');
80 $this->languageManager = $language_manager;
86 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
91 $container->get('entity.manager'),
92 $container->get('language_manager')
99 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
100 parent::init($view, $display, $options);
102 $entity_type = $this->getEntityType();
103 // Filter the actions to only include those for this entity type.
104 $this->actions = array_filter($this->actionStorage->loadMultiple(), function ($action) use ($entity_type) {
105 return $action->getType() == $entity_type;
112 public function getCacheMaxAge() {
113 // @todo Consider making the bulk operation form cacheable. See
114 // https://www.drupal.org/node/2503009.
121 public function getCacheContexts() {
122 return $this->languageManager->isMultilingual() ? $this->getEntityTranslationRenderer()->getCacheContexts() : [];
128 public function getCacheTags() {
135 public function getEntityTypeId() {
136 return $this->getEntityType();
142 protected function getEntityManager() {
143 return $this->entityManager;
149 protected function getLanguageManager() {
150 return $this->languageManager;
156 protected function getView() {
163 protected function defineOptions() {
164 $options = parent::defineOptions();
165 $options['action_title'] = ['default' => $this->t('Action')];
166 $options['include_exclude'] = [
167 'default' => 'exclude',
169 $options['selected_actions'] = [
178 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
179 $form['action_title'] = [
180 '#type' => 'textfield',
181 '#title' => $this->t('Action title'),
182 '#default_value' => $this->options['action_title'],
183 '#description' => $this->t('The title shown above the actions dropdown.'),
186 $form['include_exclude'] = [
188 '#title' => $this->t('Available actions'),
190 'exclude' => $this->t('All actions, except selected'),
191 'include' => $this->t('Only selected actions'),
193 '#default_value' => $this->options['include_exclude'],
195 $form['selected_actions'] = [
196 '#type' => 'checkboxes',
197 '#title' => $this->t('Selected actions'),
198 '#options' => $this->getBulkOptions(FALSE),
199 '#default_value' => $this->options['selected_actions'],
202 parent::buildOptionsForm($form, $form_state);
208 public function validateOptionsForm(&$form, FormStateInterface $form_state) {
209 parent::validateOptionsForm($form, $form_state);
211 $selected_actions = $form_state->getValue(['options', 'selected_actions']);
212 $form_state->setValue(['options', 'selected_actions'], array_values(array_filter($selected_actions)));
218 public function preRender(&$values) {
219 parent::preRender($values);
221 // If the view is using a table style, provide a placeholder for a
222 // "select all" checkbox.
223 if (!empty($this->view->style_plugin) && $this->view->style_plugin instanceof Table) {
224 // Add the tableselect css classes.
225 $this->options['element_label_class'] .= 'select-all';
226 // Hide the actual label of the field on the table header.
227 $this->options['label'] = '';
234 public function getValue(ResultRow $row, $field = NULL) {
235 return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->';
239 * Form constructor for the bulk form.
242 * An associative array containing the structure of the form.
243 * @param \Drupal\Core\Form\FormStateInterface $form_state
244 * The current state of the form.
246 public function viewsForm(&$form, FormStateInterface $form_state) {
247 // Make sure we do not accidentally cache this form.
248 // @todo Evaluate this again in https://www.drupal.org/node/2503009.
249 $form['#cache']['max-age'] = 0;
251 // Add the tableselect javascript.
252 $form['#attached']['library'][] = 'core/drupal.tableselect';
253 $use_revision = array_key_exists('revision', $this->view->getQuery()->getEntityTableInfo());
255 // Only add the bulk form options and buttons if there are results.
256 if (!empty($this->view->result)) {
257 // Render checkboxes for all rows.
258 $form[$this->options['id']]['#tree'] = TRUE;
259 foreach ($this->view->result as $row_index => $row) {
260 $entity = $this->getEntityTranslation($this->getEntity($row), $row);
262 $form[$this->options['id']][$row_index] = [
263 '#type' => 'checkbox',
264 // We are not able to determine a main "title" for each row, so we can
265 // only output a generic label.
266 '#title' => $this->t('Update this item'),
267 '#title_display' => 'invisible',
268 '#default_value' => !empty($form_state->getValue($this->options['id'])[$row_index]) ? 1 : NULL,
269 '#return_value' => $this->calculateEntityBulkFormKey($entity, $use_revision),
273 // Replace the form submit button label.
274 $form['actions']['submit']['#value'] = $this->t('Apply to selected items');
276 // Ensure a consistent container for filters/operations in the view header.
278 '#type' => 'container',
282 // Build the bulk operations action widget for the header.
283 // Allow themes to apply .container-inline on this separate container.
284 $form['header'][$this->options['id']] = [
285 '#type' => 'container',
287 $form['header'][$this->options['id']]['action'] = [
289 '#title' => $this->options['action_title'],
290 '#options' => $this->getBulkOptions(),
293 // Duplicate the form actions into the action container in the header.
294 $form['header'][$this->options['id']]['actions'] = $form['actions'];
297 // Remove the default actions build array.
298 unset($form['actions']);
303 * Returns the available operations for this form.
305 * @param bool $filtered
306 * (optional) Whether to filter actions to selected actions.
308 * An associative array of operations, suitable for a select element.
310 protected function getBulkOptions($filtered = TRUE) {
312 // Filter the action list.
313 foreach ($this->actions as $id => $action) {
315 $in_selected = in_array($id, $this->options['selected_actions']);
316 // If the field is configured to include only the selected actions,
317 // skip actions that were not selected.
318 if (($this->options['include_exclude'] == 'include') && !$in_selected) {
321 // Otherwise, if the field is configured to exclude the selected
322 // actions, skip actions that were selected.
323 elseif (($this->options['include_exclude'] == 'exclude') && $in_selected) {
328 $options[$id] = $action->label();
335 * Submit handler for the bulk form.
338 * An associative array containing the structure of the form.
339 * @param \Drupal\Core\Form\FormStateInterface $form_state
340 * The current state of the form.
342 * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
343 * Thrown when the user tried to access an action without access to it.
345 public function viewsFormSubmit(&$form, FormStateInterface $form_state) {
346 if ($form_state->get('step') == 'views_form_views_form') {
347 // Filter only selected checkboxes. Use the actual user input rather than
348 // the raw form values array, since the site data may change before the
349 // bulk form is submitted, which can lead to data loss.
350 $user_input = $form_state->getUserInput();
351 $selected = array_filter($user_input[$this->options['id']]);
353 $action = $this->actions[$form_state->getValue('action')];
356 foreach ($selected as $bulk_form_key) {
357 $entity = $this->loadEntityFromBulkFormKey($bulk_form_key);
359 // Skip execution if the user did not have access.
360 if (!$action->getPlugin()->access($entity, $this->view->getUser())) {
361 $this->drupalSetMessage($this->t('No access to execute %action on the @entity_type_label %entity_label.', [
362 '%action' => $action->label(),
363 '@entity_type_label' => $entity->getEntityType()->getLabel(),
364 '%entity_label' => $entity->label()
371 $entities[$bulk_form_key] = $entity;
374 $action->execute($entities);
376 $operation_definition = $action->getPluginDefinition();
377 if (!empty($operation_definition['confirm_form_route_name'])) {
379 'query' => $this->getDestinationArray(),
381 $form_state->setRedirect($operation_definition['confirm_form_route_name'], [], $options);
384 // Don't display the message unless there are some elements affected and
385 // there is no confirmation form.
387 drupal_set_message($this->formatPlural($count, '%action was applied to @count item.', '%action was applied to @count items.', [
388 '%action' => $action->label(),
396 * Returns the message to be displayed when there are no selected items.
399 * Message displayed when no items are selected.
401 protected function emptySelectedMessage() {
402 return $this->t('No items selected.');
408 public function viewsFormValidate(&$form, FormStateInterface $form_state) {
409 $selected = array_filter($form_state->getValue($this->options['id']));
410 if (empty($selected)) {
411 $form_state->setErrorByName('', $this->emptySelectedMessage());
418 public function query() {
419 if ($this->languageManager->isMultilingual()) {
420 $this->getEntityTranslationRenderer()->query($this->query, $this->relationship);
427 public function clickSortable() {
432 * Wraps drupal_set_message().
434 protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
435 drupal_set_message($message, $type, $repeat);
439 * Calculates a bulk form key.
441 * This generates a key that is used as the checkbox return value when
442 * submitting a bulk form. This key allows the entity for the row to be loaded
443 * totally independently of the executed view row.
445 * @param \Drupal\Core\Entity\EntityInterface $entity
446 * The entity to calculate a bulk form key for.
447 * @param bool $use_revision
448 * Whether the revision id should be added to the bulk form key. This should
449 * be set to TRUE only if the view is listing entity revisions.
452 * The bulk form key representing the entity's id, language and revision (if
453 * applicable) as one string.
455 * @see self::loadEntityFromBulkFormKey()
457 protected function calculateEntityBulkFormKey(EntityInterface $entity, $use_revision) {
458 $key_parts = [$entity->language()->getId(), $entity->id()];
460 if ($entity instanceof RevisionableInterface && $use_revision) {
461 $key_parts[] = $entity->getRevisionId();
464 // An entity ID could be an arbitrary string (although they are typically
465 // numeric). JSON then Base64 encoding ensures the bulk_form_key is
466 // safe to use in HTML, and that the key parts can be retrieved.
467 $key = json_encode($key_parts);
468 return base64_encode($key);
472 * Loads an entity based on a bulk form key.
474 * @param string $bulk_form_key
475 * The bulk form key representing the entity's id, language and revision (if
476 * applicable) as one string.
478 * @return \Drupal\Core\Entity\EntityInterface
479 * The entity loaded in the state (language, optionally revision) specified
480 * as part of the bulk form key.
482 protected function loadEntityFromBulkFormKey($bulk_form_key) {
483 $key = base64_decode($bulk_form_key);
484 $key_parts = json_decode($key);
487 // If there are 3 items, vid will be last.
488 if (count($key_parts) === 3) {
489 $revision_id = array_pop($key_parts);
492 // The first two items will always be langcode and ID.
493 $id = array_pop($key_parts);
494 $langcode = array_pop($key_parts);
496 // Load the entity or a specific revision depending on the given key.
497 $storage = $this->entityManager->getStorage($this->getEntityType());
498 $entity = $revision_id ? $storage->loadRevision($revision_id) : $storage->load($id);
500 if ($entity instanceof TranslatableInterface) {
501 $entity = $entity->getTranslation($langcode);