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

/**
 * @file
5
 * Contains Drupal\views_ui\ViewEditForm.
6
7
8
9
 */

namespace Drupal\views_ui;

10
use Drupal\Component\Utility\Html;
11
use Drupal\Component\Utility\UrlHelper;
12
use Drupal\Component\Utility\Xss;
13
14
15
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\ReplaceCommand;
16
use Drupal\Core\Datetime\DateFormatter;
17
use Drupal\Component\Utility\NestedArray;
18
use Drupal\Component\Utility\SafeMarkup;
19
use Drupal\Core\Form\FormStateInterface;
20
use Drupal\Core\Render\Element;
21
use Drupal\Core\Render\ElementInfoManagerInterface;
22
use Drupal\Core\Url;
23
use Drupal\user\SharedTempStoreFactory;
24
use Drupal\views\Views;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpFoundation\RequestStack;
27
use Symfony\Component\DependencyInjection\ContainerInterface;
28
29
30
31

/**
 * Form controller for the Views edit form.
 */
32
class ViewEditForm extends ViewFormBase {
33
34
35
36

  /**
   * The views temp store.
   *
37
   * @var \Drupal\user\SharedTempStore
38
39
40
41
42
43
   */
  protected $tempStore;

  /**
   * The request object.
   *
44
   * @var \Symfony\Component\HttpFoundation\RequestStack
45
   */
46
  protected $requestStack;
47

48
49
50
  /**
   * The date formatter service.
   *
51
   * @var \Drupal\Core\Datetime\DateFormatter
52
53
54
   */
  protected $dateFormatter;

55
56
57
58
59
60
61
  /**
   * The element info manager.
   *
   * @var \Drupal\Core\Render\ElementInfoManagerInterface
   */
  protected $elementInfo;

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

  /**
   * {@inheritdoc}
   */
84
  public static function create(ContainerInterface $container) {
85
    return new static(
86
      $container->get('user.shared_tempstore'),
87
      $container->get('request_stack'),
88
89
      $container->get('date.formatter'),
      $container->get('element_info')
90
91
    );
  }
92
93

