dd6b6d2f1c73814f112912c4939f50b322a61e18
[yaffs-website] / web / core / modules / views_ui / src / ViewEditForm.php
1 <?php
2
3 namespace Drupal\views_ui;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\SafeMarkup;
7 use Drupal\Core\Ajax\AjaxResponse;
8 use Drupal\Core\Ajax\HtmlCommand;
9 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
19 /**
20  * Form controller for the Views edit form.
21  */
22 class ViewEditForm extends ViewFormBase {
23
24   /**
25    * The views temp store.
26    *
27    * @var \Drupal\user\SharedTempStore
28    */
29   protected $tempStore;
30
31   /**
32    * The request object.
33    *
34    * @var \Symfony\Component\HttpFoundation\RequestStack
35    */
36   protected $requestStack;
37
38   /**
39    * The date formatter service.
40    *
41    * @var \Drupal\Core\Datetime\DateFormatterInterface
42    */
43   protected $dateFormatter;
44
45   /**
46    * The element info manager.
47    *
48    * @var \Drupal\Core\Render\ElementInfoManagerInterface
49    */
50   protected $elementInfo;
51
52   /**
53    * Constructs a new ViewEditForm object.
54    *
55    * @param \Drupal\user\SharedTempStoreFactory $temp_store_factory
56    *   The factory for the temp store object.
57    * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
58    *   The request stack object.
59    * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
60    *   The date Formatter service.
61    * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
62    *   The element info manager.
63    */
64   public function __construct(SharedTempStoreFactory $temp_store_factory, RequestStack $requestStack, DateFormatterInterface $date_formatter, ElementInfoManagerInterface $element_info) {
65     $this->tempStore = $temp_store_factory->get('views');
66     $this->requestStack = $requestStack;
67     $this->dateFormatter = $date_formatter;
68     $this->elementInfo = $element_info;
69   }
70
71   /**
72    * {@inheritdoc}
73    */
74   public static function create(ContainerInterface $container) {
75     return new static(
76       $container->get('user.shared_tempstore'),
77       $container->get('request_stack'),
78       $container->get('date.formatter'),
79       $container->get('element_info')
80     );
81   }
82
83   /**
84    * {@inheritdoc}
85    */
86   public function form(array $form, FormStateInterface $form_state) {
87     $view = $this->entity;
88     $display_id = $this->displayID;
89     // Do not allow the form to be cached, because $form_state->get('view') can become
90     // stale between page requests.
91     // See views_ui_ajax_get_form() for how this affects #ajax.
92     // @todo To remove this and allow the form to be cacheable:
93     //   - Change $form_state->get('view') to $form_state->getTemporary()['view'].
94     //   - Add a #process function to initialize $form_state->getTemporary()['view']
95     //     on cached form submissions.
96     //   - Use \Drupal\Core\Form\FormStateInterface::loadInclude().
97     $form_state->disableCache();
98
99     if ($display_id) {
100       if (!$view->getExecutable()->setDisplay($display_id)) {
101         $form['#markup'] = $this->t('Invalid display id @display', ['@display' => $display_id]);
102         return $form;
103       }
104     }
105
106     $form['#tree'] = TRUE;
107
108     $form['#attached']['library'][] = 'core/jquery.ui.tabs';
109     $form['#attached']['library'][] = 'core/jquery.ui.dialog';
110     $form['#attached']['library'][] = 'core/drupal.states';
111     $form['#attached']['library'][] = 'core/drupal.tabledrag';
112     $form['#attached']['library'][] = 'views_ui/views_ui.admin';
113     $form['#attached']['library'][] = 'views_ui/admin.styling';
114
115     $form += [
116       '#prefix' => '',
117       '#suffix' => '',
118     ];
119
120     $view_status = $view->status() ? 'enabled' : 'disabled';
121     $form['#prefix'] .= '<div class="views-edit-view views-admin ' . $view_status . ' clearfix">';
122     $form['#suffix'] = '</div>' . $form['#suffix'];
123
124     $form['#attributes']['class'] = ['form-edit'];
125
126     if ($view->isLocked()) {
127       $username = [
128         '#theme' => 'username',
129         '#account' => $this->entityManager->getStorage('user')->load($view->lock->owner),
130       ];
131       $lock_message_substitutions = [
132         '@user' => \Drupal::service('renderer')->render($username),
133         '@age' => $this->dateFormatter->formatTimeDiffSince($view->lock->updated),
134         ':url' => $view->url('break-lock-form'),
135       ];
136       $form['locked'] = [
137         '#type' => 'container',
138         '#attributes' => ['class' => ['view-locked', 'messages', 'messages--warning']],
139         '#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),
140         '#weight' => -10,
141       ];
142     }
143     else {
144       $form['changed'] = [
145         '#type' => 'container',
146         '#attributes' => ['class' => ['view-changed', 'messages', 'messages--warning']],
147         '#children' => $this->t('You have unsaved changes.'),
148         '#weight' => -10,
149       ];
150       if (empty($view->changed)) {
151         $form['changed']['#attributes']['class'][] = 'js-hide';
152       }
153     }
154
155     $form['displays'] = [
156       '#prefix' => '<h1 class="unit-title clearfix">' . $this->t('Displays') . '</h1>',
157       '#type' => 'container',
158       '#attributes' => [
159         'class' => [
160           'views-displays',
161         ],
162       ],
163     ];
164
165     $form['displays']['top'] = $this->renderDisplayTop($view);
166
167     // The rest requires a display to be selected.
168     if ($display_id) {
169       $form_state->set('display_id', $display_id);
170
171       // The part of the page where editing will take place.
172       $form['displays']['settings'] = [
173         '#type' => 'container',
174         '#id' => 'edit-display-settings',
175         '#attributes' => [
176           'class' => ['edit-display-settings'],
177         ],
178       ];
179
180       // Add a text that the display is disabled.
181       if ($view->getExecutable()->displayHandlers->has($display_id)) {
182         if (!$view->getExecutable()->displayHandlers->get($display_id)->isEnabled()) {
183           $form['displays']['settings']['disabled']['#markup'] = $this->t('This display is disabled.');
184         }
185       }
186
187       // Add the edit display content
188       $tab_content = $this->getDisplayTab($view);
189       $tab_content['#theme_wrappers'] = ['container'];
190       $tab_content['#attributes'] = ['class' => ['views-display-tab']];
191       $tab_content['#id'] = 'views-tab-' . $display_id;
192       // Mark deleted displays as such.
193       $display = $view->get('display');
194       if (!empty($display[$display_id]['deleted'])) {
195         $tab_content['#attributes']['class'][] = 'views-display-deleted';
196       }
197       // Mark disabled displays as such.
198
199       if ($view->getExecutable()->displayHandlers->has($display_id) && !$view->getExecutable()->displayHandlers->get($display_id)->isEnabled()) {
200         $tab_content['#attributes']['class'][] = 'views-display-disabled';
201       }
202       $form['displays']['settings']['settings_content'] = [
203         '#type' => 'container',
204         'tab_content' => $tab_content,
205       ];
206     }
207
208     return $form;
209   }
210
211   /**
212    * {@inheritdoc}
213    */
214   protected function actions(array $form, FormStateInterface $form_state) {
215     $actions = parent::actions($form, $form_state);
216     unset($actions['delete']);
217
218     $actions['cancel'] = [
219       '#type' => 'submit',
220       '#value' => $this->t('Cancel'),
221       '#submit' => ['::cancel'],
222       '#limit_validation_errors' => [],
223     ];
224     if ($this->entity->isLocked()) {
225       $actions['submit']['#access'] = FALSE;
226       $actions['cancel']['#access'] = FALSE;
227     }
228     return $actions;
229   }
230
231   /**
232    * {@inheritdoc}
233    */
234   public function validateForm(array &$form, FormStateInterface $form_state) {
235     parent::validateForm($form, $form_state);
236
237     $view = $this->entity;
238     if ($view->isLocked()) {
239       $form_state->setErrorByName('', $this->t('Changes cannot be made to a locked view.'));
240     }
241     foreach ($view->getExecutable()->validate() as $display_errors) {
242       foreach ($display_errors as $error) {
243         $form_state->setErrorByName('', $error);
244       }
245     }
246   }
247
248   /**
249    * {@inheritdoc}
250    */
251   public function save(array $form, FormStateInterface $form_state) {
252     $view = $this->entity;
253     $executable = $view->getExecutable();
254     $executable->initDisplay();
255
256     // Go through and remove displayed scheduled for removal.
257     $displays = $view->get('display');
258     foreach ($displays as $id => $display) {
259       if (!empty($display['deleted'])) {
260         // Remove view display from view attachment under the attachments
261         // options.
262         $display_handler = $executable->displayHandlers->get($id);
263         if ($attachments = $display_handler->getAttachedDisplays()) {
264           foreach ($attachments as $attachment) {
265             $attached_options = $executable->displayHandlers->get($attachment)->getOption('displays');
266             unset($attached_options[$id]);
267             $executable->displayHandlers->get($attachment)->setOption('displays', $attached_options);
268           }
269         }
270         $executable->displayHandlers->remove($id);
271         unset($displays[$id]);
272       }
273     }
274
275     // Rename display ids if needed.
276     foreach ($executable->displayHandlers as $id => $display) {
277       if (!empty($display->display['new_id']) && $display->display['new_id'] !== $display->display['id'] && empty($display->display['deleted'])) {
278         $new_id = $display->display['new_id'];
279         $display->display['id'] = $new_id;
280         unset($display->display['new_id']);
281         $executable->displayHandlers->set($new_id, $display);
282
283         $displays[$new_id] = $displays[$id];
284         unset($displays[$id]);
285
286         // Redirect the user to the renamed display to be sure that the page itself exists and doesn't throw errors.
287         $form_state->setRedirect('entity.view.edit_display_form', [
288           'view' => $view->id(),
289           'display_id' => $new_id,
290         ]);
291       }
292       elseif (isset($display->display['new_id'])) {
293         unset($display->display['new_id']);
294       }
295     }
296     $view->set('display', $displays);
297
298     // @todo: Revisit this when https://www.drupal.org/node/1668866 is in.
299     $query = $this->requestStack->getCurrentRequest()->query;
300     $destination = $query->get('destination');
301
302     if (!empty($destination)) {
303       // Find out the first display which has a changed path and redirect to this url.
304       $old_view = Views::getView($view->id());
305       $old_view->initDisplay();
306       foreach ($old_view->displayHandlers as $id => $display) {
307         // Only check for displays with a path.
308         $old_path = $display->getOption('path');
309         if (empty($old_path)) {
310           continue;
311         }
312
313         if (($display->getPluginId() == 'page') && ($old_path == $destination) && ($old_path != $view->getExecutable()->displayHandlers->get($id)->getOption('path'))) {
314           $destination = $view->getExecutable()->displayHandlers->get($id)->getOption('path');
315           $query->remove('destination');
316         }
317       }
318       // @todo Use Url::fromPath() once https://www.drupal.org/node/2351379 is
319       //   resolved.
320       $form_state->setRedirectUrl(Url::fromUri("base:$destination"));
321     }
322
323     $view->save();
324
325     drupal_set_message($this->t('The view %name has been saved.', ['%name' => $view->label()]));
326
327     // Remove this view from cache so we can edit it properly.
328     $this->tempStore->delete($view->id());
329   }
330
331   /**
332    * Form submission handler for the 'cancel' action.
333    *
334    * @param array $form
335    *   An associative array containing the structure of the form.
336    * @param \Drupal\Core\Form\FormStateInterface $form_state
337    *   The current state of the form.
338    */
339   public function cancel(array $form, FormStateInterface $form_state) {
340     // Remove this view from cache so edits will be lost.
341     $view = $this->entity;
342     $this->tempStore->delete($view->id());
343     $form_state->setRedirectUrl($this->entity->urlInfo('collection'));
344   }
345
346   /**
347    * Returns a renderable array representing the edit page for one display.
348    */
349   public function getDisplayTab($view) {
350     $build = [];
351     $display_id = $this->displayID;
352     $display = $view->getExecutable()->displayHandlers->get($display_id);
353     // If the plugin doesn't exist, display an error message instead of an edit
354     // page.
355     if (empty($display)) {
356       // @TODO: Improved UX for the case where a plugin is missing.
357       $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']]);
358     }
359     // Build the content of the edit page.
360     else {
361       $build['details'] = $this->getDisplayDetails($view, $display->display);
362     }
363     // In AJAX context, ViewUI::rebuildCurrentTab() returns this outside of form
364     // context, so hook_form_views_ui_edit_form_alter() is insufficient.
365     \Drupal::moduleHandler()->alter('views_ui_display_tab', $build, $view, $display_id);
366     return $build;
367   }
368
369   /**
370    * Helper function to get the display details section of the edit UI.
371    *
372    * @param $display
373    *
374    * @return array
375    *   A renderable page build array.
376    */
377   public function getDisplayDetails($view, $display) {
378     $display_title = $this->getDisplayLabel($view, $display['id'], FALSE);
379     $build = [
380       '#theme_wrappers' => ['container'],
381       '#attributes' => ['id' => 'edit-display-settings-details'],
382     ];
383
384     $is_display_deleted = !empty($display['deleted']);
385     // The master display cannot be duplicated.
386     $is_default = $display['id'] == 'default';
387     // @todo: Figure out why getOption doesn't work here.
388     $is_enabled = $view->getExecutable()->displayHandlers->get($display['id'])->isEnabled();
389
390     if ($display['id'] != 'default') {
391       $build['top']['#theme_wrappers'] = ['container'];
392       $build['top']['#attributes']['id'] = 'edit-display-settings-top';
393       $build['top']['#attributes']['class'] = ['views-ui-display-tab-actions', 'edit-display-settings-top', 'views-ui-display-tab-bucket', 'clearfix'];
394
395       // The Delete, Duplicate and Undo Delete buttons.
396       $build['top']['actions'] = [
397         '#theme_wrappers' => ['dropbutton_wrapper'],
398       ];
399
400       // Because some of the 'links' are actually submit buttons, we have to
401       // manually wrap each item in <li> and the whole list in <ul>.
402       $build['top']['actions']['prefix']['#markup'] = '<ul class="dropbutton">';
403
404       if (!$is_display_deleted) {
405         if (!$is_enabled) {
406           $build['top']['actions']['enable'] = [
407             '#type' => 'submit',
408             '#value' => $this->t('Enable @display_title', ['@display_title' => $display_title]),
409             '#limit_validation_errors' => [],
410             '#submit' => ['::submitDisplayEnable', '::submitDelayDestination'],
411             '#prefix' => '<li class="enable">',
412             "#suffix" => '</li>',
413           ];
414         }
415         // Add a link to view the page unless the view is disabled or has no
416         // path.
417         elseif ($view->status() && $view->getExecutable()->displayHandlers->get($display['id'])->hasPath()) {
418           $path = $view->getExecutable()->displayHandlers->get($display['id'])->getPath();
419           if ($path && (strpos($path, '%') === FALSE)) {
420             if (!parse_url($path, PHP_URL_SCHEME)) {
421               // @todo Views should expect and store a leading /. See:
422               //   https://www.drupal.org/node/2423913
423               $url = Url::fromUserInput('/' . ltrim($path, '/'));
424             }
425             else {
426               $url = Url::fromUri("base:$path");
427             }
428             $build['top']['actions']['path'] = [
429               '#type' => 'link',
430               '#title' => $this->t('View @display_title', ['@display_title' => $display_title]),
431               '#options' => ['alt' => [$this->t("Go to the real page for this display")]],
432               '#url' => $url,
433               '#prefix' => '<li class="view">',
434               "#suffix" => '</li>',
435             ];
436           }
437         }
438         if (!$is_default) {
439           $build['top']['actions']['duplicate'] = [
440             '#type' => 'submit',
441             '#value' => $this->t('Duplicate @display_title', ['@display_title' => $display_title]),
442             '#limit_validation_errors' => [],
443             '#submit' => ['::submitDisplayDuplicate', '::submitDelayDestination'],
444             '#prefix' => '<li class="duplicate">',
445             "#suffix" => '</li>',
446           ];
447         }
448         // Always allow a display to be deleted.
449         $build['top']['actions']['delete'] = [
450           '#type' => 'submit',
451           '#value' => $this->t('Delete @display_title', ['@display_title' => $display_title]),
452           '#limit_validation_errors' => [],
453           '#submit' => ['::submitDisplayDelete', '::submitDelayDestination'],
454           '#prefix' => '<li class="delete">',
455           "#suffix" => '</li>',
456         ];
457
458         foreach (Views::fetchPluginNames('display', NULL, [$view->get('storage')->get('base_table')]) as $type => $label) {
459           if ($type == $display['display_plugin']) {
460             continue;
461           }
462
463           $build['top']['actions']['duplicate_as'][$type] = [
464             '#type' => 'submit',
465             '#value' => $this->t('Duplicate as @type', ['@type' => $label]),
466             '#limit_validation_errors' => [],
467             '#submit' => ['::submitDuplicateDisplayAsType', '::submitDelayDestination'],
468             '#prefix' => '<li class="duplicate">',
469             '#suffix' => '</li>',
470           ];
471         }
472       }
473       else {
474         $build['top']['actions']['undo_delete'] = [
475           '#type' => 'submit',
476           '#value' => $this->t('Undo delete of @display_title', ['@display_title' => $display_title]),
477           '#limit_validation_errors' => [],
478           '#submit' => ['::submitDisplayUndoDelete', '::submitDelayDestination'],
479           '#prefix' => '<li class="undo-delete">',
480           "#suffix" => '</li>',
481         ];
482       }
483       if ($is_enabled) {
484         $build['top']['actions']['disable'] = [
485           '#type' => 'submit',
486           '#value' => $this->t('Disable @display_title', ['@display_title' => $display_title]),
487           '#limit_validation_errors' => [],
488           '#submit' => ['::submitDisplayDisable', '::submitDelayDestination'],
489           '#prefix' => '<li class="disable">',
490           "#suffix" => '</li>',
491         ];
492       }
493       $build['top']['actions']['suffix']['#markup'] = '</ul>';
494
495       // The area above the three columns.
496       $build['top']['display_title'] = [
497         '#theme' => 'views_ui_display_tab_setting',
498         '#description' => $this->t('Display name'),
499         '#link' => $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($display_title, 'display_title'),
500       ];
501     }
502
503     $build['columns'] = [];
504     $build['columns']['#theme_wrappers'] = ['container'];
505     $build['columns']['#attributes'] = ['id' => 'edit-display-settings-main', 'class' => ['clearfix', 'views-display-columns']];
506
507     $build['columns']['first']['#theme_wrappers'] = ['container'];
508     $build['columns']['first']['#attributes'] = ['class' => ['views-display-column', 'first']];
509
510     $build['columns']['second']['#theme_wrappers'] = ['container'];
511     $build['columns']['second']['#attributes'] = ['class' => ['views-display-column', 'second']];
512
513     $build['columns']['second']['settings'] = [];
514     $build['columns']['second']['header'] = [];
515     $build['columns']['second']['footer'] = [];
516     $build['columns']['second']['empty'] = [];
517     $build['columns']['second']['pager'] = [];
518
519     // The third column buckets are wrapped in details.
520     $build['columns']['third'] = [
521       '#type' => 'details',
522       '#title' => $this->t('Advanced'),
523       '#theme_wrappers' => ['details'],
524       '#attributes' => [
525         'class' => [
526           'views-display-column',
527           'third',
528         ],
529       ],
530     ];
531     // Collapse the details by default.
532     $build['columns']['third']['#open'] = \Drupal::config('views.settings')->get('ui.show.advanced_column');
533
534     // Each option (e.g. title, access, display as grid/table/list) fits into one
535     // of several "buckets," or boxes (Format, Fields, Sort, and so on).
536     $buckets = [];
537
538     // Fetch options from the display plugin, with a list of buckets they go into.
539     $options = [];
540     $view->getExecutable()->displayHandlers->get($display['id'])->optionsSummary($buckets, $options);
541
542     // Place each option into its bucket.
543     foreach ($options as $id => $option) {
544       // Each option self-identifies as belonging in a particular bucket.
545       $buckets[$option['category']]['build'][$id] = $this->buildOptionForm($view, $id, $option, $display);
546     }
547
548     // Place each bucket into the proper column.
549     foreach ($buckets as $id => $bucket) {
550       // Let buckets identify themselves as belonging in a column.
551       if (isset($bucket['column']) && isset($build['columns'][$bucket['column']])) {
552         $column = $bucket['column'];
553       }
554       // If a bucket doesn't pick one of our predefined columns to belong to, put
555       // it in the last one.
556       else {
557         $column = 'third';
558       }
559       if (isset($bucket['build']) && is_array($bucket['build'])) {
560         $build['columns'][$column][$id] = $bucket['build'];
561         $build['columns'][$column][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket';
562         $build['columns'][$column][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : '';
563         $build['columns'][$column][$id]['#name'] = $id;
564       }
565     }
566
567     $build['columns']['first']['fields'] = $this->getFormBucket($view, 'field', $display);
568     $build['columns']['first']['filters'] = $this->getFormBucket($view, 'filter', $display);
569     $build['columns']['first']['sorts'] = $this->getFormBucket($view, 'sort', $display);
570     $build['columns']['second']['header'] = $this->getFormBucket($view, 'header', $display);
571     $build['columns']['second']['footer'] = $this->getFormBucket($view, 'footer', $display);
572     $build['columns']['second']['empty'] = $this->getFormBucket($view, 'empty', $display);
573     $build['columns']['third']['arguments'] = $this->getFormBucket($view, 'argument', $display);
574     $build['columns']['third']['relationships'] = $this->getFormBucket($view, 'relationship', $display);
575
576     return $build;
577   }
578
579   /**
580    * Submit handler to add a restore a removed display to a view.
581    */
582   public function submitDisplayUndoDelete($form, FormStateInterface $form_state) {
583     $view = $this->entity;
584     // Create the new display
585     $id = $form_state->get('display_id');
586     $displays = $view->get('display');
587     $displays[$id]['deleted'] = FALSE;
588     $view->set('display', $displays);
589
590     // Store in cache
591     $view->cacheSet();
592
593     // Redirect to the top-level edit page.
594     $form_state->setRedirect('entity.view.edit_display_form', [
595       'view' => $view->id(),
596       'display_id' => $id,
597     ]);
598   }
599
600   /**
601    * Submit handler to enable a disabled display.
602    */
603   public function submitDisplayEnable($form, FormStateInterface $form_state) {
604     $view = $this->entity;
605     $id = $form_state->get('display_id');
606     // setOption doesn't work because this would might affect upper displays
607     $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', TRUE);
608
609     // Store in cache
610     $view->cacheSet();
611
612     // Redirect to the top-level edit page.
613     $form_state->setRedirect('entity.view.edit_display_form', [
614       'view' => $view->id(),
615       'display_id' => $id,
616     ]);
617   }
618
619   /**
620    * Submit handler to disable display.
621    */
622   public function submitDisplayDisable($form, FormStateInterface $form_state) {
623     $view = $this->entity;
624     $id = $form_state->get('display_id');
625     $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', FALSE);
626
627     // Store in cache
628     $view->cacheSet();
629
630     // Redirect to the top-level edit page.
631     $form_state->setRedirect('entity.view.edit_display_form', [
632       'view' => $view->id(),
633       'display_id' => $id,
634     ]);
635   }
636
637   /**
638    * Submit handler to delete a display from a view.
639    */
640   public function submitDisplayDelete($form, FormStateInterface $form_state) {
641     $view = $this->entity;
642     $display_id = $form_state->get('display_id');
643
644     // Mark the display for deletion.
645     $displays = $view->get('display');
646     $displays[$display_id]['deleted'] = TRUE;
647     $view->set('display', $displays);
648     $view->cacheSet();
649
650     // Redirect to the top-level edit page. The first remaining display will
651     // become the active display.
652     $form_state->setRedirectUrl($view->urlInfo('edit-form'));
653   }
654
655   /**
656    * Regenerate the current tab for AJAX updates.
657    *
658    * @param \Drupal\views_ui\ViewUI $view
659    *   The view to regenerate its tab.
660    * @param \Drupal\Core\Ajax\AjaxResponse $response
661    *   The response object to add new commands to.
662    * @param string $display_id
663    *   The display ID of the tab to regenerate.
664    */
665   public function rebuildCurrentTab(ViewUI $view, AjaxResponse $response, $display_id) {
666     $this->displayID = $display_id;
667     if (!$view->getExecutable()->setDisplay('default')) {
668       return;
669     }
670
671     // Regenerate the main display area.
672     $build = $this->getDisplayTab($view);
673     $response->addCommand(new HtmlCommand('#views-tab-' . $display_id, $build));
674
675     // Regenerate the top area so changes to display names and order will appear.
676     $build = $this->renderDisplayTop($view);
677     $response->addCommand(new ReplaceCommand('#views-display-top', $build));
678   }
679
680   /**
681    * Render the top of the display so it can be updated during ajax operations.
682    */
683   public function renderDisplayTop(ViewUI $view) {
684     $display_id = $this->displayID;
685     $element['#theme_wrappers'][] = 'views_ui_container';
686     $element['#attributes']['class'] = ['views-display-top', 'clearfix'];
687     $element['#attributes']['id'] = ['views-display-top'];
688
689     // Extra actions for the display
690     $element['extra_actions'] = [
691       '#type' => 'dropbutton',
692       '#attributes' => [
693         'id' => 'views-display-extra-actions',
694       ],
695       '#links' => [
696         'edit-details' => [
697           'title' => $this->t('Edit view name/description'),
698           'url' => Url::fromRoute('views_ui.form_edit_details', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
699           'attributes' => ['class' => ['views-ajax-link']],
700         ],
701         'analyze' => [
702           'title' => $this->t('Analyze view'),
703           'url' => Url::fromRoute('views_ui.form_analyze', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
704           'attributes' => ['class' => ['views-ajax-link']],
705         ],
706         'duplicate' => [
707           'title' => $this->t('Duplicate view'),
708           'url' => $view->urlInfo('duplicate-form'),
709         ],
710         'reorder' => [
711           'title' => $this->t('Reorder displays'),
712           'url' => Url::fromRoute('views_ui.form_reorder_displays', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
713           'attributes' => ['class' => ['views-ajax-link']],
714         ],
715       ],
716     ];
717
718     if ($view->access('delete')) {
719       $element['extra_actions']['#links']['delete'] = [
720         'title' => $this->t('Delete view'),
721         'url' => $view->urlInfo('delete-form'),
722       ];
723     }
724
725     // Let other modules add additional links here.
726     \Drupal::moduleHandler()->alter('views_ui_display_top_links', $element['extra_actions']['#links'], $view, $display_id);
727
728     if (isset($view->type) && $view->type != $this->t('Default')) {
729       if ($view->type == $this->t('Overridden')) {
730         $element['extra_actions']['#links']['revert'] = [
731           'title' => $this->t('Revert view'),
732           'href' => "admin/structure/views/view/{$view->id()}/revert",
733           'query' => ['destination' => $view->url('edit-form')],
734         ];
735       }
736       else {
737         $element['extra_actions']['#links']['delete'] = [
738           'title' => $this->t('Delete view'),
739           'url' => $view->urlInfo('delete-form'),
740         ];
741       }
742     }
743
744     // Determine the displays available for editing.
745     if ($tabs = $this->getDisplayTabs($view)) {
746       if ($display_id) {
747         $tabs[$display_id]['#active'] = TRUE;
748       }
749       $tabs['#prefix'] = '<h2 class="visually-hidden">' . $this->t('Secondary tabs') . '</h2><ul id = "views-display-menu-tabs" class="tabs secondary">';
750       $tabs['#suffix'] = '</ul>';
751       $element['tabs'] = $tabs;
752     }
753
754     // Buttons for adding a new display.
755     foreach (Views::fetchPluginNames('display', NULL, [$view->get('base_table')]) as $type => $label) {
756       $element['add_display'][$type] = [
757         '#type' => 'submit',
758         '#value' => $this->t('Add @display', ['@display' => $label]),
759         '#limit_validation_errors' => [],
760         '#submit' => ['::submitDisplayAdd', '::submitDelayDestination'],
761         '#attributes' => ['class' => ['add-display']],
762         // Allow JavaScript to remove the 'Add ' prefix from the button label when
763         // placing the button in a "Add" dropdown menu.
764         '#process' => array_merge(['views_ui_form_button_was_clicked'], $this->elementInfo->getInfoProperty('submit', '#process', [])),
765         '#values' => [$this->t('Add @display', ['@display' => $label]), $label],
766       ];
767     }
768
769     return $element;
770   }
771
772   /**
773    * Submit handler for form buttons that do not complete a form workflow.
774    *
775    * The Edit View form is a multistep form workflow, but with state managed by
776    * the SharedTempStore rather than $form_state->setRebuild(). Without this
777    * submit handler, buttons that add or remove displays would redirect to the
778    * destination parameter (e.g., when the Edit View form is linked to from a
779    * contextual link). This handler can be added to buttons whose form submission
780    * should not yet redirect to the destination.
781    */
782   public function submitDelayDestination($form, FormStateInterface $form_state) {
783     $request = $this->requestStack->getCurrentRequest();
784     $destination = $request->query->get('destination');
785
786     $redirect = $form_state->getRedirect();
787     // If there is a destination, and redirects are not explicitly disabled, add
788     // the destination as a query string to the redirect and suppress it for the
789     // current request.
790     if (isset($destination) && $redirect !== FALSE) {
791       // Create a valid redirect if one does not exist already.
792       if (!($redirect instanceof Url)) {
793         $redirect = Url::createFromRequest($request);
794       }
795
796       // Add the current destination to the redirect unless one exists already.
797       $options = $redirect->getOptions();
798       if (!isset($options['query']['destination'])) {
799         $options['query']['destination'] = $destination;
800         $redirect->setOptions($options);
801       }
802
803       $form_state->setRedirectUrl($redirect);
804       $request->query->remove('destination');
805     }
806   }
807
808   /**
809    * Submit handler to duplicate a display for a view.
810    */
811   public function submitDisplayDuplicate($form, FormStateInterface $form_state) {
812     $view = $this->entity;
813     $display_id = $this->displayID;
814
815     // Create the new display.
816     $displays = $view->get('display');
817     $display = $view->getExecutable()->newDisplay($displays[$display_id]['display_plugin']);
818     $new_display_id = $display->display['id'];
819     $displays[$new_display_id] = $displays[$display_id];
820     $displays[$new_display_id]['id'] = $new_display_id;
821     $view->set('display', $displays);
822
823     // By setting the current display the changed marker will appear on the new
824     // display.
825     $view->getExecutable()->current_display = $new_display_id;
826     $view->cacheSet();
827
828     // Redirect to the new display's edit page.
829     $form_state->setRedirect('entity.view.edit_display_form', [
830       'view' => $view->id(),
831       'display_id' => $new_display_id,
832     ]);
833   }
834
835   /**
836    * Submit handler to add a display to a view.
837    */
838   public function submitDisplayAdd($form, FormStateInterface $form_state) {
839     $view = $this->entity;
840     // Create the new display.
841     $parents = $form_state->getTriggeringElement()['#parents'];
842     $display_type = array_pop($parents);
843     $display = $view->getExecutable()->newDisplay($display_type);
844     $display_id = $display->display['id'];
845     // A new display got added so the asterisks symbol should appear on the new
846     // display.
847     $view->getExecutable()->current_display = $display_id;
848     $view->cacheSet();
849
850     // Redirect to the new display's edit page.
851     $form_state->setRedirect('entity.view.edit_display_form', [
852       'view' => $view->id(),
853       'display_id' => $display_id,
854     ]);
855   }
856
857   /**
858    * Submit handler to Duplicate a display as another display type.
859    */
860   public function submitDuplicateDisplayAsType($form, FormStateInterface $form_state) {
861     /** @var \Drupal\views\ViewEntityInterface $view */
862     $view = $this->entity;
863     $display_id = $this->displayID;
864
865     // Create the new display.
866     $parents = $form_state->getTriggeringElement()['#parents'];
867     $display_type = array_pop($parents);
868
869     $new_display_id = $view->duplicateDisplayAsType($display_id, $display_type);
870
871     // By setting the current display the changed marker will appear on the new
872     // display.
873     $view->getExecutable()->current_display = $new_display_id;
874     $view->cacheSet();
875
876     // Redirect to the new display's edit page.
877     $form_state->setRedirect('entity.view.edit_display_form', [
878       'view' => $view->id(),
879       'display_id' => $new_display_id,
880     ]);
881   }
882
883   /**
884    * Build a renderable array representing one option on the edit form.
885    *
886    * This function might be more logical as a method on an object, if a suitable
887    * object emerges out of refactoring.
888    */
889   public function buildOptionForm(ViewUI $view, $id, $option, $display) {
890     $option_build = [];
891     $option_build['#theme'] = 'views_ui_display_tab_setting';
892
893     $option_build['#description'] = $option['title'];
894
895     $option_build['#link'] = $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($option['value'], $id, '', empty($option['desc']) ? '' : $option['desc']);
896
897     $option_build['#links'] = [];
898     if (!empty($option['links']) && is_array($option['links'])) {
899       foreach ($option['links'] as $link_id => $link_value) {
900         $option_build['#settings_links'][] = $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($option['setting'], $link_id, 'views-button-configure', $link_value);
901       }
902     }
903
904     if (!empty($view->getExecutable()->displayHandlers->get($display['id'])->options['defaults'][$id])) {
905       $display_id = 'default';
906       $option_build['#defaulted'] = TRUE;
907     }
908     else {
909       $display_id = $display['id'];
910       if (!$view->getExecutable()->displayHandlers->get($display['id'])->isDefaultDisplay()) {
911         if ($view->getExecutable()->displayHandlers->get($display['id'])->defaultableSections($id)) {
912           $option_build['#overridden'] = TRUE;
913         }
914       }
915     }
916     $option_build['#attributes']['class'][] = Html::cleanCssIdentifier($display_id . '-' . $id);
917     return $option_build;
918   }
919
920   /**
921    * Add information about a section to a display.
922    */
923   public function getFormBucket(ViewUI $view, $type, $display) {
924     $executable = $view->getExecutable();
925     $executable->setDisplay($display['id']);
926     $executable->initStyle();
927
928     $types = $executable->getHandlerTypes();
929
930     $build = [
931       '#theme_wrappers' => ['views_ui_display_tab_bucket'],
932     ];
933
934     $build['#overridden'] = FALSE;
935     $build['#defaulted'] = FALSE;
936
937     $build['#name'] = $type;
938     $build['#title'] = $types[$type]['title'];
939
940     $rearrange_url = Url::fromRoute('views_ui.form_rearrange', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]);
941     $class = 'icon compact rearrange';
942
943     // Different types now have different rearrange forms, so we use this switch
944     // to get the right one.
945     switch ($type) {
946       case 'filter':
947         // The rearrange form for filters contains the and/or UI, so override
948         // the used path.
949         $rearrange_url = Url::fromRoute('views_ui.form_rearrange_filter', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id']]);
950         // TODO: Add another class to have another symbol for filter rearrange.
951         $class = 'icon compact rearrange';
952         break;
953       case 'field':
954         // Fetch the style plugin info so we know whether to list fields or not.
955         $style_plugin = $executable->style_plugin;
956         $uses_fields = $style_plugin && $style_plugin->usesFields();
957         if (!$uses_fields) {
958           $build['fields'][] = [
959             '#markup' => $this->t('The selected style or row format does not use fields.'),
960             '#theme_wrappers' => ['views_ui_container'],
961             '#attributes' => ['class' => ['views-display-setting']],
962           ];
963           return $build;
964         }
965         break;
966       case 'header':
967       case 'footer':
968       case 'empty':
969         if (!$executable->display_handler->usesAreas()) {
970           $build[$type][] = [
971             '#markup' => $this->t('The selected display type does not use @type plugins', ['@type' => $type]),
972             '#theme_wrappers' => ['views_ui_container'],
973             '#attributes' => ['class' => ['views-display-setting']],
974           ];
975           return $build;
976         }
977         break;
978     }
979
980     // Create an array of actions to pass to links template.
981     $actions = [];
982     $count_handlers = count($executable->display_handler->getHandlers($type));
983
984     // Create the add text variable for the add action.
985     $add_text = $this->t('Add <span class="visually-hidden">@type</span>', ['@type' => $types[$type]['ltitle']]);
986
987     $actions['add'] = [
988       'title' => $add_text,
989       'url' => Url::fromRoute('views_ui.form_add_handler', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]),
990       'attributes' => ['class' => ['icon compact add', 'views-ajax-link'], 'id' => 'views-add-' . $type],
991     ];
992     if ($count_handlers > 0) {
993       // Create the rearrange text variable for the rearrange action.
994       $rearrange_text = $type == 'filter' ? $this->t('And/Or Rearrange <span class="visually-hidden">filter criteria</span>') : $this->t('Rearrange <span class="visually-hidden">@type</span>', ['@type' => $types[$type]['ltitle']]);
995
996       $actions['rearrange'] = [
997         'title' => $rearrange_text,
998         'url' => $rearrange_url,
999         'attributes' => ['class' => [$class, 'views-ajax-link'], 'id' => 'views-rearrange-' . $type],
1000       ];
1001     }
1002
1003     // Render the array of links
1004     $build['#actions'] = [
1005       '#type' => 'dropbutton',
1006       '#links' => $actions,
1007       '#attributes' => [
1008         'class' => ['views-ui-settings-bucket-operations'],
1009       ],
1010     ];
1011
1012     if (!$executable->display_handler->isDefaultDisplay()) {
1013       if (!$executable->display_handler->isDefaulted($types[$type]['plural'])) {
1014         $build['#overridden'] = TRUE;
1015       }
1016       else {
1017         $build['#defaulted'] = TRUE;
1018       }
1019     }
1020
1021     static $relationships = NULL;
1022     if (!isset($relationships)) {
1023       // Get relationship labels.
1024       $relationships = [];
1025       foreach ($executable->display_handler->getHandlers('relationship') as $id => $handler) {
1026         $relationships[$id] = $handler->adminLabel();
1027       }
1028     }
1029
1030     // Filters can now be grouped so we do a little bit extra:
1031     $groups = [];
1032     $grouping = FALSE;
1033     if ($type == 'filter') {
1034       $group_info = $executable->display_handler->getOption('filter_groups');
1035       // If there is only one group but it is using the "OR" filter, we still
1036       // treat it as a group for display purposes, since we want to display the
1037       // "OR" label next to items within the group.
1038       if (!empty($group_info['groups']) && (count($group_info['groups']) > 1 || current($group_info['groups']) == 'OR')) {
1039         $grouping = TRUE;
1040         $groups = [0 => []];
1041       }
1042     }
1043
1044     $build['fields'] = [];
1045
1046     foreach ($executable->display_handler->getOption($types[$type]['plural']) as $id => $field) {
1047       // Build the option link for this handler ("Node: ID = article").
1048       $build['fields'][$id] = [];
1049       $build['fields'][$id]['#theme'] = 'views_ui_display_tab_setting';
1050
1051       $handler = $executable->display_handler->getHandler($type, $id);
1052       if ($handler->broken()) {
1053         $build['fields'][$id]['#class'][] = 'broken';
1054         $field_name = $handler->adminLabel();
1055         $build['fields'][$id]['#link'] = $this->l($field_name, new Url('views_ui.form_handler', [
1056           'js' => 'nojs',
1057           'view' => $view->id(),
1058           'display_id' => $display['id'],
1059           'type' => $type,
1060           'id' => $id,
1061         ], ['attributes' => ['class' => ['views-ajax-link']]]));
1062         continue;
1063       }
1064
1065       $field_name = $handler->adminLabel(TRUE);
1066       if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) {
1067         $field_name = '(' . $relationships[$field['relationship']] . ') ' . $field_name;
1068       }
1069
1070       $description = $handler->adminSummary();
1071       $link_text = $field_name . (empty($description) ? '' : " ($description)");
1072       $link_attributes = ['class' => ['views-ajax-link']];
1073       if (!empty($field['exclude'])) {
1074         $link_attributes['class'][] = 'views-field-excluded';
1075         // Add a [hidden] marker, if the field is excluded.
1076         $link_text .= ' [' . $this->t('hidden') . ']';
1077       }
1078       $build['fields'][$id]['#link'] = $this->l($link_text, new Url('views_ui.form_handler', [
1079         'js' => 'nojs',
1080         'view' => $view->id(),
1081         'display_id' => $display['id'],
1082         'type' => $type,
1083         'id' => $id,
1084       ], ['attributes' => $link_attributes]));
1085       $build['fields'][$id]['#class'][] = Html::cleanCssIdentifier($display['id'] . '-' . $type . '-' . $id);
1086
1087       if ($executable->display_handler->useGroupBy() && $handler->usesGroupBy()) {
1088         $build['fields'][$id]['#settings_links'][] = $this->l(SafeMarkup::format('<span class="label">@text</span>', ['@text' => $this->t('Aggregation settings')]), new Url('views_ui.form_handler_group', [
1089           'js' => 'nojs',
1090           'view' => $view->id(),
1091           'display_id' => $display['id'],
1092           'type' => $type,
1093           'id' => $id,
1094         ], ['attributes' => ['class' => ['views-button-configure', 'views-ajax-link'], 'title' => $this->t('Aggregation settings')]]));
1095       }
1096
1097       if ($handler->hasExtraOptions()) {
1098         $build['fields'][$id]['#settings_links'][] = $this->l(SafeMarkup::format('<span class="label">@text</span>', ['@text' => $this->t('Settings')]), new Url('views_ui.form_handler_extra', [
1099           'js' => 'nojs',
1100           'view' => $view->id(),
1101           'display_id' => $display['id'],
1102           'type' => $type,
1103           'id' => $id,
1104         ], ['attributes' => ['class' => ['views-button-configure', 'views-ajax-link'], 'title' => $this->t('Settings')]]));
1105       }
1106
1107       if ($grouping) {
1108         $gid = $handler->options['group'];
1109
1110         // Show in default group if the group does not exist.
1111         if (empty($group_info['groups'][$gid])) {
1112           $gid = 0;
1113         }
1114         $groups[$gid][] = $id;
1115       }
1116     }
1117
1118     // If using grouping, re-order fields so that they show up properly in the list.
1119     if ($type == 'filter' && $grouping) {
1120       $store = $build['fields'];
1121       $build['fields'] = [];
1122       foreach ($groups as $gid => $contents) {
1123         // Display an operator between each group.
1124         if (!empty($build['fields'])) {
1125           $build['fields'][] = [
1126             '#theme' => 'views_ui_display_tab_setting',
1127             '#class' => ['views-group-text'],
1128             '#link' => ($group_info['operator'] == 'OR' ? $this->t('OR') : $this->t('AND')),
1129           ];
1130         }
1131         // Display an operator between each pair of filters within the group.
1132         $keys = array_keys($contents);
1133         $last = end($keys);
1134         foreach ($contents as $key => $pid) {
1135           if ($key != $last) {
1136             $operator = $group_info['groups'][$gid] == 'OR' ? $this->t('OR') : $this->t('AND');
1137             $store[$pid]['#link'] = SafeMarkup::format('@link <span>@operator</span>', ['@link' => $store[$pid]['#link'], '@operator' => $operator]);
1138           }
1139           $build['fields'][$pid] = $store[$pid];
1140         }
1141       }
1142     }
1143
1144     return $build;
1145   }
1146
1147 }