ViewEditForm.php 43.9 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\views_ui;

5
use Drupal\Component\Utility\Html;
6
use Drupal\Component\Utility\SafeMarkup;
7 8 9
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\ReplaceCommand;
10
use Drupal\Core\Datetime\DateFormatterInterface;
11
use Drupal\Core\Form\FormStateInterface;
12
use Drupal\Core\Render\ElementInfoManagerInterface;
13
use Drupal\Core\Url;
14
use Drupal\user\SharedTempStoreFactory;
15
use Drupal\views\Views;
16
use Symfony\Component\DependencyInjection\ContainerInterface;
17
use Symfony\Component\HttpFoundation\RequestStack;
18
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
19 20 21

/**
 * Form controller for the Views edit form.
22 23
 *
 * @internal
24
 */
25
class ViewEditForm extends ViewFormBase {
26 27 28 29

  /**
   * The views temp store.
   *
30
   * @var \Drupal\user\SharedTempStore
31 32 33 34 35 36
   */
  protected $tempStore;

  /**
   * The request object.
   *
37
   * @var \Symfony\Component\HttpFoundation\RequestStack
38
   */
39
  protected $requestStack;
40

41 42 43
  /**
   * The date formatter service.
   *
44
   * @var \Drupal\Core\Datetime\DateFormatterInterface
45 46 47
   */
  protected $dateFormatter;

48 49 50 51 52 53 54
  /**
   * The element info manager.
   *
   * @var \Drupal\Core\Render\ElementInfoManagerInterface
   */
  protected $elementInfo;

55
  /**
56
   * Constructs a new ViewEditForm object.
57
   *
58
   * @param \Drupal\user\SharedTempStoreFactory $temp_store_factory
59
   *   The factory for the temp store object.
60 61
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   The request stack object.
62
   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
63
   *   The date Formatter service.
64 65
   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
   *   The element info manager.
66
   */
67
  public function __construct(SharedTempStoreFactory $temp_store_factory, RequestStack $requestStack, DateFormatterInterface $date_formatter, ElementInfoManagerInterface $element_info) {
68
    $this->tempStore = $temp_store_factory->get('views');
69
    $this->requestStack = $requestStack;
70
    $this->dateFormatter = $date_formatter;
71
    $this->elementInfo = $element_info;
72 73 74 75 76
  }

  /**
   * {@inheritdoc}
   */
77
  public static function create(ContainerInterface $container) {
78
    return new static(
79
      $container->get('user.shared_tempstore'),
80
      $container->get('request_stack'),
81 82
      $container->get('date.formatter'),
      $container->get('element_info')
83 84
    );
  }
85 86

