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