  /**
94
   * {@inheritdoc}
95
   */
96
  public function form(array $form, FormStateInterface $form_state) {
97
    $view = $this->entity;
98
    $display_id = $this->displayID;
99
    // Do not allow the form to be cached, because $form_state->get('view') can become
100
101
102
    // 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:
103
104
    //   - Change $form_state->get('view') to $form_state->getTemporary()['view'].
    //   - Add a #process function to initialize $form_state->getTemporary()['view']
105
    //     on cached form submissions.
106
    //   - Use \Drupal\Core\Form\FormStateInterface::loadInclude().
107
    $form_state->disableCache();
108
109

    if ($display_id) {
110
      if (!$view->getExecutable()->setDisplay($display_id)) {
111
        $form['#markup'] = $this->t('Invalid display id @display', array('@display' => $display_id));
112
113
114
115
116
117
        return $form;
      }
    }

    $form['#tree'] = TRUE;

118
119
120
121
    $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';
122
    $form['#attached']['library'][] = 'views_ui/views_ui.admin';
123
    $form['#attached']['library'][] = 'views_ui/admin.styling';
124

125
126
127
128
129
130
    $form['#attached']['drupalSettings']['views']['ajax'] = [
      'id' => '#views-ajax-body',
      'title' => '#views-ajax-title',
      'popup' => '#views-ajax-popup',
      'defaultForm' => $view->getDefaultAJAXMessage(),
    ];
131
132
133
134
135

    $form += array(
      '#prefix' => '',
      '#suffix' => '',
    );
136
137
138

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

    $form['#attributes']['class'] = array('form-edit');

143
    if ($view->isLocked()) {
144
145
146
147
148
149
      $username = array(
        '#theme' => 'username',
        '#account' => user_load($view->lock->owner),
      );
      $lock_message_substitutions = array(
        '!user' => drupal_render($username),
150
        '!age' => $this->dateFormatter->formatInterval(REQUEST_TIME - $view->lock->updated),
151
        '@url' => $view->url('break-lock-form'),
152
      );
153
154
      $form['locked'] = array(
        '#type' => 'container',
155
        '#attributes' => array('class' => array('view-locked', 'messages', 'messages--warning')),
156
        '#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),
157
158
159
160
161
162
        '#weight' => -10,
      );
    }
    else {
      $form['changed'] = array(
        '#type' => 'container',
163
        '#attributes' => array('class' => array('view-changed', 'messages', 'messages--warning')),
164
        '#children' => $this->t('You have unsaved changes.'),
165
166
167
168
169
170
171
172
        '#weight' => -10,
      );
      if (empty($view->changed)) {
        $form['changed']['#attributes']['class'][] = 'js-hide';
      }
    }

    $form['displays'] = array(
173
      '#prefix' => '<h1 class="unit-title clearfix">' . $this->t('Displays') . '</h1>',
174
175
176
177
178
179
180
181
182
183
184
185
186
      '#type' => 'container',
      '#attributes' => array(
        'class' => array(
          'views-displays',
        ),
      ),
    );


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

    // The rest requires a display to be selected.
    if ($display_id) {
187
      $form_state->set('display_id', $display_id);
188
189
190
191
192
193
194
195

      // The part of the page where editing will take place.
      $form['displays']['settings'] = array(
        '#type' => 'container',
        '#id' => 'edit-display-settings',
      );

      // Add a text that the display is disabled.
196
197
      if ($view->getExecutable()->displayHandlers->has($display_id)) {
        if (!$view->getExecutable()->displayHandlers->get($display_id)->isEnabled()) {
198
          $form['displays']['settings']['disabled']['#markup'] = $this->t('This display is disabled.');
199
200
201
202
203
204
205
206
207
208
209
210
211
212
        }
      }

      // Add the edit display content
      $tab_content = $this->getDisplayTab($view);
      $tab_content['#theme_wrappers'] = array('container');
      $tab_content['#attributes'] = array('class' => array('views-display-tab'));
      $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.
213

214
      if ($view->getExecutable()->displayHandlers->has($display_id) && !$view->getExecutable()->displayHandlers->get($display_id)->isEnabled()) {
215
216
217
218
219
220
221
222
223
224
225
226
227
        $tab_content['#attributes']['class'][] = 'views-display-disabled';
      }
      $form['displays']['settings']['settings_content'] = array(
        '#type' => 'container',
        'tab_content' => $tab_content,
      );

      // The content of the popup dialog.
      $form['ajax-area'] = array(
        '#type' => 'container',
        '#id' => 'views-ajax-popup',
      );
      $form['ajax-area']['ajax-title'] = array(
228
        '#markup' => '<div id="views-ajax-title"></div>',
229
230
231
232
233
234
235
236
237
238
239
      );
      $form['ajax-area']['ajax-body'] = array(
        '#type' => 'container',
        '#id' => 'views-ajax-body',
      );
    }

    return $form;
  }

  /**
240
   * {@inheritdoc}
241
   */
242
  protected function actions(array $form, FormStateInterface $form_state) {
243
244
245
246
    $actions = parent::actions($form, $form_state);
    unset($actions['delete']);

    $actions['cancel'] = array(
247
      '#type' => 'submit',
248
      '#value' => $this->t('Cancel'),
249
      '#submit' => array('::cancel'),
250
    );
251
252
253
254
    if ($this->entity->isLocked()) {
      $actions['submit']['#access'] = FALSE;
      $actions['cancel']['#access'] = FALSE;
    }
255
256
257
258
    return $actions;
  }

  /**
259
   * {@inheritdoc}
260
   */
261
  public function validate(array $form, FormStateInterface $form_state) {
262
263
    parent::validate($form, $form_state);

264
    $view = $this->entity;
265
    if ($view->isLocked()) {
266
      $form_state->setErrorByName('', $this->t('Changes cannot be made to a locked view.'));
267
    }
268
    foreach ($view->getExecutable()->validate() as $display_errors) {
269
      foreach ($display_errors as $error) {
270
        $form_state->setErrorByName('', $error);
271
272
273
274
275
      }
    }
  }