  /**
87
   * {@inheritdoc}
88
   */
89
  public function form(array $form, FormStateInterface $form_state) {
90
    $view = $this->entity;
91
    $display_id = $this->displayID;
92
    // Do not allow the form to be cached, because $form_state->get('view') can become
93 94 95
    // stale between page requests.
    // See views_ui_ajax_get_form() for how this affects #ajax.
    // @todo To remove this and allow the form to be cacheable:
96 97
    //   - Change $form_state->get('view') to $form_state->getTemporary()['view'].
    //   - Add a #process function to initialize $form_state->getTemporary()['view']
98
    //     on cached form submissions.
99
    //   - Use \Drupal\Core\Form\FormStateInterface::loadInclude().
100
    $form_state->disableCache();
101 102

    if ($display_id) {
103
      if (!$view->getExecutable()->setDisplay($display_id)) {
104
        $form['#markup'] = $this->t('Invalid display id @display', ['@display' => $display_id]);
105 106 107 108 109 110
        return $form;
      }
    }

    $form['#tree'] = TRUE;

111 112 113 114
    $form['#attached']['library'][] = 'core/jquery.ui.tabs';
    $form['#attached']['library'][] = 'core/jquery.ui.dialog';
    $form['#attached']['library'][] = 'core/drupal.states';
    $form['#attached']['library'][] = 'core/drupal.tabledrag';
115
    $form['#attached']['library'][] = 'views_ui/views_ui.admin';
116
    $form['#attached']['library'][] = 'views_ui/admin.styling';
117

118
    $form += [
119 120
      '#prefix' => '',
      '#suffix' => '',
121
    ];
122 123 124

    $view_status = $view->status() ? 'enabled' : 'disabled';
    $form['#prefix'] .= '<div class="views-edit-view views-admin ' . $view_status . ' clearfix">';
125 126
    $form['#suffix'] = '</div>' . $form['#suffix'];

127
    $form['#attributes']['class'] = ['form-edit'];
128

129
    if ($view->isLocked()) {
130
      $username = [
131
        '#theme' => 'username',
132
        '#account' => $this->entityManager->getStorage('user')->load($view->lock->owner),
133 134
      ];
      $lock_message_substitutions = [
135
        '@user' => \Drupal::service('renderer')->render($username),
136
        '@age' => $this->dateFormatter->formatTimeDiffSince($view->lock->updated),
137
        ':url' => $view->url('break-lock-form'),
138 139
      ];
      $form['locked'] = [
140
        '#type' => 'container',
141
        '#attributes' => ['class' => ['view-locked', 'messages', 'messages--warning']],
142
        '#children' => $this->t('This view is being edited by user @user, and is therefore locked from editing by others. This lock is @age old. Click here to <a href=":url">break this lock</a>.', $lock_message_substitutions),
143
        '#weight' => -10,
144
      ];
145 146
    }
    else {
147
      $form['changed'] = [
148
        '#type' => 'container',
149
        '#attributes' => ['class' => ['view-changed', 'messages', 'messages--warning']],
150
        '#children' => $this->t('You have unsaved changes.'),
151
        '#weight' => -10,
152
      ];
153 154 155 156 157
      if (empty($view->changed)) {
        $form['changed']['#attributes']['class'][] = 'js-hide';
      }
    }

158
    $form['displays'] = [
159
      '#prefix' => '<h1 class="unit-title clearfix">' . $this->t('Displays') . '</h1>',
160
      '#type' => 'container',
161 162
      '#attributes' => [
        'class' => [
163
          'views-displays',
164 165 166
        ],
      ],
    ];
167 168 169 170 171 172


    $form['displays']['top'] = $this->renderDisplayTop($view);

    // The rest requires a display to be selected.
    if ($display_id) {
173
      $form_state->set('display_id', $display_id);
174 175

      // The part of the page where editing will take place.
176
      $form['displays']['settings'] = [
177 178
        '#type' => 'container',
        '#id' => 'edit-display-settings',
179 180 181 182
        '#attributes' => [
          'class' => ['edit-display-settings'],
        ],
      ];
183 184

      // Add a text that the display is disabled.
185 186
      if ($view->getExecutable()->displayHandlers->has($display_id)) {
        if (!$view->getExecutable()->displayHandlers->get($display_id)->isEnabled()) {
187
          $form['displays']['settings']['disabled']['#markup'] = $this->t('This display is disabled.');
188 189 190 191 192
        }
      }

      // Add the edit display content
      $tab_content = $this->getDisplayTab($view);
193 194
      $tab_content['#theme_wrappers'] = ['container'];
      $tab_content['#attributes'] = ['class' => ['views-display-tab']];
195 196 197 198 199 200 201
      $tab_content['#id'] = 'views-tab-' . $display_id;
      // Mark deleted displays as such.
      $display = $view->get('display');
      if (!empty($display[$display_id]['deleted'])) {
        $tab_content['#attributes']['class'][] = 'views-display-deleted';
      }
      // Mark disabled displays as such.
202

203
      if ($view->getExecutable()->displayHandlers->has($display_id) && !$view->getExecutable()->displayHandlers->get($display_id)->isEnabled()) {
204 205
        $tab_content['#attributes']['class'][] = 'views-display-disabled';
      }
206
      $form['displays']['settings']['settings_content'] = [
207 208
        '#type' => 'container',
        'tab_content' => $tab_content,
209
      ];
210 211 212 213 214 215
    }

    return $form;
  }

  /**
216
   * {@inheritdoc}
217
   */
218
  protected function actions(array $form, FormStateInterface $form_state) {
219 220 221
    $actions = parent::actions($form, $form_state);
    unset($actions['delete']);

222
    $actions['cancel'] = [
223
      '#type' => 'submit',
224
      '#value' => $this->t('Cancel'),
225 226 227
      '#submit' => ['::cancel'],
      '#limit_validation_errors' => [],
    ];
228 229 230 231
    if ($this->entity->isLocked()) {
      $actions['submit']['#access'] = FALSE;
      $actions['cancel']['#access'] = FALSE;
    }
232 233 234 235
    return $actions;
  }

