comparison core/modules/views_ui/src/ViewEditForm.php @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children a9cd425dd02b
comparison
equal deleted inserted replaced
-1:000000000000 0:c75dbcec494b
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 }