  /**
276
   * {@inheritdoc}
277
   */
278
  public function save(array $form, FormStateInterface $form_state) {
279
    $view = $this->entity;
280
    $executable = $view->getExecutable();
281
    $executable->initDisplay();
282

283
284
285
286
    // Go through and remove displayed scheduled for removal.
    $displays = $view->get('display');
    foreach ($displays as $id => $display) {
      if (!empty($display['deleted'])) {
287
        $executable->displayHandlers->remove($id);
288
289
290
        unset($displays[$id]);
      }
    }
291

292
    // Rename display ids if needed.
293
    foreach ($executable->displayHandlers as $id => $display) {
294
295
      if (!empty($display->display['new_id'])) {
        $new_id = $display->display['new_id'];
296
297
298
        $display->display['id'] = $new_id;
        unset($display->display['new_id']);
        $executable->displayHandlers->set($new_id, $display);
299
300
301

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

303
        // Redirect the user to the renamed display to be sure that the page itself exists and doesn't throw errors.
304
        $form_state->setRedirect('entity.view.edit_display_form', array(
305
306
307
          'view' => $view->id(),
          'display_id' => $new_id,
        ));
308
309
310
311
312
      }
    }
    $view->set('display', $displays);

    // @todo: Revisit this when http://drupal.org/node/1668866 is in.
313
    $query = $this->requestStack->getCurrentRequest()->query;
314
    $destination = $query->get('destination');
315

316
317
    if (!empty($destination)) {
      // Find out the first display which has a changed path and redirect to this url.
318
      $old_view = Views::getView($view->id());
319
320
321
322
323
324
325
326
      $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;
        }

327
328
        if (($display->getPluginId() == 'page') && ($old_path == $destination) && ($old_path != $view->getExecutable()->displayHandlers->get($id)->getOption('path'))) {
          $destination = $view->getExecutable()->displayHandlers->get($id)->getOption('path');
329
330
331
          $query->remove('destination');
        }
      }
332
333
      // @todo Use Url::fromPath() once https://www.drupal.org/node/2351379 is
      //   resolved.
334
      $form_state->setRedirectUrl(Url::fromUri("base:$destination"));
335
336
337
    }

    $view->save();
338

339
    drupal_set_message($this->t('The view %name has been saved.', array('%name' => $view->label())));
340
341

    // Remove this view from cache so we can edit it properly.
342
    $this->tempStore->delete($view->id());
343
344
345
346
347
348
349
  }

  /**
   * Form submission handler for the 'cancel' action.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
350
351
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
352
   */
353
  public function cancel(array $form, FormStateInterface $form_state) {
354
    // Remove this view from cache so edits will be lost.
355
    $view = $this->entity;
356
    $this->tempStore->delete($view->id());
357
    $form_state->setRedirectUrl($this->entity->urlInfo('collection'));
358
359
360
361
362
363
364
  }

  /**
   * Returns a renderable array representing the edit page for one display.
   */
  public function getDisplayTab($view) {
    $build = array();
365
    $display_id = $this->displayID;
366
    $display = $view->getExecutable()->displayHandlers->get($display_id);
367
368
369
370
    // 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.
371
      $build['#markup'] = $this->t("Error: Display @display refers to a plugin named '@plugin', but that plugin is not available.", array('@display' => $display->display['id'], '@plugin' => $display->display['display_plugin']));
372
373
374
375
376
377
378
    }
    // 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.
379
    \Drupal::moduleHandler()->alter('views_ui_display_tab', $build, $view, $display_id);
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
    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);
    $build = array(
      '#theme_wrappers' => array('container'),
      '#attributes' => array('id' => 'edit-display-settings-details'),
    );

    $is_display_deleted = !empty($display['deleted']);
399
    // The master display cannot be duplicated.
400
401
    $is_default = $display['id'] == 'default';
    // @todo: Figure out why getOption doesn't work here.
402
    $is_enabled = $view->getExecutable()->displayHandlers->get($display['id'])->isEnabled();
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421

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

      // The Delete, Duplicate and Undo Delete buttons.
      $build['top']['actions'] = array(
        '#theme_wrappers' => array('dropbutton_wrapper'),
      );

      // 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) {
          $build['top']['actions']['enable'] = array(
            '#type' => 'submit',
422
            '#value' => $this->t('Enable !display_title', array('!display_title' => $display_title)),
423
            '#limit_validation_errors' => array(),
424
            '#submit' => array('::submitDisplayEnable', '::submitDelayDestination'),
425
426
427
428
            '#prefix' => '<li class="enable">',
            "#suffix" => '</li>',
          );
        }
429
430
        // Add a link to view the page unless the view is disabled or has no
        // path.