  /**
236
   * {@inheritdoc}
237
   */
238 239
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
240

241
    $view = $this->entity;
242
    if ($view->isLocked()) {
243
      $form_state->setErrorByName('', $this->t('Changes cannot be made to a locked view.'));
244
    }
245
    foreach ($view->getExecutable()->validate() as $display_errors) {
246
      foreach ($display_errors as $error) {
247
        $form_state->setErrorByName('', $error);
248 249 250 251 252
      }
    }
  }

  /**
253
   * {@inheritdoc}
254
   */
255
  public function save(array $form, FormStateInterface $form_state) {
256
    $view = $this->entity;
257
    $executable = $view->getExecutable();
258
    $executable->initDisplay();
259

260 261 262 263
    // Go through and remove displayed scheduled for removal.
    $displays = $view->get('display');
    foreach ($displays as $id => $display) {
      if (!empty($display['deleted'])) {
264 265 266 267
        // Remove view display from view attachment under the attachments
        // options.
        $display_handler = $executable->displayHandlers->get($id);
        if ($attachments = $display_handler->getAttachedDisplays()) {
268
          foreach ($attachments as $attachment) {
269 270 271 272 273
            $attached_options = $executable->displayHandlers->get($attachment)->getOption('displays');
            unset($attached_options[$id]);
            $executable->displayHandlers->get($attachment)->setOption('displays', $attached_options);
          }
        }
274
        $executable->displayHandlers->remove($id);
275 276 277
        unset($displays[$id]);
      }
    }
278

279
    // Rename display ids if needed.
280
    foreach ($executable->displayHandlers as $id => $display) {
281
      if (!empty($display->display['new_id']) && $display->display['new_id'] !== $display->display['id'] && empty($display->display['deleted'])) {
282
        $new_id = $display->display['new_id'];
283 284 285
        $display->display['id'] = $new_id;
        unset($display->display['new_id']);
        $executable->displayHandlers->set($new_id, $display);
286 287 288

        $displays[$new_id] = $displays[$id];
        unset($displays[$id]);
289

290
        // Redirect the user to the renamed display to be sure that the page itself exists and doesn't throw errors.
291
        $form_state->setRedirect('entity.view.edit_display_form', [
292 293
          'view' => $view->id(),
          'display_id' => $new_id,
294
        ]);
295
      }
296 297 298
      elseif (isset($display->display['new_id'])) {
        unset($display->display['new_id']);
      }
299 300 301
    }
    $view->set('display', $displays);

302
    // @todo: Revisit this when https://www.drupal.org/node/1668866 is in.
303
    $query = $this->requestStack->getCurrentRequest()->query;
304
    $destination = $query->get('destination');
305

306 307
    if (!empty($destination)) {
      // Find out the first display which has a changed path and redirect to this url.
308
      $old_view = Views::getView($view->id());
309 310 311 312 313 314 315 316
      $old_view->initDisplay();
      foreach ($old_view->displayHandlers as $id => $display) {
        // Only check for displays with a path.
        $old_path = $display->getOption('path');
        if (empty($old_path)) {
          continue;
        }

317 318
        if (($display->getPluginId() == 'page') && ($old_path == $destination) && ($old_path != $view->getExecutable()->displayHandlers->get($id)->getOption('path'))) {
          $destination = $view->getExecutable()->displayHandlers->get($id)->getOption('path');
319 320 321
          $query->remove('destination');
        }
      }
322 323
      // @todo Use Url::fromPath() once https://www.drupal.org/node/2351379 is
      //   resolved.
324
      $form_state->setRedirectUrl(Url::fromUri("base:$destination"));
325 326 327
    }

    $view->save();
328

329
    drupal_set_message($this->t('The view %name has been saved.', ['%name' => $view->label()]));
330 331

    // Remove this view from cache so we can edit it properly.
332
    $this->tempStore->delete($view->id());
333 334 335 336 337 338 339
  }

  /**
   * Form submission handler for the 'cancel' action.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
340 341
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
342
   */