431
432
        elseif ($view->status() && $view->getExecutable()->displayHandlers->get($display['id'])->hasPath()) {
          $path = $view->getExecutable()->displayHandlers->get($display['id'])->getPath();
433
          if ($path && (strpos($path, '%') === FALSE)) {
434
435
            $build['top']['actions']['path'] = array(
              '#type' => 'link',
436
              '#title' => $this->t('View !display_title', array('!display_title' => $display_title)),
437
              '#options' => array('alt' => array($this->t("Go to the real page for this display"))),
438
439
              // @todo Use Url::fromPath() once
              //   https://www.drupal.org/node/2351379 is resolved.
440
              '#url' => Url::fromUri("base:$path"),
441
442
443
444
445
446
447
448
              '#prefix' => '<li class="view">',
              "#suffix" => '</li>',
            );
          }
        }
        if (!$is_default) {
          $build['top']['actions']['duplicate'] = array(
            '#type' => 'submit',
449
            '#value' => $this->t('Duplicate !display_title', array('!display_title' => $display_title)),
450
            '#limit_validation_errors' => array(),
451
            '#submit' => array('::submitDisplayDuplicate', '::submitDelayDestination'),
452
453
454
455
456
457
458
            '#prefix' => '<li class="duplicate">',
            "#suffix" => '</li>',
          );
        }
        // Always allow a display to be deleted.
        $build['top']['actions']['delete'] = array(
          '#type' => 'submit',
459
          '#value' => $this->t('Delete !display_title', array('!display_title' => $display_title)),
460
          '#limit_validation_errors' => array(),
461
          '#submit' => array('::submitDisplayDelete', '::submitDelayDestination'),
462
463
464
          '#prefix' => '<li class="delete">',
          "#suffix" => '</li>',
        );
465

466
        foreach (Views::fetchPluginNames('display', NULL, array($view->get('storage')->get('base_table'))) as $type => $label) {
467
468
469
470
          if ($type == $display['display_plugin']) {
            continue;
          }

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

      // The area above the three columns.
      $build['top']['display_title'] = array(
        '#theme' => 'views_ui_display_tab_setting',
506
        '#description' => $this->t('Display name'),
507
        '#link' => $view->getExecutable()->displayHandlers->get($display['id'])->optionLink(SafeMarkup::checkPlain($display_title), 'display_title'),
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
      );
    }

    $build['columns'] = array();
    $build['columns']['#theme_wrappers'] = array('container');
    $build['columns']['#attributes'] = array('id' => 'edit-display-settings-main', 'class' => array('clearfix', 'views-display-columns'));

    $build['columns']['first']['#theme_wrappers'] = array('container');
    $build['columns']['first']['#attributes'] = array('class' => array('views-display-column', 'first'));

    $build['columns']['second']['#theme_wrappers'] = array('container');
    $build['columns']['second']['#attributes'] = array('class' => array('views-display-column', 'second'));

    $build['columns']['second']['settings'] = array();
    $build['columns']['second']['header'] = array();
    $build['columns']['second']['footer'] = array();
524
    $build['columns']['second']['empty'] = array();
525
526
    $build['columns']['second']['pager'] = array();

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

    // 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).
    $buckets = array();

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

    // 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'] : '';
571
        $build['columns'][$column][$id]['#name'] = $id;
572
573
574
575
576
577
578
579
      }
    }

    $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);
580
    $build['columns']['second']['empty'] = $this->getFormBucket($view, 'empty', $display);
581
582
583
584
585
586
587
588
589
    $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.
   */
590
  public function submitDisplayUndoDelete($form, FormStateInterface $form_state) {
591
    $view = $this->entity;
592
    // Create the new display
593
    $id = $form_state->get('display_id');
594
595
596
597
598
    $displays = $view->get('display');
    $displays[$id]['deleted'] = FALSE;
    $view->set('display', $displays);

    // Store in cache
599
    $view->cacheSet();
600
601

    // Redirect to the top-level edit page.
602
    $form_state->setRedirect('entity.view.edit_display_form', array(
603
604
605
      'view' => $view->id(),
      'display_id' => $id,
    ));
606
607
608
609
610
  }

  /**
   * Submit handler to enable a disabled display.
   */
611
  public function submitDisplayEnable($form, FormStateInterface $form_state) {
612
    $view = $this->entity;
613
    $id = $form_state->get('display_id');
614
    // setOption doesn't work because this would might affect upper displays
615
    $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', TRUE);
616
617

    // Store in cache
618
    $view->cacheSet();
619
620

    // Redirect to the top-level edit page.
621
    $form_state->setRedirect('entity.view.edit_display_form', array(
622
623
624
      'view' => $view->id(),
      'display_id' => $id,
    ));
625
626
627
628
629
  }

  /**
   * Submit handler to disable display.
   */
630
  public function submitDisplayDisable($form, FormStateInterface $form_state) {
631
    $view = $this->entity;
632
    $id = $form_state->get('display_id');
633
    $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', FALSE);
634
635

    // Store in cache
636
    $view->cacheSet();
637
638

    // Redirect to the top-level edit page.
639
    $form_state->setRedirect('entity.view.edit_display_form', array(
640
641
642
      'view' => $view->id(),
      'display_id' => $id,
    ));
643
644
645
646
647
  }

  /**
   * Submit handler to delete a display from a view.
   */
648
  public function submitDisplayDelete($form, FormStateInterface $form_state) {
649
    $view = $this->entity;
650
    $display_id = $form_state->get('display_id');
651
652
653
654
655

    // Mark the display for deletion.
    $displays = $view->get('display');
    $displays[$display_id]['deleted'] = TRUE;
    $view->set('display', $displays);
656
    $view->cacheSet();
657
658
659

    // Redirect to the top-level edit page. The first remaining display will
    // become the active display.
660
    $form_state->setRedirectUrl($view->urlInfo('edit-form'));
661
662
663
664
  }

  /**
   * Regenerate the current tab for AJAX updates.
665
666
667
668
669
670
671
   *
   * @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.
672
   */
673
  public function rebuildCurrentTab(ViewUI $view, AjaxResponse $response, $display_id) {
674
    $this->displayID = $display_id;
675
    if (!$view->getExecutable()->setDisplay('default')) {
676
677
678
679
      return;
    }

    // Regenerate the main display area.
680
    $build = $this->getDisplayTab($view);
681
    static::addMicroweights($build);
682
    $response->addCommand(new HtmlCommand('#views-tab-' . $display_id, drupal_render($build)));
683
684
685
686

    // Regenerate the top area so changes to display names and order will appear.
    $build = $this->renderDisplayTop($view);
    static::addMicroweights($build);
687
    $response->addCommand(new ReplaceCommand('#views-display-top', drupal_render($build)));
688
689
690
691
692
693
  }

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

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

728
729
730
    if ($view->access('delete')) {
      $element['extra_actions']['#links']['delete'] = array(
        'title' => $this->t('Delete view'),
731
732
        'url' => $view->urlInfo('delete-form'),
      );
733
734
    }

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

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

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

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

    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
786
   * the SharedTempStore rather than $form_state->setRebuild(). Without this
787
788
789
790
791
   * 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.
   */
792
  public function submitDelayDestination($form, FormStateInterface $form_state) {
793
794
795
796
797
798
799
800
801
802
803
    $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);
804
      }
805
806
807

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

      $form_state->setRedirectUrl($redirect);
      $request->query->remove('destination');
815
816
817
818
819
820
    }
  }

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

    // Create the new display.
    $displays = $view->get('display');
827
828
    $display = $view->getExecutable()->newDisplay($displays[$display_id]['display_plugin']);
    $new_display_id = $display->display['id'];
829
830
831
832
833
834
    $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.
835
    $view->getExecutable()->current_display = $new_display_id;
836
    $view->cacheSet();
837
838

    // Redirect to the new display's edit page.
839
    $form_state->setRedirect('entity.view.edit_display_form', array(
840
841
842
      'view' => $view->id(),
      'display_id' => $new_display_id,
    ));
843
844
845
846
847
  }

  /**
   * Submit handler to add a display to a view.
   */
848
  public function submitDisplayAdd($form, FormStateInterface $form_state) {
849
    $view = $this->entity;
850
    // Create the new display.
851
    $parents = $form_state->getTriggeringElement()['#parents'];
852
    $display_type = array_pop($parents);
853
854
    $display = $view->getExecutable()->newDisplay($display_type);
    $display_id = $display->display['id'];
855
856
    // A new display got added so the asterisks symbol should appear on the new
    // display.
857
    $view->getExecutable()->current_display = $display_id;
858
    $view->cacheSet();
859
860

    // Redirect to the new display's edit page.
861
    $form_state->setRedirect('entity.view.edit_display_form', array(
862
863
864
      'view' => $view->id(),
      'display_id' => $display_id,
    ));
865
866
  }

867
  /**
868
   * Submit handler to Duplicate a display as another display type.
869
   */