343
  public function cancel(array $form, FormStateInterface $form_state) {
344
    // Remove this view from cache so edits will be lost.
345
    $view = $this->entity;
346
    $this->tempStore->delete($view->id());
347
    $form_state->setRedirectUrl($this->entity->urlInfo('collection'));
348 349 350 351 352 353
  }

  /**
   * Returns a renderable array representing the edit page for one display.
   */
  public function getDisplayTab($view) {
354
    $build = [];
355
    $display_id = $this->displayID;
356
    $display = $view->getExecutable()->displayHandlers->get($display_id);
357 358 359 360
    // If the plugin doesn't exist, display an error message instead of an edit
    // page.
    if (empty($display)) {
      // @TODO: Improved UX for the case where a plugin is missing.
361
      $build['#markup'] = $this->t("Error: Display @display refers to a plugin named '@plugin', but that plugin is not available.", ['@display' => $display->display['id'], '@plugin' => $display->display['display_plugin']]);
362 363 364 365 366 367 368
    }
    // Build the content of the edit page.
    else {
      $build['details'] = $this->getDisplayDetails($view, $display->display);
    }
    // In AJAX context, ViewUI::rebuildCurrentTab() returns this outside of form
    // context, so hook_form_views_ui_edit_form_alter() is insufficient.
369
    \Drupal::moduleHandler()->alter('views_ui_display_tab', $build, $view, $display_id);
370 371 372 373 374 375 376 377 378 379 380 381 382
    return $build;
  }

  /**
   * Helper function to get the display details section of the edit UI.
   *
   * @param $display
   *
   * @return array
   *   A renderable page build array.
   */
  public function getDisplayDetails($view, $display) {
    $display_title = $this->getDisplayLabel($view, $display['id'], FALSE);
383 384 385 386
    $build = [
      '#theme_wrappers' => ['container'],
      '#attributes' => ['id' => 'edit-display-settings-details'],
    ];
387 388

    $is_display_deleted = !empty($display['deleted']);
389
    // The master display cannot be duplicated.
390 391
    $is_default = $display['id'] == 'default';
    // @todo: Figure out why getOption doesn't work here.
392
    $is_enabled = $view->getExecutable()->displayHandlers->get($display['id'])->isEnabled();
393 394

    if ($display['id'] != 'default') {
395
      $build['top']['#theme_wrappers'] = ['container'];
396
      $build['top']['#attributes']['id'] = 'edit-display-settings-top';
397
      $build['top']['#attributes']['class'] = ['views-ui-display-tab-actions', 'edit-display-settings-top', 'views-ui-display-tab-bucket', 'clearfix'];
398 399

      // The Delete, Duplicate and Undo Delete buttons.
400 401 402
      $build['top']['actions'] = [
        '#theme_wrappers' => ['dropbutton_wrapper'],
      ];
403 404 405 406 407 408 409

      // Because some of the 'links' are actually submit buttons, we have to
      // manually wrap each item in <li> and the whole list in <ul>.
      $build['top']['actions']['prefix']['#markup'] = '<ul class="dropbutton">';

      if (!$is_display_deleted) {
        if (!$is_enabled) {
410
          $build['top']['actions']['enable'] = [
411
            '#type' => 'submit',
412
            '#value' => $this->t('Enable @display_title', ['@display_title' => $display_title]),
413 414
            '#limit_validation_errors' => [],
            '#submit' => ['::submitDisplayEnable', '::submitDelayDestination'],
415 416
            '#prefix' => '<li class="enable">',
            "#suffix" => '</li>',
417
          ];
418
        }
419 420
        // Add a link to view the page unless the view is disabled or has no
        // path.
421 422
        elseif ($view->status() && $view->getExecutable()->displayHandlers->get($display['id'])->hasPath()) {
          $path = $view->getExecutable()->displayHandlers->get($display['id'])->getPath();
423

424
          if ($path && (strpos($path, '%') === FALSE)) {
425 426 427 428 429 430 431 432 433 434 435 436
            // Wrap this in a try/catch as trying to generate links to some
            // routes may throw a NotAcceptableHttpException if they do not
            // respond to HTML, such as RESTExports.
            try {
              if (!parse_url($path, PHP_URL_SCHEME)) {
                // @todo Views should expect and store a leading /. See:
                //   https://www.drupal.org/node/2423913
                $url = Url::fromUserInput('/' . ltrim($path, '/'));
              }
              else {
                $url = Url::fromUri("base:$path");
              }
437
            }
438 439
            catch (NotAcceptableHttpException $e) {
              $url = '/' . $path;
440
            }
441

442
            $build['top']['actions']['path'] = [
443
              '#type' => 'link',
444
              '#title' => $this->t('View @display_title', ['@display_title' => $display_title]),
445
              '#options' => ['alt' => [$this->t("Go to the real page for this display")]],
446
              '#url' => $url,
447 448
              '#prefix' => '<li class="view">',
              "#suffix" => '</li>',
449
            ];
450 451 452
          }
        }
        if (!$is_default) {
453
          $build['top']['actions']['duplicate'] = [
454
            '#type' => 'submit',
455
            '#value' => $this->t('Duplicate @display_title', ['@display_title' => $display_title]),
456 457
            '#limit_validation_errors' => [],
            '#submit' => ['::submitDisplayDuplicate', '::submitDelayDestination'],
458 459
            '#prefix' => '<li class="duplicate">',
            "#suffix" => '</li>',
460
          ];
461 462
        }
        // Always allow a display to be deleted.
463
        $build['top']['actions']['delete'] = [
464
          '#type' => 'submit',
465
          '#value' => $this->t('Delete @display_title', ['@display_title' => $display_title]),
466 467
          '#limit_validation_errors' => [],
          '#submit' => ['::submitDisplayDelete', '::submitDelayDestination'],
468 469
          '#prefix' => '<li class="delete">',
          "#suffix" => '</li>',
470
        ];
471

472
        foreach (Views::fetchPluginNames('display', NULL, [$view->get('storage')->get('base_table')]) as $type => $label) {
473 474 475 476
          if ($type == $display['display_plugin']) {
            continue;
          }

477
          $build['top']['actions']['duplicate_as'][$type] = [
478
            '#type' => 'submit',
479
            '#value' => $this->t('Duplicate as @type', ['@type' => $label]),
480 481
            '#limit_validation_errors' => [],
            '#submit' => ['::submitDuplicateDisplayAsType', '::submitDelayDestination'],
482 483
            '#prefix' => '<li class="duplicate">',
            '#suffix' => '</li>',
484
          ];
485
        }
486 487
      }
      else {
488
        $build['top']['actions']['undo_delete'] = [
489
          '#type' => 'submit',
490
          '#value' => $this->t('Undo delete of @display_title', ['@display_title' => $display_title]),
491 492
          '#limit_validation_errors' => [],
          '#submit' => ['::submitDisplayUndoDelete', '::submitDelayDestination'],
493 494
          '#prefix' => '<li class="undo-delete">',
          "#suffix" => '</li>',
495
        ];
496
      }
497
      if ($is_enabled) {
498
        $build['top']['actions']['disable'] = [
499
          '#type' => 'submit',
500
          '#value' => $this->t('Disable @display_title', ['@display_title' => $display_title]),
501 502
          '#limit_validation_errors' => [],
          '#submit' => ['::submitDisplayDisable', '::submitDelayDestination'],
503 504
          '#prefix' => '<li class="disable">',
          "#suffix" => '</li>',
505
        ];
506
      }
507 508 509
      $build['top']['actions']['suffix']['#markup'] = '</ul>';

      // The area above the three columns.
510
      $build['top']['display_title'] = [
511
        '#theme' => 'views_ui_display_tab_setting',
512
        '#description' => $this->t('Display name'),
513
        '#link' => $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($display_title, 'display_title'),
514
      ];
515 516
    }

517 518 519
    $build['columns'] = [];
    $build['columns']['#theme_wrappers'] = ['container'];
    $build['columns']['#attributes'] = ['id' => 'edit-display-settings-main', 'class' => ['clearfix', 'views-display-columns']];
520

521 522
    $build['columns']['first']['#theme_wrappers'] = ['container'];
    $build['columns']['first']['#attributes'] = ['class' => ['views-display-column', 'first']];
523

524 525
    $build['columns']['second']['#theme_wrappers'] = ['container'];
    $build['columns']['second']['#attributes'] = ['class' => ['views-display-column', 'second']];
526

527 528 529 530 531
    $build['columns']['second']['settings'] = [];
    $build['columns']['second']['header'] = [];
    $build['columns']['second']['footer'] = [];
    $build['columns']['second']['empty'] = [];
    $build['columns']['second']['pager'] = [];
532

533
    // The third column buckets are wrapped in details.
534
    $build['columns']['third'] = [
535
      '#type' => 'details',
536
      '#title' => $this->t('Advanced'),
537 538 539
      '#theme_wrappers' => ['details'],
      '#attributes' => [
        'class' => [
540 541
          'views-display-column',
          'third',
542 543 544
        ],
      ],
    ];
545
    // Collapse the details by default.
546
    $build['columns']['third']['#open'] = \Drupal::config('views.settings')->get('ui.show.advanced_column');
547 548 549

    // Each option (e.g. title, access, display as grid/table/list) fits into one
    // of several "buckets," or boxes (Format, Fields, Sort, and so on).
550
    $buckets = [];
551 552

    // Fetch options from the display plugin, with a list of buckets they go into.
553
    $options = [];
554
    $view->getExecutable()->displayHandlers->get($display['id'])->optionsSummary($buckets, $options);
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576

    // Place each option into its bucket.
    foreach ($options as $id => $option) {
      // Each option self-identifies as belonging in a particular bucket.
      $buckets[$option['category']]['build'][$id] = $this->buildOptionForm($view, $id, $option, $display);
    }

    // Place each bucket into the proper column.
    foreach ($buckets as $id => $bucket) {
      // Let buckets identify themselves as belonging in a column.
      if (isset($bucket['column']) && isset($build['columns'][$bucket['column']])) {
        $column = $bucket['column'];
      }
      // If a bucket doesn't pick one of our predefined columns to belong to, put
      // it in the last one.
      else {
        $column = 'third';
      }
      if (isset($bucket['build']) && is_array($bucket['build'])) {
        $build['columns'][$column][$id] = $bucket['build'];
        $build['columns'][$column][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket';
        $build['columns'][$column][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : '';
577
        $build['columns'][$column][$id]['#name'] = $id;
578 579 580 581 582 583 584 585
      }
    }

    $build['columns']['first']['fields'] = $this->getFormBucket($view, 'field', $display);
    $build['columns']['first']['filters'] = $this->getFormBucket($view, 'filter', $display);
    $build['columns']['first']['sorts'] = $this->getFormBucket($view, 'sort', $display);
    $build['columns']['second']['header'] = $this->getFormBucket($view, 'header', $display);
    $build['columns']['second']['footer'] = $this->getFormBucket($view, 'footer', $display);
586
    $build['columns']['second']['empty'] = $this->getFormBucket($view, 'empty', $display);
587 588 589 590 591 592 593 594 595
    $build['columns']['third']['arguments'] = $this->getFormBucket($view, 'argument', $display);
    $build['columns']['third']['relationships'] = $this->getFormBucket($view, 'relationship', $display);

    return $build;
  }

  /**
   * Submit handler to add a restore a removed display to a view.
   */
596
  public function submitDisplayUndoDelete($form, FormStateInterface $form_state) {
597
    $view = $this->entity;
598
    // Create the new display
599
    $id = $form_state->get('display_id');
600 601 602 603 604
    $displays = $view->get('display');
    $displays[$id]['deleted'] = FALSE;
    $view->set('display', $displays);

    // Store in cache
605
    $view->cacheSet();
606 607

    // Redirect to the top-level edit page.
608
    $form_state->setRedirect('entity.view.edit_display_form', [
609 610
      'view' => $view->id(),
      'display_id' => $id,
611
    ]);
612 613 614 615 616
  }

  /**
   * Submit handler to enable a disabled display.
   */
617
  public function submitDisplayEnable($form, FormStateInterface $form_state) {
618
    $view = $this->entity;
619
    $id = $form_state->get('display_id');
620
    // setOption doesn't work because this would might affect upper displays
621
    $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', TRUE);
622 623

    // Store in cache
624
    $view->cacheSet();
625 626

    // Redirect to the top-level edit page.
627
    $form_state->setRedirect('entity.view.edit_display_form', [
628 629
      'view' => $view->id(),
      'display_id' => $id,
630
    ]);
631 632 633 634 635
  }

  /**
   * Submit handler to disable display.
   */
636
  public function submitDisplayDisable($form, FormStateInterface $form_state) {
637
    $view = $this->entity;
638
    $id = $form_state->get('display_id');
639
    $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', FALSE);
640 641

    // Store in cache
642
    $view->cacheSet();
643 644

    // Redirect to the top-level edit page.
645
    $form_state->setRedirect('entity.view.edit_display_form', [
646 647
      'view' => $view->id(),
      'display_id' => $id,
648
    ]);
649 650 651 652 653
  }

  /**
   * Submit handler to delete a display from a view.
   */
654
  public function submitDisplayDelete($form, FormStateInterface $form_state) {
655
    $view = $this->entity;
656
    $display_id = $form_state->get('display_id');
657 658 659 660 661

    // Mark the display for deletion.
    $displays = $view->get('display');
    $displays[$display_id]['deleted'] = TRUE;
    $view->set('display', $displays);
662
    $view->cacheSet();
663 664 665

    // Redirect to the top-level edit page. The first remaining display will
    // become the active display.
666
    $form_state->setRedirectUrl($view->urlInfo('edit-form'));
667 668 669 670
  }

  /**
   * Regenerate the current tab for AJAX updates.
671 672 673 674 675 676 677
   *
   * @param \Drupal\views_ui\ViewUI $view
   *   The view to regenerate its tab.
   * @param \Drupal\Core\Ajax\AjaxResponse $response
   *   The response object to add new commands to.
   * @param string $display_id
   *   The display ID of the tab to regenerate.
678
   */
679
  public function rebuildCurrentTab(ViewUI $view, AjaxResponse $response, $display_id) {
680
    $this->displayID = $display_id;
681
    if (!$view->getExecutable()->setDisplay('default')) {
682 683 684 685
      return;
    }

    // Regenerate the main display area.
686
    $build = $this->getDisplayTab($view);
687
    $response->addCommand(new HtmlCommand('#views-tab-' . $display_id, $build));
688 689 690

    // Regenerate the top area so changes to display names and order will appear.
    $build = $this->renderDisplayTop($view);
691
    $response->addCommand(new ReplaceCommand('#views-display-top', $build));
692 693 694 695 696 697
  }

  /**
   * Render the top of the display so it can be updated during ajax operations.
   */
  public function renderDisplayTop(ViewUI $view) {
698
    $display_id = $this->displayID;
699
    $element['#theme_wrappers'][] = 'views_ui_container';
700 701
    $element['#attributes']['class'] = ['views-display-top', 'clearfix'];
    $element['#attributes']['id'] = ['views-display-top'];
702 703

    // Extra actions for the display
704
    $element['extra_actions'] = [
705
      '#type' => 'dropbutton',
706
      '#attributes' => [
707
        'id' => 'views-display-extra-actions',
708 709 710
      ],
      '#links' => [
        'edit-details' => [
711
          'title' => $this->t('Edit view name/description'),
712
          'url' => Url::fromRoute('views_ui.form_edit_details', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
713 714 715
          'attributes' => ['class' => ['views-ajax-link']],
        ],
        'analyze' => [
716
          'title' => $this->t('Analyze view'),
717
          'url' => Url::fromRoute('views_ui.form_analyze', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
718 719 720
          'attributes' => ['class' => ['views-ajax-link']],
        ],
        'duplicate' => [
721
          'title' => $this->t('Duplicate view'),
722
          'url' => $view->urlInfo('duplicate-form'),
723 724
        ],
        'reorder' => [
725
          'title' => $this->t('Reorder displays'),
726
          'url' => Url::fromRoute('views_ui.form_reorder_displays', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
727 728 729 730
          'attributes' => ['class' => ['views-ajax-link']],
        ],
      ],
    ];
731

732
    if ($view->access('delete')) {
733
      $element['extra_actions']['#links']['delete'] = [
734
        'title' => $this->t('Delete view'),
735
        'url' => $view->urlInfo('delete-form'),
736
      ];
737 738
    }

739
    // Let other modules add additional links here.
740
    \Drupal::moduleHandler()->alter('views_ui_display_top_links', $element['extra_actions']['#links'], $view, $display_id);
741

742 743
    if (isset($view->type) && $view->type != $this->t('Default')) {
      if ($view->type == $this->t('Overridden')) {
744
        $element['extra_actions']['#links']['revert'] = [
745
          'title' => $this->t('Revert view'),
746
          'href' => "admin/structure/views/view/{$view->id()}/revert",
747 748
          'query' => ['destination' => $view->url('edit-form')],
        ];
749 750
      }
      else {
751
        $element['extra_actions']['#links']['delete'] = [
752
          'title' => $this->t('Delete view'),
753
          'url' => $view->urlInfo('delete-form'),
754
        ];
755 756 757 758 759 760 761 762
      }
    }

    // Determine the displays available for editing.
    if ($tabs = $this->getDisplayTabs($view)) {
      if ($display_id) {
        $tabs[$display_id]['#active'] = TRUE;
      }
763
      $tabs['#prefix'] = '<h2 class="visually-hidden">' . $this->t('Secondary tabs') . '</h2><ul id = "views-display-menu-tabs" class="tabs secondary">';
764 765 766 767 768
      $tabs['#suffix'] = '</ul>';
      $element['tabs'] = $tabs;
    }

    // Buttons for adding a new display.
769 770
    foreach (Views::fetchPluginNames('display', NULL, [$view->get('base_table')]) as $type => $label) {
      $element['add_display'][$type] = [
771
        '#type' => 'submit',
772 773 774 775
        '#value' => $this->t('Add @display', ['@display' => $label]),
        '#limit_validation_errors' => [],
        '#submit' => ['::submitDisplayAdd', '::submitDelayDestination'],
        '#attributes' => ['class' => ['add-display']],
776 777
        // Allow JavaScript to remove the 'Add ' prefix from the button label when
        // placing the button in a "Add" dropdown menu.
778 779 780
        '#process' => array_merge(['views_ui_form_button_was_clicked'], $this->elementInfo->getInfoProperty('submit', '#process', [])),
        '#values' => [$this->t('Add @display', ['@display' => $label]), $label],
      ];
781 782 783 784 785 786 787 788 789
    }

    return $element;
  }

  /**
   * Submit handler for form buttons that do not complete a form workflow.
   *
   * The Edit View form is a multistep form workflow, but with state managed by
790
   * the SharedTempStore rather than $form_state->setRebuild(). Without this
791 792 793 794 795
   * submit handler, buttons that add or remove displays would redirect to the
   * destination parameter (e.g., when the Edit View form is linked to from a
   * contextual link). This handler can be added to buttons whose form submission
   * should not yet redirect to the destination.
   */
796
  public function submitDelayDestination($form, FormStateInterface $form_state) {
797 798 799 800 801 802 803 804 805 806 807
    $request = $this->requestStack->getCurrentRequest();
    $destination = $request->query->get('destination');

    $redirect = $form_state->getRedirect();
    // If there is a destination, and redirects are not explicitly disabled, add
    // the destination as a query string to the redirect and suppress it for the
    // current request.
    if (isset($destination) && $redirect !== FALSE) {
      // Create a valid redirect if one does not exist already.
      if (!($redirect instanceof Url)) {
        $redirect = Url::createFromRequest($request);
808
      }
809 810 811

      // Add the current destination to the redirect unless one exists already.
      $options = $redirect->getOptions();
812 813
      if (!isset($options['query']['destination'])) {
        $options['query']['destination'] = $destination;
814
        $redirect->setOptions($options);
815
      }
816 817 818

      $form_state->setRedirectUrl($redirect);
      $request->query->remove('destination');
819 820 821 822 823 824
    }
  }

  /**
   * Submit handler to duplicate a display for a view.
   */
825
  public function submitDisplayDuplicate($form, FormStateInterface $form_state) {
826
    $view = $this->entity;
827
    $display_id = $this->displayID;
828 829 830

    // Create the new display.
    $displays = $view->get('display');
831 832
    $display = $view->getExecutable()->newDisplay($displays[$display_id]['display_plugin']);
    $new_display_id = $display->display['id'];
833 834 835 836 837 838
    $displays[$new_display_id] = $displays[$display_id];
    $displays[$new_display_id]['id'] = $new_display_id;
    $view->set('display', $displays);

    // By setting the current display the changed marker will appear on the new
    // display.
839
    $view->getExecutable()->current_display = $new_display_id;
840
    $view->cacheSet();
841 842

    // Redirect to the new display's edit page.
843
    $form_state->setRedirect('entity.view.edit_display_form', [
844 845
      'view' => $view->id(),
      'display_id' => $new_display_id,
846
    ]);
847 848 849 850 851
  }

  /**
   * Submit handler to add a display to a view.
   */
852
  public function submitDisplayAdd($form, FormStateInterface $form_state) {
853
    $view = $this->entity;
854
    // Create the new display.
855
    $parents = $form_state->getTriggeringElement()['#parents'];