870
  public function submitDuplicateDisplayAsType($form, FormStateInterface $form_state) {
871
    /** @var \Drupal\views\ViewEntityInterface $view */
872
    $view = $this->entity;
873
    $display_id = $this->displayID;
874
875

    // Create the new display.
876
    $parents = $form_state->getTriggeringElement()['#parents'];
877
878
    $display_type = array_pop($parents);

879
    $new_display_id = $view->duplicateDisplayAsType($display_id, $display_type);
880
881
882

    // By setting the current display the changed marker will appear on the new
    // display.
883
    $view->getExecutable()->current_display = $new_display_id;
884
    $view->cacheSet();
885
886

    // Redirect to the new display's edit page.
887
    $form_state->setRedirect('entity.view.edit_display_form', array(
888
889
890
      'view' => $view->id(),
      'display_id' => $new_display_id,
    ));
891
892
  }

893
894
895
896
897
898
899
900
901
902
903
904
  /**
   * Build a renderable array representing one option on the edit form.
   *
   * This function might be more logical as a method on an object, if a suitable
   * object emerges out of refactoring.
   */
  public function buildOptionForm(ViewUI $view, $id, $option, $display) {
    $option_build = array();
    $option_build['#theme'] = 'views_ui_display_tab_setting';

    $option_build['#description'] = $option['title'];

905
    $option_build['#link'] = $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($option['value'], $id, '', empty($option['desc']) ? '' : $option['desc']);
906
907
908
909

    $option_build['#links'] = array();
    if (!empty($option['links']) && is_array($option['links'])) {
      foreach ($option['links'] as $link_id => $link_value) {
910
        $option_build['#settings_links'][] = $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($option['setting'], $link_id, 'views-button-configure', $link_value);
911
912
913
      }
    }

914
    if (!empty($view->getExecutable()->displayHandlers->get($display['id'])->options['defaults'][$id])) {
915
916
917
918
919
      $display_id = 'default';
      $option_build['#defaulted'] = TRUE;
    }
    else {
      $display_id = $display['id'];
920
921
      if (!$view->getExecutable()->displayHandlers->get($display['id'])->isDefaultDisplay()) {
        if ($view->getExecutable()->displayHandlers->get($display['id'])->defaultableSections($id)) {
922
923
924
925
          $option_build['#overridden'] = TRUE;
        }
      }
    }
926
    $option_build['#attributes']['class'][] = Html::cleanCssIdentifier($display_id . '-' . $id);
927
928
929
930
931
932
933
    return $option_build;
  }

  /**
   * Add information about a section to a display.
   */
  public function getFormBucket(ViewUI $view, $type, $display) {
934
    $executable = $view->getExecutable();
935
936
937
    $executable->setDisplay($display['id']);
    $executable->initStyle();

938
    $types = $executable->getHandlerTypes();
939

940
941
942
943
944
945
946
    $build = array(
      '#theme_wrappers' => array('views_ui_display_tab_bucket'),
    );

    $build['#overridden'] = FALSE;
    $build['#defaulted'] = FALSE;

947
948
    $build['#name'] = $type;
    $build['#title'] = $types[$type]['title'];
949

950
    $rearrange_url = Url::fromRoute('views_ui.form_rearrange', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]);
951
952
    $class = 'icon compact rearrange';

953
954
955
956
    // Different types now have different rearrange forms, so we use this switch
    // to get the right one.
    switch ($type) {
      case 'filter':
957
958
        // The rearrange form for filters contains the and/or UI, so override
        // the used path.
959
        $rearrange_url = Url::fromRoute('views_ui.form_rearrange_filter', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id']]);
960
961
962
963
964
        // TODO: Add another class to have another symbol for filter rearrange.
        $class = 'icon compact rearrange';
        break;
      case 'field':
        // Fetch the style plugin info so we know whether to list fields or not.
965
        $style_plugin = $executable->style_plugin;
966
967
968
        $uses_fields = $style_plugin && $style_plugin->usesFields();
        if (!$uses_fields) {
          $build['fields'][] = array(
969
            '#markup' => $this->t('The selected style or row format does not use fields.'),
970
971
972
973
974
            '#theme_wrappers' => array('views_ui_container'),
            '#attributes' => array('class' => array('views-display-setting')),
          );
          return $build;
        }
975
        break;
976
977
978
      case 'header':
      case 'footer':
      case 'empty':
979
        if (!$executable->display_handler->usesAreas()) {
980
          $build[$type][] = array(
981
            '#markup' => $this->t('The selected display type does not use @type plugins', array('@type' => $type)),
982
983
984
985
986
            '#theme_wrappers' => array('views_ui_container'),
            '#attributes' => array('class' => array('views-display-setting')),
          );
          return $build;
        }